Skip to content

Commit c5b0d70

Browse files
Move bulk create flows out of benchmarks (#2174)
Fixes OPS-3963
1 parent 368af90 commit c5b0d70

5 files changed

Lines changed: 240 additions & 10 deletions

File tree

packages/server/api/src/app/benchmark/connections-with-supported-blocks.ts renamed to packages/server/api/src/app/app-connection/connections-with-block-support.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { AppConnectionsWithSupportedBlocks } from '@openops/shared';
2-
import { appConnectionService } from '../app-connection/app-connection-service/app-connection-service';
3-
import { getProviderMetadataForAllBlocks } from '../app-connection/connection-providers-resolver';
2+
import { appConnectionService } from './app-connection-service/app-connection-service';
3+
import { getProviderMetadataForAllBlocks } from './connection-providers-resolver';
44

55
export async function getConnectionsWithBlockSupport(
66
projectId: string,

packages/server/api/src/app/benchmark/create-benchmark.service.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,21 +10,21 @@ import {
1010
} from '@openops/shared';
1111
import fs from 'node:fs/promises';
1212
import { IsNull } from 'typeorm';
13+
import { getConnectionsWithBlockSupport } from '../app-connection/connections-with-block-support';
14+
import {
15+
bulkCreateAndPublishFlows,
16+
type WorkflowTemplate,
17+
} from '../flows/flow/flow-bulk-create';
1318
import { flowService } from '../flows/flow/flow.service';
1419
import { flowFolderService } from '../flows/folder/folder.service';
1520
import { createBenchmarkDashboard } from '../openops-analytics/benchmark/benchmark-dashboard-service';
1621
import { attachFlowsToBenchmark } from './attach-benchmark-flows.service';
17-
import {
18-
bulkCreateAndPublishFlows,
19-
type WorkflowTemplate,
20-
} from './benchmark-flow-bulk-create';
2122
import { benchmarkFlowRepo } from './benchmark-flow.repo';
2223
import { benchmarkRepo } from './benchmark.repo';
2324
import {
2425
type CategorizedWorkflowPaths,
2526
resolveWorkflowPathsForSeed,
2627
} from './catalog-resolver';
27-
import { getConnectionsWithBlockSupport } from './connections-with-supported-blocks';
2828
import { throwValidationError } from './errors';
2929

3030
function validateBenchmarkConfiguration(config: BenchmarkConfiguration): void {

packages/server/api/src/app/benchmark/benchmark-flow-bulk-create.ts renamed to packages/server/api/src/app/flows/flow/flow-bulk-create.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import {
88
openOpsId,
99
} from '@openops/shared';
1010
import { EntityManager } from 'typeorm';
11-
import { flowRepo } from '../flows/flow/flow.repo';
11+
import { flowRepo } from './flow.repo';
1212

1313
export type WorkflowTemplate = {
1414
template: {

packages/server/api/test/unit/benchmark/create-benchmark.service.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ jest.mock('../../../src/app/benchmark/catalog-resolver', () => ({
6666

6767
const mockGetConnectionsWithBlockSupport = jest.fn();
6868
jest.mock(
69-
'../../../src/app/benchmark/connections-with-supported-blocks',
69+
'../../../src/app/app-connection/connections-with-block-support',
7070
() => ({
7171
getConnectionsWithBlockSupport: (
7272
...args: unknown[]
@@ -84,7 +84,7 @@ jest.mock('server-worker', () => ({
8484
},
8585
}));
8686

87-
jest.mock('../../../src/app/benchmark/benchmark-flow-bulk-create', () => ({
87+
jest.mock('../../../src/app/flows/flow/flow-bulk-create', () => ({
8888
bulkCreateAndPublishFlows: (
8989
...args: unknown[]
9090
): ReturnType<typeof mockBulkCreateAndPublishFlows> =>
Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
/* eslint-disable @typescript-eslint/no-explicit-any */
2+
const mockInsert = jest.fn().mockResolvedValue(undefined);
3+
const mockQuery = jest.fn().mockResolvedValue(undefined);
4+
const mockGetRepository = jest.fn().mockReturnValue({ insert: mockInsert });
5+
const mockTransaction = jest
6+
.fn()
7+
.mockImplementation(async (cb) =>
8+
cb({ getRepository: mockGetRepository, query: mockQuery }),
9+
);
10+
const mockFlowRepo = jest.fn().mockReturnValue({
11+
manager: { transaction: mockTransaction },
12+
});
13+
14+
jest.mock('../../../src/app/flows/flow/flow.repo', () => ({
15+
flowRepo: mockFlowRepo,
16+
}));
17+
18+
jest.mock('@openops/shared', () => ({
19+
...jest.requireActual('@openops/shared'),
20+
openOpsId: jest.fn(),
21+
flowHelper: {
22+
getImportOperations: jest.fn().mockReturnValue([]),
23+
apply: jest.fn().mockImplementation((version) => version),
24+
},
25+
}));
26+
27+
import {
28+
flowHelper,
29+
FlowStatus,
30+
FlowVersionState,
31+
openOpsId,
32+
} from '@openops/shared';
33+
import { bulkCreateAndPublishFlows } from '../../../src/app/flows/flow/flow-bulk-create';
34+
35+
const mockOpenOpsId = openOpsId as jest.Mock;
36+
const mockGetImportOperations = flowHelper.getImportOperations as jest.Mock;
37+
const mockApply = flowHelper.apply as jest.Mock;
38+
39+
const baseTemplate = {
40+
template: {
41+
displayName: 'Test Flow',
42+
trigger: { type: 'WEBHOOK', name: 'trigger' } as any,
43+
},
44+
};
45+
46+
const baseConnections = [
47+
{ id: 'conn-1', name: 'Test', authProviderKey: 'aws', supportedBlocks: [] },
48+
] as any[];
49+
50+
describe('bulkCreateAndPublishFlows', () => {
51+
beforeEach(() => {
52+
jest.clearAllMocks();
53+
mockGetImportOperations.mockReturnValue([]);
54+
mockApply.mockImplementation((version) => version);
55+
56+
let idCounter = 0;
57+
mockOpenOpsId.mockImplementation(() => `generated-id-${++idCounter}`);
58+
});
59+
60+
it('returns empty array when templates is empty', async () => {
61+
const result = await bulkCreateAndPublishFlows(
62+
[],
63+
baseConnections,
64+
'project-1',
65+
'folder-1',
66+
);
67+
68+
expect(result).toEqual([]);
69+
expect(mockTransaction).not.toHaveBeenCalled();
70+
});
71+
72+
it('inserts flows and versions in a transaction', async () => {
73+
await bulkCreateAndPublishFlows(
74+
[baseTemplate],
75+
baseConnections,
76+
'project-1',
77+
'folder-1',
78+
);
79+
80+
expect(mockTransaction).toHaveBeenCalledTimes(1);
81+
expect(mockGetRepository).toHaveBeenCalledWith('flow');
82+
expect(mockGetRepository).toHaveBeenCalledWith('flow_version');
83+
expect(mockInsert).toHaveBeenCalledTimes(2);
84+
});
85+
86+
it('inserts flow with correct shape', async () => {
87+
await bulkCreateAndPublishFlows(
88+
[baseTemplate],
89+
baseConnections,
90+
'project-1',
91+
'folder-1',
92+
);
93+
94+
const flowInsertCall = mockInsert.mock.calls[0][0];
95+
expect(flowInsertCall).toEqual([
96+
expect.objectContaining({
97+
projectId: 'project-1',
98+
folderId: 'folder-1',
99+
status: FlowStatus.DISABLED,
100+
publishedVersionId: null,
101+
isInternal: false,
102+
schedule: null,
103+
}),
104+
]);
105+
});
106+
107+
it('inserts version with correct shape', async () => {
108+
await bulkCreateAndPublishFlows(
109+
[baseTemplate],
110+
baseConnections,
111+
'project-1',
112+
'folder-1',
113+
);
114+
115+
const versionInsertCall = mockInsert.mock.calls[1][0];
116+
expect(versionInsertCall).toEqual([
117+
expect.objectContaining({
118+
displayName: 'Test Flow',
119+
state: FlowVersionState.LOCKED,
120+
}),
121+
]);
122+
});
123+
124+
it('runs bulk update SQL after inserts', async () => {
125+
await bulkCreateAndPublishFlows(
126+
[baseTemplate],
127+
baseConnections,
128+
'project-1',
129+
'folder-1',
130+
);
131+
132+
expect(mockQuery).toHaveBeenCalledTimes(1);
133+
const sql: string = mockQuery.mock.calls[0][0];
134+
expect(sql).toContain('UPDATE flow');
135+
expect(sql).toContain('publishedVersionId');
136+
});
137+
138+
it('sets status to ENABLED in the bulk update SQL params', async () => {
139+
await bulkCreateAndPublishFlows(
140+
[baseTemplate],
141+
baseConnections,
142+
'project-1',
143+
'folder-1',
144+
);
145+
146+
const params: string[] = mockQuery.mock.calls[0][1];
147+
expect(params).toContain(FlowStatus.ENABLED);
148+
});
149+
150+
it('returns one result per template with id and version', async () => {
151+
const result = await bulkCreateAndPublishFlows(
152+
[
153+
baseTemplate,
154+
{
155+
template: {
156+
displayName: 'Second',
157+
trigger: baseTemplate.template.trigger,
158+
},
159+
},
160+
],
161+
baseConnections,
162+
'project-1',
163+
'folder-1',
164+
);
165+
166+
expect(result).toHaveLength(2);
167+
expect(result[0]).toEqual({
168+
id: expect.any(String),
169+
version: { id: expect.any(String), displayName: 'Test Flow' },
170+
});
171+
expect(result[1]).toEqual({
172+
id: expect.any(String),
173+
version: { id: expect.any(String), displayName: 'Second' },
174+
});
175+
});
176+
177+
it('uses description from template when provided', async () => {
178+
await bulkCreateAndPublishFlows(
179+
[
180+
{
181+
template: {
182+
displayName: 'Flow',
183+
description: 'My desc',
184+
trigger: baseTemplate.template.trigger,
185+
},
186+
},
187+
],
188+
baseConnections,
189+
'project-1',
190+
'folder-1',
191+
);
192+
193+
const versionInsertCall = mockInsert.mock.calls[1][0];
194+
expect(versionInsertCall[0].description).toBe('My desc');
195+
});
196+
197+
it('defaults description to empty string when not provided', async () => {
198+
await bulkCreateAndPublishFlows(
199+
[baseTemplate],
200+
baseConnections,
201+
'project-1',
202+
'folder-1',
203+
);
204+
205+
const versionInsertCall = mockInsert.mock.calls[1][0];
206+
expect(versionInsertCall[0].description).toBe('');
207+
});
208+
209+
it('applies import operations from flowHelper', async () => {
210+
const mockOp = { type: 'UPDATE_ACTION' };
211+
mockGetImportOperations.mockReturnValue([mockOp]);
212+
mockApply.mockImplementation((version) => ({
213+
...version,
214+
displayName: 'mutated',
215+
}));
216+
217+
await bulkCreateAndPublishFlows(
218+
[baseTemplate],
219+
baseConnections,
220+
'project-1',
221+
'folder-1',
222+
);
223+
224+
expect(mockGetImportOperations).toHaveBeenCalledWith(
225+
expect.objectContaining({ type: 'WEBHOOK' }),
226+
baseConnections,
227+
);
228+
expect(mockApply).toHaveBeenCalledWith(expect.any(Object), mockOp);
229+
});
230+
});

0 commit comments

Comments
 (0)