diff --git a/src/api-client/api-client.ts b/src/api-client/api-client.ts index 59bee55..f11281d 100644 --- a/src/api-client/api-client.ts +++ b/src/api-client/api-client.ts @@ -2439,6 +2439,24 @@ export class APIClient { return response.data; } + async deleteDatasource(id: DatasourceId): Promise { + const response = await this.request('DELETE', `/datasources/${id}`); + 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, diff --git a/src/commands/datasources/datasources.test.ts b/src/commands/datasources/datasources.test.ts index 898e9ea..2587271 100644 --- a/src/commands/datasources/datasources.test.ts +++ b/src/commands/datasources/datasources.test.ts @@ -34,6 +34,16 @@ describe('datasources command', () => { previewDatasourceQuery: vi.fn().mockResolvedValue({ result: [] }), 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(() => { @@ -168,4 +178,38 @@ 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); + }); + + 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(); + }); + + 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 cb20b9b..7724885 100644 --- a/src/commands/datasources/index.ts +++ b/src/commands/datasources/index.ts @@ -20,6 +20,10 @@ import { previewDatasourceQuery as corePreviewDatasourceQuery, 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') @@ -166,6 +170,73 @@ 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`)); + }) + ); + +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 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) + .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(jsonLayoutsRecreateCommand); +jsonLayoutsCommand.addCommand(jsonLayoutsPreviewCommand); + datasourcesCommand.addCommand(listCommand); datasourcesCommand.addCommand(getCommand); datasourcesCommand.addCommand(createCommand); @@ -177,3 +248,5 @@ datasourcesCommand.addCommand(validateQueryCommand); datasourcesCommand.addCommand(previewQueryCommand); datasourcesCommand.addCommand(setDefaultCommand); datasourcesCommand.addCommand(schemaCommand); +datasourcesCommand.addCommand(deleteCommand); +datasourcesCommand.addCommand(jsonLayoutsCommand); diff --git a/src/core/datasources/datasources.test.ts b/src/core/datasources/datasources.test.ts index 8fe51eb..ca79853 100644 --- a/src/core/datasources/datasources.test.ts +++ b/src/core/datasources/datasources.test.ts @@ -11,6 +11,10 @@ import { previewDatasourceQuery, setDefaultDatasource, getDatasourceSchema, + deleteDatasource, + createDatasourceJsonLayouts, + recreateDatasourceJsonLayouts, + previewDatasourceJsonLayouts, } from './datasources.js'; describe('datasources', () => { @@ -26,6 +30,10 @@ describe('datasources', () => { previewDatasourceQuery: vi.fn(), setDefaultDatasource: vi.fn(), getDatasourceSchema: vi.fn(), + deleteDatasource: vi.fn(), + createDatasourceJsonLayouts: vi.fn(), + recreateDatasourceJsonLayouts: vi.fn(), + previewDatasourceJsonLayouts: vi.fn(), }; it('should list datasources', async () => { @@ -120,4 +128,39 @@ 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(); + }); + + 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 e3afdf4..2227ef2 100644 --- a/src/core/datasources/datasources.ts +++ b/src/core/datasources/datasources.ts @@ -128,3 +128,51 @@ 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 }; +} + +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 }; +}