This commit is contained in:
root
2025-12-14 10:39:18 +03:00
commit 639f4e2b4e
179 changed files with 21065 additions and 0 deletions

1
ticktalk/crates/db/.env Executable file
View File

@@ -0,0 +1 @@
DATABASE_URL=postgres://ticktalk_user:ticktack@localhost:5432/ticktalk_db

15
ticktalk/crates/db/Cargo.toml Executable file
View 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
View 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"

View File

View File

View 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();

View 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;

View File

@@ -0,0 +1,3 @@
DROP TABLE IF EXISTS messages;
DROP TABLE IF EXISTS chats;
DROP TABLE IF EXISTS users;

View 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);

View 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
View 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")
}

View 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,
}

View 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())
}
}

View 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)),
}
}
}

View 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;

View 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()),
}
}
}

View 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,);