feat(db): @mosaic/db — Drizzle schema, PG connection, migrations (#67)
Co-authored-by: Jason Woltje <jason@diversecanvas.com> Co-committed-by: Jason Woltje <jason@diversecanvas.com>
This commit was merged in pull request #67.
This commit is contained in:
18
packages/db/src/client.ts
Normal file
18
packages/db/src/client.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { drizzle, type PostgresJsDatabase } from 'drizzle-orm/postgres-js';
|
||||
import postgres from 'postgres';
|
||||
import * as schema from './schema.js';
|
||||
import { DEFAULT_DATABASE_URL } from './defaults.js';
|
||||
|
||||
export type Db = PostgresJsDatabase<typeof schema>;
|
||||
|
||||
export interface DbHandle {
|
||||
db: Db;
|
||||
close: () => Promise<void>;
|
||||
}
|
||||
|
||||
export function createDb(url?: string): DbHandle {
|
||||
const connectionString = url ?? process.env['DATABASE_URL'] ?? DEFAULT_DATABASE_URL;
|
||||
const sql = postgres(connectionString);
|
||||
const db = drizzle(sql, { schema });
|
||||
return { db, close: () => sql.end() };
|
||||
}
|
||||
1
packages/db/src/defaults.ts
Normal file
1
packages/db/src/defaults.ts
Normal file
@@ -0,0 +1 @@
|
||||
export const DEFAULT_DATABASE_URL = 'postgresql://mosaic:mosaic@localhost:5433/mosaic';
|
||||
@@ -1 +1,3 @@
|
||||
export const VERSION = '0.0.0';
|
||||
export { createDb, type Db, type DbHandle } from './client.js';
|
||||
export { runMigrations } from './migrate.js';
|
||||
export * from './schema.js';
|
||||
|
||||
18
packages/db/src/migrate.ts
Normal file
18
packages/db/src/migrate.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { dirname, resolve } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { drizzle } from 'drizzle-orm/postgres-js';
|
||||
import { migrate } from 'drizzle-orm/postgres-js/migrator';
|
||||
import postgres from 'postgres';
|
||||
import { DEFAULT_DATABASE_URL } from './defaults.js';
|
||||
|
||||
export async function runMigrations(url?: string): Promise<void> {
|
||||
const connectionString = url ?? process.env['DATABASE_URL'] ?? DEFAULT_DATABASE_URL;
|
||||
const sql = postgres(connectionString, { max: 1 });
|
||||
const db = drizzle(sql);
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
try {
|
||||
await migrate(db, { migrationsFolder: resolve(__dirname, '../drizzle') });
|
||||
} finally {
|
||||
await sql.end();
|
||||
}
|
||||
}
|
||||
213
packages/db/src/schema.ts
Normal file
213
packages/db/src/schema.ts
Normal file
@@ -0,0 +1,213 @@
|
||||
/**
|
||||
* Unified schema file — all tables defined here.
|
||||
* drizzle-kit reads this file directly (avoids CJS/ESM extension issues).
|
||||
*/
|
||||
|
||||
import { pgTable, text, timestamp, boolean, uuid, jsonb, index } from 'drizzle-orm/pg-core';
|
||||
|
||||
// ─── Auth (BetterAuth-compatible) ────────────────────────────────────────────
|
||||
|
||||
export const users = pgTable('users', {
|
||||
id: text('id').primaryKey(),
|
||||
name: text('name').notNull(),
|
||||
email: text('email').notNull().unique(),
|
||||
emailVerified: boolean('email_verified').notNull().default(false),
|
||||
image: text('image'),
|
||||
role: text('role').notNull().default('member'),
|
||||
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
||||
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
|
||||
});
|
||||
|
||||
export const sessions = pgTable('sessions', {
|
||||
id: text('id').primaryKey(),
|
||||
expiresAt: timestamp('expires_at', { withTimezone: true }).notNull(),
|
||||
token: text('token').notNull().unique(),
|
||||
ipAddress: text('ip_address'),
|
||||
userAgent: text('user_agent'),
|
||||
userId: text('user_id')
|
||||
.notNull()
|
||||
.references(() => users.id, { onDelete: 'cascade' }),
|
||||
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
||||
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
|
||||
});
|
||||
|
||||
export const accounts = pgTable('accounts', {
|
||||
id: text('id').primaryKey(),
|
||||
accountId: text('account_id').notNull(),
|
||||
providerId: text('provider_id').notNull(),
|
||||
userId: text('user_id')
|
||||
.notNull()
|
||||
.references(() => users.id, { onDelete: 'cascade' }),
|
||||
accessToken: text('access_token'),
|
||||
refreshToken: text('refresh_token'),
|
||||
idToken: text('id_token'),
|
||||
accessTokenExpiresAt: timestamp('access_token_expires_at', { withTimezone: true }),
|
||||
refreshTokenExpiresAt: timestamp('refresh_token_expires_at', { withTimezone: true }),
|
||||
scope: text('scope'),
|
||||
password: text('password'),
|
||||
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
||||
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
|
||||
});
|
||||
|
||||
export const verifications = pgTable('verifications', {
|
||||
id: text('id').primaryKey(),
|
||||
identifier: text('identifier').notNull(),
|
||||
value: text('value').notNull(),
|
||||
expiresAt: timestamp('expires_at', { withTimezone: true }).notNull(),
|
||||
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
||||
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
|
||||
});
|
||||
|
||||
// ─── Brain ───────────────────────────────────────────────────────────────────
|
||||
// Declared before Chat because conversations references projects.
|
||||
|
||||
export const projects = pgTable('projects', {
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
name: text('name').notNull(),
|
||||
description: text('description'),
|
||||
status: text('status', { enum: ['active', 'paused', 'completed', 'archived'] })
|
||||
.notNull()
|
||||
.default('active'),
|
||||
ownerId: text('owner_id').references(() => users.id, { onDelete: 'set null' }),
|
||||
metadata: jsonb('metadata'),
|
||||
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
||||
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
|
||||
});
|
||||
|
||||
export const missions = pgTable(
|
||||
'missions',
|
||||
{
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
name: text('name').notNull(),
|
||||
description: text('description'),
|
||||
status: text('status', { enum: ['planning', 'active', 'paused', 'completed', 'failed'] })
|
||||
.notNull()
|
||||
.default('planning'),
|
||||
projectId: uuid('project_id').references(() => projects.id, { onDelete: 'set null' }),
|
||||
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)],
|
||||
);
|
||||
|
||||
export const tasks = pgTable(
|
||||
'tasks',
|
||||
{
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
title: text('title').notNull(),
|
||||
description: text('description'),
|
||||
status: text('status', {
|
||||
enum: ['not-started', 'in-progress', 'blocked', 'done', 'cancelled'],
|
||||
})
|
||||
.notNull()
|
||||
.default('not-started'),
|
||||
priority: text('priority', { enum: ['critical', 'high', 'medium', 'low'] })
|
||||
.notNull()
|
||||
.default('medium'),
|
||||
projectId: uuid('project_id').references(() => projects.id, { onDelete: 'set null' }),
|
||||
missionId: uuid('mission_id').references(() => missions.id, { onDelete: 'set null' }),
|
||||
assignee: text('assignee'),
|
||||
tags: jsonb('tags').$type<string[]>(),
|
||||
dueDate: timestamp('due_date', { withTimezone: true }),
|
||||
metadata: jsonb('metadata'),
|
||||
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
||||
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
|
||||
},
|
||||
(t) => [
|
||||
index('tasks_project_id_idx').on(t.projectId),
|
||||
index('tasks_mission_id_idx').on(t.missionId),
|
||||
index('tasks_status_idx').on(t.status),
|
||||
],
|
||||
);
|
||||
|
||||
export const events = pgTable(
|
||||
'events',
|
||||
{
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
type: text('type').notNull(),
|
||||
title: text('title').notNull(),
|
||||
description: text('description'),
|
||||
date: timestamp('date', { withTimezone: true }).notNull().defaultNow(),
|
||||
metadata: jsonb('metadata'),
|
||||
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
||||
},
|
||||
(t) => [index('events_type_idx').on(t.type), index('events_date_idx').on(t.date)],
|
||||
);
|
||||
|
||||
export const agents = pgTable('agents', {
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
name: text('name').notNull(),
|
||||
provider: text('provider').notNull(),
|
||||
model: text('model').notNull(),
|
||||
status: text('status', { enum: ['idle', 'active', 'error', 'offline'] })
|
||||
.notNull()
|
||||
.default('idle'),
|
||||
config: jsonb('config'),
|
||||
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
||||
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
|
||||
});
|
||||
|
||||
export const tickets = pgTable(
|
||||
'tickets',
|
||||
{
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
title: text('title').notNull(),
|
||||
description: text('description'),
|
||||
status: text('status', { enum: ['open', 'in-progress', 'resolved', 'closed'] })
|
||||
.notNull()
|
||||
.default('open'),
|
||||
priority: text('priority', { enum: ['critical', 'high', 'medium', 'low'] })
|
||||
.notNull()
|
||||
.default('medium'),
|
||||
source: text('source'),
|
||||
metadata: jsonb('metadata'),
|
||||
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
||||
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
|
||||
},
|
||||
(t) => [index('tickets_status_idx').on(t.status)],
|
||||
);
|
||||
|
||||
export const appreciations = pgTable('appreciations', {
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
fromUser: text('from_user'),
|
||||
toUser: text('to_user'),
|
||||
message: text('message').notNull(),
|
||||
metadata: jsonb('metadata'),
|
||||
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
||||
});
|
||||
|
||||
// ─── Chat ────────────────────────────────────────────────────────────────────
|
||||
|
||||
export const conversations = pgTable(
|
||||
'conversations',
|
||||
{
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
title: text('title'),
|
||||
userId: text('user_id')
|
||||
.notNull()
|
||||
.references(() => users.id, { onDelete: 'cascade' }),
|
||||
projectId: uuid('project_id').references(() => projects.id, { onDelete: 'set null' }),
|
||||
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
||||
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
|
||||
},
|
||||
(t) => [
|
||||
index('conversations_user_id_idx').on(t.userId),
|
||||
index('conversations_project_id_idx').on(t.projectId),
|
||||
],
|
||||
);
|
||||
|
||||
export const messages = pgTable(
|
||||
'messages',
|
||||
{
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
conversationId: uuid('conversation_id')
|
||||
.notNull()
|
||||
.references(() => conversations.id, { onDelete: 'cascade' }),
|
||||
role: text('role', { enum: ['user', 'assistant', 'system'] }).notNull(),
|
||||
content: text('content').notNull(),
|
||||
metadata: jsonb('metadata'),
|
||||
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
||||
},
|
||||
(t) => [index('messages_conversation_id_idx').on(t.conversationId)],
|
||||
);
|
||||
Reference in New Issue
Block a user