1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
//! Generate a CMS, complete with admin interface and headless API from Rust type definitions.
//! Works in cunjunction with [serde] and [ormlite] and uses [axum] as a web server.
//!
//! Example
//!
//! ```rust,no_run
//! # use axum::extract::State;
//! 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 serde_with::{serde_as, DisplayFromStr};
//! # use thiserror::Error;
//! 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>>;
//!
//! # #[serde_as]
//! # #[derive(Debug, Error, Serialize)]
//! # enum MyError {
//! # #[error(transparent)]
//! # Ormlite(
//! # #[from]
//! # #[serde_as(as = "DisplayFromStr")]
//! # ormlite::Error
//! # ),
//! # #[error(transparent)]
//! # Sqlx(
//! # #[from]
//! # #[serde_as(as = "DisplayFromStr")]
//! # sqlx::Error
//! # ),
//! # }
//! #
//! # impl From<MyError> for AppError {
//! # fn from(value: MyError) -> Self {
//! # match value {
//! # MyError::Ormlite(e) => Self::new("Database error".to_string(), format!("{e:#}")),
//! # MyError::Sqlx(e) => Self::new("Database error".to_string(), format!("{e:#}")),
//! # }
//! # }
//! # }
//! #
//! 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`](EntityBase::name) and [`name-plural`](EntityBase::name_plural)
//! converted to [kebab-case](convert_case::Case::Kebab):
//!
//! - `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](Entity), serialized using [serde_json].
//! - `GET /api/v1/:name/:id`
//! - get an [Entity] by it's [id](ormlite::TableMeta::primary_key).
//! - 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](ormlite::TableMeta::primary_key) with the
//! request body JSON.
//! - returns the updated [Entity] as JSON.
//! - `DELETE /api/v1/:name/:id`
//! - deletes the [Entity] with the specified [id](ormlite::TableMeta::primary_key)
//! - returns the deleted Entity as JSON.
pub use App;
pub use Column;
pub use ;
pub use Input;
pub type DB = Sqlite;
pub type DB = Postgres;