diff --git a/openspec/changes/user-accounts/tasks.md b/openspec/changes/user-accounts/tasks.md index de59ca2..1c5b4ef 100644 --- a/openspec/changes/user-accounts/tasks.md +++ b/openspec/changes/user-accounts/tasks.md @@ -8,7 +8,7 @@ - [x] 2.1 `[sonnet]` Add `users` table to Drizzle schema (`src/lib/db/schema.ts`) with UUID PK, email, password_hash, name, image, provider, provider_account_id, email_verified, created_at, updated_at - [x] 2.2 `[sonnet]` Add `user_id` (uuid, FK to users.id) column to `charts`, `annotations`, `annotation_types`, `span_annotations`, `span_label_types` in schema -- [ ] 2.3 `[sonnet]` Replace unique constraints: `charts.name` → `(user_id, name)`, `annotation_types.name` → `(user_id, name)`, `span_label_types.name` → `(user_id, name)` +- [x] 2.3 `[sonnet]` Replace unique constraints: `charts.name` → `(user_id, name)`, `annotation_types.name` → `(user_id, name)`, `span_label_types.name` → `(user_id, name)` - [ ] 2.4 `[haiku]` Generate Drizzle migration with `drizzle-kit generate` - [ ] 2.5 `[opus]` Create data migration script (`scripts/migrate-users.ts`): create default admin user, backfill `user_id` on all existing rows, alter columns to NOT NULL diff --git a/src/lib/db/schema.ts b/src/lib/db/schema.ts index 5aad160..7fe10d0 100644 --- a/src/lib/db/schema.ts +++ b/src/lib/db/schema.ts @@ -16,9 +16,11 @@ export const users = pgTable('users', { export const charts = pgTable('charts', { id: serial('id').primaryKey(), user_id: uuid('user_id').references(() => users.id), // nullable for now; will be NOT NULL after data migration (task 2.5) - name: text('name').notNull().unique(), + name: text('name').notNull(), created_at: timestamp('created_at').notNull().defaultNow(), -}); +}, (table) => [ + uniqueIndex('charts_user_id_name_unique').on(table.user_id, table.name), +]); export const candles = pgTable('candles', { id: serial('id').primaryKey(), @@ -35,14 +37,16 @@ export const candles = pgTable('candles', { export const annotationTypes = pgTable('annotation_types', { id: serial('id').primaryKey(), user_id: uuid('user_id').references(() => users.id), // nullable for now; will be NOT NULL after data migration (task 2.5) - name: text('name').notNull().unique(), // internal name (e.g., 'break_up') + name: text('name').notNull(), // internal name (e.g., 'break_up') display_name: text('display_name').notNull(), // display name (e.g., 'Break Up') color: text('color').notNull(), // hex color code category: text('category').notNull(), // 'marker' or 'line' icon: text('icon'), // icon name or symbol is_active: boolean('is_active').notNull().default(true), // true = active, false = inactive created_at: timestamp('created_at').notNull().defaultNow(), -}); +}, (table) => [ + uniqueIndex('annotation_types_user_id_name_unique').on(table.user_id, table.name), +]); export const annotations = pgTable('annotations', { id: serial('id').primaryKey(), @@ -58,14 +62,16 @@ export const annotations = pgTable('annotations', { export const spanLabelTypes = pgTable('span_label_types', { id: serial('id').primaryKey(), user_id: uuid('user_id').references(() => users.id), // nullable for now; will be NOT NULL after data migration (task 2.5) - name: text('name').notNull().unique(), // internal name (e.g., 'bull_flag') + name: text('name').notNull(), // internal name (e.g., 'bull_flag') display_name: text('display_name').notNull(), // UI label (e.g., 'Bull Flag') color: text('color').notNull(), // hex color for rectangle fill hotkey: text('hotkey'), // keyboard shortcut (e.g., '1') is_active: boolean('is_active').notNull().default(true), // true = active, false = inactive sort_order: integer('sort_order').notNull().default(0), // display order created_at: timestamp('created_at').notNull().defaultNow(), -}); +}, (table) => [ + uniqueIndex('span_label_types_user_id_name_unique').on(table.user_id, table.name), +]); export const spanAnnotations = pgTable('span_annotations', { id: serial('id').primaryKey(),