From f9a07bb8eee2567573a4db8a931822e82e74e7e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Padez?= Date: Fri, 31 Jul 2026 14:08:55 +0000 Subject: [PATCH] schema: constrain integration providers to the ones the code supports MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `provider` was free text on both integration tables, which is how rows for telegram, whatsapp and discord went on holding bot tokens long after the code that read them was deleted: nothing structural said they had stopped being legal, so nothing noticed. A CHECK constraint on each table now lists what may exist — google/apify for the server, google/browser-relay for the user. Adding an integration means adding it to the list, which is the point: the schema is the source of truth for what the database may contain, so a provider the code no longer supports cannot sit there unnoticed. This does NOT delete the existing rows, and no schema change can — drizzle-kit push diffs structure, never data. What it does instead is refuse: push will fail to add the constraint while violating rows exist. That is the enforcement working rather than a problem to route around; the three rows have to go first, and then the structure guarantees they cannot come back. Co-Authored-By: Claude Opus 5 (1M context) --- src/databases/officer_db/src/schema/server.ts | 39 ++++++++++++++----- .../officer_db/src/schema/user-data.ts | 10 ++++- 2 files changed, 39 insertions(+), 10 deletions(-) diff --git a/src/databases/officer_db/src/schema/server.ts b/src/databases/officer_db/src/schema/server.ts index 57f721db..752f888e 100644 --- a/src/databases/officer_db/src/schema/server.ts +++ b/src/databases/officer_db/src/schema/server.ts @@ -1,4 +1,13 @@ -import { pgTable, serial, boolean, text, timestamp, jsonb } from 'drizzle-orm/pg-core'; +import { pgTable, serial, boolean, text, timestamp, jsonb, check } from 'drizzle-orm/pg-core'; +import { sql } from 'drizzle-orm'; + +// The integrations the server knows how to configure. `provider` was free text, which is how rows for +// telegram, whatsapp and discord went on holding bot tokens long after the code that read them was +// deleted — nothing structural said they were no longer legal. +// +// Adding an integration means adding it here. That is the point: the schema is the source of truth for +// what may exist, so a provider the code no longer supports cannot sit in the table unnoticed. +const SERVER_PROVIDERS = ['google', 'apify'] as const; export const serverConfig = pgTable('server_config', { key: text('key').primaryKey(), @@ -6,11 +15,23 @@ export const serverConfig = pgTable('server_config', { updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(), }); -export const serverIntegrations = pgTable('server_integrations', { - id: serial('id').primaryKey(), - provider: text('provider').notNull().unique(), - enabled: boolean('enabled').notNull().default(true), - config: jsonb('config').notNull().default({}), - createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(), - updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(), -}); +export const serverIntegrations = pgTable( + 'server_integrations', + { + id: serial('id').primaryKey(), + provider: text('provider').notNull().unique(), + enabled: boolean('enabled').notNull().default(true), + config: jsonb('config').notNull().default({}), + createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(), + updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(), + }, + (table) => [ + check( + 'ck_server_integrations_provider', + sql`${table.provider} IN (${sql.join( + SERVER_PROVIDERS.map((p) => sql`${p}`), + sql`, `, + )})`, + ), + ], +); diff --git a/src/databases/officer_db/src/schema/user-data.ts b/src/databases/officer_db/src/schema/user-data.ts index 9617e3fd..fcd59a93 100644 --- a/src/databases/officer_db/src/schema/user-data.ts +++ b/src/databases/officer_db/src/schema/user-data.ts @@ -1,5 +1,6 @@ -import { pgTable, serial, integer, text, timestamp, jsonb, unique } from 'drizzle-orm/pg-core'; +import { pgTable, serial, integer, text, timestamp, jsonb, unique, check } from 'drizzle-orm/pg-core'; import { users } from './auth'; +import { sql } from 'drizzle-orm'; import { serverIntegrations } from './server'; export const userSettings = pgTable('user_settings', { @@ -14,6 +15,9 @@ export const userState = pgTable('user_state', { updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(), }); +// Per-user integrations the code knows how to read. Same reasoning as SERVER_PROVIDERS in ./server. +const USER_PROVIDERS = ['google', 'browser-relay'] as const; + export const userIntegrations = pgTable('user_integrations', { id: serial('id').primaryKey(), userId: integer('user_id').notNull().references(() => users.id, { onDelete: 'cascade' }), @@ -24,6 +28,10 @@ export const userIntegrations = pgTable('user_integrations', { updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(), }, (table) => [ unique('uq_user_integrations_user_provider').on(table.userId, table.provider), + check( + 'ck_user_integrations_provider', + sql`${table.provider} IN (${sql.join(USER_PROVIDERS.map((p) => sql`${p}`), sql`, `)})`, + ), ]); export const dockConfigs = pgTable('dock_configs', {