about summary refs log tree commit diff stats
path: root/crates/rocie-server/src/storage/sql
diff options
context:
space:
mode:
Diffstat (limited to 'crates/rocie-server/src/storage/sql')
-rw-r--r--crates/rocie-server/src/storage/sql/get/mod.rs2
-rw-r--r--crates/rocie-server/src/storage/sql/get/product/mod.rs85
-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
14 files changed, 511 insertions, 52 deletions
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?;
-
-            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(),
-            });
+            let product = product_from_record!(app, record);
+
+            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>,
 }