feat(coord): DB migration — project-scoped missions, multi-tenant RBAC (#149)
Some checks failed
ci/woodpecker/push/ci Pipeline failed

Co-authored-by: Jason Woltje <jason@diversecanvas.com>
Co-committed-by: Jason Woltje <jason@diversecanvas.com>
This commit was merged in pull request #149.
This commit is contained in:
2026-03-15 19:18:18 +00:00
committed by jason.woltje
parent d1bef49b4e
commit 22a5e9791c
10 changed files with 533 additions and 6 deletions

View File

@@ -25,6 +25,9 @@ export const users = pgTable('users', {
emailVerified: boolean('email_verified').notNull().default(false),
image: text('image'),
role: text('role').notNull().default('member'),
banned: boolean('banned').default(false),
banReason: text('ban_reason'),
banExpires: timestamp('ban_expires', { withTimezone: true }),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
});
@@ -95,11 +98,18 @@ export const missions = pgTable(
.notNull()
.default('planning'),
projectId: uuid('project_id').references(() => projects.id, { onDelete: 'set null' }),
userId: text('user_id').references(() => users.id, { onDelete: 'cascade' }),
phase: text('phase'),
milestones: jsonb('milestones').$type<Record<string, unknown>[]>(),
config: jsonb('config'),
metadata: jsonb('metadata'),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
},
(t) => [index('missions_project_id_idx').on(t.projectId)],
(t) => [
index('missions_project_id_idx').on(t.projectId),
index('missions_user_id_idx').on(t.userId),
],
);
export const tasks = pgTable(
@@ -132,6 +142,40 @@ export const tasks = pgTable(
],
);
// ─── Coord Mission Tasks ─────────────────────────────────────────────────────
// Join table tracking coord-managed tasks within a mission.
// Scoped to userId for multi-tenant RBAC isolation.
export const missionTasks = pgTable(
'mission_tasks',
{
id: uuid('id').primaryKey().defaultRandom(),
missionId: uuid('mission_id')
.notNull()
.references(() => missions.id, { onDelete: 'cascade' }),
taskId: uuid('task_id').references(() => tasks.id, { onDelete: 'set null' }),
userId: text('user_id')
.notNull()
.references(() => users.id, { onDelete: 'cascade' }),
status: text('status', {
enum: ['not-started', 'in-progress', 'blocked', 'done', 'cancelled'],
})
.notNull()
.default('not-started'),
description: text('description'),
notes: text('notes'),
pr: text('pr'),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
},
(t) => [
index('mission_tasks_mission_id_idx').on(t.missionId),
index('mission_tasks_task_id_idx').on(t.taskId),
index('mission_tasks_user_id_idx').on(t.userId),
index('mission_tasks_status_idx').on(t.status),
],
);
export const events = pgTable(
'events',
{