feat(10-01): add v1.1 schema bundle (is_admin, provider_type, reminder_lead_minutes, app_config)

- users.isAdmin: boolean NOT NULL DEFAULT false (first-login-wins admin flag, D-01)
- memberCredentials.providerType: varchar(64) NOT NULL DEFAULT 'caldav' (generic provider discriminator, D-04)
- memberCredentials: UNIQUE(user_id) constraint for one-credential-per-member + upsert support (D-05)
- calendarEvents.reminderLeadMinutes: int nullable (created now, consumed by Phase 11)
- appConfig table: key VARCHAR PK, value TEXT, updated_at (setup_complete consumed by Phase 12)
This commit is contained in:
Lucas Berger
2026-06-13 14:20:10 -04:00
parent df92bd89e8
commit d0a4cb4e35
+31 -2
View File
@@ -30,6 +30,7 @@ const varcharBin = (name: string) =>
/**
* 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).
* isAdmin: first-login-wins bootstrap (D-01); gated by app_config.setup_complete in Phase 12.
*/
export const users = mysqlTable(
'users',
@@ -40,6 +41,8 @@ export const users = mysqlTable(
displayName: varchar('display_name', { length: 256 }),
color: varchar('color', { length: 7 }).notNull(), // hex e.g. '#4A90D9'
createdAt: timestamp('created_at').defaultNow().notNull(),
// v1.1 (Phase 10): admin role flag — first-login-wins; Phase 12 tightens bootstrap
isAdmin: boolean('is_admin').default(false).notNull(),
},
(t) => [
// Composite unique key — identity is iss+sub, never email (D-10)
@@ -48,9 +51,11 @@ export const users = mysqlTable(
);
/**
* Encrypted Fastmail app-password credentials per member (D-04).
* Encrypted provider credentials per member (D-04 generic provider shape, Fastmail/CalDAV first).
* Keyed by user_id (oidc_sub → user row). Backend-only, never exposed to frontend.
* Stored as JSON: { iv, authTag, ciphertext } (AES-256-GCM).
* providerType: generic discriminator (D-04); 'caldav' is the only implemented provider.
* UNIQUE(user_id) enforces one-credential-per-member (D-05) and enables onDuplicateKeyUpdate upsert.
*/
export const memberCredentials = mysqlTable(
'member_credentials',
@@ -64,8 +69,14 @@ export const memberCredentials = mysqlTable(
fastmailEmail: varchar('fastmail_email', { length: 256 }).notNull(),
createdAt: timestamp('created_at').defaultNow().notNull(),
updatedAt: timestamp('updated_at').defaultNow().onUpdateNow(),
// v1.1 (Phase 10, D-04): generic provider discriminator; 'caldav' default for existing rows
providerType: varchar('provider_type', { length: 64 }).notNull().default('caldav'),
},
(t) => [index('idx_member_credentials_user_id').on(t.userId)],
(t) => [
index('idx_member_credentials_user_id').on(t.userId),
// v1.1 (Phase 10, D-05): one credential per member; enables Drizzle onDuplicateKeyUpdate upsert
unique('uniq_member_credential_user').on(t.userId),
],
);
/**
@@ -129,6 +140,8 @@ export const calendarEvents = mysqlTable(
// Events with hasRrule=true have dtstartUtc potentially years before any window,
// so the windowed query must include them regardless of dtstartUtc range (see RESEARCH.md §Pitfall 5).
hasRrule: boolean('has_rrule').default(false).notNull(),
// v1.1 (Phase 10): reminder lead time in minutes; nullable — consumed by Phase 11
reminderLeadMinutes: int('reminder_lead_minutes'),
updatedAt: timestamp('updated_at').defaultNow().onUpdateNow(),
},
(t) => [
@@ -256,6 +269,22 @@ export const pushSubscriptions = mysqlTable(
],
);
/**
* Global app configuration — simple key/value store.
*
* v1.1 (Phase 10): ships the table; consumed by downstream phases:
* - setup_complete (boolean string 'true'/'false'): Phase 12 setup wizard writes this key
* after first-time setup; Phase 10's first-login-wins admin bootstrap reads it in Phase 12.
* Key: 'setup_complete', Value: 'true' | 'false' | null (not yet set → treated as false).
*
* Do NOT add setup_complete gating logic here — Phase 12 owns that.
*/
export const appConfig = mysqlTable('app_config', {
key: varchar('key', { length: 128 }).primaryKey(),
value: text('value'), // nullable
updatedAt: timestamp('updated_at').defaultNow().onUpdateNow(),
});
/**
* Items within a list.
*