mirror of
https://github.com/Kaelio/ktx.git
synced 2026-06-07 07:55:13 +02:00
Fix setup output formatting
This commit is contained in:
parent
347332540c
commit
496cc3a767
4 changed files with 146 additions and 7 deletions
|
|
@ -1236,6 +1236,48 @@ describe('setup databases step', () => {
|
|||
expect(config.connections.warehouse).toMatchObject({ driver: 'postgres', url: 'env:DATABASE_URL' });
|
||||
expect(await readFile(join(tempDir, 'ktx.yaml'), 'utf-8')).not.toContain('completed_steps:');
|
||||
expect(io.stderr()).toContain('Structural scan failed for warehouse.');
|
||||
expect(io.stderr()).toContain('│ Structural scan failed for warehouse.');
|
||||
expect(io.stderr()).not.toMatch(/^Structural scan failed for warehouse\./m);
|
||||
});
|
||||
|
||||
it('prints the native SQLite rebuild command when scanning hits a Node ABI mismatch', async () => {
|
||||
const io = makeIo();
|
||||
const result = await runKtxSetupDatabasesStep(
|
||||
{
|
||||
projectDir: tempDir,
|
||||
inputMode: 'disabled',
|
||||
databaseDrivers: ['postgres'],
|
||||
databaseConnectionId: 'warehouse',
|
||||
databaseUrl: 'env:DATABASE_URL',
|
||||
databaseSchemas: [],
|
||||
skipDatabases: false,
|
||||
},
|
||||
io.io,
|
||||
{
|
||||
testConnection: vi.fn(async () => 0),
|
||||
scanConnection: vi.fn(async (_projectDir: string, _connectionId: string, commandIo: KtxCliIo) => {
|
||||
commandIo.stderr.write(
|
||||
[
|
||||
"The module '/workspace/node_modules/better-sqlite3/build/Release/better_sqlite3.node'",
|
||||
'was compiled against a different Node.js version using',
|
||||
'NODE_MODULE_VERSION 147. This version of Node.js requires',
|
||||
'NODE_MODULE_VERSION 137. Please try re-compiling or re-installing',
|
||||
'the module (for instance, using `npm rebuild` or `npm install`).',
|
||||
'',
|
||||
].join('\n'),
|
||||
);
|
||||
return 1;
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
expect(result.status).toBe('failed');
|
||||
expect(io.stderr()).toContain('Native SQLite is built for a different Node.js ABI.');
|
||||
expect(io.stderr()).toContain('│ Native SQLite is built for a different Node.js ABI.');
|
||||
expect(io.stderr()).toContain('Fix: pnpm run native:rebuild');
|
||||
expect(io.stderr()).toContain(`Retry: ktx scan --project-dir ${tempDir} warehouse`);
|
||||
expect(io.stderr()).not.toContain('npm rebuild');
|
||||
expect(io.stderr()).not.toMatch(/^Native SQLite is built for a different Node.js ABI\./m);
|
||||
});
|
||||
|
||||
it('writes Historic SQL config for supported Snowflake databases after validation succeeds', async () => {
|
||||
|
|
|
|||
|
|
@ -951,6 +951,36 @@ function flushBufferedCommandOutput(io: KtxCliIo, bufferedIo: BufferedCommandIo)
|
|||
}
|
||||
}
|
||||
|
||||
function writePrefixedLines(write: (chunk: string) => void, output: string): void {
|
||||
for (const line of output.split(/\r?\n/)) {
|
||||
if (line.length > 0) {
|
||||
write(`│ ${line}\n`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function flushPrefixedBufferedCommandOutput(io: KtxCliIo, bufferedIo: BufferedCommandIo): void {
|
||||
writePrefixedLines((chunk) => io.stdout.write(chunk), bufferedIo.stdoutText());
|
||||
writePrefixedLines((chunk) => io.stderr.write(chunk), bufferedIo.stderrText());
|
||||
}
|
||||
|
||||
function nativeSqliteAbiMismatchDetail(output: string): string | null {
|
||||
const mentionsBetterSqlite = /\bbetter-sqlite3\b|better_sqlite3/i.test(output);
|
||||
const mentionsAbiMismatch = /compiled against a different Node\.js version|NODE_MODULE_VERSION/i.test(output);
|
||||
if (!mentionsBetterSqlite || !mentionsAbiMismatch) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const versionMatch = output.match(
|
||||
/compiled against[\s\S]*?NODE_MODULE_VERSION\s+(\d+)[\s\S]*?requires[\s\S]*?NODE_MODULE_VERSION\s+(\d+)/i,
|
||||
);
|
||||
if (!versionMatch) {
|
||||
return 'better-sqlite3 native module could not load for the current Node.js runtime.';
|
||||
}
|
||||
|
||||
return `better-sqlite3 was compiled for NODE_MODULE_VERSION ${versionMatch[1]}, but this Node.js requires ${versionMatch[2]}.`;
|
||||
}
|
||||
|
||||
function readOutputValue(output: string, label: string): string | undefined {
|
||||
const escapedLabel = label.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
const match = new RegExp(`^\\s*${escapedLabel}:\\s*(.+?)\\s*$`, 'im').exec(output);
|
||||
|
|
@ -1445,9 +1475,28 @@ async function validateAndScanConnection(input: {
|
|||
const scanIo = createBufferedCommandIo();
|
||||
const scanCode = await scanConnection(input.projectDir, input.connectionId, scanIo);
|
||||
if (scanCode !== 0) {
|
||||
flushBufferedCommandOutput(input.io, scanIo);
|
||||
input.io.stderr.write(`Structural scan failed for ${input.connectionId}.\n`);
|
||||
input.io.stderr.write(`Debug command: ktx scan --project-dir ${input.projectDir} ${input.connectionId}\n`);
|
||||
const nativeSqliteDetail = nativeSqliteAbiMismatchDetail(`${scanIo.stderrText()}\n${scanIo.stdoutText()}`);
|
||||
if (nativeSqliteDetail) {
|
||||
writePrefixedLines(
|
||||
(chunk) => input.io.stderr.write(chunk),
|
||||
[
|
||||
`Structural scan failed for ${input.connectionId}.`,
|
||||
'Native SQLite is built for a different Node.js ABI.',
|
||||
`Detail: ${nativeSqliteDetail}`,
|
||||
'Fix: pnpm run native:rebuild',
|
||||
`Retry: ktx scan --project-dir ${input.projectDir} ${input.connectionId}`,
|
||||
].join('\n'),
|
||||
);
|
||||
} else {
|
||||
flushPrefixedBufferedCommandOutput(input.io, scanIo);
|
||||
writePrefixedLines(
|
||||
(chunk) => input.io.stderr.write(chunk),
|
||||
[
|
||||
`Structural scan failed for ${input.connectionId}.`,
|
||||
`Debug command: ktx scan --project-dir ${input.projectDir} ${input.connectionId}`,
|
||||
].join('\n'),
|
||||
);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
const scanOutput = scanIo.stdoutText();
|
||||
|
|
|
|||
|
|
@ -176,7 +176,10 @@ describe('setup sources step', () => {
|
|||
it('writes Metabase config and validates mapping through existing mapping path', async () => {
|
||||
await addPrimarySource();
|
||||
const validateMetabase = vi.fn(async () => ({ ok: true as const, detail: 'user=admin@example.com' }));
|
||||
const runMapping = vi.fn(async () => 0);
|
||||
const runMapping = vi.fn(async (_projectDir: string, _connectionId: string, commandIo: KtxCliIo) => {
|
||||
commandIo.stdout.write('Mapping validated — 1 mapping configured\n');
|
||||
return 0;
|
||||
});
|
||||
const io = makeIo();
|
||||
|
||||
await expect(
|
||||
|
|
@ -208,7 +211,16 @@ describe('setup sources step', () => {
|
|||
syncMode: 'ALL',
|
||||
},
|
||||
});
|
||||
expect(runMapping).toHaveBeenCalledWith(projectDir, 'prod_metabase', io.io);
|
||||
expect(runMapping).toHaveBeenCalledWith(
|
||||
projectDir,
|
||||
'prod_metabase',
|
||||
expect.objectContaining({
|
||||
stdout: expect.objectContaining({ write: expect.any(Function) }),
|
||||
stderr: expect.objectContaining({ write: expect.any(Function) }),
|
||||
}),
|
||||
);
|
||||
expect(io.stdout()).toContain('│ Mapping validated — 1 mapping configured');
|
||||
expect(io.stdout()).not.toMatch(/^Mapping validated — 1 mapping configured$/m);
|
||||
});
|
||||
|
||||
it('writes Notion config with the full default knowledge create budget', async () => {
|
||||
|
|
@ -544,7 +556,14 @@ describe('setup sources step', () => {
|
|||
),
|
||||
).resolves.toEqual({ status: 'failed', projectDir });
|
||||
|
||||
expect(runMapping).toHaveBeenCalledWith(projectDir, 'metabase-main', io.io);
|
||||
expect(runMapping).toHaveBeenCalledWith(
|
||||
projectDir,
|
||||
'metabase-main',
|
||||
expect.objectContaining({
|
||||
stdout: expect.objectContaining({ write: expect.any(Function) }),
|
||||
stderr: expect.objectContaining({ write: expect.any(Function) }),
|
||||
}),
|
||||
);
|
||||
expect(io.stderr()).toContain('1: Metabase database does not match KTX connection database');
|
||||
expect(io.stderr()).not.toContain('Metabase mapping validation failed');
|
||||
});
|
||||
|
|
|
|||
|
|
@ -664,6 +664,31 @@ function splitOutputLines(output: string): string[] {
|
|||
.filter(Boolean);
|
||||
}
|
||||
|
||||
function writeSetupPrefixedLines(write: (chunk: string) => void, output: string): void {
|
||||
for (const line of output.split(/\r?\n/)) {
|
||||
if (line.length > 0) {
|
||||
write(`│ ${line}\n`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function createSetupPrefixedIo(io: KtxCliIo): KtxCliIo {
|
||||
return {
|
||||
stdout: {
|
||||
isTTY: io.stdout.isTTY,
|
||||
columns: io.stdout.columns,
|
||||
write(chunk: string) {
|
||||
writeSetupPrefixedLines((line) => io.stdout.write(line), chunk);
|
||||
},
|
||||
},
|
||||
stderr: {
|
||||
write(chunk: string) {
|
||||
writeSetupPrefixedLines((line) => io.stderr.write(line), chunk);
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function parseMappingListJson(output: string): unknown[] {
|
||||
const trimmed = output.trim();
|
||||
if (!trimmed) {
|
||||
|
|
@ -1524,7 +1549,11 @@ export async function runKtxSetupSourcesStep(
|
|||
}
|
||||
if (source === 'metabase' || source === 'looker') {
|
||||
prompts.log?.(`Validating ${sourceLabel(source)} mapping…`);
|
||||
const mappingCode = await (deps.runMapping ?? defaultRunMapping)(args.projectDir, connectionId, io);
|
||||
const mappingCode = await (deps.runMapping ?? defaultRunMapping)(
|
||||
args.projectDir,
|
||||
connectionId,
|
||||
createSetupPrefixedIo(io),
|
||||
);
|
||||
if (mappingCode !== 0) {
|
||||
await rollback?.();
|
||||
return { status: 'failed', projectDir: args.projectDir };
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue