schema: constrain integration providers to the ones the code supports

`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) <noreply@anthropic.com>
This commit is contained in:
2026-07-31 14:08:55 +00:00
co-authored by Claude Opus 5
parent c9d680b133
commit f9a07bb8ee
2 changed files with 39 additions and 10 deletions
+30 -9
View File
@@ -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', { export const serverConfig = pgTable('server_config', {
key: text('key').primaryKey(), key: text('key').primaryKey(),
@@ -6,11 +15,23 @@ export const serverConfig = pgTable('server_config', {
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(), updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
}); });
export const serverIntegrations = pgTable('server_integrations', { export const serverIntegrations = pgTable(
id: serial('id').primaryKey(), 'server_integrations',
provider: text('provider').notNull().unique(), {
enabled: boolean('enabled').notNull().default(true), id: serial('id').primaryKey(),
config: jsonb('config').notNull().default({}), provider: text('provider').notNull().unique(),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(), enabled: boolean('enabled').notNull().default(true),
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(), 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`, `,
)})`,
),
],
);
@@ -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 { users } from './auth';
import { sql } from 'drizzle-orm';
import { serverIntegrations } from './server'; import { serverIntegrations } from './server';
export const userSettings = pgTable('user_settings', { export const userSettings = pgTable('user_settings', {
@@ -14,6 +15,9 @@ export const userState = pgTable('user_state', {
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(), 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', { export const userIntegrations = pgTable('user_integrations', {
id: serial('id').primaryKey(), id: serial('id').primaryKey(),
userId: integer('user_id').notNull().references(() => users.id, { onDelete: 'cascade' }), 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(), updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
}, (table) => [ }, (table) => [
unique('uq_user_integrations_user_provider').on(table.userId, table.provider), 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', { export const dockConfigs = pgTable('dock_configs', {