diff --git a/.env b/.env new file mode 100644 index 0000000..661fcf0 --- /dev/null +++ b/.env @@ -0,0 +1 @@ +DATABASE_URL=postgres://artificiale:changeme@localhost/artificiale \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 34ffbb3..dabbbf8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -197,7 +197,11 @@ checksum = "ec837a71355b28f6556dbd569b37b3f363091c0bd4b2e735674521b4c5fd9bc5" dependencies = [ "android-tzdata", "iana-time-zone", + "js-sys", "num-traits", + "serde", + "time", + "wasm-bindgen", "winapi", ] @@ -408,7 +412,7 @@ checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" dependencies = [ "cfg-if", "libc", - "wasi", + "wasi 0.11.0+wasi-snapshot-preview1", ] [[package]] @@ -651,8 +655,10 @@ version = "0.1.0" dependencies = [ "anyhow", "axum", + "chrono", "figment", "serde", + "serde_json", "sqlx", "tokio", "tracing", @@ -700,7 +706,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2" dependencies = [ "libc", - "wasi", + "wasi 0.11.0+wasi-snapshot-preview1", "windows-sys", ] @@ -1241,6 +1247,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", + "serde_json", "sha2", "sqlx-core", "sqlx-rt", @@ -1333,6 +1340,17 @@ dependencies = [ "once_cell", ] +[[package]] +name = "time" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a" +dependencies = [ + "libc", + "wasi 0.10.0+wasi-snapshot-preview1", + "winapi", +] + [[package]] name = "tinyvec" version = "1.6.0" @@ -1600,6 +1618,7 @@ checksum = "0fa2982af2eec27de306107c027578ff7f423d65f7250e40ce0fea8f45248b81" dependencies = [ "getrandom", "rand", + "serde", ] [[package]] @@ -1623,6 +1642,12 @@ dependencies = [ "try-lock", ] +[[package]] +name = "wasi" +version = "0.10.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" + [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" diff --git a/Cargo.toml b/Cargo.toml index d91665e..92d21a8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,8 +8,10 @@ axum = "0.6" tracing = "0.1" tracing-subscriber = "0.3" tokio = { version = "1.28", features = ["full"] } -sqlx = { version = "0.6", features = [ "runtime-tokio-rustls", "postgres", "uuid", "chrono", "macros", "migrate" ] } -uuid = { version = "1.3", features = ["v4", "fast-rng"] } +sqlx = { version = "0.6", features = [ "runtime-tokio-rustls", "postgres", "uuid", "chrono", "macros", "migrate", "json" ] } +uuid = { version = "1.3", features = ["v4", "fast-rng", "serde"] } serde = { version = "1" } +serde_json = { version = "1", features = ["raw_value"] } figment = { version = "0.10", features = ["toml", "env"] } +chrono = { version = "0.4", features = ["serde"] } anyhow = "1.0" \ No newline at end of file diff --git a/migrations/20230628223219_create-page.down.sql b/migrations/20230628223219_create-page.down.sql new file mode 100644 index 0000000..12b5243 --- /dev/null +++ b/migrations/20230628223219_create-page.down.sql @@ -0,0 +1 @@ +DROP TABLE pages; \ No newline at end of file diff --git a/migrations/20230628223219_create-page.up.sql b/migrations/20230628223219_create-page.up.sql new file mode 100644 index 0000000..4f54c44 --- /dev/null +++ b/migrations/20230628223219_create-page.up.sql @@ -0,0 +1,12 @@ +CREATE TABLE pages ( + id UUID PRIMARY KEY, + author UUID NOT NULL, + slug VARCHAR NOT NULL, + title VARCHAR NOT NULL, + description TEXT, + tags VARCHAR[] NOT NULL, + blocks JSONB NOT NULL, + created_at TIMESTAMPTZ NOT NULL, + modified_at TIMESTAMPTZ, + deleted_at TIMESTAMPTZ +); \ No newline at end of file diff --git a/src/content.rs b/src/content.rs new file mode 100644 index 0000000..d84caeb --- /dev/null +++ b/src/content.rs @@ -0,0 +1,78 @@ +use chrono::{serde::*, DateTime, Utc}; +use serde::{Deserialize, Serialize}; +use sqlx::{types::Json, FromRow}; +use uuid::Uuid; + +#[derive(Deserialize, Serialize, FromRow)] +pub struct Page { + /// Page ID + pub id: Uuid, + + /// Author ID + pub author: Uuid, + + /// URL-friendly short code for the page + pub slug: String, + + /// Page title + pub title: String, + + /// Page description (for SEO/content) + pub description: Option, + + /// Page tags (for internal search) + pub tags: Vec, + + /// Page blocks (content) + pub blocks: Json>, + + /// Times + #[serde(with = "ts_seconds")] + pub created_at: DateTime, + #[serde(with = "ts_seconds_option")] + pub modified_at: Option>, + #[serde(with = "ts_seconds_option")] + pub deleted_at: Option>, +} + +#[derive(Deserialize, Serialize)] +pub enum PageBlock { + Markup(MarkupBlock), + Gallery(GalleryBlock), +} + +/// A block of content in written form (probably Markdown) +#[derive(Deserialize, Serialize)] +pub struct MarkupBlock { + /// Markup format (markdown, html, plain) + pub format: String, + + /// Markup content (before rendering) + pub content: String, +} + +/// A block containing one or more images +#[derive(Deserialize, Serialize)] +pub struct GalleryBlock { + /// Images in the gallery + pub images: Vec, +} + +/// Picture inside a gallery +#[derive(Deserialize, Serialize)] +pub struct ImageElement { + /// URL of the picture + pub url: String, + + /// Image width in pixels + pub width: i32, + + /// Image height in pixels + pub height: i32, + + /// List of thumbnails, if available + pub thumbnails: Vec, + + /// Optional caption to put near the image + pub caption: Option, +} diff --git a/src/main.rs b/src/main.rs index e80aeec..0b9a7c8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,12 +1,22 @@ -use anyhow::{Ok, Result}; -use axum::{routing::get, Router, Server}; +mod content; + +use anyhow::{anyhow, Result}; +use axum::{ + extract::State, + http::StatusCode, + response::{IntoResponse, Response}, + routing::get, + Router, Server, +}; use figment::{ providers::{Env, Format, Serialized, Toml}, Figment, }; use serde::{Deserialize, Serialize}; -use sqlx::postgres::PgPoolOptions; -use std::net::SocketAddr; +use sqlx::{postgres::PgPoolOptions, query_as, Pool, Postgres}; +use std::{net::SocketAddr, sync::Arc}; + +use crate::content::{Page, PageBlock}; #[derive(Deserialize, Serialize)] struct Config { @@ -23,8 +33,47 @@ impl Default for Config { } } -async fn root() -> &'static str { - "Hello, World!" +struct GenericError(anyhow::Error); + +impl IntoResponse for GenericError { + fn into_response(self) -> Response { + ( + StatusCode::INTERNAL_SERVER_ERROR, + format!("Something went wrong: {}", self.0), + ) + .into_response() + } +} + +impl From for GenericError { + fn from(value: sqlx::Error) -> Self { + GenericError(anyhow!("Database error: {}", value)) + } +} + +struct AppState { + database: Pool, +} + +async fn root(State(state): State>) -> Result { + let page = query_as!( + Page, + r#"select + id, + author, + title, + description, + tags, + slug, + created_at, + modified_at, + deleted_at, + blocks as "blocks!: sqlx::types::Json>" + from pages limit 1"# + ) + .fetch_one(&state.database) + .await?; + Ok(page.title) } #[tokio::main] @@ -36,8 +85,6 @@ async fn main() -> Result<()> { .merge(Env::prefixed("MABEL_")) .extract()?; - let app = Router::new().route("/", get(root)); - let pool = PgPoolOptions::new() .max_connections(5) .connect(config.database_url.as_str()) @@ -45,6 +92,10 @@ async fn main() -> Result<()> { sqlx::migrate!().run(&pool).await?; + let shared_state = Arc::new(AppState { database: pool }); + + let app = Router::new().route("/", get(root)).with_state(shared_state); + let addr: SocketAddr = config.bind.parse()?; tracing::debug!("listening on {}", addr); Server::bind(&addr)