From 2eabc64a97c2f421d7bb6f35d794c1d2784bdfdd Mon Sep 17 00:00:00 2001 From: Jonas Alves Date: Mon, 11 May 2026 15:51:00 +0100 Subject: [PATCH 1/8] feat(api-client): add deleteDatasource method (FT-1918) --- src/api-client/api-client.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/api-client/api-client.ts b/src/api-client/api-client.ts index 59bee55..8e85499 100644 --- a/src/api-client/api-client.ts +++ b/src/api-client/api-client.ts @@ -2439,6 +2439,11 @@ export class APIClient { return response.data; } + async deleteDatasource(id: DatasourceId): Promise { + const response = await this.request('DELETE', `/datasources/${id}`); + this.validateOkResponse(response, 'deleteDatasource'); + } + async cancelExportHistory( exportConfigId: ExportConfigId, historyId: number, From 968f3449e12ff6a468199eea5087dd9f7445f774 Mon Sep 17 00:00:00 2001 From: Jonas Alves Date: Mon, 11 May 2026 15:53:36 +0100 Subject: [PATCH 2/8] feat(core): add deleteDatasource (FT-1918) --- src/core/datasources/datasources.test.ts | 9 +++++++++ src/core/datasources/datasources.ts | 12 ++++++++++++ 2 files changed, 21 insertions(+) diff --git a/src/core/datasources/datasources.test.ts b/src/core/datasources/datasources.test.ts index 8fe51eb..67d2b47 100644 --- a/src/core/datasources/datasources.test.ts +++ b/src/core/datasources/datasources.test.ts @@ -11,6 +11,7 @@ import { previewDatasourceQuery, setDefaultDatasource, getDatasourceSchema, + deleteDatasource, } from './datasources.js'; describe('datasources', () => { @@ -26,6 +27,7 @@ describe('datasources', () => { previewDatasourceQuery: vi.fn(), setDefaultDatasource: vi.fn(), getDatasourceSchema: vi.fn(), + deleteDatasource: vi.fn(), }; it('should list datasources', async () => { @@ -120,4 +122,11 @@ describe('datasources', () => { expect(mockClient.getDatasourceSchema).toHaveBeenCalledWith(1); expect(result.data).toEqual(schema); }); + + it('should delete datasource', async () => { + mockClient.deleteDatasource.mockResolvedValue(undefined); + const result = await deleteDatasource(mockClient as any, { id: 1 as any }); + expect(mockClient.deleteDatasource).toHaveBeenCalledWith(1); + expect(result.data).toBeUndefined(); + }); }); diff --git a/src/core/datasources/datasources.ts b/src/core/datasources/datasources.ts index e3afdf4..271aebf 100644 --- a/src/core/datasources/datasources.ts +++ b/src/core/datasources/datasources.ts @@ -128,3 +128,15 @@ export async function getDatasourceSchema( const data = await client.getDatasourceSchema(params.id); return { data }; } + +export interface DeleteDatasourceParams { + id: DatasourceId; +} + +export async function deleteDatasource( + client: APIClient, + params: DeleteDatasourceParams +): Promise> { + await client.deleteDatasource(params.id); + return { data: undefined }; +} From b978f21e42f1cd49fd06ba27b5f3d6b7b8260307 Mon Sep 17 00:00:00 2001 From: Jonas Alves Date: Mon, 11 May 2026 15:56:05 +0100 Subject: [PATCH 3/8] feat(datasources): add delete subcommand (FT-1918) --- src/commands/datasources/datasources.test.ts | 7 +++++++ src/commands/datasources/index.ts | 14 ++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/src/commands/datasources/datasources.test.ts b/src/commands/datasources/datasources.test.ts index 898e9ea..5994583 100644 --- a/src/commands/datasources/datasources.test.ts +++ b/src/commands/datasources/datasources.test.ts @@ -34,6 +34,7 @@ describe('datasources command', () => { previewDatasourceQuery: vi.fn().mockResolvedValue({ result: [] }), setDefaultDatasource: vi.fn().mockResolvedValue(undefined), getDatasourceSchema: vi.fn().mockResolvedValue({ tables: [] }), + deleteDatasource: vi.fn().mockResolvedValue(undefined), }; beforeEach(() => { @@ -168,4 +169,10 @@ describe('datasources command', () => { expect(mockClient.getDatasourceSchema).toHaveBeenCalledWith(1); expect(printFormatted).toHaveBeenCalled(); }); + + it('should delete a datasource', async () => { + await datasourcesCommand.parseAsync(['node', 'test', 'delete', '1']); + + expect(mockClient.deleteDatasource).toHaveBeenCalledWith(1); + }); }); diff --git a/src/commands/datasources/index.ts b/src/commands/datasources/index.ts index cb20b9b..a47d94b 100644 --- a/src/commands/datasources/index.ts +++ b/src/commands/datasources/index.ts @@ -20,6 +20,7 @@ import { previewDatasourceQuery as corePreviewDatasourceQuery, setDefaultDatasource as coreSetDefaultDatasource, getDatasourceSchema as coreGetDatasourceSchema, + deleteDatasource as coreDeleteDatasource, } from '../../core/datasources/datasources.js'; export const datasourcesCommand = new Command('datasources') @@ -166,6 +167,18 @@ const schemaCommand = new Command('schema') }) ); +const deleteCommand = new Command('delete') + .description('Delete a datasource (fails if default or used by any goal)') + .argument('', 'datasource ID', parseDatasourceId) + .action( + withErrorHandling(async (id: DatasourceId) => { + const globalOptions = getGlobalOptions(deleteCommand); + const client = await getAPIClientFromOptions(globalOptions); + await coreDeleteDatasource(client, { id }); + console.log(chalk.green(`✓ Datasource ${id} deleted`)); + }) + ); + datasourcesCommand.addCommand(listCommand); datasourcesCommand.addCommand(getCommand); datasourcesCommand.addCommand(createCommand); @@ -177,3 +190,4 @@ datasourcesCommand.addCommand(validateQueryCommand); datasourcesCommand.addCommand(previewQueryCommand); datasourcesCommand.addCommand(setDefaultCommand); datasourcesCommand.addCommand(schemaCommand); +datasourcesCommand.addCommand(deleteCommand); From bcddc9b87dd8469553c0043a7928d46eeffb769e Mon Sep 17 00:00:00 2001 From: Jonas Alves Date: Mon, 11 May 2026 15:58:16 +0100 Subject: [PATCH 4/8] feat(api-client): add datasource json_layouts methods (FT-1918) --- src/api-client/api-client.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/api-client/api-client.ts b/src/api-client/api-client.ts index 8e85499..f11281d 100644 --- a/src/api-client/api-client.ts +++ b/src/api-client/api-client.ts @@ -2444,6 +2444,19 @@ export class APIClient { this.validateOkResponse(response, 'deleteDatasource'); } + async createDatasourceJsonLayouts(id: DatasourceId): Promise { + await this.request('POST', `/datasources/${id}/json_layouts/create`); + } + + async recreateDatasourceJsonLayouts(id: DatasourceId): Promise { + await this.request('POST', `/datasources/${id}/json_layouts/recreate`); + } + + async previewDatasourceJsonLayouts(id: DatasourceId): Promise { + const response = await this.request('POST', `/datasources/${id}/json_layouts/preview`); + return response.data; + } + async cancelExportHistory( exportConfigId: ExportConfigId, historyId: number, From f2a301fa9bab4b36562222801352fca9913902d8 Mon Sep 17 00:00:00 2001 From: Jonas Alves Date: Mon, 11 May 2026 16:00:54 +0100 Subject: [PATCH 5/8] feat(core): add datasource json_layouts wrappers (FT-1918) --- src/core/datasources/datasources.test.ts | 34 ++++++++++++++++++++++ src/core/datasources/datasources.ts | 36 ++++++++++++++++++++++++ 2 files changed, 70 insertions(+) diff --git a/src/core/datasources/datasources.test.ts b/src/core/datasources/datasources.test.ts index 67d2b47..ca79853 100644 --- a/src/core/datasources/datasources.test.ts +++ b/src/core/datasources/datasources.test.ts @@ -12,6 +12,9 @@ import { setDefaultDatasource, getDatasourceSchema, deleteDatasource, + createDatasourceJsonLayouts, + recreateDatasourceJsonLayouts, + previewDatasourceJsonLayouts, } from './datasources.js'; describe('datasources', () => { @@ -28,6 +31,9 @@ describe('datasources', () => { setDefaultDatasource: vi.fn(), getDatasourceSchema: vi.fn(), deleteDatasource: vi.fn(), + createDatasourceJsonLayouts: vi.fn(), + recreateDatasourceJsonLayouts: vi.fn(), + previewDatasourceJsonLayouts: vi.fn(), }; it('should list datasources', async () => { @@ -129,4 +135,32 @@ describe('datasources', () => { expect(mockClient.deleteDatasource).toHaveBeenCalledWith(1); expect(result.data).toBeUndefined(); }); + + it('should create json_layouts table', async () => { + mockClient.createDatasourceJsonLayouts.mockResolvedValue(undefined); + const result = await createDatasourceJsonLayouts(mockClient as any, { id: 1 as any }); + expect(mockClient.createDatasourceJsonLayouts).toHaveBeenCalledWith(1); + expect(result.data).toBeUndefined(); + }); + + it('should recreate json_layouts table', async () => { + mockClient.recreateDatasourceJsonLayouts.mockResolvedValue(undefined); + const result = await recreateDatasourceJsonLayouts(mockClient as any, { id: 1 as any }); + expect(mockClient.recreateDatasourceJsonLayouts).toHaveBeenCalledWith(1); + expect(result.data).toBeUndefined(); + }); + + it('should preview json_layouts table', async () => { + const preview = { + ok: true, + row_count: 42, + column_names: ['name', 'definition'], + column_types: ['String', 'String'], + rows: [['hero', '{...}']], + }; + mockClient.previewDatasourceJsonLayouts.mockResolvedValue(preview); + const result = await previewDatasourceJsonLayouts(mockClient as any, { id: 1 as any }); + expect(mockClient.previewDatasourceJsonLayouts).toHaveBeenCalledWith(1); + expect(result.data).toEqual(preview); + }); }); diff --git a/src/core/datasources/datasources.ts b/src/core/datasources/datasources.ts index 271aebf..2227ef2 100644 --- a/src/core/datasources/datasources.ts +++ b/src/core/datasources/datasources.ts @@ -140,3 +140,39 @@ export async function deleteDatasource( await client.deleteDatasource(params.id); return { data: undefined }; } + +export interface CreateDatasourceJsonLayoutsParams { + id: DatasourceId; +} + +export async function createDatasourceJsonLayouts( + client: APIClient, + params: CreateDatasourceJsonLayoutsParams +): Promise> { + await client.createDatasourceJsonLayouts(params.id); + return { data: undefined }; +} + +export interface RecreateDatasourceJsonLayoutsParams { + id: DatasourceId; +} + +export async function recreateDatasourceJsonLayouts( + client: APIClient, + params: RecreateDatasourceJsonLayoutsParams +): Promise> { + await client.recreateDatasourceJsonLayouts(params.id); + return { data: undefined }; +} + +export interface PreviewDatasourceJsonLayoutsParams { + id: DatasourceId; +} + +export async function previewDatasourceJsonLayouts( + client: APIClient, + params: PreviewDatasourceJsonLayoutsParams +): Promise> { + const data = await client.previewDatasourceJsonLayouts(params.id); + return { data }; +} From f54a92559a9bb8a86648459b12253c3a7a58a21a Mon Sep 17 00:00:00 2001 From: Jonas Alves Date: Mon, 11 May 2026 16:03:45 +0100 Subject: [PATCH 6/8] feat(datasources): add json-layouts create + preview subcommands (FT-1918) --- src/commands/datasources/datasources.test.ts | 22 ++++++++++++ src/commands/datasources/index.ts | 35 ++++++++++++++++++++ 2 files changed, 57 insertions(+) diff --git a/src/commands/datasources/datasources.test.ts b/src/commands/datasources/datasources.test.ts index 5994583..d271e1b 100644 --- a/src/commands/datasources/datasources.test.ts +++ b/src/commands/datasources/datasources.test.ts @@ -35,6 +35,15 @@ describe('datasources command', () => { setDefaultDatasource: vi.fn().mockResolvedValue(undefined), getDatasourceSchema: vi.fn().mockResolvedValue({ tables: [] }), deleteDatasource: vi.fn().mockResolvedValue(undefined), + createDatasourceJsonLayouts: vi.fn().mockResolvedValue(undefined), + recreateDatasourceJsonLayouts: vi.fn().mockResolvedValue(undefined), + previewDatasourceJsonLayouts: vi.fn().mockResolvedValue({ + ok: true, + row_count: 0, + column_names: [], + column_types: [], + rows: [], + }), }; beforeEach(() => { @@ -175,4 +184,17 @@ describe('datasources command', () => { expect(mockClient.deleteDatasource).toHaveBeenCalledWith(1); }); + + it('should create json_layouts table', async () => { + await datasourcesCommand.parseAsync(['node', 'test', 'json-layouts', 'create', '1']); + + expect(mockClient.createDatasourceJsonLayouts).toHaveBeenCalledWith(1); + }); + + it('should preview json_layouts table', async () => { + await datasourcesCommand.parseAsync(['node', 'test', 'json-layouts', 'preview', '1']); + + expect(mockClient.previewDatasourceJsonLayouts).toHaveBeenCalledWith(1); + expect(printFormatted).toHaveBeenCalled(); + }); }); diff --git a/src/commands/datasources/index.ts b/src/commands/datasources/index.ts index a47d94b..4f90056 100644 --- a/src/commands/datasources/index.ts +++ b/src/commands/datasources/index.ts @@ -21,6 +21,9 @@ import { setDefaultDatasource as coreSetDefaultDatasource, getDatasourceSchema as coreGetDatasourceSchema, deleteDatasource as coreDeleteDatasource, + createDatasourceJsonLayouts as coreCreateDatasourceJsonLayouts, + recreateDatasourceJsonLayouts as coreRecreateDatasourceJsonLayouts, + previewDatasourceJsonLayouts as corePreviewDatasourceJsonLayouts, } from '../../core/datasources/datasources.js'; export const datasourcesCommand = new Command('datasources') @@ -179,6 +182,37 @@ const deleteCommand = new Command('delete') }) ); +const jsonLayoutsCommand = new Command('json-layouts').description( + 'Manage the json_layouts table on a datasource' +); + +const jsonLayoutsCreateCommand = new Command('create') + .description('Create the json_layouts table on a datasource') + .argument('', 'datasource ID', parseDatasourceId) + .action( + withErrorHandling(async (id: DatasourceId) => { + const globalOptions = getGlobalOptions(jsonLayoutsCreateCommand); + const client = await getAPIClientFromOptions(globalOptions); + await coreCreateDatasourceJsonLayouts(client, { id }); + console.log(chalk.green(`✓ json_layouts table created on datasource ${id}`)); + }) + ); + +const jsonLayoutsPreviewCommand = new Command('preview') + .description('Preview the json_layouts table (row count + 5-row sample)') + .argument('', 'datasource ID', parseDatasourceId) + .action( + withErrorHandling(async (id: DatasourceId) => { + const globalOptions = getGlobalOptions(jsonLayoutsPreviewCommand); + const client = await getAPIClientFromOptions(globalOptions); + const result = await corePreviewDatasourceJsonLayouts(client, { id }); + printFormatted(result.data, globalOptions); + }) + ); + +jsonLayoutsCommand.addCommand(jsonLayoutsCreateCommand); +jsonLayoutsCommand.addCommand(jsonLayoutsPreviewCommand); + datasourcesCommand.addCommand(listCommand); datasourcesCommand.addCommand(getCommand); datasourcesCommand.addCommand(createCommand); @@ -191,3 +225,4 @@ datasourcesCommand.addCommand(previewQueryCommand); datasourcesCommand.addCommand(setDefaultCommand); datasourcesCommand.addCommand(schemaCommand); datasourcesCommand.addCommand(deleteCommand); +datasourcesCommand.addCommand(jsonLayoutsCommand); From 23a24f53d1ef663c1bde94e326132ff290a0413e Mon Sep 17 00:00:00 2001 From: Jonas Alves Date: Mon, 11 May 2026 16:06:54 +0100 Subject: [PATCH 7/8] feat(datasources): add json-layouts recreate subcommand with --yes guard (FT-1918) --- src/commands/datasources/datasources.test.ts | 24 ++++++++++++++++++++ src/commands/datasources/index.ts | 24 ++++++++++++++++++++ 2 files changed, 48 insertions(+) diff --git a/src/commands/datasources/datasources.test.ts b/src/commands/datasources/datasources.test.ts index d271e1b..2e693fc 100644 --- a/src/commands/datasources/datasources.test.ts +++ b/src/commands/datasources/datasources.test.ts @@ -197,4 +197,28 @@ describe('datasources command', () => { expect(mockClient.previewDatasourceJsonLayouts).toHaveBeenCalledWith(1); expect(printFormatted).toHaveBeenCalled(); }); + + it('should recreate json_layouts table when --yes is passed', async () => { + await datasourcesCommand.parseAsync([ + 'node', + 'test', + 'json-layouts', + 'recreate', + '1', + '--yes', + ]); + + expect(mockClient.recreateDatasourceJsonLayouts).toHaveBeenCalledWith(1); + }); + + it('should refuse to recreate json_layouts table without --yes', async () => { + await expect( + datasourcesCommand.parseAsync(['node', 'test', 'json-layouts', 'recreate', '1']) + ).rejects.toThrow(/process\.exit: 1/); + + expect(mockClient.recreateDatasourceJsonLayouts).not.toHaveBeenCalled(); + expect(consoleErrorSpy).toHaveBeenCalledWith( + expect.stringContaining('--yes') + ); + }); }); diff --git a/src/commands/datasources/index.ts b/src/commands/datasources/index.ts index 4f90056..7724885 100644 --- a/src/commands/datasources/index.ts +++ b/src/commands/datasources/index.ts @@ -198,6 +198,29 @@ const jsonLayoutsCreateCommand = new Command('create') }) ); +const jsonLayoutsRecreateCommand = new Command('recreate') + .description( + 'Drop and recreate the json_layouts table on a datasource (destructive — requires --yes)' + ) + .argument('', 'datasource ID', parseDatasourceId) + .option('--yes', 'confirm the destructive recreate', false) + .action( + withErrorHandling(async (id: DatasourceId, options: { yes: boolean }) => { + if (!options.yes) { + console.error( + chalk.red( + `✗ Refusing to recreate the json_layouts table on datasource ${id} without --yes. This drops the existing table.` + ) + ); + process.exit(1); + } + const globalOptions = getGlobalOptions(jsonLayoutsRecreateCommand); + const client = await getAPIClientFromOptions(globalOptions); + await coreRecreateDatasourceJsonLayouts(client, { id }); + console.log(chalk.green(`✓ json_layouts table recreated on datasource ${id}`)); + }) + ); + const jsonLayoutsPreviewCommand = new Command('preview') .description('Preview the json_layouts table (row count + 5-row sample)') .argument('', 'datasource ID', parseDatasourceId) @@ -211,6 +234,7 @@ const jsonLayoutsPreviewCommand = new Command('preview') ); jsonLayoutsCommand.addCommand(jsonLayoutsCreateCommand); +jsonLayoutsCommand.addCommand(jsonLayoutsRecreateCommand); jsonLayoutsCommand.addCommand(jsonLayoutsPreviewCommand); datasourcesCommand.addCommand(listCommand); From fda117215f8ba5865a15c084f9b4ccc9f7037c85 Mon Sep 17 00:00:00 2001 From: Jonas Alves Date: Mon, 11 May 2026 16:20:53 +0100 Subject: [PATCH 8/8] chore(datasources): apply prettier formatting (FT-1918) --- src/commands/datasources/datasources.test.ts | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/src/commands/datasources/datasources.test.ts b/src/commands/datasources/datasources.test.ts index 2e693fc..2587271 100644 --- a/src/commands/datasources/datasources.test.ts +++ b/src/commands/datasources/datasources.test.ts @@ -199,14 +199,7 @@ describe('datasources command', () => { }); it('should recreate json_layouts table when --yes is passed', async () => { - await datasourcesCommand.parseAsync([ - 'node', - 'test', - 'json-layouts', - 'recreate', - '1', - '--yes', - ]); + await datasourcesCommand.parseAsync(['node', 'test', 'json-layouts', 'recreate', '1', '--yes']); expect(mockClient.recreateDatasourceJsonLayouts).toHaveBeenCalledWith(1); }); @@ -217,8 +210,6 @@ describe('datasources command', () => { ).rejects.toThrow(/process\.exit: 1/); expect(mockClient.recreateDatasourceJsonLayouts).not.toHaveBeenCalled(); - expect(consoleErrorSpy).toHaveBeenCalledWith( - expect.stringContaining('--yes') - ); + expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining('--yes')); }); });