feat(04-01): add list tables to schema and apply via generate+migrate [BLOCKING]
- Append lists, list_shares, list_items tables to Drizzle schema (schema.ts) - lists: owner_id FK, is_shared bool default true (D-01), idx_lists_owner_id - list_shares: list_id + user_id FKs, uniq_list_share, idx_list_shares_user_id (D-02) - list_items: rank varchar for fractional-indexing (D-13), checked bool, composite indexes - Generate 0000_easy_slipstream.sql (full schema baseline) + 0001_lists_schema.sql (new tables) - Mark 0000 as applied in __drizzle_migrations (prior tables existed from manual DDL) - Apply 0001_lists_schema.sql via db:migrate — lists/list_shares/list_items now in MariaDB - Add vitest/globals + node to tsconfig types for test file compatibility - NEVER used db:push (hard project constraint — drizzle-mariadb-push-unsafe) - typecheck passes
This commit is contained in:
@@ -12,6 +12,9 @@ import {
|
||||
unique,
|
||||
} from 'drizzle-orm/mysql-core'
|
||||
|
||||
// ── Phase 4: List tables ───────────────────────────────────────────────────
|
||||
// Imported by test/setup.ts for afterEach cleanup — keep exports consistent.
|
||||
|
||||
/**
|
||||
* Members of the household — identity keyed by oidc_iss + oidc_sub (never email, per D-10).
|
||||
* Color auto-assigned from palette on first login (D-06).
|
||||
@@ -161,3 +164,78 @@ export const calendarOutbox = mysqlTable(
|
||||
index('idx_outbox_uid').on(t.uid),
|
||||
],
|
||||
)
|
||||
|
||||
/**
|
||||
* App-owned named lists — stored in MariaDB, NOT CalDAV (Phase 4).
|
||||
*
|
||||
* D-01: isShared defaults to true (collaborative household use case).
|
||||
* D-02: ownership via ownerId; sharing via listShares join table (member-count-agnostic).
|
||||
*/
|
||||
export const lists = mysqlTable(
|
||||
'lists',
|
||||
{
|
||||
id: int().primaryKey().autoincrement(),
|
||||
ownerId: int('owner_id')
|
||||
.notNull()
|
||||
.references(() => users.id, { onDelete: 'cascade' }),
|
||||
name: varchar('name', { length: 255 }).notNull(),
|
||||
// D-01: default shared — the primary grocery/hub use case is collaborative.
|
||||
isShared: boolean('is_shared').default(true).notNull(),
|
||||
createdAt: timestamp('created_at').defaultNow().notNull(),
|
||||
updatedAt: timestamp('updated_at').defaultNow().onUpdateNow(),
|
||||
},
|
||||
(t) => [index('idx_lists_owner_id').on(t.ownerId)],
|
||||
)
|
||||
|
||||
/**
|
||||
* List sharing join table (D-02, member-count-agnostic).
|
||||
*
|
||||
* One row per (list, member) pair. v1 UI treats a list as "shared" when any
|
||||
* row exists; future UI can offer per-recipient granularity without a migration.
|
||||
*/
|
||||
export const listShares = mysqlTable(
|
||||
'list_shares',
|
||||
{
|
||||
id: int().primaryKey().autoincrement(),
|
||||
listId: int('list_id')
|
||||
.notNull()
|
||||
.references(() => lists.id, { onDelete: 'cascade' }),
|
||||
userId: int('user_id')
|
||||
.notNull()
|
||||
.references(() => users.id, { onDelete: 'cascade' }),
|
||||
createdAt: timestamp('created_at').defaultNow().notNull(),
|
||||
},
|
||||
(t) => [
|
||||
unique('uniq_list_share').on(t.listId, t.userId),
|
||||
index('idx_list_shares_user_id').on(t.userId),
|
||||
],
|
||||
)
|
||||
|
||||
/**
|
||||
* Items within a list.
|
||||
*
|
||||
* D-13: rank uses fractional-indexing strings (e.g. "a0", "a1", "Zz") — a single
|
||||
* move rewrites only the moved item's rank (one-row write), which plays well with
|
||||
* SSE live sync and concurrent reorders.
|
||||
* D-05: checked items render in a "completed" section at the bottom; not removed.
|
||||
*/
|
||||
export const listItems = mysqlTable(
|
||||
'list_items',
|
||||
{
|
||||
id: int().primaryKey().autoincrement(),
|
||||
listId: int('list_id')
|
||||
.notNull()
|
||||
.references(() => lists.id, { onDelete: 'cascade' }),
|
||||
text: varchar('text', { length: 500 }).notNull(),
|
||||
checked: boolean('checked').default(false).notNull(),
|
||||
rank: varchar('rank', { length: 255 }).notNull(), // fractional-indexing string (D-13)
|
||||
createdAt: timestamp('created_at').defaultNow().notNull(),
|
||||
updatedAt: timestamp('updated_at').defaultNow().onUpdateNow(),
|
||||
},
|
||||
(t) => [
|
||||
// Composite index covers ordered list fetch: WHERE list_id=? ORDER BY rank
|
||||
index('idx_list_items_list_id_rank').on(t.listId, t.rank),
|
||||
// Secondary index for checked/unchecked split queries
|
||||
index('idx_list_items_list_id_checked').on(t.listId, t.checked),
|
||||
],
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user