Skip to content
18 changes: 18 additions & 0 deletions src/api-client/api-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2439,6 +2439,24 @@ export class APIClient {
return response.data;
}

async deleteDatasource(id: DatasourceId): Promise<void> {
const response = await this.request('DELETE', `/datasources/${id}`);
this.validateOkResponse(response, 'deleteDatasource');
}

async createDatasourceJsonLayouts(id: DatasourceId): Promise<void> {
await this.request('POST', `/datasources/${id}/json_layouts/create`);
}

async recreateDatasourceJsonLayouts(id: DatasourceId): Promise<void> {
await this.request('POST', `/datasources/${id}/json_layouts/recreate`);
}

async previewDatasourceJsonLayouts(id: DatasourceId): Promise<unknown> {
const response = await this.request('POST', `/datasources/${id}/json_layouts/preview`);
return response.data;
}

async cancelExportHistory(
exportConfigId: ExportConfigId,
historyId: number,
Expand Down
44 changes: 44 additions & 0 deletions src/commands/datasources/datasources.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(() => {
Expand Down Expand Up @@ -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'));
});
});
73 changes: 73 additions & 0 deletions src/commands/datasources/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down Expand Up @@ -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('<id>', '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('<id>', '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('<id>', '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('<id>', '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);
Expand All @@ -177,3 +248,5 @@ datasourcesCommand.addCommand(validateQueryCommand);
datasourcesCommand.addCommand(previewQueryCommand);
datasourcesCommand.addCommand(setDefaultCommand);
datasourcesCommand.addCommand(schemaCommand);
datasourcesCommand.addCommand(deleteCommand);
datasourcesCommand.addCommand(jsonLayoutsCommand);
43 changes: 43 additions & 0 deletions src/core/datasources/datasources.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ import {
previewDatasourceQuery,
setDefaultDatasource,
getDatasourceSchema,
deleteDatasource,
createDatasourceJsonLayouts,
recreateDatasourceJsonLayouts,
previewDatasourceJsonLayouts,
} from './datasources.js';

describe('datasources', () => {
Expand All @@ -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 () => {
Expand Down Expand Up @@ -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);
});
});
48 changes: 48 additions & 0 deletions src/core/datasources/datasources.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<CommandResult<void>> {
await client.deleteDatasource(params.id);
return { data: undefined };
}

export interface CreateDatasourceJsonLayoutsParams {
id: DatasourceId;
}

export async function createDatasourceJsonLayouts(
client: APIClient,
params: CreateDatasourceJsonLayoutsParams
): Promise<CommandResult<void>> {
await client.createDatasourceJsonLayouts(params.id);
return { data: undefined };
}

export interface RecreateDatasourceJsonLayoutsParams {
id: DatasourceId;
}

export async function recreateDatasourceJsonLayouts(
client: APIClient,
params: RecreateDatasourceJsonLayoutsParams
): Promise<CommandResult<void>> {
await client.recreateDatasourceJsonLayouts(params.id);
return { data: undefined };
}

export interface PreviewDatasourceJsonLayoutsParams {
id: DatasourceId;
}

export async function previewDatasourceJsonLayouts(
client: APIClient,
params: PreviewDatasourceJsonLayoutsParams
): Promise<CommandResult<unknown>> {
const data = await client.previewDatasourceJsonLayouts(params.id);
return { data };
}
Loading