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:
@@ -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', {
|
||||||
|
|||||||
Reference in New Issue
Block a user