derived_cms/lib.rs
1//! Generate a CMS, complete with admin interface and headless API from Rust type definitions.
2//! Works in cunjunction with [serde] and [ormlite] and uses [axum] as a web server.
3//!
4//! Example
5//!
6//! ```rust,no_run
7//! # use axum::extract::State;
8//! use chrono::{DateTime, Utc};
9//! use derived_cms::{App, Entity, EntityBase, Input, app::AppError, context::{Context, ContextTrait}, entity, property::{Markdown, Text, Json}};
10//! use ormlite::{Model, sqlite::Sqlite};
11//! use serde::{Deserialize, Serialize, Serializer};
12//! # use serde_with::{serde_as, DisplayFromStr};
13//! # use thiserror::Error;
14//! use ts_rs::TS;
15//! use uuid::Uuid;
16//!
17//! #[derive(Debug, Deserialize, Serialize, Entity, Model, TS)]
18//! #[ts(export)]
19//! struct Post {
20//! #[cms(id, skip_input)]
21//! #[ormlite(primary_key)]
22//! #[serde(default = "Uuid::new_v4")]
23//! id: Uuid,
24//! title: Text,
25//! date: DateTime<Utc>,
26//! #[cms(skip_column)]
27//! #[serde(default)]
28//! content: Json<Vec<Block>>,
29//! #[serde(default)]
30//! draft: bool,
31//! }
32//!
33//! type Ctx = Context<ormlite::Pool<sqlx::Sqlite>>;
34//!
35//! # #[serde_as]
36//! # #[derive(Debug, Error, Serialize)]
37//! # enum MyError {
38//! # #[error(transparent)]
39//! # Ormlite(
40//! # #[from]
41//! # #[serde_as(as = "DisplayFromStr")]
42//! # ormlite::Error
43//! # ),
44//! # #[error(transparent)]
45//! # Sqlx(
46//! # #[from]
47//! # #[serde_as(as = "DisplayFromStr")]
48//! # sqlx::Error
49//! # ),
50//! # }
51//! #
52//! # impl From<MyError> for AppError {
53//! # fn from(value: MyError) -> Self {
54//! # match value {
55//! # MyError::Ormlite(e) => Self::new("Database error".to_string(), format!("{e:#}")),
56//! # MyError::Sqlx(e) => Self::new("Database error".to_string(), format!("{e:#}")),
57//! # }
58//! # }
59//! # }
60//! #
61//! impl entity::Get<Ctx> for Post {
62//! type RequestExt = State<Ctx>;
63//! type Error = MyError;
64//!
65//! async fn get(
66//! id: &<Self as EntityBase<Ctx>>::Id,
67//! ext: Self::RequestExt,
68//! ) -> Result<Option<Self>, Self::Error> {
69//! match Self::fetch_one(id, ext.ext()).await {
70//! Ok(v) => Ok(Some(v)),
71//! Err(ormlite::Error::SqlxError(sqlx::Error::RowNotFound)) => Ok(None),
72//! Err(e) => Err(e)?,
73//! }
74//! }
75//! }
76//!
77//! impl entity::List<Ctx> for Post {
78//! type RequestExt = State<Ctx>;
79//! type Error = MyError;
80//!
81//! async fn list(ext: Self::RequestExt) -> Result<impl IntoIterator<Item = Self>, Self::Error> {
82//! Ok(Self::select().fetch_all(ext.ext()).await?)
83//! }
84//! }
85//!
86//! impl entity::Create<Ctx> for Post {
87//! type RequestExt = State<Ctx>;
88//! type Error = MyError;
89//!
90//! async fn create(
91//! data: <Self as EntityBase<Ctx>>::Create,
92//! ext: Self::RequestExt,
93//! ) -> Result<Self, Self::Error> {
94//! Ok(Self::insert(data, ext.ext()).await?)
95//! }
96//! }
97//!
98//! impl entity::Update<Ctx> for Post {
99//! type RequestExt = State<Ctx>;
100//! type Error = MyError;
101//!
102//! async fn update(
103//! id: &<Self as EntityBase<Ctx>>::Id,
104//! mut data: <Self as EntityBase<Ctx>>::Update,
105//! ext: Self::RequestExt,
106//! ) -> Result<Self, Self::Error> {
107//! data.id = *id;
108//! Ok(data.update_all_fields(ext.ext()).await?)
109//! }
110//! }
111//!
112//! impl entity::Delete<Ctx> for Post {
113//! type RequestExt = State<Ctx>;
114//! type Error = MyError;
115//!
116//! async fn delete(
117//! id: &<Self as EntityBase<Ctx>>::Id,
118//! ext: Self::RequestExt,
119//! ) -> Result<(), Self::Error> {
120//! sqlx::query("DELETE FROM post WHERE id = ?")
121//! .bind(id)
122//! .execute(ext.ext())
123//! .await?;
124//! Ok(())
125//! }
126//! }
127//!
128//! #[derive(Debug, Deserialize, Serialize, Input, TS)]
129//! #[ts(export)]
130//! #[serde(rename_all = "snake_case", tag = "type", content = "data")]
131//! pub enum Block {
132//! Separator,
133//! Text(Markdown),
134//! }
135//!
136//! #[tokio::main]
137//! async fn main() {
138//! let db = sqlx::Pool::<Sqlite>::connect("sqlite://.tmp/db.sqlite")
139//! .await
140//! .unwrap();
141//! let app = App::new().entity::<Post>().with_state(db).build("uploads");
142//! let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
143//! axum::serve(listener, app).await.unwrap();
144//! }
145//! ```
146//!
147//! ## REST API
148//!
149//! A REST API is automatically generated for all `Entities`.
150//!
151//! List of generated endpoints, with [`name`](EntityBase::name) and [`name-plural`](EntityBase::name_plural)
152//! converted to [kebab-case](convert_case::Case::Kebab):
153//!
154//! - `GET /api/v1/:name-plural`:
155//! - allows filtering by exact value in the query string, e. g. `?slug=asdf`. This currently
156//! only works for fields whose SQL representation is a string.
157//! - returns an array of [entities](Entity), serialized using [serde_json].
158//! - `GET /api/v1/:name/:id`
159//! - get an [Entity] by it's [id](ormlite::TableMeta::primary_key).
160//! - returns the requested of [Entity], serialized using [serde_json].
161//! - `POST /api/v1/:name-plural`
162//! - create a new [Entity] from the request body JSON.
163//! - returns the newly created [Entity] as JSON.
164//! - `POST /api/v1/:name/:id`
165//! - replaces the [Entity] with the specified [id](ormlite::TableMeta::primary_key) with the
166//! request body JSON.
167//! - returns the updated [Entity] as JSON.
168//! - `DELETE /api/v1/:name/:id`
169//! - deletes the [Entity] with the specified [id](ormlite::TableMeta::primary_key)
170//! - returns the deleted Entity as JSON.
171
172pub use app::App;
173pub use column::Column;
174pub use entity::{Entity, EntityBase};
175pub use input::Input;
176
177pub mod app;
178pub mod column;
179pub mod context;
180pub mod easymde;
181mod endpoints;
182pub mod entity;
183pub mod input;
184pub mod property;
185pub mod render;
186
187#[doc(hidden)]
188pub mod derive {
189 pub use generic_array;
190 pub use i18n_embed;
191 pub use maud;
192 pub use ormlite;
193}
194
195#[cfg(feature = "sqlite")]
196pub type DB = sqlx::Sqlite;
197#[cfg(feature = "postgres")]
198pub type DB = sqlx::Postgres;