chat implemented
need testing
This commit is contained in:
1 parent
c90f8cb1fa
commit
9cbdc35903
40 files changed
+7068
-10
No files matched your search
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
Reference in new issue
Block a user