aboutsummaryrefslogtreecommitdiffstats
path: root/crates/rocie-server/src/storage
diff options
context:
space:
mode:
Diffstat (limited to 'crates/rocie-server/src/storage')
-rw-r--r--crates/rocie-server/src/storage/migrate/sql/0->1.sql10
-rw-r--r--crates/rocie-server/src/storage/sql/get/mod.rs2
-rw-r--r--crates/rocie-server/src/storage/sql/get/product/mod.rs83
-rw-r--r--crates/rocie-server/src/storage/sql/get/product_parent/mod.rs87
-rw-r--r--crates/rocie-server/src/storage/sql/get/recipe/mod.rs78
-rw-r--r--crates/rocie-server/src/storage/sql/insert/mod.rs2
-rw-r--r--crates/rocie-server/src/storage/sql/insert/product/mod.rs6
-rw-r--r--crates/rocie-server/src/storage/sql/insert/product_parent/mod.rs113
-rw-r--r--crates/rocie-server/src/storage/sql/insert/recipe/mod.rs95
-rw-r--r--crates/rocie-server/src/storage/sql/mod.rs35
-rw-r--r--crates/rocie-server/src/storage/sql/product.rs9
-rw-r--r--crates/rocie-server/src/storage/sql/product_parent.rs31
-rw-r--r--crates/rocie-server/src/storage/sql/recipe.rs18
-rw-r--r--crates/rocie-server/src/storage/sql/unit.rs1
-rw-r--r--crates/rocie-server/src/storage/sql/unit_property.rs1
15 files changed, 519 insertions, 52 deletions
diff --git a/crates/rocie-server/src/storage/migrate/sql/0->1.sql b/crates/rocie-server/src/storage/migrate/sql/0->1.sql
index 7f08738..664f40f 100644
--- a/crates/rocie-server/src/storage/migrate/sql/0->1.sql
+++ b/crates/rocie-server/src/storage/migrate/sql/0->1.sql
@@ -22,6 +22,8 @@ CREATE TABLE parents (
parent TEXT DEFAULT NULL CHECK (
id IS NOT parent
),
+ name TEXT UNIQUE NOT NULL,
+ description TEXT,
FOREIGN KEY(parent) REFERENCES parents(id)
) STRICT;
@@ -97,7 +99,7 @@ END;
CREATE TABLE units (
id TEXT UNIQUE NOT NULL PRIMARY KEY,
- unit_property TEXT UNIQUE NOT NULL,
+ unit_property TEXT NOT NULL,
full_name_singular TEXT UNIQUE NOT NULL,
full_name_plural TEXT UNIQUE NOT NULL,
short_name TEXT UNIQUE NOT NULL,
@@ -111,6 +113,12 @@ CREATE TABLE unit_properties (
description TEXT
) STRICT;
+CREATE TABLE recipies (
+ id TEXT UNIQUE NOT NULL PRIMARY KEY,
+ path TEXT UNIQUE NOT NULL,
+ content TEXT NOT NULL
+) STRICT;
+
-- Encodes unit conversions:
-- {factor} {from_unit} = 1 {to_unit}
-- E.g.: 1000 g = 1 kg
diff --git a/crates/rocie-server/src/storage/sql/get/mod.rs b/crates/rocie-server/src/storage/sql/get/mod.rs
index 62047b8..1fb54b0 100644
--- a/crates/rocie-server/src/storage/sql/get/mod.rs
+++ b/crates/rocie-server/src/storage/sql/get/mod.rs
@@ -1,5 +1,7 @@
pub(crate) mod product;
+pub(crate) mod product_parent;
pub(crate) mod product_amount;
pub(crate) mod unit;
pub(crate) mod unit_property;
pub(crate) mod barcode;
+pub(crate) mod recipe;
diff --git a/crates/rocie-server/src/storage/sql/get/product/mod.rs b/crates/rocie-server/src/storage/sql/get/product/mod.rs
index 0df51d8..915da81 100644
--- a/crates/rocie-server/src/storage/sql/get/product/mod.rs
+++ b/crates/rocie-server/src/storage/sql/get/product/mod.rs
@@ -3,6 +3,7 @@ use crate::{
storage::sql::{
barcode::{Barcode, BarcodeId},
product::{Product, ProductId},
+ product_parent::ProductParentId,
unit::{UnitAmount, UnitId},
unit_property::UnitPropertyId,
},
@@ -10,11 +11,46 @@ use crate::{
use sqlx::query;
+macro_rules! product_from_record {
+ ($app:ident, $record:ident) => {{
+ let barcodes = query!(
+ "
+ SELECT id, amount, unit
+ FROM barcodes
+ WHERE product_id = ?
+ ",
+ $record.id,
+ )
+ .fetch_all(&$app.db)
+ .await?;
+
+ Self {
+ id: ProductId::from_db(&$record.id),
+ unit_property: UnitPropertyId::from_db(&$record.unit_property),
+ name: $record.name,
+ description: $record.description,
+ parent: $record
+ .parent
+ .map(|parent| ProductParentId::from_db(&parent)),
+ associated_bar_codes: barcodes
+ .into_iter()
+ .map(|record| Barcode {
+ id: BarcodeId::from_db(record.id),
+ amount: UnitAmount {
+ value: u32::try_from(record.amount).expect("Should be strictly positve"),
+ unit: UnitId::from_db(&record.unit),
+ },
+ })
+ .collect(),
+ }
+ }};
+}
+
impl Product {
pub(crate) async fn from_id(app: &App, id: ProductId) -> Result<Option<Self>, from_id::Error> {
let record = query!(
"
- SELECT name, unit_property, description, parent
+ SELECT name, id, unit_property, description, parent
FROM products
WHERE id = ?
",
@@ -24,13 +60,7 @@ impl Product {
.await?;
if let Some(record) = record {
- Ok(Some(Self {
- id,
- unit_property: UnitPropertyId::from_db(&record.unit_property),
- name: record.name,
- description: record.description,
- associated_bar_codes: vec![], // todo
- }))
+ Ok(Some(product_from_record!(app, record)))
} else {
Ok(None)
}
@@ -49,13 +79,7 @@ impl Product {
.await?;
if let Some(record) = record {
- Ok(Some(Self {
- id: ProductId::from_db(&record.id),
- unit_property: UnitPropertyId::from_db(&record.unit_property),
- name,
- description: record.description,
- associated_bar_codes: vec![], // todo
- }))
+ Ok(Some(product_from_record!(app, record)))
} else {
Ok(None)
}
@@ -73,34 +97,9 @@ impl Product {
let mut all = Vec::with_capacity(records.len());
for record in records {
- let barcodes = query!(
- "
- SELECT id, amount, unit
- FROM barcodes
- WHERE product_id = ?
-",
- record.id,
- )
- .fetch_all(&app.db)
- .await?;
+ let product = product_from_record!(app, record);
- all.push(Self {
- id: ProductId::from_db(&record.id),
- unit_property: UnitPropertyId::from_db(&record.unit_property),
- name: record.name,
- description: record.description,
- associated_bar_codes: barcodes
- .into_iter()
- .map(|record| Barcode {
- id: BarcodeId::from_db(record.id),
- amount: UnitAmount {
- value: u32::try_from(record.amount)
- .expect("Should be strictly positve"),
- unit: UnitId::from_db(&record.unit),
- },
- })
- .collect(),
- });
+ all.push(product);
}
Ok(all)
diff --git a/crates/rocie-server/src/storage/sql/get/product_parent/mod.rs b/crates/rocie-server/src/storage/sql/get/product_parent/mod.rs
new file mode 100644
index 0000000..5b85b62
--- /dev/null
+++ b/crates/rocie-server/src/storage/sql/get/product_parent/mod.rs
@@ -0,0 +1,87 @@
+use crate::{
+ app::App,
+ storage::sql::product_parent::{ProductParent, ProductParentId},
+};
+
+use sqlx::query;
+
+impl ProductParent {
+ pub(crate) async fn get_all(app: &App) -> Result<Vec<Self>, get_all::Error> {
+ let records = query!(
+ "
+ SELECT id, parent, name, description
+ FROM parents
+"
+ )
+ .fetch_all(&app.db)
+ .await?;
+
+ let mut all = Vec::with_capacity(records.len());
+ for record in records {
+ let parent = ProductParent {
+ id: ProductParentId::from_db(&record.id),
+ parent: record
+ .parent
+ .map(|parent| ProductParentId::from_db(&parent)),
+ name: record.name,
+ description: record.description,
+ };
+
+ all.push(parent);
+ }
+
+ Ok(all)
+ }
+
+ pub(crate) async fn from_id(
+ app: &App,
+ id: ProductParentId,
+ ) -> Result<Option<Self>, from_id::Error> {
+ let record = query!(
+ "
+ SELECT parent, name, description
+ FROM parents
+ WHERE id = ?
+",
+ id
+ )
+ .fetch_optional(&app.db)
+ .await?;
+
+ match record {
+ Some(record) => Ok(Some(ProductParent {
+ id,
+ parent: record
+ .parent
+ .map(|parent| ProductParentId::from_db(&parent)),
+ name: record.name,
+ description: record.description,
+ })),
+ None => Ok(None),
+ }
+ }
+}
+
+pub(crate) mod from_id {
+ use actix_web::ResponseError;
+
+ #[derive(thiserror::Error, Debug)]
+ pub(crate) enum Error {
+ #[error("Failed to execute the sql query")]
+ SqlError(#[from] sqlx::Error),
+ }
+
+ impl ResponseError for Error {}
+}
+
+pub(crate) mod get_all {
+ use actix_web::ResponseError;
+
+ #[derive(thiserror::Error, Debug)]
+ pub(crate) enum Error {
+ #[error("Failed to execute the sql query")]
+ SqlError(#[from] sqlx::Error),
+ }
+
+ impl ResponseError for Error {}
+}
diff --git a/crates/rocie-server/src/storage/sql/get/recipe/mod.rs b/crates/rocie-server/src/storage/sql/get/recipe/mod.rs
new file mode 100644
index 0000000..9d6dc79
--- /dev/null
+++ b/crates/rocie-server/src/storage/sql/get/recipe/mod.rs
@@ -0,0 +1,78 @@
+use crate::{
+ app::App,
+ storage::sql::recipe::{Recipe, RecipeId},
+};
+
+use sqlx::query;
+
+impl Recipe {
+ pub(crate) async fn from_id(app: &App, id: RecipeId) -> Result<Option<Self>, from_id::Error> {
+ let record = query!(
+ "
+ SELECT content, path
+ FROM recipies
+ WHERE id = ?
+",
+ id
+ )
+ .fetch_optional(&app.db)
+ .await?;
+
+ if let Some(record) = record {
+ Ok(Some(Self {
+ id,
+ path: record
+ .path
+ .parse()
+ .expect("Was a path before, should still be one"),
+ content: record.content,
+ }))
+ } else {
+ Ok(None)
+ }
+ }
+
+ pub(crate) async fn get_all(app: &App) -> Result<Vec<Self>, get_all::Error> {
+ let records = query!(
+ "
+ SELECT id, content, path
+ FROM recipies
+",
+ )
+ .fetch_all(&app.db)
+ .await?;
+
+ Ok(records
+ .into_iter()
+ .map(|record| Self {
+ id: RecipeId::from_db(&record.id),
+ path: record.path.parse().expect("Is still valid"),
+ content: record.content,
+ })
+ .collect())
+ }
+}
+
+pub(crate) mod from_id {
+ use actix_web::ResponseError;
+
+ #[derive(thiserror::Error, Debug)]
+ pub(crate) enum Error {
+ #[error("Failed to execute the sql query")]
+ SqlError(#[from] sqlx::Error),
+ }
+
+ impl ResponseError for Error {}
+}
+
+pub(crate) mod get_all {
+ use actix_web::ResponseError;
+
+ #[derive(thiserror::Error, Debug)]
+ pub(crate) enum Error {
+ #[error("Failed to execute the sql query")]
+ SqlError(#[from] sqlx::Error),
+ }
+
+ impl ResponseError for Error {}
+}
diff --git a/crates/rocie-server/src/storage/sql/insert/mod.rs b/crates/rocie-server/src/storage/sql/insert/mod.rs
index 3b2d702..8a15385 100644
--- a/crates/rocie-server/src/storage/sql/insert/mod.rs
+++ b/crates/rocie-server/src/storage/sql/insert/mod.rs
@@ -9,8 +9,10 @@ use sqlx::{SqliteConnection, query};
pub(crate) mod barcode;
pub(crate) mod product;
+pub(crate) mod product_parent;
pub(crate) mod unit;
pub(crate) mod unit_property;
+pub(crate) mod recipe;
pub(crate) trait Transactionable:
Sized + std::fmt::Debug + Serialize + DeserializeOwned
diff --git a/crates/rocie-server/src/storage/sql/insert/product/mod.rs b/crates/rocie-server/src/storage/sql/insert/product/mod.rs
index d762e9b..455eb4f 100644
--- a/crates/rocie-server/src/storage/sql/insert/product/mod.rs
+++ b/crates/rocie-server/src/storage/sql/insert/product/mod.rs
@@ -6,6 +6,7 @@ use crate::storage::sql::{
barcode::Barcode,
insert::{Operations, Transactionable},
product::{Product, ProductId},
+ product_parent::ProductParentId,
unit_property::UnitPropertyId,
};
@@ -15,7 +16,7 @@ pub(crate) enum Operation {
id: ProductId,
name: String,
description: Option<String>,
- parent: Option<ProductId>,
+ parent: Option<ProductParentId>,
unit_property: UnitPropertyId,
},
AssociateBarcode {
@@ -138,7 +139,7 @@ impl Product {
pub(crate) fn register(
name: String,
description: Option<String>,
- parent: Option<ProductId>,
+ parent: Option<ProductParentId>,
unit_property: UnitPropertyId,
ops: &mut Operations<Operation>,
) -> Self {
@@ -157,6 +158,7 @@ impl Product {
name,
description,
unit_property,
+ parent,
associated_bar_codes: vec![],
}
}
diff --git a/crates/rocie-server/src/storage/sql/insert/product_parent/mod.rs b/crates/rocie-server/src/storage/sql/insert/product_parent/mod.rs
new file mode 100644
index 0000000..644778f
--- /dev/null
+++ b/crates/rocie-server/src/storage/sql/insert/product_parent/mod.rs
@@ -0,0 +1,113 @@
+use serde::{Deserialize, Serialize};
+use sqlx::query;
+use uuid::Uuid;
+
+use crate::storage::sql::{
+ insert::{Operations, Transactionable},
+ product_parent::{ProductParent, ProductParentId},
+};
+
+#[derive(Debug, Deserialize, Serialize)]
+pub(crate) enum Operation {
+ RegisterProductParent {
+ id: ProductParentId,
+ name: String,
+ description: Option<String>,
+ parent: Option<ProductParentId>,
+ },
+}
+
+impl Transactionable for Operation {
+ type ApplyError = apply::Error;
+ type UndoError = undo::Error;
+
+ async fn apply(self, txn: &mut sqlx::SqliteConnection) -> Result<(), apply::Error> {
+ match self {
+ Operation::RegisterProductParent {
+ id,
+ name,
+ description,
+ parent,
+ } => {
+ query!(
+ "
+ INSERT INTO parents (id, name, description, parent)
+ VALUES (?,?,?,?)
+",
+ id,
+ name,
+ description,
+ parent
+ )
+ .execute(txn)
+ .await?;
+ }
+ }
+ Ok(())
+ }
+
+ async fn undo(self, txn: &mut sqlx::SqliteConnection) -> Result<(), undo::Error> {
+ match self {
+ Operation::RegisterProductParent {
+ id,
+ name,
+ description,
+ parent,
+ } => {
+ query!(
+ "
+ DELETE FROM products
+ WHERE id = ? AND name = ? AND description = ? AND parent = ?;
+",
+ id,
+ name,
+ description,
+ parent,
+ )
+ .execute(txn)
+ .await?;
+ }
+ }
+ Ok(())
+ }
+}
+
+pub(crate) mod undo {
+ #[derive(thiserror::Error, Debug)]
+ pub(crate) enum Error {
+ #[error("Failed to execute undo sql statments: {0}")]
+ SqlError(#[from] sqlx::Error),
+ }
+}
+pub(crate) mod apply {
+ #[derive(thiserror::Error, Debug)]
+ pub(crate) enum Error {
+ #[error("Failed to execute apply sql statments: {0}")]
+ SqlError(#[from] sqlx::Error),
+ }
+}
+
+impl ProductParent {
+ pub(crate) fn register(
+ name: String,
+ description: Option<String>,
+ parent: Option<ProductParentId>,
+ ops: &mut Operations<Operation>,
+ ) -> Self {
+ let id = ProductParentId::from(Uuid::new_v4());
+
+ ops.push(Operation::RegisterProductParent {
+ id,
+ name: name.clone(),
+ description: description.clone(),
+ parent,
+ });
+
+ Self {
+ id,
+ name,
+ description,
+ parent,
+ }
+ }
+}
diff --git a/crates/rocie-server/src/storage/sql/insert/recipe/mod.rs b/crates/rocie-server/src/storage/sql/insert/recipe/mod.rs
new file mode 100644
index 0000000..b223bfe
--- /dev/null
+++ b/crates/rocie-server/src/storage/sql/insert/recipe/mod.rs
@@ -0,0 +1,95 @@
+use std::path::PathBuf;
+
+use serde::{Deserialize, Serialize};
+use sqlx::query;
+use uuid::Uuid;
+
+use crate::storage::sql::{
+ insert::{Operations, Transactionable},
+ recipe::{Recipe, RecipeId},
+};
+
+#[derive(Debug, Deserialize, Serialize)]
+pub(crate) enum Operation {
+ New {
+ id: RecipeId,
+ path: PathBuf,
+ content: String,
+ },
+}
+
+impl Transactionable for Operation {
+ type ApplyError = apply::Error;
+ type UndoError = undo::Error;
+
+ async fn apply(self, txn: &mut sqlx::SqliteConnection) -> Result<(), apply::Error> {
+ match self {
+ Operation::New { id, path, content } => {
+ let path = path.display().to_string();
+
+ query!(
+ "
+ INSERT INTO recipies (id, path, content)
+ VALUES (?, ?, ?)
+",
+ id,
+ path,
+ content,
+ )
+ .execute(txn)
+ .await?;
+ }
+ }
+ Ok(())
+ }
+
+ async fn undo(self, txn: &mut sqlx::SqliteConnection) -> Result<(), undo::Error> {
+ match self {
+ Operation::New { id, path, content } => {
+ let path = path.display().to_string();
+
+ query!(
+ "
+ DELETE FROM recipies
+ WHERE id = ? AND path = ? AND content = ?
+",
+ id,
+ path,
+ content
+ )
+ .execute(txn)
+ .await?;
+ }
+ }
+ Ok(())
+ }
+}
+
+pub(crate) mod undo {
+ #[derive(thiserror::Error, Debug)]
+ pub(crate) enum Error {
+ #[error("Failed to execute undo sql statments: {0}")]
+ SqlError(#[from] sqlx::Error),
+ }
+}
+pub(crate) mod apply {
+ #[derive(thiserror::Error, Debug)]
+ pub(crate) enum Error {
+ #[error("Failed to execute apply sql statments: {0}")]
+ SqlError(#[from] sqlx::Error),
+ }
+}
+
+impl Recipe {
+ pub(crate) fn new(path: PathBuf, content: String, ops: &mut Operations<Operation>) -> Self {
+ let id = RecipeId::from(Uuid::new_v4());
+
+ ops.push(Operation::New {
+ id,
+ path: path.clone(),
+ content: content.clone(),
+ });
+
+ Self { id, path, content }
+ }
+}
diff --git a/crates/rocie-server/src/storage/sql/mod.rs b/crates/rocie-server/src/storage/sql/mod.rs
index a44fbad..dd46eab 100644
--- a/crates/rocie-server/src/storage/sql/mod.rs
+++ b/crates/rocie-server/src/storage/sql/mod.rs
@@ -5,14 +5,26 @@ pub(crate) mod insert;
pub(crate) mod barcode;
pub(crate) mod product;
pub(crate) mod product_amount;
+pub(crate) mod product_parent;
+pub(crate) mod recipe;
pub(crate) mod unit;
pub(crate) mod unit_property;
macro_rules! mk_id {
($name:ident and $stub_name:ident) => {
- mk_id!($name and $stub_name with uuid::Uuid, "uuid::Uuid");
+ mk_id!(
+ $name and $stub_name,
+ with uuid::Uuid, "uuid::Uuid",
+ to_string {|val: &uuid::Uuid| val.to_string()},
+ copy Copy
+ );
};
- ($name:ident and $stub_name:ident with $inner:path, $inner_string:literal $($args:meta)*) => {
+ (
+ $name:ident and $stub_name:ident,
+ with $inner:path $(=> $($args:meta)* )?, $inner_string:literal,
+ to_string $to_string:expr,
+ $(copy $copy:path)?
+ ) => {
#[derive(
serde::Deserialize,
serde::Serialize,
@@ -20,17 +32,28 @@ macro_rules! mk_id {
Default,
utoipa::ToSchema,
Clone,
- Copy,
PartialEq,
Eq,
PartialOrd,
Ord,
+ $($copy,)?
)]
pub(crate) struct $name {
+ $(
+ $(
+ #[$args]
+ )*
+ )?
value: $inner,
}
- #[derive(Deserialize, Serialize, Debug, Clone, Copy)]
+ #[derive(
+ Deserialize,
+ Serialize,
+ Debug,
+ Clone,
+ $($copy,)?
+ )]
#[serde(from = $inner_string)]
pub(crate) struct $stub_name {
value: $inner,
@@ -55,7 +78,7 @@ macro_rules! mk_id {
impl std::fmt::Display for $name {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- write!(f, "{}", self.value)
+ write!(f, "{}", $to_string(&self.value))
}
}
@@ -83,7 +106,7 @@ macro_rules! mk_id {
&self,
buf: &mut <DB as sqlx::Database>::ArgumentBuffer<'q>,
) -> Result<sqlx::encode::IsNull, sqlx::error::BoxDynError> {
- let inner = self.value.to_string();
+ let inner = $to_string(&self.value);
sqlx::Encode::<DB>::encode_by_ref(&inner, buf)
}
}
diff --git a/crates/rocie-server/src/storage/sql/product.rs b/crates/rocie-server/src/storage/sql/product.rs
index 1c5a7d8..8d2c951 100644
--- a/crates/rocie-server/src/storage/sql/product.rs
+++ b/crates/rocie-server/src/storage/sql/product.rs
@@ -1,7 +1,7 @@
use serde::{Deserialize, Serialize};
use utoipa::ToSchema;
-use crate::storage::sql::{barcode::Barcode, mk_id, unit_property::UnitPropertyId};
+use crate::storage::sql::{barcode::Barcode, mk_id, product_parent::ProductParentId, unit_property::UnitPropertyId};
/// The base of rocie.
///
@@ -12,6 +12,12 @@ pub(crate) struct Product {
/// The id of the product.
pub(crate) id: ProductId,
+ /// The parent this product has.
+ ///
+ /// This is effectively it's anchor in the product DAG.
+ #[schema(nullable = false)]
+ pub(crate) parent: Option<ProductParentId>,
+
/// The property this product is measured in.
///
/// (This is probably always either Mass, Volume or Quantity).
@@ -23,6 +29,7 @@ pub(crate) struct Product {
pub(crate) name: String,
/// An optional description of this product.
+ #[schema(nullable = false)]
pub(super) description: Option<String>,
/// Which barcodes are associated with this product.
diff --git a/crates/rocie-server/src/storage/sql/product_parent.rs b/crates/rocie-server/src/storage/sql/product_parent.rs
new file mode 100644
index 0000000..f689024
--- /dev/null
+++ b/crates/rocie-server/src/storage/sql/product_parent.rs
@@ -0,0 +1,31 @@
+use serde::{Deserialize, Serialize};
+use utoipa::ToSchema;
+
+use crate::storage::sql::mk_id;
+
+/// The grouping system for products.
+///
+/// Every Product can have a related parent, and every parent can have a parent themselves.
+/// As such, the products list constructs a DAG.
+#[derive(Clone, ToSchema, Serialize, Deserialize)]
+pub(crate) struct ProductParent {
+ /// The id of the product parent.
+ pub(crate) id: ProductParentId,
+
+ /// The optional id of the parent of this product parent.
+ ///
+ /// This must not form a cycle.
+ #[schema(nullable = false)]
+ pub(crate) parent: Option<ProductParentId>,
+
+ /// The name of the product parent.
+ ///
+ /// This should be globally unique, to make searching easier for the user.
+ pub(crate) name: String,
+
+ /// An optional description of this product parent.
+ #[schema(nullable = false)]
+ pub(super) description: Option<String>,
+}
+
+mk_id!(ProductParentId and ProductParentIdStub);
diff --git a/crates/rocie-server/src/storage/sql/recipe.rs b/crates/rocie-server/src/storage/sql/recipe.rs
new file mode 100644
index 0000000..835d98b
--- /dev/null
+++ b/crates/rocie-server/src/storage/sql/recipe.rs
@@ -0,0 +1,18 @@
+use std::path::PathBuf;
+
+use serde::{Deserialize, Serialize};
+use utoipa::ToSchema;
+
+use crate::storage::sql::mk_id;
+
+#[derive(ToSchema, Debug, Clone, Serialize, Deserialize)]
+pub(crate) struct Recipe {
+ pub(crate) id: RecipeId,
+
+ #[schema(value_type = String)]
+ pub(crate) path: PathBuf,
+
+ pub(crate) content: String,
+}
+
+mk_id!(RecipeId and RecipeIdStub);
diff --git a/crates/rocie-server/src/storage/sql/unit.rs b/crates/rocie-server/src/storage/sql/unit.rs
index d16e783..8bbfe60 100644
--- a/crates/rocie-server/src/storage/sql/unit.rs
+++ b/crates/rocie-server/src/storage/sql/unit.rs
@@ -29,6 +29,7 @@ pub(crate) struct Unit {
pub(crate) short_name: String,
/// Description of this unit.
+ #[schema(nullable = false)]
pub(crate) description: Option<String>,
/// Which property is described by this unit.
diff --git a/crates/rocie-server/src/storage/sql/unit_property.rs b/crates/rocie-server/src/storage/sql/unit_property.rs
index 9da2d2e..adb4767 100644
--- a/crates/rocie-server/src/storage/sql/unit_property.rs
+++ b/crates/rocie-server/src/storage/sql/unit_property.rs
@@ -17,6 +17,7 @@ pub(crate) struct UnitProperty {
pub(crate) units: Vec<UnitId>,
/// An description of this property.
+ #[schema(nullable = false)]
pub(crate) description: Option<String>,
}