130 lines
7.5 KiB
Markdown
130 lines
7.5 KiB
Markdown
---
|
|
phase: "10-admin-role-settings"
|
|
plan: "01"
|
|
subsystem: "database"
|
|
tags: ["schema", "migration", "mariadb", "drizzle", "admin", "seed"]
|
|
dependency_graph:
|
|
requires: []
|
|
provides:
|
|
- "users.is_admin column (boolean NOT NULL DEFAULT false)"
|
|
- "member_credentials.provider_type column (varchar(64) NOT NULL DEFAULT 'caldav')"
|
|
- "member_credentials UNIQUE(user_id) constraint uniq_member_credential_user"
|
|
- "calendar_events.reminder_lead_minutes column (int nullable)"
|
|
- "app_config table (key VARCHAR PK, value TEXT, updated_at)"
|
|
- "appConfig Drizzle table export in schema.ts"
|
|
- "migration 0001_famous_mad_thinker.sql applied to live dev MariaDB"
|
|
- "e2e global-setup seeds users id=1 with is_admin=true"
|
|
affects:
|
|
- "Phase 10 plans 02-04 (requireAdmin DB lookup, /api/me isAdmin, admin routes)"
|
|
- "Phase 11 (reminder_lead_minutes consumed)"
|
|
- "Phase 12 (app_config.setup_complete consumed)"
|
|
tech_stack:
|
|
added: []
|
|
patterns:
|
|
- "drizzle-kit generate + direct SQL apply (journal hash mismatch workaround)"
|
|
- "idempotent INSERT ON DUPLICATE KEY UPDATE for e2e seed"
|
|
key_files:
|
|
created:
|
|
- "apps/api/src/db/migrations/0001_famous_mad_thinker.sql"
|
|
- "apps/api/src/db/migrations/meta/0001_snapshot.json"
|
|
modified:
|
|
- "apps/api/src/db/schema.ts"
|
|
- "apps/api/src/db/migrations/meta/_journal.json"
|
|
- "apps/pwa/e2e/global-setup.ts"
|
|
decisions:
|
|
- "Applied migration DDL directly (mysql2) and inserted hash into __drizzle_migrations due to journal hash mismatch with legacy migration tracking; drizzle-kit migrate silently exited 1 without applying SQL"
|
|
- "Migration is additive-only (verified: grep for DROP/TRUNCATE returns 0)"
|
|
metrics:
|
|
duration_seconds: 265
|
|
completed_date: "2026-06-13"
|
|
tasks_completed: 3
|
|
files_modified: 5
|
|
---
|
|
|
|
# Phase 10 Plan 01: v1.1 DB Foundation Summary
|
|
|
|
**One-liner:** v1.1 schema migration adding users.is_admin, member_credentials.provider_type+UNIQUE(user_id), calendar_events.reminder_lead_minutes, and app_config table — applied to live dev MariaDB and seeded dev-bypass user as admin.
|
|
|
|
## Tasks Completed
|
|
|
|
| Task | Name | Commit | Files |
|
|
|------|------|--------|-------|
|
|
| 1 | Extend schema.ts with v1.1 column/table bundle | d0a4cb4 | apps/api/src/db/schema.ts |
|
|
| 2 | Generate + apply v1.1 migration to live dev DB | ad7ba3a | 0001_famous_mad_thinker.sql, meta/_journal.json, meta/0001_snapshot.json |
|
|
| 3 | Seed dev-bypass user id=1 as admin in e2e global-setup | bb00c71 | apps/pwa/e2e/global-setup.ts |
|
|
|
|
## What Was Built
|
|
|
|
### Task 1: Schema additions
|
|
|
|
Four additive changes to `apps/api/src/db/schema.ts`, all matching the existing column idiom:
|
|
|
|
1. `users.isAdmin`: `boolean('is_admin').default(false).notNull()` — copies the `allDay` boolean pattern; first-login-wins admin flag (D-01).
|
|
2. `memberCredentials.providerType`: `varchar('provider_type', { length: 64 }).notNull().default('caldav')` — generic provider discriminator (D-04); plus `unique('uniq_member_credential_user').on(t.userId)` added to the index array alongside the existing `idx_member_credentials_user_id` (D-05 one-credential-per-member enforcement + enables onDuplicateKeyUpdate upsert).
|
|
3. `calendarEvents.reminderLeadMinutes`: `int('reminder_lead_minutes')` — nullable, no `.notNull()`; consumed by Phase 11.
|
|
4. New `appConfig` table: `key VARCHAR(128) PK`, `value TEXT nullable`, `updatedAt timestamp DEFAULT NOW() ON UPDATE`; `setup_complete` key semantics documented for Phase 12.
|
|
|
|
TypeScript typecheck (`tsc --noEmit`) passes clean.
|
|
|
|
### Task 2: Migration generation and application
|
|
|
|
`pnpm --filter @familysync/api db:generate` produced `0001_famous_mad_thinker.sql` — additive-only DDL:
|
|
- `CREATE TABLE app_config`
|
|
- `ALTER TABLE calendar_events ADD reminder_lead_minutes int`
|
|
- `ALTER TABLE member_credentials ADD provider_type varchar(64) DEFAULT 'caldav' NOT NULL`
|
|
- `ALTER TABLE users ADD is_admin boolean DEFAULT false NOT NULL`
|
|
- `ALTER TABLE member_credentials ADD CONSTRAINT uniq_member_credential_user UNIQUE(user_id)`
|
|
|
|
`grep -v '^--' ... | grep -ciE 'drop (table|column)|truncate'` = **0** (additive-only confirmed).
|
|
|
|
**Migration application deviation:** `drizzle-kit migrate` exited 1 silently without applying the SQL. Root cause: the live dev DB `__drizzle_migrations` table contains 5 rows from legacy incremental development (different hashes from before the generate+migrate workflow was adopted); the journal's hash for `0000_baseline` does not match any existing row, causing drizzle-kit to stop. Resolution: applied all 5 DDL statements directly via mysql2, then inserted the correct SHA-256 hash of `0001_famous_mad_thinker.sql` into `__drizzle_migrations`. Future migrations via drizzle-kit should work correctly from this point.
|
|
|
|
Live DB verification:
|
|
```
|
|
SHOW COLUMNS FROM users LIKE 'is_admin' → 1 row
|
|
SHOW COLUMNS FROM member_credentials LIKE 'provider_type' → 1 row
|
|
SHOW COLUMNS FROM calendar_events LIKE 'reminder_lead_minutes' → 1 row
|
|
SHOW TABLES LIKE 'app_config' → 1 row
|
|
```
|
|
Query output: **MIGRATION OK**
|
|
|
|
### Task 3: E2E dev-bypass admin seed
|
|
|
|
Added idempotent seed in `apps/pwa/e2e/global-setup.ts` inside the seed block (before the FK-checks-on, matching the existing `INSERT IGNORE INTO calendars` pattern):
|
|
|
|
```sql
|
|
INSERT INTO users (id, oidc_iss, oidc_sub, display_name, color, is_admin)
|
|
VALUES (1, 'dev-bypass', 'dev-user-1', 'Dev User', '#4A90D9', true)
|
|
ON DUPLICATE KEY UPDATE is_admin=true
|
|
```
|
|
|
|
Supplies non-null `oidc_iss`, `oidc_sub`, `color` satisfying NOT NULL constraints. Idempotent via `ON DUPLICATE KEY UPDATE`. The bypass path never reads oidc_iss/oidc_sub so placeholder values are safe. Existing calendar/event/list seeds unchanged (`INSERT IGNORE INTO calendars` and `Seeded Test Event` anchors still present).
|
|
|
|
## Deviations from Plan
|
|
|
|
### Auto-fixed Issues
|
|
|
|
**1. [Rule 1 - Bug] drizzle-kit migrate journal hash mismatch — applied DDL directly**
|
|
- **Found during:** Task 2
|
|
- **Issue:** `drizzle-kit migrate` silently exited 1 without applying any SQL. The live dev DB `__drizzle_migrations` table had 5 rows with hashes from a prior incremental migration workflow (before the generate+migrate adoption in quick 260610-cr8). drizzle-kit compares the journal's first entry against the DB and stops on mismatch.
|
|
- **Fix:** Applied the 5 DDL statements from `0001_famous_mad_thinker.sql` directly via mysql2 (splitting on `--> statement-breakpoint` delimiter). Inserted the SHA-256 hash of the migration file into `__drizzle_migrations` so future drizzle-kit runs treat this migration as applied.
|
|
- **Impact:** None on correctness — the DDL is identical to what drizzle-kit would have applied. Future migrations should work normally since the hash record is now in the DB.
|
|
- **Files modified:** No extra files; the migration SQL and journal were already committed.
|
|
|
|
## Known Stubs
|
|
|
|
None. This plan creates DB infrastructure only; no UI stubs.
|
|
|
|
## Threat Flags
|
|
|
|
None. No new network endpoints, auth paths, or trust-boundary changes in this plan. The migration is schema-only; the e2e seed is guarded by the existing `NODE_ENV === 'production'` and `DEV_AUTH_BYPASS !== 'true'` fail-closed guards.
|
|
|
|
## Self-Check: PASSED
|
|
|
|
- `apps/api/src/db/schema.ts` contains `is_admin`, `provider_type`, `uniq_member_credential_user`, `reminder_lead_minutes`, `appConfig` export: verified
|
|
- `apps/api/src/db/migrations/0001_famous_mad_thinker.sql` exists and committed: verified (ad7ba3a)
|
|
- Live DB MIGRATION OK query: PASSED
|
|
- `grep -c is_admin apps/pwa/e2e/global-setup.ts` = 3 (≥1): verified
|
|
- `pnpm --filter @familysync/api exec tsc --noEmit` exits 0: verified
|
|
- Commits d0a4cb4, ad7ba3a, bb00c71 in git log: verified
|