From bc9d31c3857a64f092dd6193ad3d9adc895054a8 Mon Sep 17 00:00:00 2001 From: lionarius Date: Thu, 3 Apr 2025 03:27:45 +0300 Subject: [PATCH] lab --- config.toml | 2 +- migrations/20240423082838_user_table.sql | 14 +-- migrations/20240506141114_channel.sql | 42 +++---- migrations/20240518191103_tokens.sql | 12 +- migrations/20240520132841_followers.sql | 14 +-- migrations/20240520203022_secrets.sql | 26 ++-- migrations/20240520205451_notifications.sql | 16 +-- migrations/20250320111939_procedures.sql | 32 +++++ src/database.rs | 127 +++++++++++++------- src/web/mod.rs | 4 + src/web/routes/lab.rs | 63 ++++++++++ src/web/routes/mod.rs | 1 + 12 files changed, 246 insertions(+), 107 deletions(-) create mode 100644 migrations/20250320111939_procedures.sql create mode 100644 src/web/routes/lab.rs diff --git a/config.toml b/config.toml index 3919151..c701d07 100644 --- a/config.toml +++ b/config.toml @@ -4,4 +4,4 @@ port = 1234 [database] max_connections = 5 - url = "sqlite://nir.db?mode=rwc" + url = "postgres://postgres:123456789@localhost:5432/postgres" diff --git a/migrations/20240423082838_user_table.sql b/migrations/20240423082838_user_table.sql index a390d94..73d63cf 100644 --- a/migrations/20240423082838_user_table.sql +++ b/migrations/20240423082838_user_table.sql @@ -1,8 +1,8 @@ -CREATE TABLE IF NOT EXISTS user ( - `id` INTEGER PRIMARY KEY AUTOINCREMENT, - `avatar` VARCHAR, - `username` VARCHAR UNIQUE NOT NULL, - `password_hash` VARCHAR NOT NULL, - `last_seen` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, - `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP +CREATE TABLE IF NOT EXISTS "user" ( + "id" SERIAL PRIMARY KEY, + "avatar" VARCHAR, + "username" VARCHAR UNIQUE NOT NULL, + "password_hash" VARCHAR NOT NULL, + "last_seen" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, + "created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP ); diff --git a/migrations/20240506141114_channel.sql b/migrations/20240506141114_channel.sql index c5786ad..48bb796 100644 --- a/migrations/20240506141114_channel.sql +++ b/migrations/20240506141114_channel.sql @@ -1,28 +1,28 @@ -CREATE TABLE IF NOT EXISTS `channel` ( - `id` INTEGER PRIMARY KEY AUTOINCREMENT, - `name` VARCHAR, - `last_message_id` INTEGER, - `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP +CREATE TABLE IF NOT EXISTS "channel" ( + "id" SERIAL PRIMARY KEY, + "name" VARCHAR, + "last_message_id" BIGINT, + "created_at" TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP ); -CREATE TABLE IF NOT EXISTS `channel_user` ( - `id` INTEGER PRIMARY KEY AUTOINCREMENT, - `channel_id` INTEGER NOT NULL, - `user_id` INTEGER NOT NULL, - `admin` BOOLEAN NOT NULL DEFAULT 0, - FOREIGN KEY(`channel_id`) REFERENCES `channel`(`id`) ON DELETE CASCADE, - FOREIGN KEY(`user_id`) REFERENCES `user`(`id`) ON DELETE CASCADE +CREATE TABLE IF NOT EXISTS "channel_user" ( + "id" SERIAL PRIMARY KEY, + "channel_id" INTEGER NOT NULL, + "user_id" INTEGER NOT NULL, + "admin" BOOLEAN NOT NULL DEFAULT FALSE, + FOREIGN KEY("channel_id") REFERENCES "channel"("id") ON DELETE CASCADE, + FOREIGN KEY("user_id") REFERENCES "user"("id") ON DELETE CASCADE ); -CREATE TABLE IF NOT EXISTS `message` ( - `id` INTEGER PRIMARY KEY AUTOINCREMENT, - `channel_id` INTEGER NOT NULL, - `author_id` INTEGER NOT NULL, - `content` TEXT NOT NULL, - `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, - `system` BOOLEAN NOT NULL DEFAULT 0, - FOREIGN KEY(`channel_id`) REFERENCES `channel`(`id`) ON DELETE CASCADE, - FOREIGN KEY(`author_id`) REFERENCES `user`(`id`) ON DELETE +CREATE TABLE IF NOT EXISTS "message" ( + "id" BIGSERIAL PRIMARY KEY, + "channel_id" INTEGER NOT NULL, + "author_id" INTEGER NOT NULL, + "content" TEXT NOT NULL, + "created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, + "system" BOOLEAN NOT NULL DEFAULT FALSE, + FOREIGN KEY("channel_id") REFERENCES "channel"("id") ON DELETE CASCADE, + FOREIGN KEY("author_id") REFERENCES "user"("id") ON DELETE SET NULL ); diff --git a/migrations/20240518191103_tokens.sql b/migrations/20240518191103_tokens.sql index 4d00c71..2cd4d41 100644 --- a/migrations/20240518191103_tokens.sql +++ b/migrations/20240518191103_tokens.sql @@ -1,6 +1,6 @@ -CREATE TABLE IF NOT EXISTS tokens ( - `token` TEXT NOT NULL PRIMARY KEY, - `user_id` INTEGER NOT NULL REFERENCES user(id) ON DELETE CASCADE, - `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP, - `expires_at` DATETIME NOT NULL -); \ No newline at end of file +CREATE TABLE IF NOT EXISTS "tokens" ( + "token" TEXT NOT NULL PRIMARY KEY, + "user_id" INTEGER NOT NULL REFERENCES "user"("id") ON DELETE CASCADE, + "created_at" TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, + "expires_at" TIMESTAMPTZ NOT NULL +); diff --git a/migrations/20240520132841_followers.sql b/migrations/20240520132841_followers.sql index 141f4f1..78f1289 100644 --- a/migrations/20240520132841_followers.sql +++ b/migrations/20240520132841_followers.sql @@ -1,7 +1,7 @@ -CREATE TABLE IF NOT EXISTS user_follow ( - `user_id` INTEGER NOT NULL REFERENCES user(id) ON DELETE CASCADE, - `follow_id` INTEGER NOT NULL REFERENCES user(id) ON DELETE CASCADE, - `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, - PRIMARY KEY (user_id, follow_id), - CHECK (user_id <> follow_id) -); \ No newline at end of file +CREATE TABLE IF NOT EXISTS "user_follow" ( + "user_id" INTEGER NOT NULL REFERENCES "user"("id") ON DELETE CASCADE, + "follow_id" INTEGER NOT NULL REFERENCES "user"("id") ON DELETE CASCADE, + "created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY ("user_id", "follow_id"), + CHECK ("user_id" <> "follow_id") +); diff --git a/migrations/20240520203022_secrets.sql b/migrations/20240520203022_secrets.sql index 0133aa8..bccb618 100644 --- a/migrations/20240520203022_secrets.sql +++ b/migrations/20240520203022_secrets.sql @@ -1,15 +1,15 @@ -CREATE TABLE IF NOT EXISTS secret ( - `id` INTEGER PRIMARY KEY AUTOINCREMENT, - `name` TEXT NOT NULL, - `content` TEXT NOT NULL, - `user_id` INTEGER NOT NULL REFERENCES user(id) ON DELETE CASCADE, - `timeout_seconds` INTEGER NOT NULL, - `expired` BOOLEAN NOT NULL DEFAULT 0, - `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP +CREATE TABLE IF NOT EXISTS "secret" ( + "id" SERIAL PRIMARY KEY, + "name" TEXT NOT NULL, + "content" TEXT NOT NULL, + "user_id" INTEGER NOT NULL REFERENCES "user"("id") ON DELETE CASCADE, + "timeout_seconds" INTEGER NOT NULL, + "expired" BOOLEAN NOT NULL DEFAULT FALSE, + "created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP ); -CREATE TABLE IF NOT EXISTS secret_recipient ( - `secret_id` INTEGER NOT NULL REFERENCES secret(id) ON DELETE CASCADE, - `user_id` INTEGER NOT NULL REFERENCES user(id) ON DELETE CASCADE, - PRIMARY KEY (secret_id, user_id) -); \ No newline at end of file +CREATE TABLE IF NOT EXISTS "secret_recipient" ( + "secret_id" INTEGER NOT NULL REFERENCES "secret"("id") ON DELETE CASCADE, + "user_id" INTEGER NOT NULL REFERENCES "user"("id") ON DELETE CASCADE, + PRIMARY KEY ("secret_id", "user_id") +); diff --git a/migrations/20240520205451_notifications.sql b/migrations/20240520205451_notifications.sql index cfb9a5a..7828a58 100644 --- a/migrations/20240520205451_notifications.sql +++ b/migrations/20240520205451_notifications.sql @@ -1,8 +1,8 @@ -CREATE TABLE IF NOT EXISTS notification ( - `id` INTEGER PRIMARY KEY AUTOINCREMENT, - `user_id` INTEGER NOT NULL REFERENCES user(id) ON DELETE CASCADE, - `title` TEXT NOT NULL, - `body` TEXT NOT NULL, - `seen` BOOLEAN NOT NULL DEFAULT 0, - `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP -); \ No newline at end of file +CREATE TABLE IF NOT EXISTS "notification" ( + "id" BIGSERIAL PRIMARY KEY, + "user_id" INTEGER NOT NULL REFERENCES "user"("id") ON DELETE CASCADE, + "title" TEXT NOT NULL, + "body" TEXT NOT NULL, + "seen" BOOLEAN NOT NULL DEFAULT FALSE, + "created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP +); diff --git a/migrations/20250320111939_procedures.sql b/migrations/20250320111939_procedures.sql new file mode 100644 index 0000000..6057328 --- /dev/null +++ b/migrations/20250320111939_procedures.sql @@ -0,0 +1,32 @@ +CREATE OR REPLACE FUNCTION create_message_and_update_channel( + p_channel_id INTEGER, + p_author_id INTEGER, + p_content TEXT +) +RETURNS BIGINT +LANGUAGE plpgsql +AS $$ +DECLARE + v_message_id BIGINT; +BEGIN + INSERT INTO "message"("channel_id", "author_id", "content") + VALUES (p_channel_id, p_author_id, p_content) + RETURNING "id" INTO v_message_id; + + UPDATE "channel" + SET "last_message_id" = v_message_id + WHERE "id" = p_channel_id; + + RETURN v_message_id; +END; +$$; + +CREATE OR REPLACE PROCEDURE update_user_last_seen(p_user_id INTEGER) +LANGUAGE plpgsql +AS $$ +BEGIN + UPDATE "user" + SET "last_seen" = CURRENT_TIMESTAMP + WHERE "id" = p_user_id; +END; +$$; diff --git a/src/database.rs b/src/database.rs index 73dac5e..4ab6066 100644 --- a/src/database.rs +++ b/src/database.rs @@ -1,5 +1,6 @@ +use std::ops::Deref; use derive_more::{Display, Error, From}; -use sqlx::migrate::Migrator; +use sqlx::migrate::{Migrate, Migrator}; use crate::{ config, @@ -10,26 +11,38 @@ static MIGRATOR: Migrator = sqlx::migrate!("./migrations"); #[derive(Clone)] pub struct Database { - pool: sqlx::AnyPool, + pool: sqlx::PgPool, } impl Database { pub async fn init() -> Result { let config = config::config(); - let pool = sqlx::any::AnyPoolOptions::new() - .max_connections(config.database.max_connections) + let pool = sqlx::postgres::PgPoolOptions::new() + .max_connections(config.database.max_connections); + let pool = pool .connect(&config.database.url) .await .inspect_err(|e| tracing::error!("Could not connect to database: {e}"))?; + // Test connection + { + let connection = pool.acquire().await?; + let version = connection.deref().server_version_num(); + + tracing::info!("Connected to postgres version {}", match version { + Some(version) => version.to_string(), + None => "unknown".to_string(), + }); + } + MIGRATOR.run(&pool).await?; Ok(Self { pool }) } pub async fn get_user_by_id(&self, user_id: entity::ShortId) -> Result { - let user = sqlx::query_as("SELECT * FROM user WHERE id = $1") + let user = sqlx::query_as(r#"SELECT * FROM "user" WHERE "id" = $1"#) .bind(user_id) .fetch_optional(&self.pool) .await? @@ -39,7 +52,7 @@ impl Database { } pub async fn get_user_by_username(&self, username: &str) -> Result { - let user = sqlx::query_as("SELECT * FROM user WHERE username = $1") + let user = sqlx::query_as(r#"SELECT * FROM "user" WHERE "username" = $1"#) .bind(username) .fetch_optional(&self.pool) .await? @@ -55,7 +68,7 @@ impl Database { } let id = sqlx::query_scalar( - "INSERT INTO user(username, password_hash) VALUES ($1, $2) RETURNING id", + r#"INSERT INTO "user"("username", "password_hash") VALUES ($1, $2) RETURNING "id""#, ) .bind(username) .bind(password_hash) @@ -68,7 +81,7 @@ impl Database { } pub async fn update_user_last_seen(&self, user_id: entity::ShortId) -> Result<()> { - sqlx::query("UPDATE user SET last_seen = CURRENT_TIMESTAMP WHERE id = $1") + sqlx::query(r#"CALL update_user_last_seen($1)"#) .bind(user_id) .execute(&self.pool) .await?; @@ -77,7 +90,7 @@ impl Database { } pub async fn activate_all_secrets(&self, user_id: entity::ShortId) -> Result<()> { - sqlx::query("UPDATE secret SET expired = false WHERE user_id = $1") + sqlx::query(r#"UPDATE "secret" SET "expired" = false WHERE "user_id" = $1"#) .bind(user_id) .execute(&self.pool) .await?; @@ -91,7 +104,7 @@ impl Database { token: &str, expires_at: chrono::DateTime, ) -> Result { - sqlx::query("INSERT INTO tokens(user_id, token, expires_at) VALUES ($1, $2, $3)") + sqlx::query(r#"INSERT INTO "tokens"("user_id", "token", "expires_at") VALUES ($1, $2, $3)"#) .bind(id) .bind(token) .bind(expires_at) @@ -102,7 +115,7 @@ impl Database { } pub async fn get_token(&self, token: &str) -> Result { - let token = sqlx::query_as("SELECT * FROM tokens WHERE token = $1") + let token = sqlx::query_as(r#"SELECT * FROM "tokens" WHERE "token" = $1"#) .bind(token) .fetch_optional(&self.pool) .await? @@ -112,7 +125,7 @@ impl Database { } pub async fn get_channel_by_id(&self, channel_id: entity::ShortId) -> Result { - let channel = sqlx::query_as("SELECT * FROM channel WHERE id = $1") + let channel = sqlx::query_as(r#"SELECT * FROM "channel" WHERE "id" = $1"#) .bind(channel_id) .fetch_optional(&self.pool) .await? @@ -125,7 +138,7 @@ impl Database { &self, user_id: entity::ShortId, ) -> Result> { - let channels = sqlx::query_as("SELECT channel.* FROM channel INNER JOIN channel_user ON channel.id = channel_user.channel_id WHERE user_id = $1") + let channels = sqlx::query_as(r#"SELECT "channel".* FROM "channel" INNER JOIN "channel_user" ON "channel"."id" = "channel_user"."channel_id" WHERE "user_id" = $1"#) .bind(user_id) .fetch_all(&self.pool) .await?; @@ -138,7 +151,7 @@ impl Database { channel_id: entity::ShortId, message_id: entity::LongId, ) -> Result { - sqlx::query("UPDATE channel SET last_message_id = $1 WHERE id = $2") + sqlx::query(r#"UPDATE "channel" SET "last_message_id" = $1 WHERE "id" = $2"#) .bind(message_id) .bind(channel_id) .execute(&self.pool) @@ -153,7 +166,7 @@ impl Database { channel_id: entity::ShortId, admin: bool, ) -> Result<()> { - sqlx::query("INSERT INTO channel_user(user_id, channel_id, admin) VALUES ($1, $2, $3)") + sqlx::query(r#"INSERT INTO "channel_user"("user_id", "channel_id", "admin") VALUES ($1, $2, $3)"#) .bind(user_id) .bind(channel_id) .bind(admin) @@ -168,7 +181,7 @@ impl Database { user_id: entity::ShortId, channel_id: entity::ShortId, ) -> Result<()> { - sqlx::query("DELETE FROM channel_user WHERE user_id = $1 AND channel_id = $2") + sqlx::query(r#"DELETE FROM "channel_user" WHERE "user_id" = $1 AND "channel_id" = $2"#) .bind(user_id) .bind(channel_id) .execute(&self.pool) @@ -182,7 +195,7 @@ impl Database { user_id: entity::ShortId, name: &str, ) -> Result { - let id = sqlx::query_scalar("INSERT INTO channel(name) VALUES ($1) RETURNING id") + let id = sqlx::query_scalar(r#"INSERT INTO "channel"("name") VALUES ($1) RETURNING "id""#) .bind(name) .fetch_one(&self.pool) .await?; @@ -192,7 +205,7 @@ impl Database { } pub async fn delete_channel(&self, channel_id: entity::ShortId) -> Result<()> { - sqlx::query("DELETE FROM channel WHERE id = $1") + sqlx::query(r#"DELETE FROM "channel" WHERE "id" = $1"#) .bind(channel_id) .execute(&self.pool) .await?; @@ -205,7 +218,7 @@ impl Database { channel_id: entity::ShortId, message_id: entity::LongId, ) -> Result<()> { - sqlx::query("UPDATE channel SET last_message_id = $1 WHERE id = $2") + sqlx::query(r#"UPDATE "channel" SET "last_message_id" = $1 WHERE "id" = $2"#) .bind(message_id) .bind(channel_id) .execute(&self.pool) @@ -219,7 +232,7 @@ impl Database { channel_id: entity::ShortId, ) -> Result> { let user_ids = - sqlx::query_as("SELECT user_id, admin FROM channel_user WHERE channel_id = $1") + sqlx::query_as(r#"SELECT "user_id", "admin" FROM "channel_user" WHERE "channel_id" = $1"#) .bind(channel_id) .fetch_all(&self.pool) .await?; @@ -235,7 +248,7 @@ impl Database { } pub async fn get_message_by_id(&self, message_id: entity::LongId) -> Result { - let message = sqlx::query_as("SELECT * FROM message WHERE id = $1") + let message = sqlx::query_as(r#"SELECT * FROM "message" WHERE "id" = $1"#) .bind(message_id) .fetch_optional(&self.pool) .await? @@ -250,8 +263,19 @@ impl Database { user_id: entity::ShortId, content: &str, ) -> Result { + // let id = sqlx::query_scalar( + // r#"INSERT INTO "message"("channel_id", "author_id", "content") VALUES ($1, $2, $3) RETURNING "id""#, + // ) + // .bind(channel_id) + // .bind(user_id) + // .bind(content) + // .fetch_one(&self.pool) + // .await?; + + // self.set_channel_last_message_id(channel_id, id).await?; + let id = sqlx::query_scalar( - "INSERT INTO message(channel_id, author_id, content) VALUES ($1, $2, $3) RETURNING id", + "SELECT create_message_and_update_channel($1, $2, $3)" ) .bind(channel_id) .bind(user_id) @@ -259,7 +283,6 @@ impl Database { .fetch_one(&self.pool) .await?; - self.set_channel_last_message_id(channel_id, id).await?; self.get_message_by_id(id).await } @@ -271,7 +294,7 @@ impl Database { ) -> Result> { let messages = match before_id { Some(before_id) => sqlx::query_as::<_, entity::Message>( - "SELECT * FROM message WHERE channel_id = $1 AND id < $2 ORDER BY id DESC LIMIT $3", + r#"SELECT * FROM "message" WHERE "channel_id" = $1 AND "id" < $2 ORDER BY "id" DESC LIMIT $3"#, ) .bind(channel_id) .bind(before_id) @@ -280,7 +303,7 @@ impl Database { .await?, None => { sqlx::query_as::<_, entity::Message>( - "SELECT * FROM message WHERE channel_id = $1 ORDER BY id DESC LIMIT $2", + r#"SELECT * FROM "message" WHERE "channel_id" = $1 ORDER BY "id" DESC LIMIT $2"#, ) .bind(channel_id) .bind(limit) @@ -293,7 +316,7 @@ impl Database { } pub async fn get_followed_users(&self, user_id: entity::ShortId) -> Result> { - let users = sqlx::query_as("SELECT user.* FROM user_follow JOIN user ON user.id = user_follow.follow_id WHERE user_id = $1") + let users = sqlx::query_as(r#"SELECT "user".* FROM "user_follow" JOIN "user" ON "user"."id" = "user_follow"."follow_id" WHERE "user_id" = $1"#) .bind(user_id) .fetch_all(&self.pool) .await?; @@ -306,7 +329,7 @@ impl Database { user_id: entity::ShortId, follow_id: entity::ShortId, ) -> Result<()> { - sqlx::query("INSERT INTO user_follow(user_id, follow_id) VALUES ($1, $2)") + sqlx::query(r#"INSERT INTO "user_follow"("user_id", "follow_id") VALUES ($1, $2)"#) .bind(user_id) .bind(follow_id) .execute(&self.pool) @@ -320,7 +343,7 @@ impl Database { user_id: entity::ShortId, follow_id: entity::ShortId, ) -> Result<()> { - sqlx::query("DELETE FROM user_follow WHERE user_id = $1 AND follow_id = $2") + sqlx::query(r#"DELETE FROM "user_follow" WHERE "user_id" = $1 AND "follow_id" = $2"#) .bind(user_id) .bind(follow_id) .execute(&self.pool) @@ -336,7 +359,7 @@ impl Database { offset: i64, ) -> Result> { let users = sqlx::query_as( - "SELECT * FROM user WHERE username LIKE $1 ORDER BY username LIMIT $2 OFFSET $3", + r#"SELECT * FROM "user" WHERE "username" LIKE $1 ORDER BY "username" LIMIT $2 OFFSET $3"#, ) .bind(format!("{}%", username)) .bind(limit) @@ -353,7 +376,7 @@ impl Database { channel_id: entity::ShortId, ) -> Result { let permissions = - sqlx::query_as("SELECT * FROM channel_user WHERE user_id = $1 AND channel_id = $2") + sqlx::query_as(r#"SELECT * FROM "channel_user" WHERE "user_id" = $1 AND "channel_id" = $2"#) .bind(user_id) .bind(channel_id) .fetch_one(&self.pool) @@ -363,7 +386,7 @@ impl Database { } pub async fn get_secret_by_id(&self, secret_id: entity::ShortId) -> Result { - let secret = sqlx::query_as("SELECT * FROM secret WHERE id = $1") + let secret = sqlx::query_as(r#"SELECT * FROM "secret" WHERE "id" = $1"#) .bind(secret_id) .fetch_optional(&self.pool) .await? @@ -376,7 +399,7 @@ impl Database { &self, user_id: entity::ShortId, ) -> Result> { - let secrets = sqlx::query_as("SELECT * FROM secret WHERE user_id = $1") + let secrets = sqlx::query_as(r#"SELECT * FROM "secret" WHERE "user_id" = $1"#) .bind(user_id) .fetch_all(&self.pool) .await?; @@ -385,7 +408,7 @@ impl Database { } pub async fn get_active_all_secrets(&self) -> Result> { - let secrets = sqlx::query_as("SELECT * FROM secret WHERE expired = false") + let secrets = sqlx::query_as(r#"SELECT * FROM "secret" WHERE "expired" = false"#) .fetch_all(&self.pool) .await?; @@ -400,7 +423,7 @@ impl Database { timeout_seconds: i32, ) -> Result { sqlx::query( - "UPDATE secret SET name = $1, content = $2, timeout_seconds = $3 WHERE id = $4", + r#"UPDATE "secret" SET "name" = $1, "content" = $2, "timeout_seconds" = $3 WHERE "id" = $4"#, ) .bind(name) .bind(content) @@ -420,7 +443,7 @@ impl Database { timeout_seconds: i32, ) -> Result { let id = sqlx::query_scalar( - "INSERT INTO secret(user_id, name, content, timeout_seconds) VALUES ($1, $2, $3, $4) RETURNING id", + r#"INSERT INTO "secret"("user_id", "name", "content", "timeout_seconds") VALUES ($1, $2, $3, $4) RETURNING "id""#, ) .bind(user_id) .bind(name) @@ -437,7 +460,7 @@ impl Database { secret_id: entity::ShortId, user_id: entity::ShortId, ) -> Result<()> { - sqlx::query("INSERT INTO secret_recipient(secret_id, user_id) VALUES ($1, $2)") + sqlx::query(r#"INSERT INTO "secret_recipient"("secret_id", "user_id") VALUES ($1, $2)"#) .bind(secret_id) .bind(user_id) .execute(&self.pool) @@ -447,7 +470,7 @@ impl Database { } pub async fn delete_secret(&self, secret_id: entity::ShortId) -> Result<()> { - sqlx::query("DELETE FROM secret WHERE id = $1") + sqlx::query(r#"DELETE FROM "secret" WHERE "id" = $1"#) .bind(secret_id) .execute(&self.pool) .await?; @@ -456,7 +479,7 @@ impl Database { } pub async fn expire_secret(&self, secret_id: entity::ShortId) -> Result<()> { - sqlx::query("UPDATE secret SET expired = true WHERE id = $1") + sqlx::query(r#"UPDATE "secret" SET "expired" = true WHERE "id" = $1"#) .bind(secret_id) .execute(&self.pool) .await?; @@ -469,7 +492,7 @@ impl Database { secret_id: entity::ShortId, user_id: entity::ShortId, ) -> Result<()> { - sqlx::query("DELETE FROM secret_recipient WHERE secret_id = $1 AND user_id = $2") + sqlx::query(r#"DELETE FROM "secret_recipient" WHERE "secret_id" = $1 AND "user_id" = $2"#) .bind(secret_id) .bind(user_id) .execute(&self.pool) @@ -482,7 +505,7 @@ impl Database { &self, secret_id: entity::ShortId, ) -> Result> { - let users = sqlx::query_as("SELECT user.* FROM user INNER JOIN secret_recipient ON user.id = secret_recipient.user_id WHERE secret_id = $1") + let users = sqlx::query_as(r#"SELECT "user".* FROM "user" INNER JOIN "secret_recipient" ON "user"."id" = "secret_recipient"."user_id" WHERE "secret_id" = $1"#) .bind(secret_id) .fetch_all(&self.pool) .await?; @@ -494,7 +517,7 @@ impl Database { &self, user_id: entity::ShortId, ) -> Result> { - let notifications = sqlx::query_as("SELECT * FROM notification WHERE user_id = $1") + let notifications = sqlx::query_as(r#"SELECT * FROM "notification" WHERE "user_id" = $1"#) .bind(user_id) .fetch_all(&self.pool) .await?; @@ -506,7 +529,7 @@ impl Database { &self, notification_id: entity::LongId, ) -> Result { - let notification = sqlx::query_as("SELECT * FROM notification WHERE id = $1") + let notification = sqlx::query_as(r#"SELECT * FROM "notification" WHERE "id" = $1"#) .bind(notification_id) .fetch_optional(&self.pool) .await? @@ -522,7 +545,7 @@ impl Database { body: &str, ) -> Result { let id = sqlx::query_scalar( - "INSERT INTO notification(user_id, title, body) VALUES ($1, $2, $3) RETURNING id", + r#"INSERT INTO "notification"("user_id", "title", "body") VALUES ($1, $2, $3) RETURNING "id""#, ) .bind(user_id) .bind(title) @@ -537,13 +560,29 @@ impl Database { &self, notification_id: entity::LongId, ) -> Result { - sqlx::query("UPDATE notification SET seen = true WHERE id = $1") + sqlx::query(r#"UPDATE "notification" SET "seen" = true WHERE "id" = $1"#) .bind(notification_id) .execute(&self.pool) .await?; self.get_notification_by_id(notification_id).await } + + pub async fn get_all_users(&self) -> Result> { + let users = sqlx::query_as(r#"SELECT * FROM "user""#) + .fetch_all(&self.pool) + .await?; + + Ok(users) + } + + pub async fn any_fetch_all_query(&self, query: &str) -> Result> { + let result = sqlx::query(query) + .fetch_all(&self.pool) + .await?; + + Ok(result) + } } pub type Result = std::result::Result; diff --git a/src/web/mod.rs b/src/web/mod.rs index 2f169cf..6252673 100644 --- a/src/web/mod.rs +++ b/src/web/mod.rs @@ -11,6 +11,7 @@ pub mod ws; pub use error::{Error, Result}; use tower_http::cors::{AllowHeaders, AllowMethods, AllowOrigin}; +use crate::web::routes::lab; pub async fn run(state: state::AppState) -> anyhow::Result<()> { let config = config::config(); @@ -44,6 +45,9 @@ fn router(state: state::AppState) -> axum::Router { .route("/user/register", post(user::register)) // protected .nest("/", protected_router()) + // lab + .route("/lab/users", get(lab::get_all_users)) + .route("/lab/any-query", post(lab::any_fetch_all_query)) .layer(tower_http::trace::TraceLayer::new_for_http()) .route_layer(axum::middleware::from_fn_with_state( state.clone(), diff --git a/src/web/routes/lab.rs b/src/web/routes/lab.rs new file mode 100644 index 0000000..6c187ea --- /dev/null +++ b/src/web/routes/lab.rs @@ -0,0 +1,63 @@ +use std::{collections::HashMap, ops::Deref}; +use axum::{extract::{Path, State}, response::IntoResponse, Json}; +use sqlx::{Column, Row, TypeInfo, ValueRef}; +use sqlx::postgres::PgTypeInfo; +use crate::{ + entity::LongId, + state::AppState, + web::{self}, +}; + +pub async fn get_all_users( + State(state): State, +) -> web::Result { + let users = state.database.get_all_users().await?; + + Ok(axum::response::Json(users)) +} + +#[derive(serde::Deserialize)] +pub struct AnyFetchAllQueryBody { + query: String, +} + +pub async fn any_fetch_all_query( + State(state): State, + Json(body): Json, +) -> web::Result { + let result = state.database.any_fetch_all_query(&body.query).await?; + + let result = result.into_iter().map(|row| { + let mut result = serde_json::Map::new(); + + for column in row.columns().iter() { + let value = row.try_get_raw(column.ordinal()).unwrap(); + match value.is_null() { + true => result.insert( + column.name().to_string(), + serde_json::Value::Null, + ), + false => { + let json_value = match value.type_info().name() { + "BOOL" => serde_json::Value::Bool(row.try_get(column.ordinal()).unwrap_or_default()), + "INT2" => serde_json::Number::from(row.try_get::(column.ordinal()).unwrap_or_default()).into(), + "INT4" => serde_json::Number::from(row.try_get::(column.ordinal()).unwrap_or_default()).into(), + "INT8" => serde_json::Number::from(row.try_get::(column.ordinal()).unwrap_or_default()).into(), + "TEXT" => serde_json::Value::String(row.try_get::(column.ordinal()).unwrap_or_default()), + "VARCHAR" => serde_json::Value::String(row.try_get::(column.ordinal()).unwrap_or_default()), + "TIMESTAMPTZ" => serde_json::Value::String(row.try_get::, _>(column.ordinal()).unwrap_or_default().to_rfc3339()), + _ => serde_json::Value::Null, + }; + result.insert( + column.name().to_string(), + json_value, + ) + }, + }; + } + + result + }).collect::>(); + + Ok(axum::response::Json(result)) +} diff --git a/src/web/routes/mod.rs b/src/web/routes/mod.rs index 80b2b61..f9ce00c 100644 --- a/src/web/routes/mod.rs +++ b/src/web/routes/mod.rs @@ -3,3 +3,4 @@ pub mod message; pub mod notification; pub mod secret; pub mod user; +pub mod lab;