feat(api): unified Profile schema + consent backfill (ADR-0014 step 1-2)
Adds user_preferences, user_consents, user_contexts and the tone / tip_kinds_json columns on users. Backfills consent_given=1 rows into user_consents as data:core; INSERT OR IGNORE keeps it idempotent and respects later revocations. Migration body moves to db/migrations.ts so tests can apply it to a fresh in-memory handle without opening the prod DB on import. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -7,12 +7,50 @@ export const users = sqliteTable('users', {
|
||||
image: text('image'),
|
||||
googleId: text('google_id').unique(),
|
||||
role: text('role').notNull().default('user'), // 'user' | 'admin'
|
||||
// Legacy single-bit consent. Superseded by user_consents (consent_key='data:core').
|
||||
// Kept for one release per ADR-0014 migration plan; reads consult both, writes go to user_consents only.
|
||||
consentGiven: integer('consent_given', { mode: 'boolean' }).notNull().default(false),
|
||||
consentAt: text('consent_at'),
|
||||
// Stable globals (ADR-0014). Per-agent prefs land in user_preferences instead.
|
||||
tone: text('tone'), // 'direct' | 'gentle' | 'motivational'
|
||||
tipKindsJson: text('tip_kinds_json'), // JSON array of allowed tip kinds; null = all
|
||||
createdAt: text('created_at').notNull(),
|
||||
deletedAt: text('deleted_at'),
|
||||
});
|
||||
|
||||
// ── Unified Profile model (ADR-0014) ────────────────────────────────────────
|
||||
// Open-ended per-scope preferences. `scope` is 'orchestrator' or 'agent:<id>';
|
||||
// the agent's pref_schema (from its manifest) validates value_json on read.
|
||||
// `source='inferred'` is written by the inference framework (#111); never
|
||||
// overwrites a `source='user'` row.
|
||||
export const userPreferences = sqliteTable('user_preferences', {
|
||||
userId: text('user_id').notNull().references(() => users.id),
|
||||
scope: text('scope').notNull(), // 'orchestrator' | 'agent:<id>'
|
||||
key: text('key').notNull(),
|
||||
valueJson: text('value_json').notNull(),
|
||||
source: text('source').notNull().default('user'), // 'user' | 'inferred'
|
||||
updatedAt: text('updated_at').notNull(),
|
||||
});
|
||||
|
||||
// Per-key consent. Revocation writes `revoked_at`; rows are never deleted
|
||||
// so audits stay clean. `revoked_at IS NULL` = currently active.
|
||||
export const userConsents = sqliteTable('user_consents', {
|
||||
userId: text('user_id').notNull().references(() => users.id),
|
||||
consentKey: text('consent_key').notNull(), // 'data:core' | 'data:todoist' | 'agent:<id>' | …
|
||||
grantedAt: text('granted_at').notNull(),
|
||||
revokedAt: text('revoked_at'),
|
||||
});
|
||||
|
||||
// User-named contexts (work / home / vacation). M2 ships manual toggle only;
|
||||
// auto-inference is per-agent (#112–#116).
|
||||
export const userContexts = sqliteTable('user_contexts', {
|
||||
userId: text('user_id').notNull().references(() => users.id),
|
||||
name: text('name').notNull(),
|
||||
active: integer('active', { mode: 'boolean' }).notNull().default(false),
|
||||
scheduleJson: text('schedule_json'), // optional: when active
|
||||
createdAt: text('created_at').notNull(),
|
||||
});
|
||||
|
||||
export const integrationTokens = sqliteTable('integration_tokens', {
|
||||
id: text('id').primaryKey(),
|
||||
userId: text('user_id').notNull().references(() => users.id),
|
||||
|
||||
Reference in New Issue
Block a user