1
0
forked from boos/BoosAPI

fix(db): add schema compatibility for account_tokens.token_group column

Existing databases created before the token_group column was added would
fail on data import. Add accountTokenSchemaCompatibility module to
ALTER TABLE add the missing column, following the established pattern.
This commit is contained in:
Hureru
2026-03-11 01:15:11 +08:00
parent 457b4915da
commit 5c026df760
2 changed files with 66 additions and 0 deletions
@@ -0,0 +1,64 @@
export type AccountTokenSchemaDialect = 'sqlite' | 'mysql' | 'postgres';
export interface AccountTokenSchemaInspector {
dialect: AccountTokenSchemaDialect;
tableExists(table: string): Promise<boolean>;
columnExists(table: string, column: string): Promise<boolean>;
execute(sqlText: string): Promise<void>;
}
type AccountTokenColumnCompatibilitySpec = {
table: 'account_tokens';
column: string;
addSql: Record<AccountTokenSchemaDialect, string>;
};
const ACCOUNT_TOKEN_COLUMN_COMPATIBILITY_SPECS: AccountTokenColumnCompatibilitySpec[] = [
{
table: 'account_tokens',
column: 'token_group',
addSql: {
sqlite: 'ALTER TABLE account_tokens ADD COLUMN token_group text;',
mysql: 'ALTER TABLE `account_tokens` ADD COLUMN `token_group` TEXT NULL',
postgres: 'ALTER TABLE "account_tokens" ADD COLUMN "token_group" TEXT',
},
},
];
function normalizeSchemaErrorMessage(error: unknown): string {
if (typeof error === 'object' && error && 'message' in error) {
return String((error as { message?: unknown }).message || '');
}
return String(error || '');
}
function isDuplicateColumnError(error: unknown): boolean {
const lowered = normalizeSchemaErrorMessage(error).toLowerCase();
return lowered.includes('duplicate column')
|| lowered.includes('already exists')
|| lowered.includes('duplicate column name');
}
async function executeAddColumn(inspector: AccountTokenSchemaInspector, sqlText: string): Promise<void> {
try {
await inspector.execute(sqlText);
} catch (error) {
if (!isDuplicateColumnError(error)) {
throw error;
}
}
}
export async function ensureAccountTokenSchemaCompatibility(inspector: AccountTokenSchemaInspector): Promise<void> {
for (const spec of ACCOUNT_TOKEN_COLUMN_COMPATIBILITY_SPECS) {
const hasTable = await inspector.tableExists(spec.table);
if (!hasTable) {
continue;
}
const hasColumn = await inspector.columnExists(spec.table, spec.column);
if (!hasColumn) {
await executeAddColumn(inspector, spec.addSql[inspector.dialect]);
}
}
}
+2
View File
@@ -6,6 +6,7 @@ import { dirname, resolve } from 'node:path';
import { ensureSiteSchemaCompatibility, type SiteSchemaInspector } from './siteSchemaCompatibility.js'; import { ensureSiteSchemaCompatibility, type SiteSchemaInspector } from './siteSchemaCompatibility.js';
import { ensureRouteGroupingSchemaCompatibility } from './routeGroupingSchemaCompatibility.js'; import { ensureRouteGroupingSchemaCompatibility } from './routeGroupingSchemaCompatibility.js';
import { ensureProxyFileSchemaCompatibility } from './proxyFileSchemaCompatibility.js'; import { ensureProxyFileSchemaCompatibility } from './proxyFileSchemaCompatibility.js';
import { ensureAccountTokenSchemaCompatibility } from './accountTokenSchemaCompatibility.js';
export type RuntimeSchemaDialect = 'sqlite' | 'mysql' | 'postgres'; export type RuntimeSchemaDialect = 'sqlite' | 'mysql' | 'postgres';
@@ -241,6 +242,7 @@ export async function ensureRuntimeDatabaseSchema(client: RuntimeSchemaClient):
await ensureSiteSchemaCompatibility(createSiteSchemaInspector(client)); await ensureSiteSchemaCompatibility(createSiteSchemaInspector(client));
await ensureRouteGroupingSchemaCompatibility(createSiteSchemaInspector(client)); await ensureRouteGroupingSchemaCompatibility(createSiteSchemaInspector(client));
await ensureProxyFileSchemaCompatibility(createSiteSchemaInspector(client)); await ensureProxyFileSchemaCompatibility(createSiteSchemaInspector(client));
await ensureAccountTokenSchemaCompatibility(createSiteSchemaInspector(client));
} }
export async function bootstrapRuntimeDatabaseSchema(input: RuntimeSchemaConnectionInput): Promise<void> { export async function bootstrapRuntimeDatabaseSchema(input: RuntimeSchemaConnectionInput): Promise<void> {