mirror of
https://github.com/dadosfera/maestro.git
synced 2026-09-21 03:44:49 +00:00
feat(cdc): batch table removal — reconfigure connectors once
Removing N tables via Edit Objects previously looped a single-table
DELETE per table (maestro deleteTable hardcodes job_ids:[one]), so the
Debezium source + Snowflake sink connectors were rewritten/restarted once
per table. platform-api's DELETE /pipeline/:id/jobs already batches (2
connector writes total for any N), but nothing above it used the array.
New maestro DELETE /pipelines/:pipelineId/inputs/:inputId/tables takes
{ table_names: [] }: soft-deletes each in DynamoDB (tracking successes),
resolves all table_names -> job_ids from the platform pipeline in one GET,
then makes ONE DELETE /pipeline/:id/jobs with all job_ids. All-or-nothing:
any failure (a later mark, an unmatched table, or the platform delete)
rolls back only the marks made in this call.
The single-table deleteTable route is kept (unchanged) — nothing else
depends on removing it, and that's a separable cleanup.
Tests: N tables -> one platform DELETE with all job_ids and no per-job
call; rollback on platform failure; rollback + no delete when a later mark
fails; 404 for an unmatched table. 8 controller specs pass; maestro builds.
Co-Authored-By: WOZCODE <contact@withwoz.com>
This commit is contained in:
@@ -199,3 +199,113 @@ describe('PlatformApiController - addTable', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('PlatformApiController - deleteTables (batch)', () => {
|
||||
let controller: PlatformApiController;
|
||||
let platformApiService: { proxy: jest.Mock };
|
||||
let inputsService: { markTableDeleted: jest.Mock; unmarkTableDeleted: jest.Mock };
|
||||
|
||||
beforeEach(async () => {
|
||||
platformApiService = { proxy: jest.fn() };
|
||||
inputsService = {
|
||||
markTableDeleted: jest.fn().mockResolvedValue({ is_deleted: true, deleted_at: 't' }),
|
||||
unmarkTableDeleted: jest.fn().mockResolvedValue({}),
|
||||
};
|
||||
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
controllers: [PlatformApiController],
|
||||
providers: [
|
||||
{ provide: PlatformApiService, useValue: platformApiService },
|
||||
{ provide: ElasticsearchService, useValue: {} },
|
||||
{ provide: DynamoDBService, useValue: {} },
|
||||
{ provide: CustomersService, useValue: {} },
|
||||
{ provide: CatalogService, useValue: {} },
|
||||
{ provide: InputsService, useValue: inputsService },
|
||||
{ provide: PipelinesService, useValue: {} },
|
||||
{ provide: DadosferaLogger, useValue: { logger } },
|
||||
],
|
||||
}).compile();
|
||||
|
||||
controller = module.get<PlatformApiController>(PlatformApiController);
|
||||
});
|
||||
|
||||
const pipelineWithJobs = () => ({
|
||||
jobs: [
|
||||
{ job_id: 'p_0', input: { connector: 'cdc', table_name: 'pedidos' } },
|
||||
{ job_id: 'p_1', input: { connector: 'cdc', table_name: 'clientes' } },
|
||||
{ job_id: 'p_2', input: { connector: 'cdc', table_name: 'produtos' } },
|
||||
],
|
||||
});
|
||||
|
||||
it('removes N tables in ONE platform call (connectors reconfigured once)', async () => {
|
||||
platformApiService.proxy.mockImplementation((method: string) =>
|
||||
Promise.resolve(method === 'GET' ? pipelineWithJobs() : {}),
|
||||
);
|
||||
|
||||
await controller.deleteTables('pid', 'iid', { table_names: ['pedidos', 'produtos'] }, mockUser);
|
||||
|
||||
// one mark per table
|
||||
expect(inputsService.markTableDeleted).toHaveBeenCalledTimes(2);
|
||||
|
||||
// exactly one DELETE to the batch endpoint, with BOTH job_ids
|
||||
const deleteCalls = platformApiService.proxy.mock.calls.filter(
|
||||
([m, p]: any[]) => m === 'DELETE' && p === '/pipeline/pid/jobs',
|
||||
);
|
||||
expect(deleteCalls).toHaveLength(1);
|
||||
expect(deleteCalls[0][3]).toEqual({
|
||||
job_ids: ['p_0', 'p_2'],
|
||||
delete_snowflake_tables: false,
|
||||
});
|
||||
// never the per-job route
|
||||
const perJob = platformApiService.proxy.mock.calls.some(
|
||||
([m, p]: any[]) => m === 'DELETE' && String(p).startsWith('/jobs/'),
|
||||
);
|
||||
expect(perJob).toBe(false);
|
||||
});
|
||||
|
||||
it('rolls back only this call\'s marks when the platform delete fails', async () => {
|
||||
platformApiService.proxy.mockImplementation((method: string) => {
|
||||
if (method === 'GET') return Promise.resolve(pipelineWithJobs());
|
||||
return Promise.reject(new Error('platform boom'));
|
||||
});
|
||||
|
||||
await expect(
|
||||
controller.deleteTables('pid', 'iid', { table_names: ['pedidos', 'clientes'] }, mockUser),
|
||||
).rejects.toThrow();
|
||||
|
||||
// both marks rolled back, nothing else
|
||||
expect(inputsService.unmarkTableDeleted).toHaveBeenCalledTimes(2);
|
||||
const unmarked = inputsService.unmarkTableDeleted.mock.calls.map((c: any[]) => c[0].table_name).sort();
|
||||
expect(unmarked).toEqual(['clientes', 'pedidos']);
|
||||
});
|
||||
|
||||
it('rolls back the marks made so far if a later mark fails (atomic)', async () => {
|
||||
// second mark fails → first must be rolled back, no platform delete attempted
|
||||
inputsService.markTableDeleted
|
||||
.mockResolvedValueOnce({ is_deleted: true, deleted_at: 't' })
|
||||
.mockRejectedValueOnce(new Error('dynamo boom'));
|
||||
|
||||
await expect(
|
||||
controller.deleteTables('pid', 'iid', { table_names: ['pedidos', 'clientes'] }, mockUser),
|
||||
).rejects.toThrow();
|
||||
|
||||
expect(inputsService.unmarkTableDeleted).toHaveBeenCalledTimes(1);
|
||||
expect(inputsService.unmarkTableDeleted.mock.calls[0][0].table_name).toBe('pedidos');
|
||||
// never reached the platform delete
|
||||
const attemptedDelete = platformApiService.proxy.mock.calls.some(([m]: any[]) => m === 'DELETE');
|
||||
expect(attemptedDelete).toBe(false);
|
||||
});
|
||||
|
||||
it('404s when a requested table has no matching job', async () => {
|
||||
platformApiService.proxy.mockImplementation((method: string) =>
|
||||
Promise.resolve(method === 'GET' ? pipelineWithJobs() : {}),
|
||||
);
|
||||
|
||||
await expect(
|
||||
controller.deleteTables('pid', 'iid', { table_names: ['pedidos', 'ghost'] }, mockUser),
|
||||
).rejects.toThrow();
|
||||
// the successful mark (pedidos) must be rolled back
|
||||
expect(inputsService.unmarkTableDeleted).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user