init
This commit is contained in:
1
ticktalk/crates/db/.env
Executable file
1
ticktalk/crates/db/.env
Executable file
@@ -0,0 +1 @@
|
||||
DATABASE_URL=postgres://ticktalk_user:ticktack@localhost:5432/ticktalk_db
|
||||
15
ticktalk/crates/db/Cargo.toml
Executable file
15
ticktalk/crates/db/Cargo.toml
Executable file
@@ -0,0 +1,15 @@
|
||||
[package]
|
||||
name = "ticktalk-db"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
chrono = { workspace = true }
|
||||
bb8 = "0.9.0"
|
||||
diesel = { version = "2.2.0", features = ["postgres", "chrono", "uuid"] }
|
||||
diesel-async = { version = "0.7.4", features = ["postgres", "pool", "bb8"] }
|
||||
diesel-derive-enum = { version = "2.1.0", features = ["postgres"] }
|
||||
diesel_migrations = "2.3.0"
|
||||
thiserror = { workspace = true }
|
||||
uuid = { workspace = true }
|
||||
dotenv = "0.15.0"
|
||||
9
ticktalk/crates/db/diesel.toml
Executable file
9
ticktalk/crates/db/diesel.toml
Executable file
@@ -0,0 +1,9 @@
|
||||
# For documentation on how to configure this file,
|
||||
# see https://diesel.rs/guides/configuring-diesel-cli
|
||||
|
||||
[print_schema]
|
||||
file = "src/schema.rs"
|
||||
custom_type_derives = ["diesel::query_builder::QueryId", "Clone"]
|
||||
|
||||
[migrations_directory]
|
||||
dir = "migrations"
|
||||
0
ticktalk/crates/db/migrations/.diesel_lock
Executable file
0
ticktalk/crates/db/migrations/.diesel_lock
Executable file
0
ticktalk/crates/db/migrations/.keep
Executable file
0
ticktalk/crates/db/migrations/.keep
Executable file
@@ -0,0 +1,6 @@
|
||||
-- This file was automatically created by Diesel to setup helper functions
|
||||
-- and other internal bookkeeping. This file is safe to edit, any future
|
||||
-- changes will be added to existing projects as new migrations.
|
||||
|
||||
DROP FUNCTION IF EXISTS diesel_manage_updated_at(_tbl regclass);
|
||||
DROP FUNCTION IF EXISTS diesel_set_updated_at();
|
||||
36
ticktalk/crates/db/migrations/00000000000000_diesel_initial_setup/up.sql
Executable file
36
ticktalk/crates/db/migrations/00000000000000_diesel_initial_setup/up.sql
Executable file
@@ -0,0 +1,36 @@
|
||||
-- This file was automatically created by Diesel to setup helper functions
|
||||
-- and other internal bookkeeping. This file is safe to edit, any future
|
||||
-- changes will be added to existing projects as new migrations.
|
||||
|
||||
|
||||
|
||||
|
||||
-- Sets up a trigger for the given table to automatically set a column called
|
||||
-- `updated_at` whenever the row is modified (unless `updated_at` was included
|
||||
-- in the modified columns)
|
||||
--
|
||||
-- # Example
|
||||
--
|
||||
-- ```sql
|
||||
-- CREATE TABLE users (id SERIAL PRIMARY KEY, updated_at TIMESTAMP NOT NULL DEFAULT NOW());
|
||||
--
|
||||
-- SELECT diesel_manage_updated_at('users');
|
||||
-- ```
|
||||
CREATE OR REPLACE FUNCTION diesel_manage_updated_at(_tbl regclass) RETURNS VOID AS $$
|
||||
BEGIN
|
||||
EXECUTE format('CREATE TRIGGER set_updated_at BEFORE UPDATE ON %s
|
||||
FOR EACH ROW EXECUTE PROCEDURE diesel_set_updated_at()', _tbl);
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
CREATE OR REPLACE FUNCTION diesel_set_updated_at() RETURNS trigger AS $$
|
||||
BEGIN
|
||||
IF (
|
||||
NEW IS DISTINCT FROM OLD AND
|
||||
NEW.updated_at IS NOT DISTINCT FROM OLD.updated_at
|
||||
) THEN
|
||||
NEW.updated_at := current_timestamp;
|
||||
END IF;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
3
ticktalk/crates/db/migrations/2025-12-10-190915-0000_setup_db/down.sql
Executable file
3
ticktalk/crates/db/migrations/2025-12-10-190915-0000_setup_db/down.sql
Executable file
@@ -0,0 +1,3 @@
|
||||
DROP TABLE IF EXISTS messages;
|
||||
DROP TABLE IF EXISTS chats;
|
||||
DROP TABLE IF EXISTS users;
|
||||
28
ticktalk/crates/db/migrations/2025-12-10-190915-0000_setup_db/up.sql
Executable file
28
ticktalk/crates/db/migrations/2025-12-10-190915-0000_setup_db/up.sql
Executable file
@@ -0,0 +1,28 @@
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
username VARCHAR(100) UNIQUE NOT NULL,
|
||||
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS chats (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
first_user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
second_user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||
UNIQUE(first_user_id, second_user_id),
|
||||
CHECK (first_user_id != second_user_id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS messages (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
sender_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
recipient_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
chat_id UUID NOT NULL REFERENCES chats(id) ON DELETE CASCADE,
|
||||
content TEXT NOT NULL,
|
||||
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP NOT NULL
|
||||
);
|
||||
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_messages_chat_id ON messages(chat_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_messages_sender_id ON messages(sender_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_messages_chat_created ON messages(chat_id, created_at);
|
||||
25
ticktalk/crates/db/src/errors.rs
Executable file
25
ticktalk/crates/db/src/errors.rs
Executable file
@@ -0,0 +1,25 @@
|
||||
use diesel_async::pooled_connection::bb8::RunError;
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum RepositoryError {
|
||||
#[error("Database error: {0}")]
|
||||
Query(#[from] diesel::result::Error),
|
||||
|
||||
#[error("Connection error: {0}")]
|
||||
Connection(#[from] diesel::ConnectionError),
|
||||
|
||||
#[error("Pool error: {0}")]
|
||||
Pool(#[from] RunError),
|
||||
|
||||
#[error("Not found")]
|
||||
NotFound,
|
||||
|
||||
#[error("Already exists")]
|
||||
AlreadyExists,
|
||||
|
||||
#[error("Validation error: {0}")]
|
||||
Validation(String),
|
||||
|
||||
#[error("Database conflict")]
|
||||
Conflict,
|
||||
}
|
||||
32
ticktalk/crates/db/src/lib.rs
Executable file
32
ticktalk/crates/db/src/lib.rs
Executable file
@@ -0,0 +1,32 @@
|
||||
pub mod errors;
|
||||
pub mod models;
|
||||
pub mod repositories;
|
||||
mod schema;
|
||||
|
||||
use diesel_async::pooled_connection::AsyncDieselConnectionManager;
|
||||
use diesel_migrations::{EmbeddedMigrations, embed_migrations};
|
||||
use std::env;
|
||||
|
||||
pub use diesel_async::AsyncPgConnection;
|
||||
|
||||
pub const MIGRATIONS: EmbeddedMigrations = embed_migrations!("migrations");
|
||||
|
||||
pub type DbPool = bb8::Pool<AsyncDieselConnectionManager<AsyncPgConnection>>;
|
||||
|
||||
fn database_url() -> String {
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
dotenv::dotenv().ok();
|
||||
}
|
||||
env::var("DATABASE_URL").expect("DATABASE_URL must be set")
|
||||
}
|
||||
|
||||
pub async fn create_db_pool() -> DbPool {
|
||||
let database_url = database_url();
|
||||
let config = AsyncDieselConnectionManager::<AsyncPgConnection>::new(database_url);
|
||||
|
||||
DbPool::builder()
|
||||
.build(config)
|
||||
.await
|
||||
.expect("Failed to create pool")
|
||||
}
|
||||
56
ticktalk/crates/db/src/models.rs
Executable file
56
ticktalk/crates/db/src/models.rs
Executable file
@@ -0,0 +1,56 @@
|
||||
use chrono::NaiveDateTime;
|
||||
use diesel::prelude::*;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Debug, Clone, Queryable, Selectable, Identifiable)]
|
||||
#[diesel(table_name = crate::schema::users)]
|
||||
#[diesel(check_for_backend(diesel::pg::Pg))]
|
||||
pub struct User {
|
||||
pub id: Uuid,
|
||||
pub username: String,
|
||||
pub created_at: NaiveDateTime,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Insertable)]
|
||||
#[diesel(table_name = crate::schema::users)]
|
||||
pub struct NewUser {
|
||||
pub username: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Queryable, Selectable, Identifiable)]
|
||||
#[diesel(table_name = crate::schema::chats)]
|
||||
#[diesel(check_for_backend(diesel::pg::Pg))]
|
||||
pub struct Chat {
|
||||
pub id: Uuid,
|
||||
pub first_user_id: Uuid,
|
||||
pub second_user_id: Uuid,
|
||||
pub created_at: NaiveDateTime,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Insertable)]
|
||||
#[diesel(table_name = crate::schema::chats)]
|
||||
pub struct NewChat {
|
||||
pub first_user_id: Uuid,
|
||||
pub second_user_id: Uuid,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Queryable, Selectable, Identifiable)]
|
||||
#[diesel(table_name = crate::schema::messages)]
|
||||
#[diesel(check_for_backend(diesel::pg::Pg))]
|
||||
pub struct Message {
|
||||
pub id: Uuid,
|
||||
pub sender_id: Uuid,
|
||||
pub recipient_id: Uuid,
|
||||
pub content: String,
|
||||
pub created_at: NaiveDateTime,
|
||||
pub chat_id: Uuid,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Insertable)]
|
||||
#[diesel(table_name = crate::schema::messages)]
|
||||
pub struct NewMessage {
|
||||
pub sender_id: Uuid,
|
||||
pub recipient_id: Uuid,
|
||||
pub content: String,
|
||||
pub chat_id: Uuid,
|
||||
}
|
||||
60
ticktalk/crates/db/src/repositories/chat.rs
Executable file
60
ticktalk/crates/db/src/repositories/chat.rs
Executable file
@@ -0,0 +1,60 @@
|
||||
use crate::errors::RepositoryError;
|
||||
use crate::models::Chat;
|
||||
use crate::{DbPool, models::NewChat};
|
||||
use diesel::{BoolExpressionMethods, ExpressionMethods, QueryDsl, SelectableHelper};
|
||||
use diesel_async::RunQueryDsl;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ChatRepository {
|
||||
pool: DbPool,
|
||||
}
|
||||
|
||||
impl ChatRepository {
|
||||
pub fn new(pool: DbPool) -> Self {
|
||||
Self { pool }
|
||||
}
|
||||
|
||||
pub async fn find_by_id(&self, chat_id: Uuid) -> Result<Option<Chat>, RepositoryError> {
|
||||
use crate::schema::chats::dsl::*;
|
||||
|
||||
let mut conn = self.pool.get().await?;
|
||||
|
||||
match chats
|
||||
.find(chat_id)
|
||||
.select(Chat::as_select())
|
||||
.first(&mut conn)
|
||||
.await
|
||||
{
|
||||
Ok(chat) => Ok(Some(chat)),
|
||||
Err(diesel::result::Error::NotFound) => Ok(None),
|
||||
Err(e) => Err(RepositoryError::Query(e)),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn create(&self, new_chat: NewChat) -> Result<Chat, RepositoryError> {
|
||||
use crate::schema::chats::dsl::*;
|
||||
|
||||
let mut conn = self.pool.get().await?;
|
||||
|
||||
diesel::insert_into(chats)
|
||||
.values(&new_chat)
|
||||
.returning(Chat::as_returning())
|
||||
.get_result(&mut conn)
|
||||
.await
|
||||
.map_err(|e| e.into())
|
||||
}
|
||||
|
||||
pub async fn find_by_user(&self, user: Uuid) -> Result<Vec<Chat>, RepositoryError> {
|
||||
use crate::schema::chats::dsl::*;
|
||||
|
||||
let mut conn = self.pool.get().await?;
|
||||
|
||||
chats
|
||||
.filter(first_user_id.eq(user).or(second_user_id.eq(user)))
|
||||
.select(Chat::as_select())
|
||||
.load(&mut conn)
|
||||
.await
|
||||
.map_err(|e| e.into())
|
||||
}
|
||||
}
|
||||
47
ticktalk/crates/db/src/repositories/message.rs
Executable file
47
ticktalk/crates/db/src/repositories/message.rs
Executable file
@@ -0,0 +1,47 @@
|
||||
use crate::{
|
||||
DbPool,
|
||||
errors::RepositoryError,
|
||||
models::{Message, NewMessage},
|
||||
};
|
||||
use diesel::SelectableHelper;
|
||||
use diesel::prelude::*;
|
||||
use diesel_async::RunQueryDsl;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct MessageRepository {
|
||||
pool: DbPool,
|
||||
}
|
||||
|
||||
impl MessageRepository {
|
||||
pub fn new(pool: DbPool) -> Self {
|
||||
Self { pool }
|
||||
}
|
||||
|
||||
pub async fn create(&self, new_message: NewMessage) -> Result<Message, RepositoryError> {
|
||||
use crate::schema::messages::dsl::*;
|
||||
|
||||
let mut conn = self.pool.get().await?;
|
||||
|
||||
match diesel::insert_into(messages)
|
||||
.values(&new_message)
|
||||
.returning(Message::as_returning())
|
||||
.get_result(&mut conn)
|
||||
.await
|
||||
{
|
||||
Ok(message) => Ok(message),
|
||||
Err(e) => Err(RepositoryError::from(e)),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn find_by_chat_id(&self, c_id: Uuid) -> Result<Vec<Message>, RepositoryError> {
|
||||
use crate::schema::messages::dsl::*;
|
||||
|
||||
let mut conn = self.pool.get().await?;
|
||||
|
||||
match messages.filter(chat_id.eq(c_id)).load(&mut conn).await {
|
||||
Ok(chat_messages) => Ok(chat_messages),
|
||||
Err(e) => Err(RepositoryError::from(e)),
|
||||
}
|
||||
}
|
||||
}
|
||||
7
ticktalk/crates/db/src/repositories/mod.rs
Executable file
7
ticktalk/crates/db/src/repositories/mod.rs
Executable file
@@ -0,0 +1,7 @@
|
||||
mod chat;
|
||||
mod message;
|
||||
mod user;
|
||||
|
||||
pub use self::chat::ChatRepository;
|
||||
pub use self::message::MessageRepository;
|
||||
pub use self::user::UserRepository;
|
||||
85
ticktalk/crates/db/src/repositories/user.rs
Executable file
85
ticktalk/crates/db/src/repositories/user.rs
Executable file
@@ -0,0 +1,85 @@
|
||||
use crate::errors::RepositoryError;
|
||||
use crate::models::{NewUser, User};
|
||||
use crate::schema::users;
|
||||
use diesel::result::Error as DieselError;
|
||||
use diesel::{ExpressionMethods, QueryDsl, SelectableHelper};
|
||||
use diesel_async::RunQueryDsl;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::DbPool;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct UserRepository {
|
||||
pool: DbPool,
|
||||
}
|
||||
|
||||
impl UserRepository {
|
||||
pub fn new(pool: DbPool) -> Self {
|
||||
Self { pool }
|
||||
}
|
||||
|
||||
pub async fn find_by_id(&self, user_id: Uuid) -> Result<Option<User>, RepositoryError> {
|
||||
use crate::schema::users::dsl::*;
|
||||
let mut conn = self.pool.get().await?;
|
||||
|
||||
match users
|
||||
.find(user_id)
|
||||
.select(User::as_select())
|
||||
.first(&mut conn)
|
||||
.await
|
||||
{
|
||||
Ok(user) => Ok(Some(user)),
|
||||
Err(diesel::result::Error::NotFound) => Ok(None),
|
||||
Err(e) => Err(RepositoryError::Query(e)),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn find_by_username(&self, user_name: String) -> Result<Option<User>, RepositoryError> {
|
||||
use crate::schema::users::dsl::*;
|
||||
let mut conn = self.pool.get().await?;
|
||||
|
||||
match users
|
||||
.filter(username.eq(user_name))
|
||||
.select(User::as_select())
|
||||
.first(&mut conn)
|
||||
.await
|
||||
{
|
||||
Ok(user) => Ok(Some(user)),
|
||||
Err(diesel::result::Error::NotFound) => Ok(None),
|
||||
Err(e) => Err(RepositoryError::Query(e)),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn create(&self, user: NewUser) -> Result<User, RepositoryError> {
|
||||
use crate::schema::users::dsl::*;
|
||||
|
||||
let mut conn = self.pool.get().await?;
|
||||
|
||||
match diesel::insert_into(users)
|
||||
.values(&user)
|
||||
.returning(User::as_returning())
|
||||
.get_result(&mut conn)
|
||||
.await
|
||||
{
|
||||
Ok(created) => Ok(created),
|
||||
Err(DieselError::DatabaseError(
|
||||
diesel::result::DatabaseErrorKind::UniqueViolation,
|
||||
ref info,
|
||||
)) => {
|
||||
// Проверяем, какое именно ограничение нарушено
|
||||
let constraint_name = info.constraint_name();
|
||||
|
||||
let error_message = if constraint_name == Some("users_username_key") {
|
||||
"Пользователь с таким username уже существует".to_string()
|
||||
} else if constraint_name == Some("users_email_key") {
|
||||
"Пользователь с таким email уже существует".to_string()
|
||||
} else {
|
||||
"Нарушено ограничение уникальности".to_string()
|
||||
};
|
||||
|
||||
Err(RepositoryError::Conflict)
|
||||
}
|
||||
Err(e) => Err(e.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
33
ticktalk/crates/db/src/schema.rs
Executable file
33
ticktalk/crates/db/src/schema.rs
Executable file
@@ -0,0 +1,33 @@
|
||||
// @generated automatically by Diesel CLI.
|
||||
|
||||
diesel::table! {
|
||||
chats (id) {
|
||||
id -> Uuid,
|
||||
first_user_id -> Uuid,
|
||||
second_user_id -> Uuid,
|
||||
created_at -> Timestamp,
|
||||
}
|
||||
}
|
||||
|
||||
diesel::table! {
|
||||
messages (id) {
|
||||
id -> Uuid,
|
||||
sender_id -> Uuid,
|
||||
recipient_id -> Uuid,
|
||||
content -> Text,
|
||||
created_at -> Timestamp,
|
||||
chat_id -> Uuid,
|
||||
}
|
||||
}
|
||||
|
||||
diesel::table! {
|
||||
users (id) {
|
||||
id -> Uuid,
|
||||
username -> Text,
|
||||
created_at -> Timestamp,
|
||||
}
|
||||
}
|
||||
|
||||
diesel::joinable!(messages -> chats (chat_id));
|
||||
|
||||
diesel::allow_tables_to_appear_in_same_query!(chats, messages, users,);
|
||||
Reference in New Issue
Block a user