Add feed model, link items to feeds

This commit is contained in:
Tyler Hallada 2023-05-07 21:25:22 -04:00
parent f30be5f451
commit b2a5bf5882
10 changed files with 188 additions and 16 deletions

View File

@ -1,9 +1,23 @@
CREATE TABLE IF NOT EXISTS "items" ( CREATE TYPE feed_type AS ENUM ('atom', 'rss');
CREATE TABLE IF NOT EXISTS "feeds" (
"id" SERIAL PRIMARY KEY NOT NULL, "id" SERIAL PRIMARY KEY NOT NULL,
"title" VARCHAR(255) NOT NULL, "title" VARCHAR(255) NOT NULL,
"url" VARCHAR(2048) NOT NULL, "url" VARCHAR(2048) NOT NULL,
"type" feed_type NOT NULL,
"description" TEXT, "description" TEXT,
"created_at" timestamp(3) NOT NULL, "created_at" timestamp(3) NOT NULL,
"updated_at" timestamp(3) NOT NULL, "updated_at" timestamp(3) NOT NULL,
"deleted_at" timestamp(3) "deleted_at" timestamp(3)
); );
CREATE TABLE IF NOT EXISTS "items" (
"id" SERIAL PRIMARY KEY NOT NULL,
"title" VARCHAR(255) NOT NULL,
"url" VARCHAR(2048) NOT NULL,
"description" TEXT,
"feed_id" INTEGER REFERENCES "feeds"(id) NOT NULL,
"created_at" timestamp(3) NOT NULL,
"updated_at" timestamp(3) NOT NULL,
"deleted_at" timestamp(3)
);

View File

@ -22,8 +22,11 @@ pub enum Error {
#[error("validation error in request body")] #[error("validation error in request body")]
InvalidEntity(#[from] ValidationErrors), InvalidEntity(#[from] ValidationErrors),
#[error("{0} not found")] #[error("{0}: {1} not found")]
NotFound(&'static str), NotFound(&'static str, i32),
#[error("referenced {0}: {1} not found")]
RelationNotFound(&'static str, i32),
} }
pub type Result<T, E = Error> = ::std::result::Result<T, E>; pub type Result<T, E = Error> = ::std::result::Result<T, E>;
@ -64,9 +67,9 @@ impl Error {
use Error::*; use Error::*;
match self { match self {
NotFound(_) => StatusCode::NOT_FOUND, NotFound(_, _) => StatusCode::NOT_FOUND,
Sqlx(_) | Anyhow(_) => StatusCode::INTERNAL_SERVER_ERROR, Sqlx(_) | Anyhow(_) => StatusCode::INTERNAL_SERVER_ERROR,
InvalidEntity(_) => StatusCode::UNPROCESSABLE_ENTITY, InvalidEntity(_) | RelationNotFound(_, _) => StatusCode::UNPROCESSABLE_ENTITY,
} }
} }
} }

19
src/handlers/feed.rs Normal file
View File

@ -0,0 +1,19 @@
use axum::{
extract::{Path, State},
Json,
};
use sqlx::PgPool;
use crate::error::Error;
use crate::models::feed::{create_feed, get_feed, CreateFeed, Feed};
pub async fn get(State(pool): State<PgPool>, Path(id): Path<i32>) -> Result<Json<Feed>, Error> {
Ok(Json(get_feed(pool, id).await?))
}
pub async fn post(
State(pool): State<PgPool>,
Json(payload): Json<CreateFeed>,
) -> Result<Json<Feed>, Error> {
Ok(Json(create_feed(pool, payload).await?))
}

9
src/handlers/feeds.rs Normal file
View File

@ -0,0 +1,9 @@
use axum::{extract::State, Json};
use sqlx::PgPool;
use crate::error::Error;
use crate::models::feed::{get_feeds, Feed};
pub async fn get(State(pool): State<PgPool>) -> Result<Json<Vec<Feed>>, Error> {
Ok(Json(get_feeds(pool).await?))
}

View File

@ -1,13 +1,13 @@
use axum::{extract::{State, Path}, Json}; use axum::{
extract::{Path, State},
Json,
};
use sqlx::PgPool; use sqlx::PgPool;
use crate::error::Error; use crate::error::Error;
use crate::models::item::{create_item, get_item, CreateItem, Item}; use crate::models::item::{create_item, get_item, CreateItem, Item};
pub async fn get( pub async fn get(State(pool): State<PgPool>, Path(id): Path<i32>) -> Result<Json<Item>, Error> {
State(pool): State<PgPool>,
Path(id): Path<i32>,
) -> Result<Json<Item>, Error> {
Ok(Json(get_item(pool, id).await?)) Ok(Json(get_item(pool, id).await?))
} }

View File

@ -1,2 +1,4 @@
pub mod feed;
pub mod feeds;
pub mod item; pub mod item;
pub mod items; pub mod items;

View File

