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
+24 -3
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', {
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', {
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`, `,
)})`,
),
],
);
@@ -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', {