feat: enhance TokenRoutes with edit functionality and route visibility management
- Added editing capabilities for route management in TokenRoutes, allowing users to modify existing routes. - Introduced a reset function for the route form to clear inputs after adding or editing routes. - Implemented a visibility filter for routes, ensuring exact routes covered by group routes are hidden from the main list. - Updated tests to cover new functionalities, including editing routes and verifying visibility logic. - Created a new helper function to build a list of visible routes based on defined criteria. - Enhanced schema compatibility checks for site configurations, ensuring necessary columns are added to the database.
This commit is contained in:
@@ -5,6 +5,7 @@ import { drizzle as drizzleSqliteProxy } from 'drizzle-orm/sqlite-proxy';
|
||||
import { drizzle as drizzleMysqlProxy } from 'drizzle-orm/mysql-proxy';
|
||||
import { drizzle as drizzlePgProxy } from 'drizzle-orm/pg-proxy';
|
||||
import * as schema from './schema.js';
|
||||
import { ensureSiteSchemaCompatibility, type SiteSchemaInspector } from './siteSchemaCompatibility.js';
|
||||
import { config } from '../config.js';
|
||||
import { mkdirSync } from 'fs';
|
||||
import { dirname, resolve } from 'path';
|
||||
@@ -221,6 +222,73 @@ function ensureSiteGlobalWeightSchema() {
|
||||
`);
|
||||
}
|
||||
|
||||
function createSqliteSiteSchemaInspector(): SiteSchemaInspector {
|
||||
return {
|
||||
dialect: 'sqlite',
|
||||
tableExists: async (table) => tableExists(table),
|
||||
columnExists: async (table, column) => tableColumnExists(table, column),
|
||||
execute: async (sqlText) => {
|
||||
requireSqliteConnection().exec(sqlText);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export async function ensureSiteCompatibilityColumns(): Promise<void> {
|
||||
if (runtimeDbDialect === 'sqlite') {
|
||||
await ensureSiteSchemaCompatibility(createSqliteSiteSchemaInspector());
|
||||
return;
|
||||
}
|
||||
|
||||
if (runtimeDbDialect === 'mysql') {
|
||||
if (!mysqlPool) return;
|
||||
|
||||
await ensureSiteSchemaCompatibility({
|
||||
dialect: 'mysql',
|
||||
tableExists: async (table) => {
|
||||
const [rows] = await mysqlPool!.query(
|
||||
'SELECT 1 FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = ? LIMIT 1',
|
||||
[table],
|
||||
);
|
||||
return Array.isArray(rows) && rows.length > 0;
|
||||
},
|
||||
columnExists: async (table, column) => {
|
||||
const [rows] = await mysqlPool!.query(
|
||||
'SELECT 1 FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = ? AND column_name = ? LIMIT 1',
|
||||
[table, column],
|
||||
);
|
||||
return Array.isArray(rows) && rows.length > 0;
|
||||
},
|
||||
execute: async (sqlText) => {
|
||||
await mysqlPool!.query(sqlText);
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!pgPool) return;
|
||||
|
||||
await ensureSiteSchemaCompatibility({
|
||||
dialect: 'postgres',
|
||||
tableExists: async (table) => {
|
||||
const result = await pgPool!.query(
|
||||
'SELECT 1 FROM information_schema.tables WHERE table_schema = current_schema() AND table_name = $1 LIMIT 1',
|
||||
[table],
|
||||
);
|
||||
return Number(result.rowCount || 0) > 0;
|
||||
},
|
||||
columnExists: async (table, column) => {
|
||||
const result = await pgPool!.query(
|
||||
'SELECT 1 FROM information_schema.columns WHERE table_schema = current_schema() AND table_name = $1 AND column_name = $2 LIMIT 1',
|
||||
[table, column],
|
||||
);
|
||||
return Number(result.rowCount || 0) > 0;
|
||||
},
|
||||
execute: async (sqlText) => {
|
||||
await pgPool!.query(sqlText);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function ensureRouteGroupingSchema() {
|
||||
const sqlite = requireSqliteConnection();
|
||||
if (!tableExists('token_routes') || !tableExists('route_channels')) {
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { ensureSiteSchemaCompatibility, type SiteSchemaInspector } from './siteSchemaCompatibility.js';
|
||||
|
||||
function createInspector(
|
||||
dialect: SiteSchemaInspector['dialect'],
|
||||
options?: {
|
||||
hasSitesTable?: boolean;
|
||||
existingColumns?: string[];
|
||||
},
|
||||
) {
|
||||
const executedSql: string[] = [];
|
||||
const hasSitesTable = options?.hasSitesTable ?? true;
|
||||
const existingColumns = new Set(options?.existingColumns ?? []);
|
||||
|
||||
const inspector: SiteSchemaInspector = {
|
||||
dialect,
|
||||
async tableExists(table) {
|
||||
return table === 'sites' && hasSitesTable;
|
||||
},
|
||||
async columnExists(table, column) {
|
||||
return table === 'sites' && existingColumns.has(column);
|
||||
},
|
||||
async execute(sqlText) {
|
||||
executedSql.push(sqlText);
|
||||
},
|
||||
};
|
||||
|
||||
return { inspector, executedSql };
|
||||
}
|
||||
|
||||
describe('ensureSiteSchemaCompatibility', () => {
|
||||
it.each([
|
||||
{
|
||||
dialect: 'postgres' as const,
|
||||
expectedSql: [
|
||||
'ALTER TABLE "sites" ADD COLUMN "proxy_url" TEXT',
|
||||
'ALTER TABLE "sites" ADD COLUMN "use_system_proxy" BOOLEAN DEFAULT FALSE',
|
||||
'UPDATE "sites" SET "use_system_proxy" = FALSE WHERE "use_system_proxy" IS NULL',
|
||||
'ALTER TABLE "sites" ADD COLUMN "external_checkin_url" TEXT',
|
||||
'ALTER TABLE "sites" ADD COLUMN "global_weight" DOUBLE PRECISION DEFAULT 1',
|
||||
'UPDATE "sites" SET "global_weight" = 1 WHERE "global_weight" IS NULL OR "global_weight" <= 0',
|
||||
],
|
||||
},
|
||||
{
|
||||
dialect: 'mysql' as const,
|
||||
expectedSql: [
|
||||
'ALTER TABLE `sites` ADD COLUMN `proxy_url` TEXT NULL',
|
||||
'ALTER TABLE `sites` ADD COLUMN `use_system_proxy` BOOLEAN DEFAULT FALSE',
|
||||
'UPDATE `sites` SET `use_system_proxy` = FALSE WHERE `use_system_proxy` IS NULL',
|
||||
'ALTER TABLE `sites` ADD COLUMN `external_checkin_url` TEXT NULL',
|
||||
'ALTER TABLE `sites` ADD COLUMN `global_weight` DOUBLE DEFAULT 1',
|
||||
'UPDATE `sites` SET `global_weight` = 1 WHERE `global_weight` IS NULL OR `global_weight` <= 0',
|
||||
],
|
||||
},
|
||||
])('adds missing site proxy columns for $dialect', async ({ dialect, expectedSql }) => {
|
||||
const { inspector, executedSql } = createInspector(dialect);
|
||||
|
||||
await ensureSiteSchemaCompatibility(inspector);
|
||||
|
||||
expect(executedSql).toEqual(expectedSql);
|
||||
});
|
||||
|
||||
it('skips schema changes when sites table does not exist', async () => {
|
||||
const { inspector, executedSql } = createInspector('postgres', { hasSitesTable: false });
|
||||
|
||||
await ensureSiteSchemaCompatibility(inspector);
|
||||
|
||||
expect(executedSql).toEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,101 @@
|
||||
export type SiteSchemaDialect = 'sqlite' | 'mysql' | 'postgres';
|
||||
|
||||
export interface SiteSchemaInspector {
|
||||
dialect: SiteSchemaDialect;
|
||||
tableExists(table: string): Promise<boolean>;
|
||||
columnExists(table: string, column: string): Promise<boolean>;
|
||||
execute(sqlText: string): Promise<void>;
|
||||
}
|
||||
|
||||
type SiteColumnCompatibilitySpec = {
|
||||
column: string;
|
||||
addSql: Record<SiteSchemaDialect, string>;
|
||||
normalizeSql?: Record<SiteSchemaDialect, string>;
|
||||
};
|
||||
|
||||
const SITE_COLUMN_COMPATIBILITY_SPECS: SiteColumnCompatibilitySpec[] = [
|
||||
{
|
||||
column: 'proxy_url',
|
||||
addSql: {
|
||||
sqlite: 'ALTER TABLE sites ADD COLUMN proxy_url text;',
|
||||
mysql: 'ALTER TABLE `sites` ADD COLUMN `proxy_url` TEXT NULL',
|
||||
postgres: 'ALTER TABLE "sites" ADD COLUMN "proxy_url" TEXT',
|
||||
},
|
||||
},
|
||||
{
|
||||
column: 'use_system_proxy',
|
||||
addSql: {
|
||||
sqlite: 'ALTER TABLE sites ADD COLUMN use_system_proxy integer DEFAULT 0;',
|
||||
mysql: 'ALTER TABLE `sites` ADD COLUMN `use_system_proxy` BOOLEAN DEFAULT FALSE',
|
||||
postgres: 'ALTER TABLE "sites" ADD COLUMN "use_system_proxy" BOOLEAN DEFAULT FALSE',
|
||||
},
|
||||
normalizeSql: {
|
||||
sqlite: 'UPDATE sites SET use_system_proxy = 0 WHERE use_system_proxy IS NULL;',
|
||||
mysql: 'UPDATE `sites` SET `use_system_proxy` = FALSE WHERE `use_system_proxy` IS NULL',
|
||||
postgres: 'UPDATE "sites" SET "use_system_proxy" = FALSE WHERE "use_system_proxy" IS NULL',
|
||||
},
|
||||
},
|
||||
{
|
||||
column: 'external_checkin_url',
|
||||
addSql: {
|
||||
sqlite: 'ALTER TABLE sites ADD COLUMN external_checkin_url text;',
|
||||
mysql: 'ALTER TABLE `sites` ADD COLUMN `external_checkin_url` TEXT NULL',
|
||||
postgres: 'ALTER TABLE "sites" ADD COLUMN "external_checkin_url" TEXT',
|
||||
},
|
||||
},
|
||||
{
|
||||
column: 'global_weight',
|
||||
addSql: {
|
||||
sqlite: 'ALTER TABLE sites ADD COLUMN global_weight real DEFAULT 1;',
|
||||
mysql: 'ALTER TABLE `sites` ADD COLUMN `global_weight` DOUBLE DEFAULT 1',
|
||||
postgres: 'ALTER TABLE "sites" ADD COLUMN "global_weight" DOUBLE PRECISION DEFAULT 1',
|
||||
},
|
||||
normalizeSql: {
|
||||
sqlite: 'UPDATE sites SET global_weight = 1 WHERE global_weight IS NULL OR global_weight <= 0;',
|
||||
mysql: 'UPDATE `sites` SET `global_weight` = 1 WHERE `global_weight` IS NULL OR `global_weight` <= 0',
|
||||
postgres: 'UPDATE "sites" SET "global_weight" = 1 WHERE "global_weight" IS NULL OR "global_weight" <= 0',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
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: SiteSchemaInspector, sqlText: string): Promise<void> {
|
||||
try {
|
||||
await inspector.execute(sqlText);
|
||||
} catch (error) {
|
||||
if (!isDuplicateColumnError(error)) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function ensureSiteSchemaCompatibility(inspector: SiteSchemaInspector): Promise<void> {
|
||||
const hasSitesTable = await inspector.tableExists('sites');
|
||||
if (!hasSitesTable) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const spec of SITE_COLUMN_COMPATIBILITY_SPECS) {
|
||||
const hasColumn = await inspector.columnExists('sites', spec.column);
|
||||
if (!hasColumn) {
|
||||
await executeAddColumn(inspector, spec.addSql[inspector.dialect]);
|
||||
}
|
||||
|
||||
if (spec.normalizeSql) {
|
||||
await inspector.execute(spec.normalizeSql[inspector.dialect]);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user