feat: auth middleware, brain data layer, Valkey queue (P1-002, P1-003, P1-004)

Auth middleware (P1-002):
- DatabaseModule provides Db instance with graceful shutdown
- AuthModule mounts BetterAuth at /api/auth/* via toNodeHandler
- AuthGuard validates sessions via BetterAuth API
- CurrentUser decorator extracts user from request

Brain data layer (P1-003):
- CRUD repositories for projects, missions, tasks, conversations
- createBrain(db) factory returns all repositories
- Re-exports drizzle-orm query helpers from @mosaic/db to avoid
  duplicate package resolution

Queue (P1-004):
- ioredis-based Valkey client with createQueue/createQueueClient
- Enqueue/dequeue, pub/sub, queue length operations

Closes #11, Closes #12, Closes #13

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-12 21:37:33 -05:00
parent cbac5902db
commit 4c5e4ae016
20 changed files with 2043 additions and 4290 deletions

View File

@@ -16,6 +16,7 @@
"test": "vitest run --passWithNoTests"
},
"dependencies": {
"@mosaic/db": "workspace:^",
"@mosaic/types": "workspace:*"
},
"devDependencies": {

View File

@@ -0,0 +1,21 @@
import type { Db } from '@mosaic/db';
import { createProjectsRepo, type ProjectsRepo } from './projects.js';
import { createMissionsRepo, type MissionsRepo } from './missions.js';
import { createTasksRepo, type TasksRepo } from './tasks.js';
import { createConversationsRepo, type ConversationsRepo } from './conversations.js';
export interface Brain {
projects: ProjectsRepo;
missions: MissionsRepo;
tasks: TasksRepo;
conversations: ConversationsRepo;
}
export function createBrain(db: Db): Brain {
return {
projects: createProjectsRepo(db),
missions: createMissionsRepo(db),
tasks: createTasksRepo(db),
conversations: createConversationsRepo(db),
};
}

View File

@@ -0,0 +1,49 @@
import { eq, type Db, conversations, messages } from '@mosaic/db';
export type Conversation = typeof conversations.$inferSelect;
export type NewConversation = typeof conversations.$inferInsert;
export type Message = typeof messages.$inferSelect;
export type NewMessage = typeof messages.$inferInsert;
export function createConversationsRepo(db: Db) {
return {
async findAll(userId: string): Promise<Conversation[]> {
return db.select().from(conversations).where(eq(conversations.userId, userId));
},
async findById(id: string): Promise<Conversation | undefined> {
const rows = await db.select().from(conversations).where(eq(conversations.id, id));
return rows[0];
},
async create(data: NewConversation): Promise<Conversation> {
const rows = await db.insert(conversations).values(data).returning();
return rows[0]!;
},
async update(id: string, data: Partial<NewConversation>): Promise<Conversation | undefined> {
const rows = await db
.update(conversations)
.set({ ...data, updatedAt: new Date() })
.where(eq(conversations.id, id))
.returning();
return rows[0];
},
async remove(id: string): Promise<boolean> {
const rows = await db.delete(conversations).where(eq(conversations.id, id)).returning();
return rows.length > 0;
},
async findMessages(conversationId: string): Promise<Message[]> {
return db.select().from(messages).where(eq(messages.conversationId, conversationId));
},
async addMessage(data: NewMessage): Promise<Message> {
const rows = await db.insert(messages).values(data).returning();
return rows[0]!;
},
};
}
export type ConversationsRepo = ReturnType<typeof createConversationsRepo>;

View File

@@ -1 +1,22 @@
export const VERSION = '0.0.0';
export { createBrain, type Brain } from './brain.js';
export {
createProjectsRepo,
type ProjectsRepo,
type Project,
type NewProject,
} from './projects.js';
export {
createMissionsRepo,
type MissionsRepo,
type Mission,
type NewMission,
} from './missions.js';
export { createTasksRepo, type TasksRepo, type Task, type NewTask } from './tasks.js';
export {
createConversationsRepo,
type ConversationsRepo,
type Conversation,
type NewConversation,
type Message,
type NewMessage,
} from './conversations.js';

View File

@@ -0,0 +1,42 @@
import { eq, type Db, missions } from '@mosaic/db';
export type Mission = typeof missions.$inferSelect;
export type NewMission = typeof missions.$inferInsert;
export function createMissionsRepo(db: Db) {
return {
async findAll(): Promise<Mission[]> {
return db.select().from(missions);
},
async findById(id: string): Promise<Mission | undefined> {
const rows = await db.select().from(missions).where(eq(missions.id, id));
return rows[0];
},
async findByProject(projectId: string): Promise<Mission[]> {
return db.select().from(missions).where(eq(missions.projectId, projectId));
},
async create(data: NewMission): Promise<Mission> {
const rows = await db.insert(missions).values(data).returning();
return rows[0]!;
},
async update(id: string, data: Partial<NewMission>): Promise<Mission | undefined> {
const rows = await db
.update(missions)
.set({ ...data, updatedAt: new Date() })
.where(eq(missions.id, id))
.returning();
return rows[0];
},
async remove(id: string): Promise<boolean> {
const rows = await db.delete(missions).where(eq(missions.id, id)).returning();
return rows.length > 0;
},
};
}
export type MissionsRepo = ReturnType<typeof createMissionsRepo>;

View File

@@ -0,0 +1,38 @@
import { eq, type Db, projects } from '@mosaic/db';
export type Project = typeof projects.$inferSelect;
export type NewProject = typeof projects.$inferInsert;
export function createProjectsRepo(db: Db) {
return {
async findAll(): Promise<Project[]> {
return db.select().from(projects);
},
async findById(id: string): Promise<Project | undefined> {
const rows = await db.select().from(projects).where(eq(projects.id, id));
return rows[0];
},
async create(data: NewProject): Promise<Project> {
const rows = await db.insert(projects).values(data).returning();
return rows[0]!;
},
async update(id: string, data: Partial<NewProject>): Promise<Project | undefined> {
const rows = await db
.update(projects)
.set({ ...data, updatedAt: new Date() })
.where(eq(projects.id, id))
.returning();
return rows[0];
},
async remove(id: string): Promise<boolean> {
const rows = await db.delete(projects).where(eq(projects.id, id)).returning();
return rows.length > 0;
},
};
}
export type ProjectsRepo = ReturnType<typeof createProjectsRepo>;

View File

@@ -0,0 +1,50 @@
import { eq, type Db, tasks } from '@mosaic/db';
export type Task = typeof tasks.$inferSelect;
export type NewTask = typeof tasks.$inferInsert;
export function createTasksRepo(db: Db) {
return {
async findAll(): Promise<Task[]> {
return db.select().from(tasks);
},
async findById(id: string): Promise<Task | undefined> {
const rows = await db.select().from(tasks).where(eq(tasks.id, id));
return rows[0];
},
async findByProject(projectId: string): Promise<Task[]> {
return db.select().from(tasks).where(eq(tasks.projectId, projectId));
},
async findByMission(missionId: string): Promise<Task[]> {
return db.select().from(tasks).where(eq(tasks.missionId, missionId));
},
async findByStatus(status: Task['status']): Promise<Task[]> {
return db.select().from(tasks).where(eq(tasks.status, status));
},
async create(data: NewTask): Promise<Task> {
const rows = await db.insert(tasks).values(data).returning();
return rows[0]!;
},
async update(id: string, data: Partial<NewTask>): Promise<Task | undefined> {
const rows = await db
.update(tasks)
.set({ ...data, updatedAt: new Date() })
.where(eq(tasks.id, id))
.returning();
return rows[0];
},
async remove(id: string): Promise<boolean> {
const rows = await db.delete(tasks).where(eq(tasks.id, id)).returning();
return rows.length > 0;
},
};
}
export type TasksRepo = ReturnType<typeof createTasksRepo>;