add page CRUD
This commit is contained in:
parent
9d6586063d
commit
3d50d2b4b0
8 changed files with 239 additions and 26 deletions
4
migrations/20230706231224_add-slug-constraint.down.sql
Normal file
4
migrations/20230706231224_add-slug-constraint.down.sql
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
-- Remove constraint and index (page_site_slug_unique, page_site_slug_idx)
|
||||||
|
ALTER TABLE pages DROP CONSTRAINT pages_site_slug_unique;
|
||||||
|
|
||||||
|
DROP INDEX IF EXISTS pages_site_slug_unique;
|
6
migrations/20230706231224_add-slug-constraint.up.sql
Normal file
6
migrations/20230706231224_add-slug-constraint.up.sql
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
-- Add index/constraint on page site and slug
|
||||||
|
CREATE UNIQUE INDEX pages_site_slug_idx
|
||||||
|
ON pages (site, slug);
|
||||||
|
|
||||||
|
ALTER TABLE pages
|
||||||
|
ADD CONSTRAINT pages_site_slug_unique UNIQUE USING INDEX pages_site_slug_idx;
|
|
@ -38,11 +38,6 @@ pub struct Page {
|
||||||
pub deleted_at: Option<NaiveDateTime>,
|
pub deleted_at: Option<NaiveDateTime>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
pub trait PageRepository {
|
|
||||||
async fn get_page_from_url(&self, site: &str, slug: &str) -> Result<Page, Error>;
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize)]
|
#[derive(Deserialize, Serialize)]
|
||||||
#[serde(tag = "kind")]
|
#[serde(tag = "kind")]
|
||||||
pub enum PageBlock {
|
pub enum PageBlock {
|
||||||
|
@ -77,3 +72,55 @@ pub struct ImageElement {
|
||||||
/// Optional caption to put near the image
|
/// Optional caption to put near the image
|
||||||
pub caption: Option<String>,
|
pub caption: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Data to create a new page
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct CreatePageData {
|
||||||
|
pub slug: String,
|
||||||
|
pub title: String,
|
||||||
|
pub description: Option<String>,
|
||||||
|
pub tags: Vec<String>,
|
||||||
|
pub blocks: Vec<PageBlock>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Data to update a new page
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct UpdatePageData {
|
||||||
|
pub slug: Option<String>,
|
||||||
|
pub title: Option<String>,
|
||||||
|
pub description: Option<String>,
|
||||||
|
pub tags: Option<Vec<String>>,
|
||||||
|
pub blocks: Option<Vec<PageBlock>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
pub trait PageRepository {
|
||||||
|
/// Get a page from its slug
|
||||||
|
async fn get_page_from_url(&self, site: &str, slug: &str) -> Result<Page, Error>;
|
||||||
|
|
||||||
|
/// Create a new page
|
||||||
|
async fn create_page(
|
||||||
|
&self,
|
||||||
|
owner: &Uuid,
|
||||||
|
site: &Uuid,
|
||||||
|
data: CreatePageData,
|
||||||
|
) -> Result<(), Error>;
|
||||||
|
|
||||||
|
/// Update a page
|
||||||
|
async fn update_page(
|
||||||
|
&self,
|
||||||
|
owner: &Uuid,
|
||||||
|
site: &Uuid,
|
||||||
|
slug: &str,
|
||||||
|
data: UpdatePageData,
|
||||||
|
) -> Result<(), Error>;
|
||||||
|
|
||||||
|
/// Delete a page
|
||||||
|
async fn delete_page(
|
||||||
|
&self,
|
||||||
|
owner: &Uuid,
|
||||||
|
site: &Uuid,
|
||||||
|
name: &str,
|
||||||
|
soft_delete: bool,
|
||||||
|
) -> Result<(), Error>;
|
||||||
|
}
|
||||||
|
|
|
@ -41,14 +41,15 @@ pub struct APISite {
|
||||||
|
|
||||||
/// Data required to create a new site
|
/// Data required to create a new site
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
pub struct CreateSiteOptions {
|
pub struct CreateSiteData {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub title: String,
|
pub title: String,
|
||||||
|
pub description: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Data required to update a site's info
|
/// Data required to update a site's info
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
pub struct UpdateSiteOptions {
|
pub struct UpdateSiteData {
|
||||||
pub name: Option<String>,
|
pub name: Option<String>,
|
||||||
pub title: Option<String>,
|
pub title: Option<String>,
|
||||||
pub description: Option<String>,
|
pub description: Option<String>,
|
||||||
|
@ -60,16 +61,19 @@ pub trait SiteRepository {
|
||||||
async fn get_site_by_name(&self, name: &str) -> Result<APISite, Error>;
|
async fn get_site_by_name(&self, name: &str) -> Result<APISite, Error>;
|
||||||
|
|
||||||
/// Create a new instance of a site and store it
|
/// Create a new instance of a site and store it
|
||||||
async fn create_site(&self, owner: &Uuid, options: &CreateSiteOptions) -> Result<(), Error>;
|
async fn create_site(&self, owner: &Uuid, options: CreateSiteData) -> Result<(), Error>;
|
||||||
|
|
||||||
/// Update an existing site's info
|
/// Update an existing site's info
|
||||||
async fn update_site(
|
async fn update_site(
|
||||||
&self,
|
&self,
|
||||||
owner: &Uuid,
|
owner: &Uuid,
|
||||||
name: &str,
|
name: &str,
|
||||||
options: &UpdateSiteOptions,
|
options: UpdateSiteData,
|
||||||
) -> Result<(), Error>;
|
) -> Result<(), Error>;
|
||||||
|
|
||||||
/// Delete a site
|
/// Delete a site
|
||||||
async fn delete_site(&self, owner: &Uuid, name: &str, soft_delete: bool) -> Result<(), Error>;
|
async fn delete_site(&self, owner: &Uuid, name: &str, soft_delete: bool) -> Result<(), Error>;
|
||||||
|
|
||||||
|
/// Resolve a site's name to its ID
|
||||||
|
async fn get_site_id(&self, name: &str, owner: &Uuid) -> Result<Uuid, Error>;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,11 @@
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
|
use serde_json::json;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::content::{
|
use crate::content::{
|
||||||
self,
|
self,
|
||||||
page::{Page, PageRepository},
|
page::{CreatePageData, Page, PageRepository, UpdatePageData},
|
||||||
|
Error,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::Database;
|
use super::Database;
|
||||||
|
@ -18,9 +21,95 @@ impl PageRepository for Database {
|
||||||
.fetch_one(&self.pool)
|
.fetch_one(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| match e {
|
.map_err(|e| match e {
|
||||||
sqlx::Error::RowNotFound => content::Error::NotFound,
|
sqlx::Error::RowNotFound => Error::NotFound,
|
||||||
_ => e.into(),
|
_ => e.into(),
|
||||||
})?;
|
})?;
|
||||||
Ok(page)
|
Ok(page)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn create_page(
|
||||||
|
&self,
|
||||||
|
owner: &Uuid,
|
||||||
|
site: &Uuid,
|
||||||
|
data: CreatePageData,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
sqlx::query!(
|
||||||
|
"INSERT INTO pages (author, site, slug, title, description, tags, blocks) VALUES ($1, $2, $3, $4, $5, $6, $7)",
|
||||||
|
owner,
|
||||||
|
site,
|
||||||
|
data.slug,
|
||||||
|
data.title,
|
||||||
|
data.description.unwrap_or_else(|| "".to_string()),
|
||||||
|
&data.tags,
|
||||||
|
json!(data.blocks),
|
||||||
|
).execute(&self.pool).await.map_err(|err| match err {
|
||||||
|
sqlx::Error::Database(dberr) if dberr.constraint() == Some("pages_site_slug_unique") => {
|
||||||
|
Error::IdentifierNotAvailable
|
||||||
|
}
|
||||||
|
_ => err.into(),
|
||||||
|
})?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn update_page(
|
||||||
|
&self,
|
||||||
|
owner: &Uuid,
|
||||||
|
site: &Uuid,
|
||||||
|
page: &str,
|
||||||
|
data: UpdatePageData,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
let result = sqlx::query!(
|
||||||
|
"UPDATE pages SET title = COALESCE($1, title), description = COALESCE($2, description), tags = COALESCE($3, tags), blocks = COALESCE($4, blocks) WHERE author = $5 AND site = $6 AND slug = $7 AND deleted_at IS NULL",
|
||||||
|
data.title,
|
||||||
|
data.description,
|
||||||
|
data.tags.as_deref(),
|
||||||
|
data.blocks.map(|x| json!(x)),
|
||||||
|
owner,
|
||||||
|
site,
|
||||||
|
page
|
||||||
|
).execute(&self.pool).await.map_err(|err| match err {
|
||||||
|
sqlx::Error::Database(dberr) if dberr.constraint() == Some("pages_site_slug_unique") => {
|
||||||
|
Error::IdentifierNotAvailable
|
||||||
|
}
|
||||||
|
_ => err.into(),
|
||||||
|
})?;
|
||||||
|
|
||||||
|
if result.rows_affected() == 0 {
|
||||||
|
return Err(Error::NotFound);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn delete_page(
|
||||||
|
&self,
|
||||||
|
owner: &Uuid,
|
||||||
|
site: &Uuid,
|
||||||
|
page: &str,
|
||||||
|
soft_delete: bool,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
let result = if soft_delete {
|
||||||
|
sqlx::query!(
|
||||||
|
"UPDATE pages SET deleted_at = NOW() WHERE author = $1 AND site = $2 AND slug = $3 AND deleted_at IS NULL",
|
||||||
|
owner,
|
||||||
|
site,
|
||||||
|
page
|
||||||
|
).execute(&self.pool).await?
|
||||||
|
} else {
|
||||||
|
sqlx::query!(
|
||||||
|
"DELETE FROM pages WHERE author = $1 AND site = $2 AND slug = $3",
|
||||||
|
owner,
|
||||||
|
site,
|
||||||
|
page
|
||||||
|
)
|
||||||
|
.execute(&self.pool)
|
||||||
|
.await?
|
||||||
|
};
|
||||||
|
|
||||||
|
if result.rows_affected() == 0 {
|
||||||
|
return Err(Error::NotFound);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,7 @@ use async_trait::async_trait;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::content::{
|
use crate::content::{
|
||||||
site::{APISite, CreateSiteOptions, SiteRepository, UpdateSiteOptions},
|
site::{APISite, CreateSiteData, SiteRepository, UpdateSiteData},
|
||||||
Error,
|
Error,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -24,12 +24,13 @@ impl SiteRepository for Database {
|
||||||
Ok(site)
|
Ok(site)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn create_site(&self, owner: &Uuid, options: &CreateSiteOptions) -> Result<(), Error> {
|
async fn create_site(&self, owner: &Uuid, options: CreateSiteData) -> Result<(), Error> {
|
||||||
sqlx::query!(
|
sqlx::query!(
|
||||||
"INSERT INTO sites (name, owner, title) VALUES ($1, $2, $3)",
|
"INSERT INTO sites (name, owner, title, description) VALUES ($1, $2, $3, $4)",
|
||||||
options.name,
|
options.name,
|
||||||
owner,
|
owner,
|
||||||
options.title,
|
options.title,
|
||||||
|
options.description,
|
||||||
)
|
)
|
||||||
.execute(&self.pool)
|
.execute(&self.pool)
|
||||||
.await
|
.await
|
||||||
|
@ -46,7 +47,7 @@ impl SiteRepository for Database {
|
||||||
&self,
|
&self,
|
||||||
owner: &Uuid,
|
owner: &Uuid,
|
||||||
name: &str,
|
name: &str,
|
||||||
options: &UpdateSiteOptions,
|
options: UpdateSiteData,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
let result = sqlx::query!(
|
let result = sqlx::query!(
|
||||||
"UPDATE sites SET name = COALESCE($1, name), title = COALESCE($2, title), description = COALESCE($3, description), modified_at = NOW() WHERE name = $4 AND owner = $5 AND deleted_at IS NULL",
|
"UPDATE sites SET name = COALESCE($1, name), title = COALESCE($2, title), description = COALESCE($3, description), modified_at = NOW() WHERE name = $4 AND owner = $5 AND deleted_at IS NULL",
|
||||||
|
@ -95,4 +96,19 @@ impl SiteRepository for Database {
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn get_site_id(&self, name: &str, owner: &Uuid) -> Result<Uuid, Error> {
|
||||||
|
let record = sqlx::query!(
|
||||||
|
"SELECT id FROM sites WHERE name = $1 AND owner = $2 AND deleted_at IS NULL",
|
||||||
|
name,
|
||||||
|
owner
|
||||||
|
)
|
||||||
|
.fetch_one(&self.pool)
|
||||||
|
.await
|
||||||
|
.map_err(|e| match e {
|
||||||
|
sqlx::Error::RowNotFound => Error::NotFound,
|
||||||
|
_ => e.into(),
|
||||||
|
})?;
|
||||||
|
Ok(record.id)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,10 +4,17 @@ use axum::{
|
||||||
routing::{get, post},
|
routing::{get, post},
|
||||||
Json, Router,
|
Json, Router,
|
||||||
};
|
};
|
||||||
|
use serde_json::json;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
content::page::PageRepository, database::Database, http::error::ApiError, state::AppState,
|
content::{
|
||||||
|
page::{CreatePageData, PageRepository, UpdatePageData},
|
||||||
|
site::SiteRepository,
|
||||||
|
},
|
||||||
|
database::Database,
|
||||||
|
http::{error::ApiError, json::JsonBody, session::RequireUser},
|
||||||
|
state::AppState,
|
||||||
};
|
};
|
||||||
|
|
||||||
async fn get_page<Repo: PageRepository>(
|
async fn get_page<Repo: PageRepository>(
|
||||||
|
@ -17,15 +24,55 @@ async fn get_page<Repo: PageRepository>(
|
||||||
Ok(Json(repository.get_page_from_url(&site, &slug).await?))
|
Ok(Json(repository.get_page_from_url(&site, &slug).await?))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn create_page<Repo: PageRepository>(//repository: Repo,
|
async fn create_page<Repo: PageRepository + SiteRepository>(
|
||||||
//Path(site): Path<String>,
|
repository: Repo,
|
||||||
//RequireUser(user): RequireUser,
|
Path(site): Path<String>,
|
||||||
|
RequireUser(user): RequireUser,
|
||||||
|
JsonBody(page): JsonBody<CreatePageData>,
|
||||||
) -> Result<impl IntoResponse, ApiError<'static>> {
|
) -> Result<impl IntoResponse, ApiError<'static>> {
|
||||||
Ok(Json("todo"))
|
// Resolve site
|
||||||
|
let site_id = repository.get_site_id(&site, &user.id).await?;
|
||||||
|
|
||||||
|
repository.create_page(&user.id, &site_id, page).await?;
|
||||||
|
Ok(Json(json!({ "ok": true })))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn update_page<Repo: PageRepository + SiteRepository>(
|
||||||
|
repository: Repo,
|
||||||
|
Path((site, slug)): Path<(String, String)>,
|
||||||
|
RequireUser(user): RequireUser,
|
||||||
|
JsonBody(page): JsonBody<UpdatePageData>,
|
||||||
|
) -> Result<impl IntoResponse, ApiError<'static>> {
|
||||||
|
// Resolve site
|
||||||
|
let site_id = repository.get_site_id(&site, &user.id).await?;
|
||||||
|
|
||||||
|
repository
|
||||||
|
.update_page(&user.id, &site_id, &slug, page)
|
||||||
|
.await?;
|
||||||
|
Ok(Json(json!({ "ok": true })))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn delete_page<Repo: PageRepository + SiteRepository>(
|
||||||
|
repository: Repo,
|
||||||
|
Path((site, slug)): Path<(String, String)>,
|
||||||
|
RequireUser(user): RequireUser,
|
||||||
|
) -> Result<impl IntoResponse, ApiError<'static>> {
|
||||||
|
// Resolve site
|
||||||
|
let site_id = repository.get_site_id(&site, &user.id).await?;
|
||||||
|
|
||||||
|
repository
|
||||||
|
.delete_page(&user.id, &site_id, &slug, true)
|
||||||
|
.await?;
|
||||||
|
Ok(Json(json!({ "ok": true })))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn router() -> Router<Arc<AppState>> {
|
pub fn router() -> Router<Arc<AppState>> {
|
||||||
Router::new()
|
Router::new()
|
||||||
.route("/:site", post(create_page::<Database>))
|
.route("/:site", post(create_page::<Database>))
|
||||||
.route("/:site/:slug", get(get_page::<Database>))
|
.route(
|
||||||
|
"/:site/:slug",
|
||||||
|
get(get_page::<Database>)
|
||||||
|
.put(update_page::<Database>)
|
||||||
|
.delete(delete_page::<Database>),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,7 +8,7 @@ use serde_json::json;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
content::site::{CreateSiteOptions, SiteRepository, UpdateSiteOptions},
|
content::site::{CreateSiteData, SiteRepository, UpdateSiteData},
|
||||||
database::Database,
|
database::Database,
|
||||||
http::{error::ApiError, json::JsonBody, session::RequireUser},
|
http::{error::ApiError, json::JsonBody, session::RequireUser},
|
||||||
state::AppState,
|
state::AppState,
|
||||||
|
@ -24,9 +24,9 @@ async fn get_site<Repo: SiteRepository>(
|
||||||
async fn create_site<Repo: SiteRepository>(
|
async fn create_site<Repo: SiteRepository>(
|
||||||
repository: Repo,
|
repository: Repo,
|
||||||
RequireUser(user): RequireUser,
|
RequireUser(user): RequireUser,
|
||||||
JsonBody(options): JsonBody<CreateSiteOptions>,
|
JsonBody(options): JsonBody<CreateSiteData>,
|
||||||
) -> Result<impl IntoResponse, ApiError<'static>> {
|
) -> Result<impl IntoResponse, ApiError<'static>> {
|
||||||
repository.create_site(&user.id, &options).await?;
|
repository.create_site(&user.id, options).await?;
|
||||||
Ok(Json(json!({"ok": true})))
|
Ok(Json(json!({"ok": true})))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -34,9 +34,9 @@ async fn update_site<Repo: SiteRepository>(
|
||||||
repository: Repo,
|
repository: Repo,
|
||||||
Path(name): Path<String>,
|
Path(name): Path<String>,
|
||||||
RequireUser(user): RequireUser,
|
RequireUser(user): RequireUser,
|
||||||
JsonBody(options): JsonBody<UpdateSiteOptions>,
|
JsonBody(options): JsonBody<UpdateSiteData>,
|
||||||
) -> Result<impl IntoResponse, ApiError<'static>> {
|
) -> Result<impl IntoResponse, ApiError<'static>> {
|
||||||
repository.update_site(&user.id, &name, &options).await?;
|
repository.update_site(&user.id, &name, options).await?;
|
||||||
Ok(Json(json!({"ok": true})))
|
Ok(Json(json!({"ok": true})))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue