mirror of
https://github.com/Kaelio/ktx.git
synced 2026-06-13 08:15:14 +02:00
fix(config): reject reserved ingest connection ids
This commit is contained in:
parent
ca61f3e08e
commit
23dba892cd
9 changed files with 132 additions and 10 deletions
|
|
@ -1,4 +1,5 @@
|
|||
import { type Command, InvalidArgumentError, Option } from '@commander-js/extra-typings';
|
||||
import { reservedKtxIngestConnectionIdMessage } from '@ktx/context/project';
|
||||
import type { KtxCliCommandContext } from '../cli-program.js';
|
||||
import { resolveCommandProjectDir } from '../cli-program.js';
|
||||
import type { KtxSetupDatabaseDriver } from '../setup-databases.js';
|
||||
|
|
@ -268,6 +269,10 @@ export function registerSetupCommands(program: Command, context: KtxCliCommandCo
|
|||
if (!/^[a-zA-Z0-9][a-zA-Z0-9_-]*$/.test(value)) {
|
||||
throw new InvalidArgumentError(`Unsafe connection id: ${value}`);
|
||||
}
|
||||
const reservedMessage = reservedKtxIngestConnectionIdMessage(value);
|
||||
if (reservedMessage) {
|
||||
throw new InvalidArgumentError(reservedMessage);
|
||||
}
|
||||
return value;
|
||||
})
|
||||
.option('--database-url <url>', 'URL, env:NAME, or file:/path for one new URL-style database connection')
|
||||
|
|
|
|||
|
|
@ -1175,6 +1175,18 @@ describe('runKtxCli', () => {
|
|||
);
|
||||
});
|
||||
|
||||
it('rejects reserved setup database connection ids before dispatch', async () => {
|
||||
const testIo = makeIo();
|
||||
const setup = vi.fn(async () => 0);
|
||||
|
||||
await expect(
|
||||
runKtxCli(['setup', '--new-database-connection-id', 'status', '--no-input'], testIo.io, { setup }),
|
||||
).resolves.toBe(1);
|
||||
|
||||
expect(setup).not.toHaveBeenCalled();
|
||||
expect(testIo.stderr()).toContain('"status" is reserved for ktx ingest status; choose a different connection id.');
|
||||
});
|
||||
|
||||
it('dispatches setup source flags', async () => {
|
||||
const setup = vi.fn(async () => 0);
|
||||
const testIo = makeIo();
|
||||
|
|
|
|||
|
|
@ -1717,6 +1717,26 @@ describe('setup databases step', () => {
|
|||
expect(io.stderr()).toContain('Missing database connection id');
|
||||
});
|
||||
|
||||
it('rejects reserved non-interactive database connection ids', async () => {
|
||||
const io = makeIo();
|
||||
|
||||
const result = await runKtxSetupDatabasesStep(
|
||||
{
|
||||
projectDir: tempDir,
|
||||
inputMode: 'disabled',
|
||||
databaseDrivers: ['postgres'],
|
||||
databaseConnectionId: 'replay',
|
||||
databaseUrl: 'env:DATABASE_URL',
|
||||
databaseSchemas: [],
|
||||
skipDatabases: false,
|
||||
},
|
||||
io.io,
|
||||
);
|
||||
|
||||
expect(result.status).toBe('failed');
|
||||
expect(io.stderr()).toContain('"replay" is reserved for ktx ingest replay; choose a different connection id.');
|
||||
});
|
||||
|
||||
it('leaves setup incomplete when primary sources are skipped', async () => {
|
||||
const io = makeIo();
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import { fileURLToPath } from 'node:url';
|
|||
import { promisify } from 'node:util';
|
||||
import type { HistoricSqlDialect } from '@ktx/context/ingest';
|
||||
import {
|
||||
assertKtxConnectionIdIsNotReserved,
|
||||
type KtxProjectConnectionConfig,
|
||||
loadKtxProject,
|
||||
markKtxSetupStateStepComplete,
|
||||
|
|
@ -227,6 +228,13 @@ function unique(values: string[]): string[] {
|
|||
return [...new Set(values.filter((value) => value.trim().length > 0))];
|
||||
}
|
||||
|
||||
function assertSafeDatabaseConnectionId(connectionId: string): void {
|
||||
if (!/^[a-zA-Z0-9][a-zA-Z0-9_-]*$/.test(connectionId)) {
|
||||
throw new Error(`Unsafe connection id: ${connectionId}`);
|
||||
}
|
||||
assertKtxConnectionIdIsNotReserved(connectionId);
|
||||
}
|
||||
|
||||
function historicSqlConfigRecord(connection: KtxProjectConnectionConfig | undefined): Record<string, unknown> | null {
|
||||
const historicSql = connection?.historicSql;
|
||||
return historicSql && typeof historicSql === 'object' && !Array.isArray(historicSql)
|
||||
|
|
@ -1665,10 +1673,12 @@ async function chooseConnectionIdForDriver(input: {
|
|||
prompts: KtxSetupDatabasesPromptAdapter;
|
||||
}): Promise<{ kind: 'existing' | 'new'; connectionId: string } | 'back' | 'missing-input'> {
|
||||
if (input.args.databaseConnectionId) {
|
||||
assertSafeDatabaseConnectionId(input.args.databaseConnectionId);
|
||||
return { kind: 'new', connectionId: input.args.databaseConnectionId };
|
||||
}
|
||||
if (input.args.inputMode === 'disabled') {
|
||||
if (!input.args.databaseConnectionId) return 'missing-input';
|
||||
assertSafeDatabaseConnectionId(input.args.databaseConnectionId);
|
||||
return { kind: 'new', connectionId: input.args.databaseConnectionId };
|
||||
}
|
||||
|
||||
|
|
@ -1684,6 +1694,7 @@ async function chooseConnectionIdForDriver(input: {
|
|||
});
|
||||
if (entered === undefined) return 'back';
|
||||
const connectionId = entered.trim() || defaultId;
|
||||
assertSafeDatabaseConnectionId(connectionId);
|
||||
return connectionId ? { kind: 'new', connectionId } : 'missing-input';
|
||||
}
|
||||
|
||||
|
|
@ -1708,6 +1719,7 @@ async function chooseConnectionIdForDriver(input: {
|
|||
});
|
||||
if (entered === undefined) continue;
|
||||
const connectionId = entered.trim() || defaultId;
|
||||
assertSafeDatabaseConnectionId(connectionId);
|
||||
return connectionId ? { kind: 'new', connectionId } : 'missing-input';
|
||||
}
|
||||
}
|
||||
|
|
@ -1780,12 +1792,18 @@ export async function runKtxSetupDatabasesStep(
|
|||
|
||||
for (const driver of drivers) {
|
||||
const project = await loadKtxProject({ projectDir: args.projectDir });
|
||||
const connectionChoice = await chooseConnectionIdForDriver({
|
||||
driver,
|
||||
connections: project.config.connections,
|
||||
args,
|
||||
prompts,
|
||||
});
|
||||
let connectionChoice: Awaited<ReturnType<typeof chooseConnectionIdForDriver>>;
|
||||
try {
|
||||
connectionChoice = await chooseConnectionIdForDriver({
|
||||
driver,
|
||||
connections: project.config.connections,
|
||||
args,
|
||||
prompts,
|
||||
});
|
||||
} catch (error) {
|
||||
io.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
|
||||
return { status: 'failed', projectDir: args.projectDir };
|
||||
}
|
||||
if (connectionChoice === 'back') {
|
||||
if (!canReturnToDriverSelection) return { status: 'back', projectDir: args.projectDir };
|
||||
returnToDriverSelection = true;
|
||||
|
|
|
|||
|
|
@ -254,6 +254,31 @@ describe('setup sources step', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('rejects reserved interactive source connection ids', async () => {
|
||||
await addPrimarySource();
|
||||
const io = makeIo();
|
||||
|
||||
const result = await runKtxSetupSourcesStep(
|
||||
{
|
||||
projectDir,
|
||||
inputMode: 'auto',
|
||||
runInitialSourceIngest: false,
|
||||
skipSources: false,
|
||||
},
|
||||
io.io,
|
||||
{
|
||||
prompts: prompts({
|
||||
multiselect: [['notion']],
|
||||
text: ['status', 'env:NOTION_TOKEN'],
|
||||
select: ['env', 'all_accessible'],
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
expect(result.status).toBe('failed');
|
||||
expect(io.stderr()).toContain('"status" is reserved for ktx ingest status; choose a different connection id.');
|
||||
});
|
||||
|
||||
it('uses selected Notion roots when root page ids are provided even if crawl mode says all accessible', async () => {
|
||||
await addPrimarySource();
|
||||
const validateNotion = vi.fn(async () => ({ ok: true as const, detail: 'roots=1' }));
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import {
|
|||
testRepoConnection,
|
||||
} from '@ktx/context/ingest';
|
||||
import {
|
||||
assertKtxConnectionIdIsNotReserved,
|
||||
type KtxProjectConfig,
|
||||
type KtxProjectConnectionConfig,
|
||||
loadKtxProject,
|
||||
|
|
@ -201,6 +202,7 @@ function assertSafeConnectionId(connectionId: string): void {
|
|||
if (!/^[a-zA-Z0-9][a-zA-Z0-9_-]*$/.test(connectionId)) {
|
||||
throw new Error(`Unsafe connection id: ${connectionId}`);
|
||||
}
|
||||
assertKtxConnectionIdIsNotReserved(connectionId);
|
||||
}
|
||||
|
||||
function credentialRef(value: string | undefined, label: string): string {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue