#cms #web-server #web #framework #database-interface #properties #markdown-text

derived-cms

Generate a CMS, complete with admin interface, headless API and database interface from Rust type definitions. Works in cunjunction with serde and ormlite and uses axum as a web server.

3 unstable releases

new 0.3.1 Oct 25, 2024
0.3.0 Oct 25, 2024
0.2.0 Oct 19, 2024

#171 in HTTP server

Download history 107/week @ 2024-10-14 226/week @ 2024-10-21

333 downloads per month

MPL-2.0 license

81KB
2K SLoC

derived-cms

Generate a CMS, complete with admin interface and headless API interface from Rust type definitions. Works in cunjunction with serde and ormlite and uses axum as a web server.

Example

use chrono::{DateTime, Utc};
use derived_cms::{App, Entity, EntityBase, Input, app::AppError, context::{Context, ContextTrait}, entity, property::{Markdown, Text, Json}};
use ormlite::{Model, sqlite::Sqlite};
use serde::{Deserialize, Serialize, Serializer};
use ts_rs::TS;
use uuid::Uuid;

#[derive(Debug, Deserialize, Serialize, Entity, Model, TS)]
#[ts(export)]
struct Post {
    #[cms(id, skip_input)]
    #[ormlite(primary_key)]
    #[serde(default = "Uuid::new_v4")]
    id: Uuid,
    title: Text,
    date: DateTime<Utc>,
    #[cms(skip_column)]
    #[serde(default)]
    content: Json<Vec<Block>>,
    #[serde(default)]
    draft: bool,
}

type Ctx = Context<ormlite::Pool<sqlx::Sqlite>>;

impl entity::Get<Ctx> for Post {
    type RequestExt = State<Ctx>;
    type Error = MyError;

    async fn get(
        id: &<Self as EntityBase<Ctx>>::Id,
        ext: Self::RequestExt,
    ) -> Result<Option<Self>, Self::Error> {
        match Self::fetch_one(id, ext.ext()).await {
            Ok(v) => Ok(Some(v)),
            Err(ormlite::Error::SqlxError(sqlx::Error::RowNotFound)) => Ok(None),
            Err(e) => Err(e)?,
        }
    }
}

impl entity::List<Ctx> for Post {
    type RequestExt = State<Ctx>;
    type Error = MyError;

    async fn list(ext: Self::RequestExt) -> Result<impl IntoIterator<Item = Self>, Self::Error> {
        Ok(Self::select().fetch_all(ext.ext()).await?)
    }
}

impl entity::Create<Ctx> for Post {
    type RequestExt = State<Ctx>;
    type Error = MyError;

    async fn create(
        data: <Self as EntityBase<Ctx>>::Create,
        ext: Self::RequestExt,
    ) -> Result<Self, Self::Error> {
        Ok(Self::insert(data, ext.ext()).await?)
    }
}

impl entity::Update<Ctx> for Post {
    type RequestExt = State<Ctx>;
    type Error = MyError;

    async fn update(
        id: &<Self as EntityBase<Ctx>>::Id,
        mut data: <Self as EntityBase<Ctx>>::Update,
        ext: Self::RequestExt,
    ) -> Result<Self, Self::Error> {
        data.id = *id;
        Ok(data.update_all_fields(ext.ext()).await?)
    }
}

impl entity::Delete<Ctx> for Post {
    type RequestExt = State<Ctx>;
    type Error = MyError;

    async fn delete(
        id: &<Self as EntityBase<Ctx>>::Id,
        ext: Self::RequestExt,
    ) -> Result<(), Self::Error> {
        sqlx::query("DELETE FROM post WHERE id = ?")
            .bind(id)
            .execute(ext.ext())
            .await?;
        Ok(())
    }
}

#[derive(Debug, Deserialize, Serialize, Input, TS)]
#[ts(export)]
#[serde(rename_all = "snake_case", tag = "type", content = "data")]
pub enum Block {
    Separator,
    Text(Markdown),
}

#[tokio::main]
async fn main() {
    let db = sqlx::Pool::<Sqlite>::connect("sqlite://.tmp/db.sqlite")
        .await
        .unwrap();
    let app = App::new().entity::<Post>().with_state(db).build("uploads");
    let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
    axum::serve(listener, app).await.unwrap();
}

REST API

A REST API is automatically generated for all Entities.

List of generated endpoints, with name and name-plural converted to kebab-case:

  • GET /api/v1/:name-plural:
    • allows filtering by exact value in the query string, e. g. ?slug=asdf. This currently only works for fields whose SQL representation is a string.
    • returns an array of entities, serialized using serde_json.
  • GET /api/v1/:name/:id
    • get an Entity by it's id.
    • returns the requested of Entity, serialized using serde_json.
  • POST /api/v1/:name-plural
    • create a new Entity from the request body JSON.
    • returns the newly created Entity as JSON.
  • POST /api/v1/:name/:id
    • replaces the Entity with the specified id with the request body JSON.
    • returns the updated Entity as JSON.
  • DELETE /api/v1/:name/:id
    • deletes the Entity with the specified id
    • returns the deleted Entity as JSON.

Dependencies

~67MB
~1M SLoC