Merge branch 'main' of git.hackbever.be:rusty-bever/blog
commit
b80a060faf
37
README.md
37
README.md
|
@ -1,3 +1,38 @@
|
||||||
# blog
|
# blog
|
||||||
|
|
||||||
Handles the contents of the blogging section.
|
Handles the contents of the blogging section.
|
||||||
|
|
||||||
|
## Schema & Data Objects
|
||||||
|
|
||||||
|
All objects have a personal UUID ID, generated automatically by PostgreSQL.
|
||||||
|
|
||||||
|
### Sections
|
||||||
|
|
||||||
|
Sections group posts according to a common subject or some other metric by
|
||||||
|
which they could be grouped. They can be seen as sub-blogs within the larger
|
||||||
|
system. Each post can be part of only one section, & posts not part of a
|
||||||
|
section will not be shown in the public UI.
|
||||||
|
|
||||||
|
A section has a title & optional description, along with a shortname. The
|
||||||
|
shortname is what will be used inside URLs for routing.
|
||||||
|
|
||||||
|
A section can be part of the default posts list, meaning all posts created in
|
||||||
|
this section will be shown on the homepage. The default list is an aggregated
|
||||||
|
list containing all default sections.
|
||||||
|
|
||||||
|
A section can be private, meaning all posts created in this section will not be
|
||||||
|
shown on the any public page without authentification. Furthermore, it can
|
||||||
|
specify wether posts should have titles, or are not allowed to. This is to
|
||||||
|
allow for creating microblogs, which do not require titles.
|
||||||
|
|
||||||
|
### Posts & Versions
|
||||||
|
|
||||||
|
A post represents a publication in a specific section. The posts table itself
|
||||||
|
only specifies which section a post belongs to & wether or not the post is
|
||||||
|
private. Any content of the actual post is stored a a version. A private post
|
||||||
|
can only be seen by logged-in users with the right authorization.
|
||||||
|
|
||||||
|
Each version has its own publication date, with the last publication being
|
||||||
|
shown when visiting the post. The UI however should also expose a way to show
|
||||||
|
previous versions of the post. Each version has its own title (if allowed) &
|
||||||
|
content. A version can be a draft. This means that the version will not be shown in the public UI, allowing the user to finish it at a later time.
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
-- This file should undo anything in `up.sql`
|
-- This file should undo anything in `up.sql`
|
||||||
drop trigger insert_enforce_post_titles on posts;
|
drop trigger insert_enforce_version_titles on versions;
|
||||||
drop trigger update_enforce_post_titles on posts;
|
drop trigger update_enforce_version_titles on versions;
|
||||||
drop function enforce_post_titles;
|
drop function enforce_version_titles;
|
||||||
|
|
||||||
|
drop table versions cascade;
|
||||||
drop table posts cascade;
|
drop table posts cascade;
|
||||||
drop table sections cascade;
|
drop table sections cascade;
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
-- Your SQL goes here
|
|
||||||
create table sections (
|
create table sections (
|
||||||
id uuid DEFAULT gen_random_uuid() PRIMARY KEY,
|
id uuid DEFAULT gen_random_uuid() PRIMARY KEY,
|
||||||
|
|
||||||
|
@ -11,48 +10,80 @@ create table sections (
|
||||||
-- Wether to show the section in the default list on the homepage
|
-- Wether to show the section in the default list on the homepage
|
||||||
is_default boolean NOT NULL DEFAULT false,
|
is_default boolean NOT NULL DEFAULT false,
|
||||||
-- Wether the posts should contain titles or not
|
-- Wether the posts should contain titles or not
|
||||||
has_titles boolean NOT NULL DEFAULT true
|
has_titles boolean NOT NULL DEFAULT true,
|
||||||
|
-- Wether posts in this section should be shown publicly
|
||||||
|
is_private boolean NOT NULL DEFAULT false
|
||||||
);
|
);
|
||||||
|
|
||||||
create table posts (
|
create table posts (
|
||||||
id uuid DEFAULT gen_random_uuid() PRIMARY KEY,
|
id uuid DEFAULT gen_random_uuid() PRIMARY KEY,
|
||||||
|
|
||||||
section_id uuid NOT NULL REFERENCES sections(id) ON DELETE CASCADE,
|
-- Posts shouldn't get deleted when we delete a section, as they're the
|
||||||
-- Title of the post
|
-- most valuable part of a blog
|
||||||
-- Wether this is NULL or not is enforced using the enforce_post_titles trigger
|
section_id uuid NOT NULL REFERENCES sections(id) ON DELETE SET NULL,
|
||||||
title varchar(255),
|
|
||||||
-- Post date, defaults to today
|
-- Wether a post should be private
|
||||||
publish_date date NOT NULL DEFAULT now(),
|
is_private boolean NOT NULL DEFAULT false,
|
||||||
-- Content of the post
|
-- Wether the post is archived
|
||||||
content text NOT NULL
|
is_archived boolean NOT NULL DEFAULT false
|
||||||
);
|
);
|
||||||
|
|
||||||
create function enforce_post_titles() returns trigger as $enforce_post_titles$
|
create table versions (
|
||||||
|
id uuid DEFAULT gen_random_uuid() PRIMARY KEY,
|
||||||
|
|
||||||
|
-- A version should be deleted when its referenced post is deleted
|
||||||
|
post_id uuid NOT NULL REFERENCES posts(id) ON DELETE CASCADE,
|
||||||
|
|
||||||
|
-- Title of the post. Wether this is NULL or not is enforced using the
|
||||||
|
-- enforce_post_titles trigger.
|
||||||
|
title varchar(255),
|
||||||
|
-- Publish date
|
||||||
|
publish_date date,
|
||||||
|
-- Content of the post, in Markdown
|
||||||
|
content text NOT NULL DEFAULT '',
|
||||||
|
-- Wether the version is still a draft
|
||||||
|
is_draft boolean NOT NULL default true,
|
||||||
|
|
||||||
|
-- This check allows draft posts to be created without having to enter a
|
||||||
|
-- publish date, but forces them to have one if they're not a draft.
|
||||||
|
CHECK (is_draft OR publish_date IS NOT NULL)
|
||||||
|
);
|
||||||
|
|
||||||
|
create function enforce_version_titles() returns trigger as $$
|
||||||
begin
|
begin
|
||||||
|
-- Draft versions shouldn't be evaluated.
|
||||||
|
if new.is_draft then
|
||||||
|
return new;
|
||||||
|
end if;
|
||||||
|
|
||||||
-- Check for a wrongfully null title
|
-- Check for a wrongfully null title
|
||||||
if new.title is null and exists (
|
if new.title is null and exists (
|
||||||
select 1 from sections where id = new.section_id and has_titles
|
select 1 from posts
|
||||||
|
inner join sections on posts.section_id = sections.id
|
||||||
|
where posts.id = new.post_id and sections.has_titles
|
||||||
) then
|
) then
|
||||||
raise exception 'Expected a post title, but got null.';
|
raise exception 'Expected a post title, but got null.';
|
||||||
end if;
|
end if;
|
||||||
|
|
||||||
if new.title is not null and exists (
|
if new.title is not null and exists (
|
||||||
select 1 from sections where id = new.section_id and not has_titles
|
select 1 from posts
|
||||||
|
inner join sections on posts.section_id = sections.id
|
||||||
|
where posts.id = new.post_id and not sections.has_titles
|
||||||
) then
|
) then
|
||||||
raise exception 'Expected an empty post title, but got a value.';
|
raise exception 'Expected an empty post title, but got a value.';
|
||||||
end if;
|
end if;
|
||||||
|
|
||||||
return new;
|
return new;
|
||||||
end;
|
end;
|
||||||
$enforce_post_titles$ language plpgsql;
|
$$ language plpgsql;
|
||||||
|
|
||||||
create trigger insert_enforce_post_titles
|
create trigger insert_enforce_version_titles
|
||||||
before insert on posts
|
before insert on versions
|
||||||
for each row
|
for each row
|
||||||
execute function enforce_post_titles();
|
execute function enforce_version_titles();
|
||||||
|
|
||||||
create trigger update_enforce_post_titles
|
create trigger update_enforce_version_titles
|
||||||
before update of title on posts
|
before update of title on versions
|
||||||
for each row
|
for each row
|
||||||
when (old.title is distinct from new.title)
|
when (old.title is distinct from new.title)
|
||||||
execute function enforce_post_titles();
|
execute function enforce_version_titles();
|
||||||
|
|
|
@ -6,3 +6,6 @@ pub mod sections;
|
||||||
|
|
||||||
pub use posts::{NewPost, PatchPost, Post};
|
pub use posts::{NewPost, PatchPost, Post};
|
||||||
pub use sections::{NewSection, PatchSection, Section};
|
pub use sections::{NewSection, PatchSection, Section};
|
||||||
|
|
||||||
|
pub const MAX_POSTS: u32 = 64;
|
||||||
|
pub const MAX_SECTIONS: u32 = 64;
|
||||||
|
|
|
@ -6,6 +6,7 @@ use uuid::Uuid;
|
||||||
|
|
||||||
use crate::schema::{posts, posts::dsl::*};
|
use crate::schema::{posts, posts::dsl::*};
|
||||||
|
|
||||||
|
/// A post inside the database.
|
||||||
#[derive(Queryable, Serialize)]
|
#[derive(Queryable, Serialize)]
|
||||||
pub struct Post
|
pub struct Post
|
||||||
{
|
{
|
||||||
|
@ -16,6 +17,7 @@ pub struct Post
|
||||||
pub content: String,
|
pub content: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A new post to be added to the database.
|
||||||
#[derive(Deserialize, Insertable)]
|
#[derive(Deserialize, Insertable)]
|
||||||
#[table_name = "posts"]
|
#[table_name = "posts"]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
|
@ -27,6 +29,7 @@ pub struct NewPost
|
||||||
pub content: String,
|
pub content: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A patch to be applied to a row in the database.
|
||||||
#[derive(Deserialize, AsChangeset)]
|
#[derive(Deserialize, AsChangeset)]
|
||||||
#[table_name = "posts"]
|
#[table_name = "posts"]
|
||||||
pub struct PatchPost
|
pub struct PatchPost
|
||||||
|
@ -37,15 +40,18 @@ pub struct PatchPost
|
||||||
pub content: Option<String>,
|
pub content: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get a list of posts, specified by the offset & a limit. The maximum for `limit_` is determined
|
||||||
|
/// by `super::MAX_POSTS`.
|
||||||
pub fn get(conn: &PgConnection, offset_: u32, limit_: u32) -> RbResult<Vec<Post>>
|
pub fn get(conn: &PgConnection, offset_: u32, limit_: u32) -> RbResult<Vec<Post>>
|
||||||
{
|
{
|
||||||
Ok(posts
|
Ok(posts
|
||||||
.offset(offset_.into())
|
.offset(offset_.into())
|
||||||
.limit(limit_.into())
|
.limit(std::cmp::min(limit_, super::MAX_POSTS).into())
|
||||||
.load(conn)
|
.load(conn)
|
||||||
.map_err(|_| RbError::DbError("Couldn't query posts."))?)
|
.map_err(|_| RbError::DbError("Couldn't query posts."))?)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Try to find a post given its id (primary key).
|
||||||
pub fn find(conn: &PgConnection, id_: &Uuid) -> RbOption<Post>
|
pub fn find(conn: &PgConnection, id_: &Uuid) -> RbOption<Post>
|
||||||
{
|
{
|
||||||
match posts.find(id_).first(conn) {
|
match posts.find(id_).first(conn) {
|
||||||
|
@ -55,6 +61,7 @@ pub fn find(conn: &PgConnection, id_: &Uuid) -> RbOption<Post>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Create a new post & store it in the database.
|
||||||
pub fn create(conn: &PgConnection, new_post: &NewPost) -> RbResult<Post>
|
pub fn create(conn: &PgConnection, new_post: &NewPost) -> RbResult<Post>
|
||||||
{
|
{
|
||||||
Ok(insert_into(posts)
|
Ok(insert_into(posts)
|
||||||
|
@ -65,6 +72,7 @@ pub fn create(conn: &PgConnection, new_post: &NewPost) -> RbResult<Post>
|
||||||
// TODO check for conflict?
|
// TODO check for conflict?
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Update a post in the database with a given ID, returning the updated row.
|
||||||
pub fn update(conn: &PgConnection, post_id: &Uuid, patch_post: &PatchPost) -> RbResult<Post>
|
pub fn update(conn: &PgConnection, post_id: &Uuid, patch_post: &PatchPost) -> RbResult<Post>
|
||||||
{
|
{
|
||||||
Ok(diesel::update(posts.filter(id.eq(post_id)))
|
Ok(diesel::update(posts.filter(id.eq(post_id)))
|
||||||
|
@ -73,6 +81,7 @@ pub fn update(conn: &PgConnection, post_id: &Uuid, patch_post: &PatchPost) -> Rb
|
||||||
.map_err(|_| RbError::DbError("Couldn't update post."))?)
|
.map_err(|_| RbError::DbError("Couldn't update post."))?)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Delete a post with a given ID.
|
||||||
pub fn delete(conn: &PgConnection, post_id: &Uuid) -> RbResult<()>
|
pub fn delete(conn: &PgConnection, post_id: &Uuid) -> RbResult<()>
|
||||||
{
|
{
|
||||||
diesel::delete(posts.filter(id.eq(post_id)))
|
diesel::delete(posts.filter(id.eq(post_id)))
|
||||||
|
|
|
@ -5,6 +5,7 @@ use uuid::Uuid;
|
||||||
|
|
||||||
use crate::schema::{sections, sections::dsl::*};
|
use crate::schema::{sections, sections::dsl::*};
|
||||||
|
|
||||||
|
/// A section inside the database.
|
||||||
#[derive(Queryable, Serialize)]
|
#[derive(Queryable, Serialize)]
|
||||||
pub struct Section
|
pub struct Section
|
||||||
{
|
{
|
||||||
|
@ -16,6 +17,8 @@ pub struct Section
|
||||||
pub has_titles: bool,
|
pub has_titles: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A new section to add. Any `Option` values will get replaced by their default value in the
|
||||||
|
/// database.
|
||||||
#[derive(Serialize, Deserialize, Insertable)]
|
#[derive(Serialize, Deserialize, Insertable)]
|
||||||
#[table_name = "sections"]
|
#[table_name = "sections"]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
|
@ -28,6 +31,7 @@ pub struct NewSection
|
||||||
pub has_titles: Option<bool>,
|
pub has_titles: Option<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A patch to apply to a section.
|
||||||
#[derive(Deserialize, AsChangeset)]
|
#[derive(Deserialize, AsChangeset)]
|
||||||
#[table_name = "sections"]
|
#[table_name = "sections"]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
|
@ -40,15 +44,18 @@ pub struct PatchSection
|
||||||
has_titles: Option<bool>,
|
has_titles: Option<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get an amount of sections from the database, given an offset & limit. The maximum value for
|
||||||
|
/// `limit_` is determined by `super::MAX_SECTIONS`.
|
||||||
pub fn get(conn: &PgConnection, offset_: u32, limit_: u32) -> RbResult<Vec<Section>>
|
pub fn get(conn: &PgConnection, offset_: u32, limit_: u32) -> RbResult<Vec<Section>>
|
||||||
{
|
{
|
||||||
Ok(sections
|
Ok(sections
|
||||||
.offset(offset_.into())
|
.offset(offset_.into())
|
||||||
.limit(limit_.into())
|
.limit(std::cmp::min(limit_, super::MAX_SECTIONS).into())
|
||||||
.load(conn)
|
.load(conn)
|
||||||
.map_err(|_| RbError::DbError("Couldn't query sections."))?)
|
.map_err(|_| RbError::DbError("Couldn't query sections."))?)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Try to find a section given its shortname.
|
||||||
pub fn find_with_shortname(conn: &PgConnection, shortname_: &str) -> RbOption<Section>
|
pub fn find_with_shortname(conn: &PgConnection, shortname_: &str) -> RbOption<Section>
|
||||||
{
|
{
|
||||||
match sections.filter(shortname.eq(shortname_)).first(conn) {
|
match sections.filter(shortname.eq(shortname_)).first(conn) {
|
||||||
|
@ -58,6 +65,7 @@ pub fn find_with_shortname(conn: &PgConnection, shortname_: &str) -> RbOption<Se
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Create a new section.
|
||||||
pub fn create(conn: &PgConnection, new_section: &NewSection) -> RbResult<Section>
|
pub fn create(conn: &PgConnection, new_section: &NewSection) -> RbResult<Section>
|
||||||
{
|
{
|
||||||
Ok(insert_into(sections)
|
Ok(insert_into(sections)
|
||||||
|
@ -68,6 +76,7 @@ pub fn create(conn: &PgConnection, new_section: &NewSection) -> RbResult<Section
|
||||||
// TODO check for conflict?
|
// TODO check for conflict?
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Update a section given its ID.
|
||||||
pub fn update(conn: &PgConnection, post_id: &Uuid, patch_post: &PatchSection) -> RbResult<Section>
|
pub fn update(conn: &PgConnection, post_id: &Uuid, patch_post: &PatchSection) -> RbResult<Section>
|
||||||
{
|
{
|
||||||
Ok(diesel::update(sections.filter(id.eq(post_id)))
|
Ok(diesel::update(sections.filter(id.eq(post_id)))
|
||||||
|
@ -76,6 +85,7 @@ pub fn update(conn: &PgConnection, post_id: &Uuid, patch_post: &PatchSection) ->
|
||||||
.map_err(|_| RbError::DbError("Couldn't update section."))?)
|
.map_err(|_| RbError::DbError("Couldn't update section."))?)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update a section given its shortname.
|
||||||
pub fn update_with_shortname(
|
pub fn update_with_shortname(
|
||||||
conn: &PgConnection,
|
conn: &PgConnection,
|
||||||
shortname_: &str,
|
shortname_: &str,
|
||||||
|
@ -88,6 +98,7 @@ pub fn update_with_shortname(
|
||||||
.map_err(|_| RbError::DbError("Couldn't update section with shortname."))?)
|
.map_err(|_| RbError::DbError("Couldn't update section with shortname."))?)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Delete a section given its ID.
|
||||||
pub fn delete(conn: &PgConnection, post_id: &Uuid) -> RbResult<()>
|
pub fn delete(conn: &PgConnection, post_id: &Uuid) -> RbResult<()>
|
||||||
{
|
{
|
||||||
diesel::delete(sections.filter(id.eq(post_id)))
|
diesel::delete(sections.filter(id.eq(post_id)))
|
||||||
|
|
12
src/main.rs
12
src/main.rs
|
@ -27,19 +27,22 @@ pub fn auth_header() -> rocket::http::Header<'static>
|
||||||
return rocket::http::Header::new("Authorization", "Bearer eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjVjMjM2OTI0NjY4ZDQzZWFiNGNmNDczYjk1YWZiNzgzIiwidXNlcm5hbWUiOiJKb2huIERvZSIsImFkbWluIjp0cnVlLCJleHAiOjE1MTYyMzkwMjIwfQ.if939L9le8LP-dtXnQs-mHPkb-VieRAvAfSu20755jY");
|
return rocket::http::Header::new("Authorization", "Bearer eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjVjMjM2OTI0NjY4ZDQzZWFiNGNmNDczYjk1YWZiNzgzIiwidXNlcm5hbWUiOiJKb2huIERvZSIsImFkbWluIjp0cnVlLCJleHAiOjE1MTYyMzkwMjIwfQ.if939L9le8LP-dtXnQs-mHPkb-VieRAvAfSu20755jY");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Used by Rocket to store database connections.
|
||||||
#[database("postgres_rb")]
|
#[database("postgres_rb")]
|
||||||
pub struct RbDbConn(diesel::PgConnection);
|
pub struct RbDbConn(diesel::PgConnection);
|
||||||
|
|
||||||
|
/// Handles all error status codes.
|
||||||
#[catch(default)]
|
#[catch(default)]
|
||||||
fn default_catcher(status: Status, _: &Request) -> Value
|
fn default_catcher(status: Status, _: &Request) -> Value
|
||||||
{
|
{
|
||||||
json!({"status": status.code, "message": ""})
|
json!({"status": status.code, "message": ""})
|
||||||
}
|
}
|
||||||
|
|
||||||
embed_migrations!();
|
/// Rocket fairing that executes the necessary migrations in our database.
|
||||||
|
|
||||||
async fn run_db_migrations(rocket: Rocket<Build>) -> Result<Rocket<Build>, Rocket<Build>>
|
async fn run_db_migrations(rocket: Rocket<Build>) -> Result<Rocket<Build>, Rocket<Build>>
|
||||||
{
|
{
|
||||||
|
embed_migrations!();
|
||||||
|
|
||||||
let conn = RbDbConn::get_one(&rocket)
|
let conn = RbDbConn::get_one(&rocket)
|
||||||
.await
|
.await
|
||||||
.expect("database connection");
|
.expect("database connection");
|
||||||
|
@ -50,12 +53,15 @@ async fn run_db_migrations(rocket: Rocket<Build>) -> Result<Rocket<Build>, Rocke
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Struct to deserialize from the config file. It contains any custom configuration our
|
||||||
|
/// application might need besides the default Rocket variables.
|
||||||
#[derive(Debug, Deserialize, Serialize)]
|
#[derive(Debug, Deserialize, Serialize)]
|
||||||
pub struct RbConfig
|
pub struct RbConfig
|
||||||
{
|
{
|
||||||
jwt: JwtConf,
|
jwt: JwtConf,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The main entrypoint of our program. It launches the Rocket instance.
|
||||||
#[launch]
|
#[launch]
|
||||||
fn rocket() -> _
|
fn rocket() -> _
|
||||||
{
|
{
|
||||||
|
@ -69,8 +75,6 @@ fn rocket() -> _
|
||||||
"Run database migrations",
|
"Run database migrations",
|
||||||
run_db_migrations,
|
run_db_migrations,
|
||||||
))
|
))
|
||||||
// .attach(AdHoc::try_on_ignite("Create admin user", create_admin_user))
|
|
||||||
// .attach(AdHoc::config::<JwtConf>())
|
|
||||||
.register("/", catchers![default_catcher])
|
.register("/", catchers![default_catcher])
|
||||||
.mount(
|
.mount(
|
||||||
"/v1/sections",
|
"/v1/sections",
|
||||||
|
|
|
@ -2,9 +2,8 @@ table! {
|
||||||
posts (id) {
|
posts (id) {
|
||||||
id -> Uuid,
|
id -> Uuid,
|
||||||
section_id -> Uuid,
|
section_id -> Uuid,
|
||||||
title -> Nullable<Varchar>,
|
is_private -> Bool,
|
||||||
publish_date -> Date,
|
is_archived -> Bool,
|
||||||
content -> Text,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -16,9 +15,26 @@ table! {
|
||||||
description -> Nullable<Text>,
|
description -> Nullable<Text>,
|
||||||
is_default -> Bool,
|
is_default -> Bool,
|
||||||
has_titles -> Bool,
|
has_titles -> Bool,
|
||||||
|
is_private -> Bool,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
table! {
|
||||||
|
versions (id) {
|
||||||
|
id -> Uuid,
|
||||||
|
post_id -> Uuid,
|
||||||
|
title -> Nullable<Varchar>,
|
||||||
|
publish_date -> Nullable<Date>,
|
||||||
|
content -> Text,
|
||||||
|
is_draft -> Bool,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
joinable!(posts -> sections (section_id));
|
joinable!(posts -> sections (section_id));
|
||||||
|
joinable!(versions -> posts (post_id));
|
||||||
|
|
||||||
allow_tables_to_appear_in_same_query!(posts, sections,);
|
allow_tables_to_appear_in_same_query!(
|
||||||
|
posts,
|
||||||
|
sections,
|
||||||
|
versions,
|
||||||
|
);
|
||||||
|
|
|
@ -7,6 +7,7 @@ use rocket::serde::json::Json;
|
||||||
|
|
||||||
use crate::RbDbConn;
|
use crate::RbDbConn;
|
||||||
|
|
||||||
|
/// Get one or more posts.
|
||||||
#[get("/?<offset>&<limit>")]
|
#[get("/?<offset>&<limit>")]
|
||||||
pub async fn get(conn: RbDbConn, offset: u32, limit: u32) -> RbResult<Json<Vec<db::Post>>>
|
pub async fn get(conn: RbDbConn, offset: u32, limit: u32) -> RbResult<Json<Vec<db::Post>>>
|
||||||
{
|
{
|
||||||
|
@ -15,6 +16,7 @@ pub async fn get(conn: RbDbConn, offset: u32, limit: u32) -> RbResult<Json<Vec<d
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Create a new post.
|
||||||
#[post("/", data = "<new_post>")]
|
#[post("/", data = "<new_post>")]
|
||||||
pub async fn create(
|
pub async fn create(
|
||||||
_admin: Admin,
|
_admin: Admin,
|
||||||
|
@ -28,6 +30,7 @@ pub async fn create(
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get a post given its ID.
|
||||||
#[get("/<id>")]
|
#[get("/<id>")]
|
||||||
pub async fn find(conn: RbDbConn, id: uuid::Uuid) -> RbOption<Json<db::Post>>
|
pub async fn find(conn: RbDbConn, id: uuid::Uuid) -> RbOption<Json<db::Post>>
|
||||||
{
|
{
|
||||||
|
@ -37,6 +40,7 @@ pub async fn find(conn: RbDbConn, id: uuid::Uuid) -> RbOption<Json<db::Post>>
|
||||||
.and_then(|p| Some(Json(p))))
|
.and_then(|p| Some(Json(p))))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Patch a post given its ID.
|
||||||
#[patch("/<id>", data = "<patch_post>")]
|
#[patch("/<id>", data = "<patch_post>")]
|
||||||
pub async fn patch(
|
pub async fn patch(
|
||||||
_admin: Admin,
|
_admin: Admin,
|
||||||
|
@ -51,6 +55,7 @@ pub async fn patch(
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Delete a post given its ID..
|
||||||
#[delete("/<id>")]
|
#[delete("/<id>")]
|
||||||
pub async fn delete(_admin: Admin, conn: RbDbConn, id: uuid::Uuid) -> RbResult<()>
|
pub async fn delete(_admin: Admin, conn: RbDbConn, id: uuid::Uuid) -> RbResult<()>
|
||||||
{
|
{
|
||||||
|
|
|
@ -9,6 +9,8 @@ use rocket::serde::json::Json;
|
||||||
|
|
||||||
use crate::RbDbConn;
|
use crate::RbDbConn;
|
||||||
|
|
||||||
|
/// Get multiple sections given an offset & a limit. The limit is bound by
|
||||||
|
/// `rb_blog::db::MAX_SECTIONS`.
|
||||||
#[get("/?<offset>&<limit>")]
|
#[get("/?<offset>&<limit>")]
|
||||||
pub async fn get(conn: RbDbConn, offset: u32, limit: u32) -> RbResult<Json<Vec<db::Section>>>
|
pub async fn get(conn: RbDbConn, offset: u32, limit: u32) -> RbResult<Json<Vec<db::Section>>>
|
||||||
{
|
{
|
||||||
|
@ -18,6 +20,7 @@ pub async fn get(conn: RbDbConn, offset: u32, limit: u32) -> RbResult<Json<Vec<d
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Create a new section.
|
||||||
#[post("/", data = "<new_section>")]
|
#[post("/", data = "<new_section>")]
|
||||||
pub async fn create(
|
pub async fn create(
|
||||||
_admin: Admin,
|
_admin: Admin,
|
||||||
|
@ -31,6 +34,7 @@ pub async fn create(
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get a section by its shortname.
|
||||||
#[get("/<shortname>")]
|
#[get("/<shortname>")]
|
||||||
pub async fn find(conn: RbDbConn, shortname: String) -> RbOption<Json<db::Section>>
|
pub async fn find(conn: RbDbConn, shortname: String) -> RbOption<Json<db::Section>>
|
||||||
{
|
{
|
||||||
|
@ -40,6 +44,7 @@ pub async fn find(conn: RbDbConn, shortname: String) -> RbOption<Json<db::Sectio
|
||||||
.and_then(|p| Some(Json(p))))
|
.and_then(|p| Some(Json(p))))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Patch a section given its shortname.
|
||||||
#[patch("/<shortname>", data = "<patch_section>")]
|
#[patch("/<shortname>", data = "<patch_section>")]
|
||||||
pub async fn patch(
|
pub async fn patch(
|
||||||
_admin: Admin,
|
_admin: Admin,
|
||||||
|
@ -56,6 +61,7 @@ pub async fn patch(
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Delete a section given its ID.
|
||||||
#[delete("/<id>")]
|
#[delete("/<id>")]
|
||||||
pub async fn delete(_admin: Admin, conn: RbDbConn, id: uuid::Uuid) -> RbResult<()>
|
pub async fn delete(_admin: Admin, conn: RbDbConn, id: uuid::Uuid) -> RbResult<()>
|
||||||
{
|
{
|
||||||
|
|
Reference in New Issue