Skip to content

Commit

Permalink
wip
Browse files Browse the repository at this point in the history
  • Loading branch information
zaknesler committed Apr 27, 2024
1 parent f9c17de commit 704abdd
Show file tree
Hide file tree
Showing 17 changed files with 152 additions and 67 deletions.
9 changes: 9 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ sqlx = "0.7"
thiserror = "1.0"
tokio = "1.37"
tracing = "0.1"
uuid = { version = "1.8", features = ["v4", "fast-rng", "serde"] }

[package]
name = "blend"
Expand Down
10 changes: 8 additions & 2 deletions crates/blend-db/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,13 @@ chrono = { workspace = true, features = ["serde"] }
futures = "0.3"
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
sqlx = { workspace = true, features = ["runtime-tokio", "sqlite", "migrate"] }
sqlx = { workspace = true, features = [
"runtime-tokio",
"sqlite",
"migrate",
"uuid",
] }
thiserror = { workspace = true }
tracing = { workspace = true }
ts-rs = { version = "8.1", features = ["chrono-impl"] }
ts-rs = { version = "8.1", features = ["chrono-impl", "uuid-impl"] }
uuid = { workspace = true }
2 changes: 1 addition & 1 deletion crates/blend-db/migrations/20240422000000_feeds.sql
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
CREATE TABLE IF NOT EXISTS feeds (
id INTEGER PRIMARY KEY NOT NULL,
uuid TEXT PRIMARY KEY NOT NULL,
title TEXT,
url TEXT,
published_at DATETIME,
Expand Down
9 changes: 5 additions & 4 deletions crates/blend-db/src/model/feed.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
use chrono::NaiveDateTime;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use sqlx::prelude::FromRow;
use ts_rs::TS;
use uuid::Uuid;

#[derive(TS)]
#[ts(export, export_to = "../../../ui/src/types/bindings/feed.ts")]
#[derive(Debug, Serialize, Deserialize, FromRow)]
pub struct Feed {
pub id: i64,
pub uuid: Uuid,
pub url: Option<String>,
pub title: Option<String>,
pub published_at: Option<NaiveDateTime>,
pub updated_at: Option<NaiveDateTime>,
pub published_at: Option<DateTime<Utc>>,
pub updated_at: Option<DateTime<Utc>>,
}
51 changes: 26 additions & 25 deletions crates/blend-db/src/repo/feed.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
use crate::{
error::{DbError, DbResult},
model,
};
use crate::{error::DbResult, model};
use chrono::{DateTime, Utc};

const COLUMNS: &str = r#"uuid, url, title, published_at, updated_at"#;

pub struct FeedRepo {
ctx: blend_context::Context,
}
Expand All @@ -21,40 +20,42 @@ impl FeedRepo {
}

pub async fn get_feeds(&self) -> DbResult<Vec<model::Feed>> {
sqlx::query_as!(model::Feed, "SELECT * FROM feeds")
let query = format!("SELECT {} FROM feeds", COLUMNS);

sqlx::query_as::<_, model::Feed>(&query)
.fetch_all(&self.ctx.db)
.await
.map_err(|err| err.into())
}

pub async fn get_feed(&self, id: i64) -> DbResult<Option<model::Feed>> {
sqlx::query_as!(model::Feed, "SELECT * FROM feeds WHERE id = ?1", id)
pub async fn get_feed(&self, uuid: uuid::Uuid) -> DbResult<Option<model::Feed>> {
let query = format!("SELECT {} FROM feeds WHERE uuid = ?1", COLUMNS);

sqlx::query_as::<_, model::Feed>(&query)
.bind(uuid)
.fetch_optional(&self.ctx.db)
.await
.map_err(|err| err.into())
}

pub async fn create_feed(&self, data: CreateFeedParams) -> DbResult<model::Feed> {
let mut conn = self.ctx.db.acquire().await?;

let id: i64 = sqlx::query!(
let query = format!(
r#"
INSERT INTO feeds ( title, url, published_at, updated_at )
VALUES ( ?1, ?2, ?3, ?4 )
INSERT INTO feeds ( uuid, title, url, published_at, updated_at )
VALUES ( ?1, ?2, ?3, ?4, ?5 )
RETURNING {}
"#,
data.title,
data.url,
data.published_at,
data.updated_at,
)
.execute(&mut *conn)
.await?
.last_insert_rowid();

let feed = self
.get_feed(id)
.await?
.ok_or_else(|| DbError::CouldNotFetchRowAfterInsertion)?;
COLUMNS
);

let feed = sqlx::query_as::<_, model::Feed>(&query)
.bind(uuid::Uuid::new_v4())
.bind(data.title)
.bind(data.url)
.bind(data.published_at)
.bind(data.updated_at)
.fetch_one(&self.ctx.db)
.await?;

Ok(feed)
}
Expand Down
2 changes: 2 additions & 0 deletions crates/blend-parse/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,7 @@ pub mod error;
pub async fn parse_url(url: &str) -> ParseResult<Feed> {
let res = reqwest::get(url).await?.text().await?;
let feed = feed_rs::parser::parse(res.as_bytes())?;

// TODO: create Feed entity and map all necessary fields from the feed_rs model
Ok(feed)
}
1 change: 1 addition & 0 deletions crates/blend-web/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,5 @@ tokio = { workspace = true, features = ["macros", "rt-multi-thread"] }
tower-cookies = "0.10"
tower-http = { version = "0.5", features = ["trace", "cors", "fs"] }
tracing = { workspace = true }
uuid = { workspace = true }
validator = { version = "0.18", features = ["derive"] }
35 changes: 30 additions & 5 deletions crates/blend-web/src/router/feed.rs
Original file line number Diff line number Diff line change
@@ -1,19 +1,21 @@
use crate::error::WebResult;
use crate::error::{WebError, WebResult};
use axum::{
extract::State,
extract::{Path, State},
response::IntoResponse,
routing::{get, post},
Json, Router,
};
use blend_db::repo;
use serde::Deserialize;
use serde_json::json;
use uuid::Uuid;
use validator::Validate;

pub fn router(ctx: blend_context::Context) -> Router {
Router::new()
.route("/", get(index))
.route("/", post(add))
.route("/", post(create))
.route("/:uuid", get(view))
// .route_layer(middleware::from_fn_with_state(
// ctx.clone(),
// crate::middleware::auth::middleware,
Expand All @@ -32,21 +34,44 @@ struct AddFeedParams {
url: String,
}

async fn add(
async fn create(
State(ctx): State<blend_context::Context>,
Json(data): Json<AddFeedParams>,
) -> WebResult<impl IntoResponse> {
data.validate()?;

let parsed = blend_parse::parse_url(&data.url).await?;

let link = parsed
.links
.iter()
.find(|link| link.rel.as_ref().is_some_and(|rel| rel == "self"));

let feed = repo::feed::FeedRepo::new(ctx)
.create_feed(repo::feed::CreateFeedParams {
title: parsed.title.map(|title| title.content),
url: Some("https://blog.rust-lang.org/feed.xml".to_string()),
url: link.map(|link| link.href.clone()),
published_at: parsed.published,
updated_at: parsed.updated,
})
.await?;

Ok(Json(json!({ "data": feed })))
}

#[derive(Deserialize)]
struct ViewFeedParams {
uuid: Uuid,
}

async fn view(
State(ctx): State<blend_context::Context>,
Path(params): Path<ViewFeedParams>,
) -> WebResult<impl IntoResponse> {
let feed = repo::feed::FeedRepo::new(ctx)
.get_feed(params.uuid)
.await?
.ok_or_else(|| WebError::NotFoundError)?;

Ok(Json(json!({ "data": feed })))
}
11 changes: 8 additions & 3 deletions ui/src/api/feeds.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,15 @@ export const getFeeds = async () => {
};

export const addFeed = async (params: { url: string }) => {
type Response = ApiResponse<{
title: string;
}>;
type Response = ApiResponse<Feed>;

const res = await axios.post<Response>(apiUrl('/feeds'), params);
return res.data.data;
};

export const getFeed = async (uuid: string) => {
type Response = ApiResponse<Feed>;

const res = await axios.get<Response>(apiUrl(`/feeds/${uuid}`));
return res.data.data;
};
24 changes: 11 additions & 13 deletions ui/src/components/modals/create-feed.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,18 @@ import { QUERY_KEYS } from '~/constants/query';
import { inputClass } from '~/constants/ui/input';
import { Button } from '../ui/button';
import { cx } from 'class-variance-authority';
import { useNavigate } from '@solidjs/router';
import { Spinner } from '../ui/spinner';

type CreateFeedProps = {
triggerClass?: string;
};

export const CreateFeed: Component<CreateFeedProps> = ({ triggerClass }) => {
const queryClient = useQueryClient();
const navigate = useNavigate();

const [open, setOpen] = createSignal(false);
const [value, setValue] = createSignal('https://blog.rust-lang.org/feed.xml');
const [inputElement, setInputElement] = createSignal<HTMLDivElement>();

Expand All @@ -29,22 +33,20 @@ export const CreateFeed: Component<CreateFeedProps> = ({ triggerClass }) => {

if (!value()) return;

await add.mutateAsync({ url: value() });
const feed = await add.mutateAsync({ url: value() });
queryClient.invalidateQueries({ queryKey: [QUERY_KEYS.FEEDS] });
navigate(`/feeds/${feed.uuid}`);
setOpen(false);
};

const handleOpenAutoFocus = (event: Event) => {
event.preventDefault();
inputElement()?.focus();
};

const handleOpenChange = (open: boolean) => {
if (open) add.reset();
};

return (
<>
<Dialog.Root onOpenChange={handleOpenChange}>
<Dialog.Root open={open()} onOpenChange={setOpen}>
<Dialog.Trigger asChild>
<As component={Button} class={cx('inline-flex items-center gap-2 text-sm', triggerClass)} size="sm">
Add feed
Expand All @@ -53,11 +55,11 @@ export const CreateFeed: Component<CreateFeedProps> = ({ triggerClass }) => {
</Dialog.Trigger>

<Dialog.Portal>
<Dialog.Overlay class="animate-overlayHide ui-expanded:animate-overlayShow fixed inset-0 z-50 bg-black/25 backdrop-blur" />
<Dialog.Overlay class="fixed inset-0 z-50 animate-overlayHide bg-black/25 backdrop-blur ui-expanded:animate-overlayShow" />

<div class="fixed inset-0 z-50 flex items-end justify-center p-8 sm:items-center">
<Dialog.Content
class="animate-contentHide ui-expanded:animate-contentShow z-50 w-full overflow-hidden rounded-lg border border-gray-200 bg-white shadow-lg transition-all md:max-w-sm"
class="z-50 w-full animate-contentHide overflow-hidden rounded-lg border border-gray-200 bg-white shadow-lg transition-all ui-expanded:animate-contentShow md:max-w-sm"
onOpenAutoFocus={handleOpenAutoFocus}
>
<div class="flex flex-col gap-2 border-b bg-gray-50 p-4">
Expand Down Expand Up @@ -91,16 +93,12 @@ export const CreateFeed: Component<CreateFeedProps> = ({ triggerClass }) => {

<Switch>
<Match when={add.isPending}>
<p>Loading...</p>
<Spinner />
</Match>

<Match when={add.isError}>
<p>Error: {add.error?.message}</p>
</Match>

<Match when={add.isSuccess}>
<pre class="w-full overflow-x-auto">{JSON.stringify(add.data, null, 2)}</pre>
</Match>
</Switch>
</div>
</Dialog.Content>
Expand Down
Loading

0 comments on commit 704abdd

Please sign in to comment.