Compare commits

...

2 Commits

Author SHA1 Message Date
Hamcha 6058346808
add default collection field
continuous-integration/drone/push Build is failing Details
2023-07-14 12:24:08 +02:00
Hamcha 3c015177a0
add collection fetching 2023-07-14 12:21:42 +02:00
15 changed files with 516 additions and 275 deletions

View File

@ -0,0 +1,2 @@
ALTER TABLE sites
DROP COLUMN default_collection;

View File

@ -0,0 +1,4 @@
ALTER TABLE sites
ADD COLUMN default_collection UUID REFERENCES collections (
id
) ON DELETE SET NULL;

View File

@ -1,5 +1,88 @@
{
"db": "PostgreSQL",
"0e52c31f550313507192bd0436e469dc2b01e67bd19ecdd26e663fbb5af8929b": {
"describe": {
"columns": [
{
"name": "author_display_name",
"ordinal": 0,
"type_info": "Varchar"
},
{
"name": "author_username",
"ordinal": 1,
"type_info": "Varchar"
},
{
"name": "slug",
"ordinal": 2,
"type_info": "Varchar"
},
{
"name": "title",
"ordinal": 3,
"type_info": "Varchar"
},
{
"name": "description",
"ordinal": 4,
"type_info": "Text"
},
{
"name": "tags",
"ordinal": 5,
"type_info": "VarcharArray"
},
{
"name": "blocks!: Json<Vec<PostBlock>>",
"ordinal": 6,
"type_info": "Jsonb"
},
{
"name": "created_at",
"ordinal": 7,
"type_info": "Timestamp"
},
{
"name": "modified_at",
"ordinal": 8,
"type_info": "Timestamp"
},
{
"name": "published",
"ordinal": 9,
"type_info": "Bool"
},
{
"name": "collections",
"ordinal": 10,
"type_info": "UuidArray"
}
],
"nullable": [
null,
false,
false,
false,
true,
false,
false,
false,
true,
false,
false
],
"parameters": {
"Left": [
"Uuid",
"Uuid",
"Timestamp",
"Int8"
]
}
},
"query": "SELECT\n COALESCE(users.display_name,users.name) as author_display_name,\n users.name as author_username,\n pages.slug,\n pages.title,\n pages.description,\n pages.tags,\n pages.blocks as \"blocks!: Json<Vec<PostBlock>>\",\n pages.created_at,\n pages.modified_at,\n pages.published,\n pages.collections\n FROM pages\n JOIN\n users ON author = users.id\n WHERE\n pages.site = $1\n AND $2 = ANY(collections)\n AND pages.deleted_at IS NULL\n AND published = true\n AND pages.created_at <= $3\n ORDER BY created_at DESC\n LIMIT $4\n "
},
"11e96cfd8c2736f13ce55975ea910dd68640f6f14e38a4b3342d514804e3de27": {
"describe": {
"columns": [],
@ -12,228 +95,43 @@
},
"query": "DELETE FROM sessions WHERE id = $1"
},
"23f1a1f2aa96a5a23880f330d04f5dc695a148f4aa50b8c76378b68944569e44": {
"1d21e65cb21ec57986f154395627de9aefbf77f88c892c5e093787ec53d623d3": {
"describe": {
"columns": [
{
"name": "author_display_name",
"name": "id",
"ordinal": 0,
"type_info": "Varchar"
},
{
"name": "author_username",
"ordinal": 1,
"type_info": "Varchar"
"type_info": "Uuid"
},
{
"name": "slug",
"ordinal": 2,
"ordinal": 1,
"type_info": "Varchar"
},
{
"name": "title",
"ordinal": 3,
"type_info": "Varchar"
},
{
"name": "description",
"ordinal": 4,
"type_info": "Text"
},
{
"name": "tags",
"ordinal": 5,
"type_info": "VarcharArray"
},
{
"name": "blocks!: Json<Vec<PostBlock>>",
"ordinal": 6,
"type_info": "Jsonb"
},
{
"name": "created_at",
"ordinal": 7,
"type_info": "Timestamp"
},
{
"name": "modified_at",
"ordinal": 8,
"type_info": "Timestamp"
},
{
"name": "published",
"ordinal": 9,
"type_info": "Bool"
}
],
"nullable": [
null,
false,
false,
false,
true,
false,
false,
false,
true,
false
],
"parameters": {
"Left": [
"Text",
"Text"
]
}
},
"query": "SELECT\n COALESCE(users.display_name,users.name) as author_display_name,\n users.name as author_username,\n pages.slug,\n pages.title,\n pages.description,\n pages.tags,\n pages.blocks as \"blocks!: Json<Vec<PostBlock>>\",\n pages.created_at,\n pages.modified_at,\n pages.published\n FROM pages \n JOIN\n sites ON site = sites.id\n JOIN\n users ON author = users.id\n WHERE\n slug = $1\n AND sites.name = $2 \n AND pages.deleted_at IS NULL\n AND sites.deleted_at IS NULL"
},
"28ec2833283df836e457161f44f0fbc75ed37dcc0ba63eb09bf285aae2f7a380": {
"describe": {
"columns": [],
"nullable": [],
"parameters": {
"Left": [
"Uuid",
"Uuid",
"Varchar",
"Varchar"
]
}
},
"query": "INSERT INTO collections (id, site, slug, name)\n VALUES ($1, $2, $3, $4)"
},
"29e0259def5c6fdc34c6a18345150d2714736b91c1f3040fcb07d77cbe2552e1": {
"describe": {
"columns": [
{
"name": "name",
"ordinal": 0,
"type_info": "Varchar"
},
{
"name": "owner",
"ordinal": 1,
"type_info": "Varchar"
},
{
"name": "owner_display_name",
"ordinal": 2,
"type_info": "Varchar"
},
{
"name": "title",
"name": "parent",
"ordinal": 3,
"type_info": "Varchar"
},
{
"name": "description",
"ordinal": 4,
"type_info": "Text"
},
{
"name": "created_at",
"ordinal": 5,
"type_info": "Timestamp"
},
{
"name": "collections!: Vec<CollectionNameAndSlug>",
"ordinal": 6,
"type_info": "RecordArray"
"type_info": "Uuid"
}
],
"nullable": [
false,
false,
null,
false,
true,
false,
null
true
],
"parameters": {
"Left": [
"Text"
"Uuid"
]
}
},
"query": "SELECT\n sites.name,\n users.name as owner,\n COALESCE(users.display_name,users.name) as owner_display_name,\n title,\n description,\n sites.created_at,\n array_agg(row(collections.slug, collections.name)) as \"collections!: Vec<CollectionNameAndSlug>\"\n FROM sites\n JOIN\n users ON sites.owner = users.id\n JOIN\n collections ON collections.site = sites.id\n WHERE\n sites.name = $1\n AND sites.deleted_at IS NULL\n GROUP BY\n sites.name, users.name, users.display_name, title, description, sites.created_at"
},
"320625aa3fac73cb43d6a68592767174558ab1a6923d3574ad629d1a1bd0d4a6": {
"describe": {
"columns": [
{
"name": "author_display_name",
"ordinal": 0,
"type_info": "Varchar"
},
{
"name": "author_username",
"ordinal": 1,
"type_info": "Varchar"
},
{
"name": "slug",
"ordinal": 2,
"type_info": "Varchar"
},
{
"name": "title",
"ordinal": 3,
"type_info": "Varchar"
},
{
"name": "description",
"ordinal": 4,
"type_info": "Text"
},
{
"name": "tags",
"ordinal": 5,
"type_info": "VarcharArray"
},
{
"name": "blocks!: Json<Vec<PostBlock>>",
"ordinal": 6,
"type_info": "Jsonb"
},
{
"name": "created_at",
"ordinal": 7,
"type_info": "Timestamp"
},
{
"name": "modified_at",
"ordinal": 8,
"type_info": "Timestamp"
},
{
"name": "published",
"ordinal": 9,
"type_info": "Bool"
}
],
"nullable": [
null,
false,
false,
false,
true,
false,
false,
false,
true,
false
],
"parameters": {
"Left": [
"Uuid",
"Uuid",
"Timestamp",
"Int8"
]
}
},
"query": "SELECT\n COALESCE(users.display_name,users.name) as author_display_name,\n users.name as author_username,\n pages.slug,\n pages.title,\n pages.description,\n pages.tags,\n pages.blocks as \"blocks!: Json<Vec<PostBlock>>\",\n pages.created_at,\n pages.modified_at,\n pages.published\n FROM pages\n JOIN\n users ON author = users.id\n WHERE\n site = $1\n AND $2 = ANY(collections)\n AND pages.deleted_at IS NULL\n AND published = true\n AND pages.created_at <= $3\n ORDER BY created_at DESC\n LIMIT $4\n "
"query": "SELECT id, slug, name, parent FROM collections WHERE site = $1"
},
"35deaad55b84124dcba02c555718fe815003e3f50b1c8ee2fc7e540009fa5aef": {
"describe": {
@ -274,6 +172,68 @@
},
"query": "INSERT INTO pages (id, author, site, slug, title, description, tags, blocks, collections, published)\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)"
},
"3b27a97c644fda93d0e9b605b606509b4cf77a1877949d0e614a8725666e7e71": {
"describe": {
"columns": [
{
"name": "name",
"ordinal": 0,
"type_info": "Varchar"
},
{
"name": "owner",
"ordinal": 1,
"type_info": "Varchar"
},
{
"name": "owner_display_name",
"ordinal": 2,
"type_info": "Varchar"
},
{
"name": "title",
"ordinal": 3,
"type_info": "Varchar"
},
{
"name": "description",
"ordinal": 4,
"type_info": "Text"
},
{
"name": "created_at",
"ordinal": 5,
"type_info": "Timestamp"
},
{
"name": "default_collection",
"ordinal": 6,
"type_info": "Uuid"
},
{
"name": "collections!: Vec<CollectionData>",
"ordinal": 7,
"type_info": "RecordArray"
}
],
"nullable": [
false,
false,
null,
false,
true,
false,
true,
null
],
"parameters": {
"Left": [
"Text"
]
}
},
"query": "SELECT\n sites.name,\n users.name as owner,\n COALESCE(users.display_name,users.name) as owner_display_name,\n title,\n description,\n sites.created_at,\n default_collection,\n array_agg(row(collections.id, collections.slug, collections.name, parent)) as \"collections!: Vec<CollectionData>\"\n FROM sites\n JOIN\n users ON sites.owner = users.id\n JOIN\n collections ON collections.site = sites.id\n WHERE\n sites.name = $1\n AND sites.deleted_at IS NULL\n GROUP BY\n sites.name, users.name, users.display_name, title, description, sites.created_at, default_collection"
},
"562254387ad52ad5ee3cf806085873485d6b8a0b372ac59a0796b9f0c8910cf2": {
"describe": {
"columns": [
@ -311,6 +271,23 @@
},
"query": "UPDATE sites SET deleted_at = NOW() WHERE name = $1 AND owner = $2 AND deleted_at IS NULL"
},
"6690726421106e317b58513f5d0a2eb0a38f15e1e89eb10695eb00e8bb3076d9": {
"describe": {
"columns": [],
"nullable": [],
"parameters": {
"Left": [
"Varchar",
"Varchar",
"Text",
"Uuid",
"Text",
"Uuid"
]
}
},
"query": "UPDATE sites SET\n name = COALESCE($1, name),\n title = COALESCE($2, title),\n description = COALESCE($3, description),\n default_collection = COALESCE($4, default_collection),\n modified_at = NOW()\n WHERE\n name = $5\n AND owner = $6\n AND deleted_at IS NULL"
},
"742b9eef92ad404b3b1ec285ce8b99e945b1b910f25bff376466c704a79fc9c8": {
"describe": {
"columns": [],
@ -366,6 +343,32 @@
},
"query": "INSERT INTO users ( id, name, password, roles )\n VALUES ( $1,$2,$3,$4 ) RETURNING id, created_at"
},
"8c2ad67b10d926e72efdcf63140c6cc34790aca9a90b52f555e745bbb30faffa": {
"describe": {
"columns": [
{
"name": "id",
"ordinal": 0,
"type_info": "Uuid"
},
{
"name": "owner",
"ordinal": 1,
"type_info": "Uuid"
}
],
"nullable": [
false,
false
],
"parameters": {
"Left": [
"Text"
]
}
},
"query": "SELECT id, owner FROM sites WHERE name = $1 AND deleted_at IS NULL"
},
"9f37c6f3929ca1bf2a6ec55291d652b216419fd13e27b3d61d23c9cf900c501e": {
"describe": {
"columns": [
@ -387,6 +390,19 @@
},
"query": "UPDATE sessions SET expires_at = $1 WHERE id = $2 RETURNING id"
},
"b85aa5820a73028398878a86ed550ea42daa362a8b0124f9ed626769a22bc047": {
"describe": {
"columns": [],
"nullable": [],
"parameters": {
"Left": [
"Uuid",
"Uuid"
]
}
},
"query": "UPDATE sites\n SET default_collection = $1\n WHERE id = $2"
},
"c567d96c92638902bc90f5fc0aa5bbbe0b6d26607767720a87fdbe67335a0180": {
"describe": {
"columns": [
@ -485,6 +501,87 @@
},
"query": "SELECT\n sessions.id AS session_id,\n sessions.actor AS session_actor,\n sessions.secret,\n sessions.created_at AS session_created_at,\n sessions.expires_at,\n users.id AS user_id,\n users.name,\n users.email,\n users.display_name,\n users.bio,\n users.roles,\n users.created_at AS user_created_at,\n users.modified_at,\n users.deleted_at\n FROM\n sessions\n JOIN\n users ON sessions.actor = users.id\n WHERE\n sessions.id = $1"
},
"cb17620d355a66a591cfae86e63dc293664a85777b39d7d19f2493a7388ecaf6": {
"describe": {
"columns": [
{
"name": "author_display_name",
"ordinal": 0,
"type_info": "Varchar"
},
{
"name": "author_username",
"ordinal": 1,
"type_info": "Varchar"
},
{
"name": "slug",
"ordinal": 2,
"type_info": "Varchar"
},
{
"name": "title",
"ordinal": 3,
"type_info": "Varchar"
},
{
"name": "description",
"ordinal": 4,
"type_info": "Text"
},
{
"name": "tags",
"ordinal": 5,
"type_info": "VarcharArray"
},
{
"name": "blocks!: Json<Vec<PostBlock>>",
"ordinal": 6,
"type_info": "Jsonb"
},
{
"name": "created_at",
"ordinal": 7,
"type_info": "Timestamp"
},
{
"name": "modified_at",
"ordinal": 8,
"type_info": "Timestamp"
},
{
"name": "published",
"ordinal": 9,
"type_info": "Bool"
},
{
"name": "collections",
"ordinal": 10,
"type_info": "UuidArray"
}
],
"nullable": [
null,
false,
false,
false,
true,
false,
false,
false,
true,
false,
false
],
"parameters": {
"Left": [
"Text",
"Text"
]
}
},
"query": "SELECT\n COALESCE(users.display_name,users.name) as author_display_name,\n users.name as author_username,\n pages.slug,\n pages.title,\n pages.description,\n pages.tags,\n pages.blocks as \"blocks!: Json<Vec<PostBlock>>\",\n pages.created_at,\n pages.modified_at,\n pages.published,\n pages.collections\n FROM pages \n JOIN\n sites ON site = sites.id\n JOIN\n users ON author = users.id\n WHERE\n slug = $1\n AND sites.name = $2 \n AND pages.deleted_at IS NULL\n AND sites.deleted_at IS NULL"
},
"cde4892e52ef14049256499eb4b0529a3b1cefd8f83ef3dc7cb201ee4a8253a3": {
"describe": {
"columns": [],
@ -498,22 +595,6 @@
},
"query": "DELETE FROM sites WHERE name = $1 AND owner = $2"
},
"d0005084c0ae3939460c3c727b29899c65a05e53fbc477cdda9665e7f243d5e9": {
"describe": {
"columns": [],
"nullable": [],
"parameters": {
"Left": [
"Varchar",
"Varchar",
"Text",
"Text",
"Uuid"
]
}
},
"query": "UPDATE sites SET\n name = COALESCE($1, name),\n title = COALESCE($2, title),\n description = COALESCE($3, description),\n modified_at = NOW()\n WHERE\n name = $4\n AND owner = $5\n AND deleted_at IS NULL"
},
"d9649c375db5825adda3b91c52468235fd9c361b827df64350c7e65bca0f00b4": {
"describe": {
"columns": [],
@ -552,6 +633,29 @@
},
"query": "INSERT INTO sites (id, name, owner, title, description)\n VALUES ($1, $2, $3, $4, $5) RETURNING id"
},
"eaf5344fe77b96ba3697e0454989d0e4d2fd074a850e7c8bafeaa9a19c50b71a": {
"describe": {
"columns": [
{
"name": "id",
"ordinal": 0,
"type_info": "Uuid"
}
],
"nullable": [
false
],
"parameters": {
"Left": [
"Uuid",
"Uuid",
"Varchar",
"Varchar"
]
}
},
"query": "INSERT INTO collections (id, site, slug, name)\n VALUES ($1, $2, $3, $4)\n RETURNING id"
},
"ecbbf6f400e058cd6e8695354d65a1883f974a39e11ac5ba0f7fba408fcf6b2a": {
"describe": {
"columns": [],
@ -571,26 +675,5 @@
}
},
"query": "UPDATE pages SET title = COALESCE($1, title), description = COALESCE($2, description), tags = COALESCE($3, tags), blocks = COALESCE($4, blocks), collections = COALESCE($8, collections), published = COALESCE($9, published) WHERE author = $5 AND site = $6 AND slug = $7 AND deleted_at IS NULL"
},
"f3d0f52ab3c9d7ed46086d21cd88207a75d9f2e2990f3fb721f0e14e1ec52e10": {
"describe": {
"columns": [
{
"name": "id",
"ordinal": 0,
"type_info": "Uuid"
}
],
"nullable": [
false
],
"parameters": {
"Left": [
"Text",
"Uuid"
]
}
},
"query": "SELECT id FROM sites WHERE name = $1 AND owner = $2 AND deleted_at IS NULL"
}
}

View File

@ -7,7 +7,7 @@ use super::Error;
pub const DEFAULT_COLLECTIONS: [(&str, &str); 2] = [("blog", "Blog"), ("pages", "Pages")];
#[derive(Deserialize, Serialize, FromRow)]
#[derive(Debug, Deserialize, Serialize, FromRow)]
pub struct Collection {
/// Collection ID
pub id: Uuid,
@ -25,8 +25,18 @@ pub struct Collection {
pub parent: Option<Uuid>,
}
#[derive(Debug, Serialize)]
pub struct CollectionData {
pub id: Uuid,
pub slug: String,
pub name: String,
pub parent: Option<Uuid>,
}
#[async_trait]
pub trait CollectionRepository {
async fn list_collections(&self, site: &Uuid) -> Result<Vec<CollectionData>, Error>;
/// Create default collections for a site
async fn create_default_collections(&self, site: &Uuid) -> Result<(), Error>;
}

View File

@ -7,6 +7,9 @@ pub enum Error {
#[error("Resource not found")]
NotFound,
#[error("No access to resource")]
AccessDenied,
#[error("Resource identifier not available")]
IdentifierNotAvailable,

View File

@ -118,6 +118,7 @@ pub struct PostWithAuthor {
pub created_at: NaiveDateTime,
pub modified_at: Option<NaiveDateTime>,
pub published: bool,
pub collections: Vec<Uuid>,
}
impl AsCursor<NaiveDateTime> for PostWithAuthor {

View File

@ -4,7 +4,7 @@ use serde::{Deserialize, Serialize};
use sqlx::FromRow;
use uuid::Uuid;
use super::Error;
use super::{collection::CollectionData, Error};
#[derive(Deserialize, Serialize, FromRow)]
pub struct Site {
@ -23,6 +23,9 @@ pub struct Site {
/// Site description (like a user's bio)
pub description: Option<String>,
/// Default collection (for homepage)
pub default_collection: Option<Uuid>,
/// Times
pub created_at: NaiveDateTime,
pub modified_at: Option<NaiveDateTime>,
@ -38,13 +41,8 @@ pub struct SiteData {
pub title: String,
pub description: Option<String>,
pub created_at: NaiveDateTime,
pub collections: Vec<CollectionNameAndSlug>,
}
#[derive(Debug, Serialize)]
pub struct CollectionNameAndSlug {
pub name: String,
pub slug: String,
pub collections: Vec<CollectionData>,
pub default_collection: Option<Uuid>,
}
/// Data required to create a new site
@ -61,6 +59,7 @@ pub struct UpdateSiteData {
pub name: Option<String>,
pub title: Option<String>,
pub description: Option<String>,
pub default_collection: Option<Uuid>,
}
#[async_trait]
@ -83,5 +82,5 @@ pub trait SiteRepository {
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>;
async fn get_site_id_and_owner(&self, name: &str) -> Result<(Uuid, Uuid), Error>;
}

View File

@ -1,3 +1,6 @@
use serde::Serialize;
#[derive(Serialize)]
pub struct CursorList<T, C> {
pub items: Vec<T>,
pub next_cursor: Option<C>,

View File

@ -1,8 +1,9 @@
use async_trait::async_trait;
use std::vec;
use uuid::Uuid;
use crate::content::{
collection::{CollectionRepository, DEFAULT_COLLECTIONS},
collection::{CollectionData, CollectionRepository, DEFAULT_COLLECTIONS},
Error,
};
@ -10,22 +11,45 @@ use super::Database;
#[async_trait]
impl CollectionRepository for Database {
async fn list_collections(&self, site: &Uuid) -> Result<Vec<CollectionData>, Error> {
Ok(sqlx::query_as!(
CollectionData,
r#"SELECT id, slug, name, parent FROM collections WHERE site = $1"#,
site
)
.fetch_all(&self.pool)
.await?)
}
async fn create_default_collections(&self, site: &Uuid) -> Result<(), Error> {
let mut tx = self.pool.begin().await?;
let mut uuids = vec![];
for (slug, name) in DEFAULT_COLLECTIONS {
sqlx::query!(
let record = sqlx::query!(
r#"INSERT INTO collections (id, site, slug, name)
VALUES ($1, $2, $3, $4)"#,
VALUES ($1, $2, $3, $4)
RETURNING id"#,
Uuid::now_v7(),
site,
slug,
name
)
.execute(&mut tx)
.fetch_one(&mut tx)
.await?;
uuids.push(record.id);
}
sqlx::query!(
r#"UPDATE sites
SET default_collection = $1
WHERE id = $2"#,
uuids[0],
site
)
.execute(&mut tx)
.await?;
tx.commit().await?;
Ok(())

View File

@ -37,12 +37,13 @@ impl PostRepository for Database {
pages.blocks as "blocks!: Json<Vec<PostBlock>>",
pages.created_at,
pages.modified_at,
pages.published
pages.published,
pages.collections
FROM pages
JOIN
users ON author = users.id
WHERE
site = $1
pages.site = $1
AND $2 = ANY(collections)
AND pages.deleted_at IS NULL
AND published = true
@ -78,7 +79,8 @@ impl PostRepository for Database {
pages.blocks as "blocks!: Json<Vec<PostBlock>>",
pages.created_at,
pages.modified_at,
pages.published
pages.published,
pages.collections
FROM pages
JOIN
sites ON site = sites.id

View File

@ -3,33 +3,42 @@ use sqlx::Postgres;
use uuid::Uuid;
use crate::content::{
site::{CollectionNameAndSlug, CreateSiteData, SiteData, SiteRepository, UpdateSiteData},
collection::CollectionData,
site::{CreateSiteData, SiteData, SiteRepository, UpdateSiteData},
Error,
};
use super::Database;
impl sqlx::Type<Postgres> for CollectionNameAndSlug {
impl sqlx::Type<Postgres> for CollectionData {
fn type_info() -> sqlx::postgres::PgTypeInfo {
sqlx::postgres::PgTypeInfo::with_name("collection_name_slug")
sqlx::postgres::PgTypeInfo::with_name("collection_data")
}
}
impl<'r> sqlx::Decode<'r, Postgres> for CollectionNameAndSlug {
impl<'r> sqlx::Decode<'r, Postgres> for CollectionData {
fn decode(
value: sqlx::postgres::PgValueRef<'r>,
) -> Result<Self, Box<dyn std::error::Error + 'static + Send + Sync>> {
let mut decoder = sqlx::postgres::types::PgRecordDecoder::new(value)?;
let slug = decoder.try_decode::<String>()?;
let id = decoder.try_decode::<Uuid>()?;
let name = decoder.try_decode::<String>()?;
Ok(Self { name, slug })
let slug = decoder.try_decode::<String>()?;
let parent = decoder.try_decode::<Option<Uuid>>()?;
Ok(Self {
id,
name,
slug,
parent,
})
}
}
#[async_trait]
impl SiteRepository for Database {
async fn get_site_by_name(&self, name: &str) -> Result<SiteData, Error> {
let site = sqlx::query!(
let site = sqlx::query_as!(
SiteData,
r#"SELECT
sites.name,
users.name as owner,
@ -37,7 +46,8 @@ impl SiteRepository for Database {
title,
description,
sites.created_at,
array_agg(row(collections.slug, collections.name)) as "collections!: Vec<CollectionNameAndSlug>"
default_collection,
array_agg(row(collections.id, collections.slug, collections.name, parent)) as "collections!: Vec<CollectionData>"
FROM sites
JOIN
users ON sites.owner = users.id
@ -47,7 +57,7 @@ impl SiteRepository for Database {
sites.name = $1
AND sites.deleted_at IS NULL
GROUP BY
sites.name, users.name, users.display_name, title, description, sites.created_at"#,
sites.name, users.name, users.display_name, title, description, sites.created_at, default_collection"#,
name
)
.fetch_one(&self.pool)
@ -56,15 +66,7 @@ impl SiteRepository for Database {
sqlx::Error::RowNotFound => Error::NotFound,
_ => e.into(),
})?;
Ok(SiteData {
name: site.name,
owner: site.owner,
owner_display_name: site.owner_display_name,
title: site.title,
description: site.description,
created_at: site.created_at,
collections: site.collections,
})
Ok(site)
}
async fn create_site(&self, owner: &Uuid, options: CreateSiteData) -> Result<Uuid, Error> {
@ -99,14 +101,16 @@ impl SiteRepository for Database {
name = COALESCE($1, name),
title = COALESCE($2, title),
description = COALESCE($3, description),
default_collection = COALESCE($4, default_collection),
modified_at = NOW()
WHERE
name = $4
AND owner = $5
name = $5
AND owner = $6
AND deleted_at IS NULL"#,
options.name,
options.title,
options.description,
options.default_collection,
name,
owner,
)
@ -153,11 +157,10 @@ impl SiteRepository for Database {
Ok(())
}
async fn get_site_id(&self, name: &str, owner: &Uuid) -> Result<Uuid, Error> {
async fn get_site_id_and_owner(&self, name: &str) -> Result<(Uuid, Uuid), Error> {
let record = sqlx::query!(
"SELECT id FROM sites WHERE name = $1 AND owner = $2 AND deleted_at IS NULL",
"SELECT id, owner FROM sites WHERE name = $1 AND deleted_at IS NULL",
name,
owner
)
.fetch_one(&self.pool)
.await
@ -165,6 +168,6 @@ impl SiteRepository for Database {
sqlx::Error::RowNotFound => Error::NotFound,
_ => e.into(),
})?;
Ok(record.id)
Ok((record.id, record.owner))
}
}

View File

@ -20,6 +20,11 @@ const ERR_NOT_AVAILABLE: ApiError<'static> = ApiError::Client {
code: "id-not-available",
message: "the chosen identifier is not available",
};
const ERR_UNAUTHORIZED: ApiError<'static> = ApiError::Client {
status: StatusCode::FORBIDDEN,
code: "access-denied",
message: "you are not authorized to perform this action",
};
#[derive(Error, Debug)]
pub enum ApiError<'a> {
@ -80,6 +85,7 @@ impl From<content::Error> for ApiError<'_> {
match err {
content::Error::NotFound => ERR_NOT_FOUND,
content::Error::IdentifierNotAvailable => ERR_NOT_AVAILABLE,
content::Error::AccessDenied => ERR_UNAUTHORIZED,
content::Error::QueryFailed(err) => err.into(),
}
}

87
src/routes/collections.rs Normal file
View File

@ -0,0 +1,87 @@
use axum::{
extract::{Path, Query},
response::IntoResponse,
routing::{get, post},
Json, Router,
};
use chrono::NaiveDateTime;
use serde::Deserialize;
use std::sync::Arc;
use uuid::Uuid;
use crate::{
content::{collection::CollectionRepository, post::PostRepository, site::SiteRepository},
database::Database,
http::error::ApiError,
state::AppState,
};
const DEFAULT_PAGE_SIZE: i64 = 10;
async fn create_collection<Repo: CollectionRepository>(repository: Repo) {
todo!()
}
async fn get_collection<Repo: CollectionRepository>(repository: Repo) {
todo!()
}
async fn list_collections_for_site<Repo: SiteRepository + CollectionRepository>(
repository: Repo,
Path(site): Path<String>,
) -> Result<impl IntoResponse, ApiError<'static>> {
let (site_id, _) = repository.get_site_id_and_owner(&site).await?;
Ok(Json(repository.list_collections(&site_id).await?))
}
async fn update_collection<Repo: CollectionRepository>(repository: Repo) {
todo!()
}
async fn delete_collection<Repo: CollectionRepository>(repository: Repo) {
todo!()
}
#[derive(Deserialize, Default)]
struct PaginationQuery {
before: Option<NaiveDateTime>,
limit: Option<i64>,
}
async fn list_posts_in_collection<Repo: SiteRepository + PostRepository>(
repository: Repo,
Path((site, id)): Path<(String, Uuid)>,
query: Query<PaginationQuery>,
) -> Result<impl IntoResponse, ApiError<'static>> {
// Resolve site
let (site_id, _) = repository.get_site_id_and_owner(&site).await?;
let results = repository
.list_posts_in_collection(
&site_id,
&id,
query.before,
query.limit.unwrap_or(DEFAULT_PAGE_SIZE),
)
.await?;
Ok(Json(results))
}
pub fn router() -> Router<Arc<AppState>> {
Router::new()
.route(
"/:site",
get(list_collections_for_site::<Database>).post(create_collection::<Database>),
)
.route(
"/:site/:id",
get(get_collection::<Database>)
.put(update_collection::<Database>)
.delete(delete_collection::<Database>),
)
.route(
"/:site/:id/posts",
get(list_posts_in_collection::<Database>),
)
}

View File

@ -5,6 +5,7 @@ use crate::state::AppState;
mod admin;
mod auth;
mod collections;
mod posts;
mod sites;
@ -14,4 +15,5 @@ pub fn create_router() -> Router<Arc<AppState>> {
.nest("/admin", admin::router())
.nest("/posts", posts::router())
.nest("/sites", sites::router())
.nest("/collections", collections::router())
}

View File

@ -53,7 +53,11 @@ async fn create_post<Repo: PostRepository + SiteRepository>(
JsonBody(data): JsonBody<CreatePostData>,
) -> Result<impl IntoResponse, ApiError<'static>> {
// Resolve site
let site_id = repository.get_site_id(&site, &user.id).await?;
let (site_id, owner) = repository.get_site_id_and_owner(&site).await?;
if user.id != owner {
return Err(Error::AccessDenied.into());
}
repository.create_post(&user.id, &site_id, data).await?;
Ok(Json(json!({ "ok": true })))
@ -66,7 +70,11 @@ async fn update_post<Repo: PostRepository + SiteRepository>(
JsonBody(data): JsonBody<UpdatePostData>,
) -> Result<impl IntoResponse, ApiError<'static>> {
// Resolve site
let site_id = repository.get_site_id(&site, &user.id).await?;
let (site_id, owner) = repository.get_site_id_and_owner(&site).await?;
if user.id != owner {
return Err(Error::AccessDenied.into());
}
repository
.update_post(&user.id, &site_id, &slug, data)
@ -80,7 +88,11 @@ async fn delete_post<Repo: PostRepository + SiteRepository>(
RequireUser(user): RequireUser,
) -> Result<impl IntoResponse, ApiError<'static>> {
// Resolve site
let site_id = repository.get_site_id(&site, &user.id).await?;
let (site_id, owner) = repository.get_site_id_and_owner(&site).await?;
if user.id != owner {
return Err(Error::AccessDenied.into());
}
repository
.delete_post(&user.id, &site_id, &slug, true)