chat implemented

need testing
This commit is contained in:
dimitar committed 2026-04-03 03:25:11 +02:00
1 parent c90f8cb1fa
commit 9cbdc35903
40 files changed
+7068 -10

No files matched your search

+35 -2
View File
@@ -1,9 +1,16 @@
import Database from "better-sqlite3";
import { drizzle } from "drizzle-orm/better-sqlite3";
import { existsSync, mkdirSync } from "node:fs";
import path from "node:path";
import * as schema from "./schema";
// Configurable database path with intelligent defaults
const dbPath = "./data/fitai.db";
// Configurable database path with intelligent defaults.
// Priority:
// 1) FITAI_DATABASE_PATH / DATABASE_PATH
// 2) ./data/fitai.db from current working directory
// 3) monorepo shared admin database path
const dbPath = resolveDatabasePath();
mkdirSync(path.dirname(dbPath), { recursive: true });
const sqlite = new Database(dbPath);
export const db = drizzle(sqlite, { schema });
@@ -21,3 +28,29 @@ export {
lte,
like,
} from "drizzle-orm";
function resolveDatabasePath(): string {
const envPath = process.env.FITAI_DATABASE_PATH ?? process.env.DATABASE_PATH;
if (envPath && envPath.trim().length > 0) {
return path.resolve(envPath);
}
const cwdDefault = path.resolve(process.cwd(), "data", "fitai.db");
if (existsSync(cwdDefault)) {
return cwdDefault;
}
const repoRoot = path.resolve(__dirname, "..", "..", "..");
const sharedAdminDb = path.join(
repoRoot,
"apps",
"admin",
"data",
"fitai.db",
);
if (existsSync(sharedAdminDb)) {
return sharedAdminDb;
}
return cwdDefault;
}
+140
View File
@@ -8,6 +8,9 @@ import {
uniqueIndex,
} from "drizzle-orm/sqlite-core";
const chatThreadTypeValues = ["gym", "dm"] as const;
const chatMessageKindValues = ["text", "system"] as const;
export const users = sqliteTable(
"users",
{
@@ -574,6 +577,135 @@ export const trainerClientAssignments = sqliteTable(
}),
);
export const chatThreads = sqliteTable(
"chat_threads",
{
id: text("id").primaryKey(),
type: text("type", { enum: chatThreadTypeValues }).notNull(),
gymId: text("gym_id").references(() => gyms.id, { onDelete: "cascade" }),
trainerId: text("trainer_id").references(() => users.id, {
onDelete: "cascade",
}),
clientId: text("client_id").references(() => users.id, {
onDelete: "cascade",
}),
createdBy: text("created_by")
.notNull()
.references(() => users.id, { onDelete: "cascade" }),
lastMessageAt: integer("last_message_at", { mode: "timestamp" }),
createdAt: integer("created_at", { mode: "timestamp" })
.notNull()
.$defaultFn(() => new Date()),
updatedAt: integer("updated_at", { mode: "timestamp" })
.notNull()
.$defaultFn(() => new Date()),
archivedAt: integer("archived_at", { mode: "timestamp" }),
},
(table) => ({
typeIdx: index("chat_threads_type_idx").on(table.type),
gymTypeLastMessageIdx: index("chat_threads_gym_type_last_msg_idx").on(
table.gymId,
table.type,
table.lastMessageAt,
),
trainerClientTypeIdx: uniqueIndex(
"chat_threads_trainer_client_type_idx",
).on(table.trainerId, table.clientId, table.type),
}),
);
export const chatThreadMembers = sqliteTable(
"chat_thread_members",
{
id: text("id").primaryKey(),
threadId: text("thread_id")
.notNull()
.references(() => chatThreads.id, { onDelete: "cascade" }),
userId: text("user_id")
.notNull()
.references(() => users.id, { onDelete: "cascade" }),
roleInThread: text("role_in_thread", {
enum: ["superAdmin", "admin", "trainer", "client"],
}).notNull(),
joinedAt: integer("joined_at", { mode: "timestamp" })
.notNull()
.$defaultFn(() => new Date()),
leftAt: integer("left_at", { mode: "timestamp" }),
muted: integer("muted", { mode: "boolean" }).notNull().default(false),
lastReadMessageId: text("last_read_message_id"),
lastReadAt: integer("last_read_at", { mode: "timestamp" }),
},
(table) => ({
threadUserUniqueIdx: uniqueIndex("chat_thread_members_thread_user_idx").on(
table.threadId,
table.userId,
),
userThreadIdx: index("chat_thread_members_user_thread_idx").on(
table.userId,
table.threadId,
),
}),
);
export const chatMessages = sqliteTable(
"chat_messages",
{
id: text("id").primaryKey(),
threadId: text("thread_id")
.notNull()
.references(() => chatThreads.id, { onDelete: "cascade" }),
senderUserId: text("sender_user_id")
.notNull()
.references(() => users.id, { onDelete: "cascade" }),
body: text("body").notNull(),
kind: text("kind", { enum: chatMessageKindValues })
.notNull()
.default("text"),
attachmentsJson: text("attachments_json"),
clientMessageId: text("client_message_id"),
createdAt: integer("created_at", { mode: "timestamp" })
.notNull()
.$defaultFn(() => new Date()),
editedAt: integer("edited_at", { mode: "timestamp" }),
deletedAt: integer("deleted_at", { mode: "timestamp" }),
},
(table) => ({
threadCreatedAtIdx: index("chat_messages_thread_created_at_idx").on(
table.threadId,
table.createdAt,
),
senderClientMessageUniqueIdx: uniqueIndex(
"chat_messages_sender_client_msg_idx",
).on(table.senderUserId, table.clientMessageId),
}),
);
export const chatMessageReads = sqliteTable(
"chat_message_reads",
{
id: text("id").primaryKey(),
messageId: text("message_id")
.notNull()
.references(() => chatMessages.id, { onDelete: "cascade" }),
userId: text("user_id")
.notNull()
.references(() => users.id, { onDelete: "cascade" }),
readAt: integer("read_at", { mode: "timestamp" })
.notNull()
.$defaultFn(() => new Date()),
},
(table) => ({
messageUserUniqueIdx: uniqueIndex("chat_message_reads_message_user_idx").on(
table.messageId,
table.userId,
),
userReadAtIdx: index("chat_message_reads_user_read_at_idx").on(
table.userId,
table.readAt,
),
}),
);
export type User = typeof users.$inferSelect;
export type NewUser = typeof users.$inferInsert;
export type Client = typeof clients.$inferSelect;
@@ -603,3 +735,11 @@ export type TrainerClientAssignment =
typeof trainerClientAssignments.$inferSelect;
export type NewTrainerClientAssignment =
typeof trainerClientAssignments.$inferInsert;
export type ChatThread = typeof chatThreads.$inferSelect;
export type NewChatThread = typeof chatThreads.$inferInsert;
export type ChatThreadMember = typeof chatThreadMembers.$inferSelect;
export type NewChatThreadMember = typeof chatThreadMembers.$inferInsert;
export type ChatMessage = typeof chatMessages.$inferSelect;
export type NewChatMessage = typeof chatMessages.$inferInsert;
export type ChatMessageRead = typeof chatMessageReads.$inferSelect;
export type NewChatMessageRead = typeof chatMessageReads.$inferInsert;