@ -27,6 +27,9 @@ async fn main() -> anyhow::Result<()> {
sqlx::migrate!().run(&pool).await?; sqlx::migrate!().run(&pool).await?;
let app = Router::new() let app = Router::new()
.route("/v1/feeds", get(handlers::feeds::get))
.route("/v1/feed", post(handlers::feed::post))
.route("/v1/feed/:id", get(handlers::feed::get))
.route("/v1/items", get(handlers::items::get)) .route("/v1/items", get(handlers::items::get))
.route("/v1/item", post(handlers::item::post)) .route("/v1/item", post(handlers::item::post))
.route("/v1/item/:id", get(handlers::item::get)) .route("/v1/item/:id", get(handlers::item::get))

109
src/models/feed.rs Normal file
View File

@ -0,0 +1,109 @@
use chrono::NaiveDateTime;
use serde::{Deserialize, Serialize};
use sqlx::PgPool;
use validator::Validate;
use crate::error::{Error, Result};
#[derive(Debug, Serialize, Deserialize, sqlx::Type)]
#[sqlx(type_name = "feed_type", rename_all = "lowercase")]
#[serde(rename_all = "lowercase")]
pub enum FeedType {
Atom,
Rss,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Feed {
id: i32,
title: String,
url: String,
#[serde(rename = "type")]
feed_type: FeedType,
description: Option<String>,
created_at: NaiveDateTime,
updated_at: NaiveDateTime,
deleted_at: Option<NaiveDateTime>,
}
#[derive(Debug, Deserialize, Validate)]
pub struct CreateFeed {
#[validate(length(max = 255))]
title: String,
#[validate(url)]
url: String,
#[serde(rename = "type")]
feed_type: FeedType,
#[validate(length(max = 524288))]
description: Option<String>,
}
pub async fn get_feed(pool: PgPool, id: i32) -> Result<Feed> {
sqlx::query_as!(
Feed,
// Unable to SELECT * here due to https://github.com/launchbadge/sqlx/issues/1004
r#"SELECT
id,
title,
url,
type as "feed_type: FeedType",
description,
created_at,
updated_at,
deleted_at
FROM feeds WHERE id = $1"#,
id
)
.fetch_one(&pool)
.await
.map_err(|error| {
if let sqlx::error::Error::RowNotFound = error {
return Error::NotFound("feed", id);
}
Error::Sqlx(error)
})
}
pub async fn get_feeds(pool: PgPool) -> sqlx::Result<Vec<Feed>> {
sqlx::query_as!(
Feed,
r#"SELECT
id,
title,
url,
type as "feed_type: FeedType",
description,
created_at,
updated_at,
deleted_at
FROM feeds"#)
.fetch_all(&pool)
.await
}
pub async fn create_feed(pool: PgPool, payload: CreateFeed) -> Result<Feed> {
payload.validate()?;
Ok(sqlx::query_as!(
Feed,
r#"INSERT INTO feeds (
title, url, type, description, created_at, updated_at
) VALUES (
$1, $2, $3, $4, now(), now()
) RETURNING
id,
title,
url,
type as "feed_type: FeedType",
description,
created_at,
updated_at,
deleted_at
"#,
payload.title,
payload.url,
payload.feed_type as FeedType,
payload.description
)
.fetch_one(&pool)
.await?)
}

View File

@ -11,6 +11,7 @@ pub struct Item {
title: String, title: String,
url: String, url: String,
description: Option<String>, description: Option<String>,
feed_id: i32,
created_at: NaiveDateTime, created_at: NaiveDateTime,
updated_at: NaiveDateTime, updated_at: NaiveDateTime,
deleted_at: Option<NaiveDateTime>, deleted_at: Option<NaiveDateTime>,
@ -24,6 +25,8 @@ pub struct CreateItem {
url: String, url: String,
#[validate(length(max = 524288))] #[validate(length(max = 524288))]
description: Option<String>, description: Option<String>,
#[validate(range(min = 1))]
feed_id: i32,
} }
pub async fn get_item(pool: PgPool, id: i32) -> Result<Item> { pub async fn get_item(pool: PgPool, id: i32) -> Result<Item> {
@ -32,7 +35,7 @@ pub async fn get_item(pool: PgPool, id: i32) -> Result<Item> {
.await .await
.map_err(|error| { .map_err(|error| {
if let sqlx::error::Error::RowNotFound = error { if let sqlx::error::Error::RowNotFound = error {
return Error::NotFound("item"); return Error::NotFound("item", id);
} }
Error::Sqlx(error) Error::Sqlx(error)
}) })
@ -46,17 +49,26 @@ pub async fn get_items(pool: PgPool) -> sqlx::Result<Vec<Item>> {
pub async fn create_item(pool: PgPool, payload: CreateItem) -> Result<Item> { pub async fn create_item(pool: PgPool, payload: CreateItem) -> Result<Item> {
payload.validate()?; payload.validate()?;
Ok(sqlx::query_as!( sqlx::query_as!(
Item, Item,
"INSERT INTO items ( "INSERT INTO items (
title, url, description, created_at, updated_at title, url, description, feed_id, created_at, updated_at
) VALUES ( ) VALUES (
$1, $2, $3, now(), now() $1, $2, $3, $4, now(), now()
) RETURNING *", ) RETURNING *",
payload.title, payload.title,
payload.url, payload.url,
payload.description payload.description,
payload.feed_id,
) )
.fetch_one(&pool) .fetch_one(&pool)
.await?) .await
.map_err(|error| {
if let sqlx::error::Error::Database(ref psql_error) = error {
if psql_error.code().as_deref() == Some("23503") {
return Error::RelationNotFound("feed", payload.feed_id);
}
}
Error::Sqlx(error)
})
} }

View File

@ -1 +1,2 @@
pub mod item; pub mod item;
pub mod feed;