From 323fe471c5af6cec7a5a003d6bbfd2701cc73af8 Mon Sep 17 00:00:00 2001 From: fef Date: Tue, 6 Dec 2022 02:40:42 +0100 Subject: [PATCH] add statuses This commit requires wiping the database, because it changes the accounts migration as well. --- migrations/20221205020531_create_accounts.sql | 4 +- migrations/20221205235226_create_statuses.sql | 12 ++++ src/core/mod.rs | 8 ++- src/data/account/mod.rs | 3 + src/data/{account.rs => account/pg.rs} | 0 src/data/mod.rs | 4 ++ src/data/status/mod.rs | 3 + src/data/status/pg.rs | 70 +++++++++++++++++++ src/main.rs | 1 + src/model/mod.rs | 4 ++ src/model/status.rs | 31 ++++++++ src/repo/mod.rs | 6 +- src/repo/status.rs | 39 +++++++++++ src/route/api/v1/accounts.rs | 9 +++ 14 files changed, 190 insertions(+), 4 deletions(-) create mode 100644 migrations/20221205235226_create_statuses.sql create mode 100644 src/data/account/mod.rs rename src/data/{account.rs => account/pg.rs} (100%) create mode 100644 src/data/status/mod.rs create mode 100644 src/data/status/pg.rs create mode 100644 src/model/status.rs create mode 100644 src/repo/status.rs diff --git a/migrations/20221205020531_create_accounts.sql b/migrations/20221205020531_create_accounts.sql index bf386ed..8f76a7f 100644 --- a/migrations/20221205020531_create_accounts.sql +++ b/migrations/20221205020531_create_accounts.sql @@ -1,10 +1,10 @@ CREATE TABLE accounts ( - id SERIAL PRIMARY KEY, + id BIGSERIAL PRIMARY KEY, name VARCHAR NOT NULL, domain VARCHAR NOT NULL, display_name VARCHAR DEFAULT NULL, created_at TIMESTAMP NOT NULL DEFAULT now(), - updated_at TIMESTAMP NOT NULL DEFAULT NOW() + updated_at TIMESTAMP NOT NULL DEFAULT now() ); CREATE UNIQUE INDEX index_accounts_on_name_and_domain ON accounts (name, domain); diff --git a/migrations/20221205235226_create_statuses.sql b/migrations/20221205235226_create_statuses.sql new file mode 100644 index 0000000..13a3dd6 --- /dev/null +++ b/migrations/20221205235226_create_statuses.sql @@ -0,0 +1,12 @@ +CREATE TABLE statuses ( + id BIGSERIAL PRIMARY KEY, + account_id BIGINT REFERENCES accounts(id), + uri VARCHAR, + content TEXT NOT NULL, + summary TEXT DEFAULT NULL, + sensitive BOOLEAN NOT NULL DEFAULT FALSE, + created_at TIMESTAMP NOT NULL DEFAULT now(), + updated_at TIMESTAMP NOT NULL DEFAULT now() +); + +CREATE INDEX index_statuses_on_created_at ON statuses (created_at); diff --git a/src/core/mod.rs b/src/core/mod.rs index 34f914b..917ab08 100644 --- a/src/core/mod.rs +++ b/src/core/mod.rs @@ -1,12 +1,14 @@ use actix_web::body::BoxBody; use actix_web::http::StatusCode; use actix_web::{HttpResponse, ResponseError}; +use chrono::prelude::*; use serde::{Serialize, Serializer}; +use std::time::Instant; use std::{fmt, io}; pub use log::{debug, error, info, trace, warn}; -pub type Id = i32; +pub type Id = i64; pub type Result = std::result::Result; @@ -17,6 +19,10 @@ pub enum Error { NotFound, } +pub fn utc_now() -> NaiveDateTime { + Utc::now().naive_utc() +} + impl ResponseError for Error { fn status_code(&self) -> StatusCode { match self { diff --git a/src/data/account/mod.rs b/src/data/account/mod.rs new file mode 100644 index 0000000..1b4d041 --- /dev/null +++ b/src/data/account/mod.rs @@ -0,0 +1,3 @@ +mod pg; + +pub use pg::PgAccountDataSource; diff --git a/src/data/account.rs b/src/data/account/pg.rs similarity index 100% rename from src/data/account.rs rename to src/data/account/pg.rs diff --git a/src/data/mod.rs b/src/data/mod.rs index b0edc6c..bdc7b5d 100644 --- a/src/data/mod.rs +++ b/src/data/mod.rs @@ -1 +1,5 @@ pub mod account; +pub mod status; + +pub use account::*; +pub use status::*; diff --git a/src/data/status/mod.rs b/src/data/status/mod.rs new file mode 100644 index 0000000..56804c3 --- /dev/null +++ b/src/data/status/mod.rs @@ -0,0 +1,3 @@ +mod pg; + +pub use pg::PgStatusDataSource; diff --git a/src/data/status/pg.rs b/src/data/status/pg.rs new file mode 100644 index 0000000..ead1e68 --- /dev/null +++ b/src/data/status/pg.rs @@ -0,0 +1,70 @@ +use chrono::prelude::*; +use serde; +use sqlx::{Executor, PgPool}; + +use crate::core::*; +use crate::model::status::{NewStatus, Status}; + +pub struct PgStatusDataSource { + pool: PgPool, +} + +impl PgStatusDataSource { + pub fn new(pool: PgPool) -> PgStatusDataSource { + PgStatusDataSource { pool } + } + + pub async fn by_id(&self, id: I) -> Result + where + I: Into + Send, + { + let id: Id = id.into(); + let status: Status = sqlx::query_as("SELECT * FROM statuses WHERE id = $1") + .bind(id) + .fetch_one(&self.pool) + .await?; + Ok(status) + } + + pub async fn by_account(&self, id: I, since: NaiveDateTime) -> Result> + where + I: Into + Send, + { + let id: Id = id.into(); + let statuses: Vec = sqlx::query_as( + "SELECT * FROM statuses \ + WHERE account_id = $1 AND created_at < $1 \ + ORDER BY created_at \ + LIMIT 20", + ) + .bind(id) + .bind(since) + .fetch_all(&self.pool) + .await?; + Ok(statuses) + } + + pub async fn create(&self, new: T) -> Result + where + T: Into + Send, + { + let new = new.into(); + let status: Status = sqlx::query_as( + "INSERT INTO statuses ( \ + account_id, \ + uri, \ + content, \ + summary, \ + sensitive \ + ) VALUES ($1, $2, $3, $4, $5)", + ) + .bind(new.account_id) + .bind(new.uri) + .bind(new.content) + .bind(new.summary) + .bind(new.sensitive) + .fetch_one(&self.pool) + .await?; + Ok(status) + } +} diff --git a/src/main.rs b/src/main.rs index 1e9e5e1..cca28c8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -27,6 +27,7 @@ async fn main() -> std::io::Result<()> { info!("Starting application"); HttpServer::new(move || { App::new() + .wrap(actix_web::middleware::DefaultHeaders::new().add(("Server", "nyano"))) .app_data(state.clone()) .configure(route::configure) }) diff --git a/src/model/mod.rs b/src/model/mod.rs index b0edc6c..b9fd852 100644 --- a/src/model/mod.rs +++ b/src/model/mod.rs @@ -1 +1,5 @@ pub mod account; +pub mod status; + +pub use account::{Account, NewAccount}; +pub use status::{NewStatus, Status}; diff --git a/src/model/status.rs b/src/model/status.rs new file mode 100644 index 0000000..b29fa99 --- /dev/null +++ b/src/model/status.rs @@ -0,0 +1,31 @@ +use chrono::prelude::*; +use serde::{Deserialize, Serialize}; +use sqlx::FromRow; + +use crate::core::*; + +#[derive(Deserialize, Serialize, FromRow)] +pub struct Status { + pub id: Id, + pub account_id: Id, + pub uri: Option, + pub content: String, + pub summary: Option, + pub sensitive: bool, + pub created_at: NaiveDateTime, + pub updated_at: NaiveDateTime, +} + +pub struct NewStatus { + pub account_id: Id, + pub uri: String, + pub content: String, + pub summary: Option, + pub sensitive: bool, +} + +impl Into for Status { + fn into(self) -> Id { + self.id + } +} diff --git a/src/repo/mod.rs b/src/repo/mod.rs index dc8013d..c056e41 100644 --- a/src/repo/mod.rs +++ b/src/repo/mod.rs @@ -1,19 +1,23 @@ use sqlx::PgPool; pub mod account; +pub mod status; use account::AccountRepo; +use status::StatusRepo; /// The central collection of all data accessible to the app. /// This is included in `AppState` so it is accessible everywhere. /// All interactions with resident data must happen through this interface. pub struct AppRepo { pub accounts: AccountRepo, + pub statuses: StatusRepo, } impl AppRepo { pub fn new(db_pool: PgPool) -> AppRepo { AppRepo { - accounts: AccountRepo::new(db_pool), + accounts: AccountRepo::new(db_pool.clone()), + statuses: StatusRepo::new(db_pool.clone()), } } } diff --git a/src/repo/status.rs b/src/repo/status.rs new file mode 100644 index 0000000..4843208 --- /dev/null +++ b/src/repo/status.rs @@ -0,0 +1,39 @@ +use chrono::NaiveDateTime; +use sqlx::PgPool; + +use crate::core::*; +use crate::data::PgStatusDataSource; +use crate::model::{NewStatus, Status}; + +pub struct StatusRepo { + db: PgStatusDataSource, +} + +impl StatusRepo { + pub fn new(db_pool: PgPool) -> StatusRepo { + StatusRepo { + db: PgStatusDataSource::new(db_pool), + } + } + + pub async fn by_id(&self, id: I) -> Result + where + I: Into + Send, + { + self.db.by_id(id).await + } + + pub async fn by_account(&self, account_id: I, since: NaiveDateTime) -> Result> + where + I: Into + Send, + { + self.db.by_account(account_id, since).await + } + + pub async fn create(&self, new: T) -> Result + where + T: Into + Send, + { + self.db.create(new).await + } +} diff --git a/src/route/api/v1/accounts.rs b/src/route/api/v1/accounts.rs index 0049735..fc6d94c 100644 --- a/src/route/api/v1/accounts.rs +++ b/src/route/api/v1/accounts.rs @@ -11,6 +11,15 @@ async fn get_by_id(path: web::Path, state: AppState) -> Result Ok(HttpResponse::Ok().json(account)) } +#[get("/{id}/statuses")] +async fn get_statuses(path: web::Path, state: AppState) -> Result { + let id = path.into_inner(); + let account = state.repo.accounts.by_id(id).await?; + let statuses = state.repo.statuses.by_account(account, utc_now()).await?; + Ok(HttpResponse::Ok().json(statuses)) +} + pub fn configure(cfg: &mut web::ServiceConfig) { cfg.service(get_by_id); + cfg.service(get_statuses); }