mirror of
https://github.com/Kaelio/ktx.git
synced 2026-06-19 08:28:06 +02:00
feat(cli): add edit flow for setup connections (#77)
* feat(cli): add edit flow for primary database connections in setup Allow users to edit existing primary database connections during setup instead of only adding new ones. Preselects existing values (URL, schemas, tables) so users can adjust without re-entering everything. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(cli): add edit flow for context source connections in setup Allow users to edit existing context source connections during setup. Preselects existing values (URLs, credentials, repo details) and offers a "Keep existing credential" option for sensitive fields. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(cli): rename "Add more" to "Add additional" in primary sources menu Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
c31281c643
commit
9ecb8cb119
4 changed files with 1553 additions and 142 deletions
|
|
@ -240,8 +240,9 @@ describe('setup databases step', () => {
|
|||
expect(prompts.select).toHaveBeenCalledWith({
|
||||
message: 'Configure PostgreSQL',
|
||||
options: [
|
||||
{ value: 'existing:warehouse', label: 'Use existing PostgreSQL connection: warehouse' },
|
||||
{ value: 'new', label: 'Add new PostgreSQL connection' },
|
||||
{ value: 'existing:warehouse', label: 'Keep existing PostgreSQL connection: warehouse' },
|
||||
{ value: 'edit:warehouse', label: 'Edit PostgreSQL connection: warehouse' },
|
||||
{ value: 'new', label: 'Add another PostgreSQL connection' },
|
||||
{ value: 'back', label: 'Back' },
|
||||
],
|
||||
});
|
||||
|
|
@ -564,7 +565,8 @@ describe('setup databases step', () => {
|
|||
message: 'Primary sources already configured: warehouse\nWhat would you like to do?',
|
||||
options: [
|
||||
{ value: 'continue', label: 'Continue to knowledge sources' },
|
||||
{ value: 'add', label: 'Add another primary source' },
|
||||
{ value: 'edit', label: 'Edit an existing primary source' },
|
||||
{ value: 'add', label: 'Add additional primary sources' },
|
||||
],
|
||||
});
|
||||
expect(testConnection).not.toHaveBeenCalled();
|
||||
|
|
@ -608,11 +610,16 @@ describe('setup databases step', () => {
|
|||
connectionIds: ['warehouse', 'mysql-warehouse'],
|
||||
});
|
||||
expect(prompts.multiselect).toHaveBeenCalledTimes(1);
|
||||
expect(prompts.multiselect).toHaveBeenCalledWith(expect.objectContaining({
|
||||
initialValues: ['postgres'],
|
||||
required: true,
|
||||
}));
|
||||
expect(prompts.select).toHaveBeenCalledWith({
|
||||
message: 'Primary sources already configured: warehouse\nWhat would you like to do?',
|
||||
options: [
|
||||
{ value: 'continue', label: 'Continue to knowledge sources' },
|
||||
{ value: 'add', label: 'Add another primary source' },
|
||||
{ value: 'edit', label: 'Edit an existing primary source' },
|
||||
{ value: 'add', label: 'Add additional primary sources' },
|
||||
],
|
||||
});
|
||||
expect(testConnection).toHaveBeenCalledTimes(1);
|
||||
|
|
@ -642,11 +649,16 @@ describe('setup databases step', () => {
|
|||
connectionIds: ['postgres-warehouse', 'mysql-warehouse'],
|
||||
});
|
||||
expect(prompts.multiselect).toHaveBeenCalledTimes(2);
|
||||
expect(prompts.multiselect).toHaveBeenNthCalledWith(2, expect.objectContaining({
|
||||
initialValues: ['postgres'],
|
||||
required: true,
|
||||
}));
|
||||
expect(prompts.select).toHaveBeenCalledWith({
|
||||
message: 'Primary sources already configured: postgres-warehouse\nWhat would you like to do?',
|
||||
options: [
|
||||
{ value: 'continue', label: 'Continue to knowledge sources' },
|
||||
{ value: 'add', label: 'Add another primary source' },
|
||||
{ value: 'edit', label: 'Edit an existing primary source' },
|
||||
{ value: 'add', label: 'Add additional primary sources' },
|
||||
],
|
||||
});
|
||||
const config = parseKtxProjectConfig(await readFile(join(tempDir, 'ktx.yaml'), 'utf-8'));
|
||||
|
|
@ -675,12 +687,17 @@ describe('setup databases step', () => {
|
|||
connectionIds: ['postgres-warehouse'],
|
||||
});
|
||||
expect(prompts.multiselect).toHaveBeenCalledTimes(2);
|
||||
expect(prompts.multiselect).toHaveBeenNthCalledWith(2, expect.objectContaining({
|
||||
initialValues: ['postgres'],
|
||||
required: true,
|
||||
}));
|
||||
expect(io.stdout()).not.toContain('KTX cannot work without at least one primary source');
|
||||
expect(prompts.select).toHaveBeenNthCalledWith(2, {
|
||||
message: 'Primary sources already configured: postgres-warehouse\nWhat would you like to do?',
|
||||
options: [
|
||||
{ value: 'continue', label: 'Continue to knowledge sources' },
|
||||
{ value: 'add', label: 'Add another primary source' },
|
||||
{ value: 'edit', label: 'Edit an existing primary source' },
|
||||
{ value: 'add', label: 'Add additional primary sources' },
|
||||
],
|
||||
});
|
||||
});
|
||||
|
|
@ -715,16 +732,389 @@ describe('setup databases step', () => {
|
|||
);
|
||||
|
||||
expect(result).toEqual({ status: 'ready', projectDir: tempDir, connectionIds: ['warehouse'] });
|
||||
expect(prompts.multiselect).toHaveBeenCalledWith(expect.objectContaining({
|
||||
initialValues: ['postgres'],
|
||||
required: true,
|
||||
}));
|
||||
expect(io.stdout()).not.toContain('KTX cannot work without at least one primary source');
|
||||
expect(prompts.select).toHaveBeenNthCalledWith(2, {
|
||||
message: 'Primary sources already configured: warehouse\nWhat would you like to do?',
|
||||
options: [
|
||||
{ value: 'continue', label: 'Continue to knowledge sources' },
|
||||
{ value: 'add', label: 'Add another primary source' },
|
||||
{ value: 'edit', label: 'Edit an existing primary source' },
|
||||
{ value: 'add', label: 'Add additional primary sources' },
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('returns from primary source edit selection back to the configured source menu', async () => {
|
||||
await writeFile(
|
||||
join(tempDir, 'ktx.yaml'),
|
||||
[
|
||||
'project: warehouse',
|
||||
'connections:',
|
||||
' warehouse:',
|
||||
' driver: postgres',
|
||||
' url: env:DATABASE_URL',
|
||||
'setup:',
|
||||
' database_connection_ids:',
|
||||
' - warehouse',
|
||||
'',
|
||||
].join('\n'),
|
||||
'utf-8',
|
||||
);
|
||||
await writeKtxSetupState(tempDir, { completed_steps: ['databases'] });
|
||||
const prompts = makePromptAdapter({
|
||||
selectValues: ['edit', 'back', 'continue'],
|
||||
});
|
||||
const testConnection = vi.fn(async () => 0);
|
||||
const scanConnection = vi.fn(async () => 0);
|
||||
|
||||
const result = await runKtxSetupDatabasesStep(
|
||||
{ projectDir: tempDir, inputMode: 'auto', skipDatabases: false, databaseSchemas: [] },
|
||||
makeIo().io,
|
||||
{ prompts, testConnection, scanConnection },
|
||||
);
|
||||
|
||||
expect(result).toEqual({ status: 'ready', projectDir: tempDir, connectionIds: ['warehouse'] });
|
||||
expect(prompts.select).toHaveBeenNthCalledWith(2, {
|
||||
message: 'Primary source to edit',
|
||||
options: [
|
||||
{ value: 'warehouse', label: 'warehouse (PostgreSQL)' },
|
||||
{ value: 'back', label: 'Back' },
|
||||
],
|
||||
});
|
||||
expect(prompts.select).toHaveBeenNthCalledWith(3, {
|
||||
message: 'Primary sources already configured: warehouse\nWhat would you like to do?',
|
||||
options: [
|
||||
{ value: 'continue', label: 'Continue to knowledge sources' },
|
||||
{ value: 'edit', label: 'Edit an existing primary source' },
|
||||
{ value: 'add', label: 'Add additional primary sources' },
|
||||
],
|
||||
});
|
||||
expect(testConnection).not.toHaveBeenCalled();
|
||||
expect(scanConnection).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('reruns table selection after editing schema scope so stale enabled tables are removed', async () => {
|
||||
await writeFile(
|
||||
join(tempDir, 'ktx.yaml'),
|
||||
[
|
||||
'project: warehouse',
|
||||
'connections:',
|
||||
' warehouse:',
|
||||
' driver: postgres',
|
||||
' url: env:DATABASE_URL',
|
||||
' schemas:',
|
||||
' - public',
|
||||
' enabled_tables:',
|
||||
' - public.orders',
|
||||
'setup:',
|
||||
' database_connection_ids:',
|
||||
' - warehouse',
|
||||
'',
|
||||
].join('\n'),
|
||||
'utf-8',
|
||||
);
|
||||
await writeKtxSetupState(tempDir, { completed_steps: ['databases'] });
|
||||
const prompts = makePromptAdapter({
|
||||
textValues: ['env:DATABASE_URL'],
|
||||
multiselectValues: [['analytics']],
|
||||
});
|
||||
let primaryMenuCount = 0;
|
||||
vi.mocked(prompts.select).mockImplementation(async (options) => {
|
||||
if (options.message === 'Primary sources already configured: warehouse\nWhat would you like to do?') {
|
||||
primaryMenuCount += 1;
|
||||
return primaryMenuCount === 1 ? 'edit' : 'continue';
|
||||
}
|
||||
if (options.message === 'Primary source to edit') return 'warehouse';
|
||||
if (options.message === 'How do you want to connect to PostgreSQL?') return 'url';
|
||||
return 'back';
|
||||
});
|
||||
const testConnection = vi.fn(async () => 0);
|
||||
const scanConnection = vi.fn(async () => 0);
|
||||
const listSchemas = vi.fn(async () => ['analytics', 'public']);
|
||||
const listTables = vi.fn(async () => [{ schema: 'analytics', name: 'customers', kind: 'table' as const }]);
|
||||
|
||||
const result = await runKtxSetupDatabasesStep(
|
||||
{ projectDir: tempDir, inputMode: 'auto', skipDatabases: false, databaseSchemas: [] },
|
||||
makeIo().io,
|
||||
{ prompts, testConnection, scanConnection, listSchemas, listTables },
|
||||
);
|
||||
|
||||
expect(result).toEqual({ status: 'ready', projectDir: tempDir, connectionIds: ['warehouse'] });
|
||||
expect(prompts.text).toHaveBeenCalledWith({
|
||||
message: textInputPrompt('PostgreSQL connection URL'),
|
||||
placeholder: 'env:DATABASE_URL',
|
||||
initialValue: 'env:DATABASE_URL',
|
||||
});
|
||||
expect(listTables).toHaveBeenCalledWith(tempDir, 'warehouse');
|
||||
expect(testConnection).toHaveBeenCalledWith(tempDir, 'warehouse', expect.anything());
|
||||
expect(scanConnection).toHaveBeenCalledWith(tempDir, 'warehouse', expect.anything());
|
||||
const config = parseKtxProjectConfig(await readFile(join(tempDir, 'ktx.yaml'), 'utf-8'));
|
||||
expect(config.connections.warehouse).toMatchObject({
|
||||
schemas: ['analytics'],
|
||||
enabled_tables: ['analytics.customers'],
|
||||
});
|
||||
});
|
||||
|
||||
it('preselects existing schema and table choices when editing a primary source', async () => {
|
||||
await writeFile(
|
||||
join(tempDir, 'ktx.yaml'),
|
||||
[
|
||||
'project: warehouse',
|
||||
'connections:',
|
||||
' warehouse:',
|
||||
' driver: postgres',
|
||||
' url: env:DATABASE_URL',
|
||||
' schemas:',
|
||||
' - public',
|
||||
' enabled_tables:',
|
||||
' - public.customers',
|
||||
' - public.orders',
|
||||
'setup:',
|
||||
' database_connection_ids:',
|
||||
' - warehouse',
|
||||
'',
|
||||
].join('\n'),
|
||||
'utf-8',
|
||||
);
|
||||
await writeKtxSetupState(tempDir, { completed_steps: ['databases'] });
|
||||
const prompts = makePromptAdapter({
|
||||
textValues: ['env:DATABASE_URL'],
|
||||
multiselectValues: [['public'], ['public.customers', 'public.orders']],
|
||||
});
|
||||
let primaryMenuCount = 0;
|
||||
vi.mocked(prompts.select).mockImplementation(async (options) => {
|
||||
if (options.message === 'Primary sources already configured: warehouse\nWhat would you like to do?') {
|
||||
primaryMenuCount += 1;
|
||||
return primaryMenuCount === 1 ? 'edit' : 'continue';
|
||||
}
|
||||
if (options.message === 'Primary source to edit') return 'warehouse';
|
||||
if (options.message === 'How do you want to connect to PostgreSQL?') return 'url';
|
||||
if (options.message.startsWith('Tables found in selected schemas')) return 'customize';
|
||||
return 'back';
|
||||
});
|
||||
const listSchemas = vi.fn(async () => ['orbit_analytics', 'orbit_raw', 'public']);
|
||||
const listTables = vi.fn(async () => [
|
||||
{ schema: 'public', name: 'customers', kind: 'table' as const },
|
||||
{ schema: 'public', name: 'orders', kind: 'table' as const },
|
||||
{ schema: 'public', name: 'products', kind: 'table' as const },
|
||||
]);
|
||||
|
||||
const result = await runKtxSetupDatabasesStep(
|
||||
{ projectDir: tempDir, inputMode: 'auto', skipDatabases: false, databaseSchemas: [] },
|
||||
makeIo().io,
|
||||
{
|
||||
prompts,
|
||||
testConnection: vi.fn(async () => 0),
|
||||
scanConnection: vi.fn(async () => 0),
|
||||
listSchemas,
|
||||
listTables,
|
||||
},
|
||||
);
|
||||
|
||||
expect(result).toEqual({ status: 'ready', projectDir: tempDir, connectionIds: ['warehouse'] });
|
||||
expect(prompts.multiselect).toHaveBeenNthCalledWith(1, {
|
||||
message: expect.stringContaining('PostgreSQL schemas to scan'),
|
||||
options: [
|
||||
{ value: 'orbit_analytics', label: 'orbit_analytics' },
|
||||
{ value: 'orbit_raw', label: 'orbit_raw' },
|
||||
{ value: 'public', label: 'public' },
|
||||
],
|
||||
initialValues: ['public'],
|
||||
required: true,
|
||||
});
|
||||
expect(prompts.multiselect).toHaveBeenNthCalledWith(2, {
|
||||
message: expect.stringContaining('Tables to enable for warehouse'),
|
||||
options: [
|
||||
{ value: 'public.customers', label: 'public.customers' },
|
||||
{ value: 'public.orders', label: 'public.orders' },
|
||||
{ value: 'public.products', label: 'public.products' },
|
||||
],
|
||||
initialValues: ['public.customers', 'public.orders'],
|
||||
required: true,
|
||||
});
|
||||
const config = parseKtxProjectConfig(await readFile(join(tempDir, 'ktx.yaml'), 'utf-8'));
|
||||
expect(config.connections.warehouse).toMatchObject({
|
||||
schemas: ['public'],
|
||||
enabled_tables: ['public.customers', 'public.orders'],
|
||||
});
|
||||
});
|
||||
|
||||
it('returns to the configured primary menu when backing out of schema review during edit', async () => {
|
||||
await writeFile(
|
||||
join(tempDir, 'ktx.yaml'),
|
||||
[
|
||||
'project: warehouse',
|
||||
'connections:',
|
||||
' warehouse:',
|
||||
' driver: postgres',
|
||||
' url: env:DATABASE_URL',
|
||||
' schemas:',
|
||||
' - public',
|
||||
' enabled_tables:',
|
||||
' - public.orders',
|
||||
'setup:',
|
||||
' database_connection_ids:',
|
||||
' - warehouse',
|
||||
'',
|
||||
].join('\n'),
|
||||
'utf-8',
|
||||
);
|
||||
await writeKtxSetupState(tempDir, { completed_steps: ['databases'] });
|
||||
const prompts = makePromptAdapter({
|
||||
textValues: ['env:DATABASE_URL'],
|
||||
multiselectValues: [['back']],
|
||||
});
|
||||
let primaryMenuCount = 0;
|
||||
vi.mocked(prompts.select).mockImplementation(async (options) => {
|
||||
if (options.message === 'Primary sources already configured: warehouse\nWhat would you like to do?') {
|
||||
primaryMenuCount += 1;
|
||||
return primaryMenuCount === 1 ? 'edit' : 'continue';
|
||||
}
|
||||
if (options.message === 'Primary source to edit') return 'warehouse';
|
||||
if (options.message === 'How do you want to connect to PostgreSQL?') return 'url';
|
||||
return 'back';
|
||||
});
|
||||
const testConnection = vi.fn(async () => 0);
|
||||
const scanConnection = vi.fn(async () => 0);
|
||||
const listSchemas = vi.fn(async () => ['analytics', 'public']);
|
||||
const listTables = vi.fn(async () => [{ schema: 'analytics', name: 'customers', kind: 'table' as const }]);
|
||||
|
||||
const result = await runKtxSetupDatabasesStep(
|
||||
{ projectDir: tempDir, inputMode: 'auto', skipDatabases: false, databaseSchemas: [] },
|
||||
makeIo().io,
|
||||
{ prompts, testConnection, scanConnection, listSchemas, listTables },
|
||||
);
|
||||
|
||||
expect(result).toEqual({ status: 'ready', projectDir: tempDir, connectionIds: ['warehouse'] });
|
||||
expect(primaryMenuCount).toBe(2);
|
||||
expect(testConnection).toHaveBeenCalledWith(tempDir, 'warehouse', expect.anything());
|
||||
expect(scanConnection).not.toHaveBeenCalled();
|
||||
expect(listTables).not.toHaveBeenCalled();
|
||||
const config = parseKtxProjectConfig(await readFile(join(tempDir, 'ktx.yaml'), 'utf-8'));
|
||||
expect(config.connections.warehouse).toMatchObject({
|
||||
url: 'env:DATABASE_URL',
|
||||
schemas: ['public'],
|
||||
enabled_tables: ['public.orders'],
|
||||
});
|
||||
});
|
||||
|
||||
it('returns to the configured primary menu when backing out of table review during edit', async () => {
|
||||
await writeFile(
|
||||
join(tempDir, 'ktx.yaml'),
|
||||
[
|
||||
'project: warehouse',
|
||||
'connections:',
|
||||
' warehouse:',
|
||||
' driver: postgres',
|
||||
' url: env:DATABASE_URL',
|
||||
' schemas:',
|
||||
' - public',
|
||||
' enabled_tables:',
|
||||
' - public.orders',
|
||||
'setup:',
|
||||
' database_connection_ids:',
|
||||
' - warehouse',
|
||||
'',
|
||||
].join('\n'),
|
||||
'utf-8',
|
||||
);
|
||||
await writeKtxSetupState(tempDir, { completed_steps: ['databases'] });
|
||||
const prompts = makePromptAdapter({ textValues: ['env:DATABASE_URL'] });
|
||||
let primaryMenuCount = 0;
|
||||
vi.mocked(prompts.select).mockImplementation(async (options) => {
|
||||
if (options.message === 'Primary sources already configured: warehouse\nWhat would you like to do?') {
|
||||
primaryMenuCount += 1;
|
||||
return primaryMenuCount === 1 ? 'edit' : 'continue';
|
||||
}
|
||||
if (options.message === 'Primary source to edit') return 'warehouse';
|
||||
if (options.message === 'How do you want to connect to PostgreSQL?') return 'url';
|
||||
if (options.message.startsWith('Tables found in selected schemas')) return 'back';
|
||||
return 'back';
|
||||
});
|
||||
const testConnection = vi.fn(async () => 0);
|
||||
const scanConnection = vi.fn(async () => 0);
|
||||
const listSchemas = vi.fn(async () => ['public']);
|
||||
const listTables = vi.fn(async () => [
|
||||
{ schema: 'public', name: 'customers', kind: 'table' as const },
|
||||
{ schema: 'public', name: 'orders', kind: 'table' as const },
|
||||
]);
|
||||
|
||||
const result = await runKtxSetupDatabasesStep(
|
||||
{ projectDir: tempDir, inputMode: 'auto', skipDatabases: false, databaseSchemas: [] },
|
||||
makeIo().io,
|
||||
{ prompts, testConnection, scanConnection, listSchemas, listTables },
|
||||
);
|
||||
|
||||
expect(result).toEqual({ status: 'ready', projectDir: tempDir, connectionIds: ['warehouse'] });
|
||||
expect(primaryMenuCount).toBe(2);
|
||||
expect(listTables).toHaveBeenCalledWith(tempDir, 'warehouse');
|
||||
expect(scanConnection).not.toHaveBeenCalled();
|
||||
const config = parseKtxProjectConfig(await readFile(join(tempDir, 'ktx.yaml'), 'utf-8'));
|
||||
expect(config.connections.warehouse).toMatchObject({
|
||||
url: 'env:DATABASE_URL',
|
||||
schemas: ['public'],
|
||||
enabled_tables: ['public.orders'],
|
||||
});
|
||||
});
|
||||
|
||||
it('restores an existing primary source edit when the follow-up scan fails', async () => {
|
||||
await writeFile(
|
||||
join(tempDir, 'ktx.yaml'),
|
||||
[
|
||||
'project: warehouse',
|
||||
'connections:',
|
||||
' warehouse:',
|
||||
' driver: postgres',
|
||||
' url: env:DATABASE_URL',
|
||||
' schemas:',
|
||||
' - public',
|
||||
' enabled_tables:',
|
||||
' - public.orders',
|
||||
'setup:',
|
||||
' database_connection_ids:',
|
||||
' - warehouse',
|
||||
'',
|
||||
].join('\n'),
|
||||
'utf-8',
|
||||
);
|
||||
await writeKtxSetupState(tempDir, { completed_steps: ['databases'] });
|
||||
const prompts = makePromptAdapter({
|
||||
textValues: ['env:DATABASE_URL'],
|
||||
multiselectValues: [['public']],
|
||||
});
|
||||
vi.mocked(prompts.select).mockImplementation(async (options) => {
|
||||
if (options.message === 'Primary sources already configured: warehouse\nWhat would you like to do?') return 'edit';
|
||||
if (options.message === 'Primary source to edit') return 'warehouse';
|
||||
if (options.message === 'How do you want to connect to PostgreSQL?') return 'url';
|
||||
if (options.message.startsWith('Tables found in selected schemas')) return 'all';
|
||||
return 'back';
|
||||
});
|
||||
const listTables = vi.fn(async () => [
|
||||
{ schema: 'public', name: 'customers', kind: 'table' as const },
|
||||
{ schema: 'public', name: 'orders', kind: 'table' as const },
|
||||
]);
|
||||
|
||||
const result = await runKtxSetupDatabasesStep(
|
||||
{ projectDir: tempDir, inputMode: 'auto', skipDatabases: false, databaseSchemas: [] },
|
||||
makeIo().io,
|
||||
{
|
||||
prompts,
|
||||
testConnection: vi.fn(async () => 0),
|
||||
scanConnection: vi.fn(async () => 1),
|
||||
listTables,
|
||||
},
|
||||
);
|
||||
|
||||
expect(result).toEqual({ status: 'failed', projectDir: tempDir });
|
||||
const config = parseKtxProjectConfig(await readFile(join(tempDir, 'ktx.yaml'), 'utf-8'));
|
||||
expect(config.connections.warehouse).toMatchObject({
|
||||
enabled_tables: ['public.orders'],
|
||||
});
|
||||
});
|
||||
|
||||
it('lets Escape from connection fields return to connection method selection', async () => {
|
||||
const prompts = makePromptAdapter({
|
||||
selectValues: ['fields', 'url'],
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue