@izara_project/izara-core-generate-service-code 1.0.99 → 1.0.100
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/compile/compileProject.js +16 -5
- package/src/compile/flowValidator.js +3 -1
- package/src/compile/loadSchemas.js +18 -4
- package/src/compile/schemaValidatorSetup.js +16 -0
- package/src/emit/application/coreFunction/emitCoreFunctionArtifacts.js +108 -108
- package/src/emit/application/coreFunction/templates/InitialSetupRunner.ejs +2 -2
- package/src/emit/application/crud/templates/CreateComplete_Main.ejs +2 -0
- package/src/emit/application/crud/templates/CreateLogicalLib.ejs +14 -7
- package/src/emit/application/crud/templates/DeleteComplete_Main.ejs +2 -0
- package/src/emit/application/crud/templates/FlowEndpoint_HdrWbs.ejs +7 -5
- package/src/emit/application/crud/templates/UpdateComplete_Main.ejs +2 -0
- package/src/emit/application/custom/customFlowPaths.js +13 -16
- package/src/emit/application/custom/customFlowS3.js +274 -63
- package/src/emit/application/custom/customFlows.js +131 -0
- package/src/emit/application/custom/emitCustomFlowApplicationArtifacts.js +3 -1
- package/src/emit/application/custom/templates/s3/ConfirmAfterUpload_HdrSqs.ejs +110 -0
- package/src/emit/application/custom/templates/s3/ConfirmAfterUpload_Main.ejs +89 -0
- package/src/emit/application/custom/templates/s3/ConfirmComplete_HdrSqs.ejs +110 -0
- package/src/emit/application/custom/templates/s3/ConfirmComplete_Main.ejs +89 -0
- package/src/emit/application/custom/templates/s3/CreateObject_HdrWbs.ejs +2 -1
- package/src/emit/application/custom/templates/s3/CreateObject_Main.ejs +4 -0
- package/src/emit/application/custom/templates/s3/CreatePreSignUrl_HdrSqs.ejs +42 -0
- package/src/emit/application/custom/templates/s3/CreatePreSignUrl_Main.ejs +23 -0
- package/src/emit/application/custom/templates/s3/ProcessS3File_Main.ejs +135 -27
- package/src/emit/application/custom/templates/s3/Start_Main.ejs +197 -0
- package/src/emit/application/rbac/emitDynamicRbacFlowSchemas.js +5 -1
- package/src/emit/application/rbac/emitRbacApplicationArtifacts.js +67 -60
- package/src/emit/application/rbac/templates/ReservedDynamicUsage_Main.ejs +27 -9
- package/src/emit/application/rbac/templates/UserRbacFlow_Main.ejs +4 -3
- package/src/emit/application/system/templates/Register_HdrWbs.ejs +2 -1
- package/src/emit/infrastructure/functions/functionYamlArtifact.js +3 -8
- package/src/emit/infrastructure/functions/templates/S3Function_Yaml.ejs +2 -3
- package/src/emit/infrastructure/resources/iamResourceArtifacts.js +22 -0
- package/src/emit/infrastructure/resources/resourceState.js +15 -5
- package/src/emit/infrastructure/resources/s3ResourceArtifacts.js +19 -3
- package/src/emit/infrastructure/resources/templates/S3Bucket_Yaml.ejs +1 -1
- package/src/emit/infrastructure/resources/templates/SystemDynamoDB_Yaml.ejs +2 -2
- package/src/emit/schemas/error/emitErrorSchemas.js +120 -10
- package/src/emit/schemas/error/templates/errorConsts.ejs +12 -0
- package/src/generate.js +32 -30
- package/test/integration/publicApi.test.js +33 -24
- package/test/unit/application/createTargetRoleHandler.test.js +33 -10
- package/test/unit/application/customFlowApplicationArtifacts.test.js +6 -6
- package/test/unit/application/customFlowS3.test.js +730 -0
- package/test/unit/infrastructure/functionYamlArtifact.test.js +6 -4
- package/test/unit/infrastructure/resourceArtifacts.test.js +51 -0
- package/test/unit/schemas/dynamicRbacFlowSchemas.test.js +8 -6
- package/test/unit/schemas/dynamicRelationshipFlowSchemas.test.js +51 -0
- package/test/unit/schemas/errorSchemas.test.js +84 -5
|
@@ -0,0 +1,730 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import test from 'node:test';
|
|
3
|
+
import { emitS3Flow } from '../../../src/emit/application/custom/customFlowS3.js';
|
|
4
|
+
|
|
5
|
+
test('emitS3Flow with 3-step pipeline generates exactly 3 pairs of artifacts', async () => {
|
|
6
|
+
const mockFlow = {
|
|
7
|
+
flowTag: 'FeedChunkRecordUrl',
|
|
8
|
+
initiateBy: 'user',
|
|
9
|
+
handleObj: 'one',
|
|
10
|
+
statusType: 'statusField',
|
|
11
|
+
event: ['s3'],
|
|
12
|
+
objType: {
|
|
13
|
+
objectType: 'feedChunk',
|
|
14
|
+
serviceTag: 'FeedChunk'
|
|
15
|
+
},
|
|
16
|
+
bucketName: 'video-record-temp',
|
|
17
|
+
flowSteps: [
|
|
18
|
+
{ stepName: 'Start', event: ['wbs'] },
|
|
19
|
+
{ stepName: 'CreatePreSignUrl', event: ['extTopic'] },
|
|
20
|
+
{ stepName: 'ProcessAfterUpload', event: ['s3'] }
|
|
21
|
+
]
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const source = 'app/src/generatedCode/FlowSchemas/FeedChunkRecordUrl/source';
|
|
25
|
+
const handlerRoot = 'src/generatedCode/FlowSchemas/FeedChunkRecordUrl/source';
|
|
26
|
+
|
|
27
|
+
const result = await emitS3Flow(mockFlow, source, handlerRoot);
|
|
28
|
+
|
|
29
|
+
assert.equal(result.functions.length, 3);
|
|
30
|
+
const functionNames = result.functions.map((fn) => fn.split(':')[0].trim());
|
|
31
|
+
assert.deepEqual(functionNames, [
|
|
32
|
+
'FeedChunkRecordUrlHdrWbs',
|
|
33
|
+
'FeedChunkRecordUrlCreatePreSignUrlHdrSqs',
|
|
34
|
+
'FeedChunkRecordUrlProcessAfterUploadHdrS3'
|
|
35
|
+
]);
|
|
36
|
+
|
|
37
|
+
const artifactPaths = result.artifacts.map((a) => a.path);
|
|
38
|
+
assert.ok(artifactPaths.includes(`${source}/FeedChunkRecordUrl_Main.js`));
|
|
39
|
+
assert.ok(artifactPaths.includes(`${source}/FeedChunkRecordUrl_HdrWbs.js`));
|
|
40
|
+
assert.ok(artifactPaths.includes(`${source}/FeedChunkRecordUrlCreatePreSignUrl_Main.js`));
|
|
41
|
+
assert.ok(artifactPaths.includes(`${source}/FeedChunkRecordUrlCreatePreSignUrl_HdrSqs.js`));
|
|
42
|
+
assert.ok(artifactPaths.includes(`${source}/FeedChunkRecordUrlProcessAfterUpload_Main.js`));
|
|
43
|
+
assert.ok(artifactPaths.includes(`${source}/FeedChunkRecordUrlProcessAfterUpload_HdrS3.js`));
|
|
44
|
+
|
|
45
|
+
const mainArtifact = result.artifacts.find(
|
|
46
|
+
(a) => a.path === `${source}/FeedChunkRecordUrl_Main.js`
|
|
47
|
+
);
|
|
48
|
+
assert.ok(
|
|
49
|
+
mainArtifact.content.includes("'FeedChunkRecordUrlCreatePreSignUrlHdrSqs'")
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
const sqsHdrArtifact = result.artifacts.find(
|
|
53
|
+
(a) => a.path === `${source}/FeedChunkRecordUrlCreatePreSignUrl_HdrSqs.js`
|
|
54
|
+
);
|
|
55
|
+
assert.ok(
|
|
56
|
+
sqsHdrArtifact.content.includes("required: ['objType', 'objInstanceFull']")
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
const sqsFunctionYaml = result.functions.find((fn) =>
|
|
60
|
+
fn.startsWith('FeedChunkRecordUrlCreatePreSignUrlHdrSqs:')
|
|
61
|
+
);
|
|
62
|
+
assert.ok(
|
|
63
|
+
sqsFunctionYaml.includes(
|
|
64
|
+
'${self:custom.iz_resourcePrefix}FeedChunkRecordUrlCreatePreSignUrlHdrSqs'
|
|
65
|
+
)
|
|
66
|
+
);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test('emitS3Flow with ADR-016 deferred-node pipeline generates correct 3 pairs of artifacts', async () => {
|
|
70
|
+
const mockFlow = {
|
|
71
|
+
flowTag: 'FeedChunkRecordUrl',
|
|
72
|
+
initiateBy: 'user',
|
|
73
|
+
handleObj: 'one',
|
|
74
|
+
statusType: 'statusField',
|
|
75
|
+
event: ['s3'],
|
|
76
|
+
objType: {
|
|
77
|
+
objectType: 'feedChunk',
|
|
78
|
+
serviceTag: 'FeedChunk'
|
|
79
|
+
},
|
|
80
|
+
bucketName: 'video-record-temp',
|
|
81
|
+
flowSteps: [
|
|
82
|
+
{ stepName: 'Start', event: ['wbs'] },
|
|
83
|
+
{ stepName: 'ProcessAfterUpload', event: ['s3'] },
|
|
84
|
+
{ stepName: 'ConfirmAfterUpload', event: ['extTopic'] }
|
|
85
|
+
]
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const source = 'app/src/generatedCode/FlowSchemas/FeedChunkRecordUrl/source';
|
|
89
|
+
const handlerRoot = 'src/generatedCode/FlowSchemas/FeedChunkRecordUrl/source';
|
|
90
|
+
|
|
91
|
+
const mockObjects = [
|
|
92
|
+
{
|
|
93
|
+
objectType: 'feedChunk',
|
|
94
|
+
belongTo: { serviceTag: 'vidFeedOperation', objectType: 'operation' }
|
|
95
|
+
}
|
|
96
|
+
];
|
|
97
|
+
const result = await emitS3Flow(mockFlow, source, handlerRoot, {
|
|
98
|
+
objects: mockObjects
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
assert.equal(result.functions.length, 3);
|
|
102
|
+
const functionNames = result.functions.map((fn) => fn.split(':')[0].trim());
|
|
103
|
+
assert.deepEqual(functionNames, [
|
|
104
|
+
'FeedChunkRecordUrlHdrWbs',
|
|
105
|
+
'FeedChunkRecordUrlProcessAfterUploadHdrS3',
|
|
106
|
+
'FeedChunkRecordUrlConfirmAfterUploadHdrSqs'
|
|
107
|
+
]);
|
|
108
|
+
|
|
109
|
+
const artifactPaths = result.artifacts.map((a) => a.path);
|
|
110
|
+
assert.ok(artifactPaths.includes(`${source}/FeedChunkRecordUrl_Main.js`));
|
|
111
|
+
assert.ok(artifactPaths.includes(`${source}/FeedChunkRecordUrl_HdrWbs.js`));
|
|
112
|
+
assert.ok(artifactPaths.includes(`${source}/FeedChunkRecordUrlProcessAfterUpload_Main.js`));
|
|
113
|
+
assert.ok(artifactPaths.includes(`${source}/FeedChunkRecordUrlProcessAfterUpload_HdrS3.js`));
|
|
114
|
+
assert.ok(artifactPaths.includes(`${source}/FeedChunkRecordUrlConfirmAfterUpload_Main.js`));
|
|
115
|
+
assert.ok(artifactPaths.includes(`${source}/FeedChunkRecordUrlConfirmAfterUpload_HdrSqs.js`));
|
|
116
|
+
|
|
117
|
+
const sqsFunctionYaml = result.functions.find((fn) =>
|
|
118
|
+
fn.startsWith('FeedChunkRecordUrlConfirmAfterUploadHdrSqs:')
|
|
119
|
+
);
|
|
120
|
+
assert.ok(
|
|
121
|
+
sqsFunctionYaml.includes(
|
|
122
|
+
'${self:custom.iz_resourcePrefix}FeedChunkRecordUrlConfirmAfterUploadHdrSqs'
|
|
123
|
+
)
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
const startMainArtifact = result.artifacts.find(
|
|
127
|
+
(a) => a.path === `${source}/FeedChunkRecordUrl_Main.js`
|
|
128
|
+
);
|
|
129
|
+
assert.ok(
|
|
130
|
+
startMainArtifact.content.includes('createPresignedPost'),
|
|
131
|
+
'Start_Main should generate S3 presigned post URL'
|
|
132
|
+
);
|
|
133
|
+
assert.ok(
|
|
134
|
+
startMainArtifact.content.includes('uuidv7'),
|
|
135
|
+
'Start_Main should generate identifier with uuidv7'
|
|
136
|
+
);
|
|
137
|
+
assert.ok(
|
|
138
|
+
!startMainArtifact.content.includes("snsTopicArnByFlowSchema(_izContext, 'Create_In')"),
|
|
139
|
+
'Start_Main must not publish to Create_In topic'
|
|
140
|
+
);
|
|
141
|
+
assert.ok(
|
|
142
|
+
startMainArtifact.content.includes("Bucket: 'video-record-temp'"),
|
|
143
|
+
'Start_Main must bind bucket directly from schema'
|
|
144
|
+
);
|
|
145
|
+
assert.ok(
|
|
146
|
+
!startMainArtifact.content.includes('process.env.iz_s3BucketName'),
|
|
147
|
+
'Start_Main must not fallback to environment variable'
|
|
148
|
+
);
|
|
149
|
+
assert.ok(
|
|
150
|
+
startMainArtifact.content.includes("['eq', '$x-amz-meta-operationId', targetOperationId]"),
|
|
151
|
+
'Start_Main must enforce operationId condition'
|
|
152
|
+
);
|
|
153
|
+
assert.ok(
|
|
154
|
+
startMainArtifact.content.includes('requestParams.targetIdentifiers?.operationId'),
|
|
155
|
+
'Start_Main should dynamically resolve operationId from belongTo schema'
|
|
156
|
+
);
|
|
157
|
+
assert.ok(
|
|
158
|
+
startMainArtifact.content.includes("throw new Error('targetIdentifiers.operationId is required')"),
|
|
159
|
+
'Start_Main should enforce targetIdentifiers.operationId is required'
|
|
160
|
+
);
|
|
161
|
+
assert.ok(
|
|
162
|
+
startMainArtifact.content.includes("const submittedByUserId = _izContext.correlationIds?.get("),
|
|
163
|
+
'Start_Main must extract submittedByUserId from correlationIds'
|
|
164
|
+
);
|
|
165
|
+
assert.ok(
|
|
166
|
+
startMainArtifact.content.includes("['starts-with', '$x-amz-meta-submittedByUserId', '']"),
|
|
167
|
+
'Start_Main must include x-amz-meta-submittedByUserId condition'
|
|
168
|
+
);
|
|
169
|
+
assert.ok(
|
|
170
|
+
startMainArtifact.content.includes("'x-amz-meta-submittedByUserId': submittedByUserId"),
|
|
171
|
+
'Start_Main must include x-amz-meta-submittedByUserId field'
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
const procMainArtifact = result.artifacts.find(
|
|
175
|
+
(a) => a.path === `${source}/FeedChunkRecordUrlProcessAfterUpload_Main.js`
|
|
176
|
+
);
|
|
177
|
+
assert.ok(
|
|
178
|
+
procMainArtifact.content.includes("snsTopicArnByFlowSchema(_izContext, 'Create_In')"),
|
|
179
|
+
'ProcessAfterUpload_Main must dispatch to Create_In topic'
|
|
180
|
+
);
|
|
181
|
+
assert.ok(
|
|
182
|
+
procMainArtifact.content.includes('operationId: parentId'),
|
|
183
|
+
'ProcessAfterUpload_Main should include parentIdKey in targetIdentifiers'
|
|
184
|
+
);
|
|
185
|
+
assert.ok(
|
|
186
|
+
procMainArtifact.content.includes('FeedChunkRecordUrlConfirmAfterUploadHdrSqs'),
|
|
187
|
+
'ProcessAfterUpload_Main must target ConfirmAfterUploadHdrSqs for callback'
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
const confirmMainArtifact = result.artifacts.find(
|
|
191
|
+
(a) => a.path === `${source}/FeedChunkRecordUrlConfirmAfterUpload_Main.js`
|
|
192
|
+
);
|
|
193
|
+
assert.ok(
|
|
194
|
+
confirmMainArtifact.content.includes('objInstanceFull'),
|
|
195
|
+
'ConfirmAfterUpload_Main must receive objInstanceFull'
|
|
196
|
+
);
|
|
197
|
+
assert.ok(
|
|
198
|
+
!confirmMainArtifact.content.includes('createPresignedPost'),
|
|
199
|
+
'ConfirmAfterUpload_Main must not generate presigned URLs'
|
|
200
|
+
);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
test('emitS3Flow preserves legacy 5 parts when flowSteps has no steps or legacy 5 steps', async () => {
|
|
204
|
+
const legacyFlow = {
|
|
205
|
+
flowTag: 'FeedChunkRecordUrl',
|
|
206
|
+
initiateBy: 'user',
|
|
207
|
+
handleObj: 'one',
|
|
208
|
+
statusType: 'statusField',
|
|
209
|
+
event: ['s3'],
|
|
210
|
+
objType: {
|
|
211
|
+
objectType: 'feedChunk',
|
|
212
|
+
serviceTag: 'FeedChunk'
|
|
213
|
+
},
|
|
214
|
+
bucketName: 'video-record-temp'
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
const source = 'app/src/generatedCode/FlowSchemas/FeedChunkRecordUrl/source';
|
|
218
|
+
const handlerRoot = 'src/generatedCode/FlowSchemas/FeedChunkRecordUrl/source';
|
|
219
|
+
|
|
220
|
+
const result = await emitS3Flow(legacyFlow, source, handlerRoot);
|
|
221
|
+
assert.equal(result.functions.length, 5);
|
|
222
|
+
|
|
223
|
+
const legacySqsHdrArtifact = result.artifacts.find((a) =>
|
|
224
|
+
a.path.includes('CreatePreSignUrl_HdrSqs.js')
|
|
225
|
+
);
|
|
226
|
+
assert.ok(
|
|
227
|
+
legacySqsHdrArtifact.content.includes(
|
|
228
|
+
"required: ['returnMessage', 'foundErrors', 'passStatus']"
|
|
229
|
+
)
|
|
230
|
+
);
|
|
231
|
+
|
|
232
|
+
const legacySqsFunctionYaml = result.functions.find((fn) =>
|
|
233
|
+
fn.startsWith('FeedChunkRecordUrlCreatePreSignUrlHdrSqs:')
|
|
234
|
+
);
|
|
235
|
+
assert.ok(
|
|
236
|
+
legacySqsFunctionYaml.includes('CreatePresignUrlHdrSqs')
|
|
237
|
+
);
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
test('createResourceState generates SQS queue and SNS subscription for 3-step S3 flow', async () => {
|
|
241
|
+
const { createResourceState } = await import(
|
|
242
|
+
'../../../src/emit/infrastructure/resources/resourceState.js'
|
|
243
|
+
);
|
|
244
|
+
|
|
245
|
+
const mockFlow = {
|
|
246
|
+
flowTag: 'FeedChunkRecordUrl',
|
|
247
|
+
initiateBy: 'user',
|
|
248
|
+
handleObj: 'one',
|
|
249
|
+
statusType: 'statusField',
|
|
250
|
+
event: ['s3'],
|
|
251
|
+
objType: {
|
|
252
|
+
objectType: 'feedChunk',
|
|
253
|
+
serviceTag: 'FeedChunk'
|
|
254
|
+
},
|
|
255
|
+
bucketName: 'video-record-temp',
|
|
256
|
+
flowSteps: [
|
|
257
|
+
{ stepName: 'Start', event: ['wbs'] },
|
|
258
|
+
{ stepName: 'CreatePreSignUrl', event: ['extTopic'] },
|
|
259
|
+
{ stepName: 'ProcessAfterUpload', event: ['s3'] }
|
|
260
|
+
]
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
const state = createResourceState({
|
|
264
|
+
objects: [],
|
|
265
|
+
relationships: [],
|
|
266
|
+
flows: [mockFlow]
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
assert.deepEqual(state.customSqsQueues, [
|
|
270
|
+
'FeedChunkRecordUrlCreatePreSignUrlHdrSqs'
|
|
271
|
+
]);
|
|
272
|
+
assert.equal(state.s3Subscriptions.length, 1);
|
|
273
|
+
assert.equal(
|
|
274
|
+
state.s3Subscriptions[0].queueName,
|
|
275
|
+
'FeedChunkRecordUrlCreatePreSignUrlHdrSqs'
|
|
276
|
+
);
|
|
277
|
+
assert.equal(
|
|
278
|
+
state.s3Subscriptions[0].subscriptionName,
|
|
279
|
+
'SubscriptionOutCreateToFeedChunkRecordUrlCreatePreSignUrlHdrSqs'
|
|
280
|
+
);
|
|
281
|
+
assert.equal(state.s3Subscriptions[0].topicName, 'OutCreate');
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
test('roleTopicNames includes Create_In for S3 custom flow role', async () => {
|
|
285
|
+
const { roleTopicNames } = await import(
|
|
286
|
+
'../../../src/emit/infrastructure/resources/iamResourceArtifacts.js'
|
|
287
|
+
);
|
|
288
|
+
|
|
289
|
+
const mockFlow = {
|
|
290
|
+
flowTag: 'FeedChunkRecordUrl',
|
|
291
|
+
event: ['s3'],
|
|
292
|
+
flowSteps: [
|
|
293
|
+
{ stepName: 'Start', event: ['wbs'] },
|
|
294
|
+
{ stepName: 'CreatePreSignUrl', event: ['extTopic'] },
|
|
295
|
+
{ stepName: 'ProcessAfterUpload', event: ['s3'] }
|
|
296
|
+
]
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
const state = {
|
|
300
|
+
flows: [mockFlow],
|
|
301
|
+
outTopics: []
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
const topics = roleTopicNames(state, 'FeedChunkRecordUrlRole');
|
|
305
|
+
assert.ok(topics.includes('Create_In'));
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
test('createResourceState generates SQS queue and SNS subscription for ADR-016 deferred-node flow', async () => {
|
|
309
|
+
const { createResourceState } = await import(
|
|
310
|
+
'../../../src/emit/infrastructure/resources/resourceState.js'
|
|
311
|
+
);
|
|
312
|
+
|
|
313
|
+
const mockFlow = {
|
|
314
|
+
flowTag: 'FeedChunkRecordUrl',
|
|
315
|
+
initiateBy: 'user',
|
|
316
|
+
handleObj: 'one',
|
|
317
|
+
statusType: 'statusField',
|
|
318
|
+
event: ['s3'],
|
|
319
|
+
objType: {
|
|
320
|
+
objectType: 'feedChunk',
|
|
321
|
+
serviceTag: 'FeedChunk'
|
|
322
|
+
},
|
|
323
|
+
bucketName: 'video-record-temp',
|
|
324
|
+
flowSteps: [
|
|
325
|
+
{ stepName: 'Start', event: ['wbs'] },
|
|
326
|
+
{ stepName: 'ProcessAfterUpload', event: ['s3'] },
|
|
327
|
+
{ stepName: 'ConfirmAfterUpload', event: ['extTopic'] }
|
|
328
|
+
]
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
const state = createResourceState({
|
|
332
|
+
objects: [],
|
|
333
|
+
relationships: [],
|
|
334
|
+
flows: [mockFlow]
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
assert.deepEqual(state.customSqsQueues, [
|
|
338
|
+
'FeedChunkRecordUrlConfirmAfterUploadHdrSqs'
|
|
339
|
+
]);
|
|
340
|
+
assert.equal(state.s3Subscriptions.length, 1);
|
|
341
|
+
assert.equal(
|
|
342
|
+
state.s3Subscriptions[0].queueName,
|
|
343
|
+
'FeedChunkRecordUrlConfirmAfterUploadHdrSqs'
|
|
344
|
+
);
|
|
345
|
+
assert.equal(
|
|
346
|
+
state.s3Subscriptions[0].subscriptionName,
|
|
347
|
+
'SubscriptionOutCreateToFeedChunkRecordUrlConfirmAfterUploadHdrSqs'
|
|
348
|
+
);
|
|
349
|
+
assert.equal(state.s3Subscriptions[0].topicName, 'OutCreate');
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
test('emitS3Flow with ConfirmComplete step generates correct artifacts and SQS filter', async () => {
|
|
353
|
+
const mockFlow = {
|
|
354
|
+
flowTag: 'FeedChunkRecordUrl',
|
|
355
|
+
initiateBy: 'user',
|
|
356
|
+
handleObj: 'one',
|
|
357
|
+
statusType: 'statusField',
|
|
358
|
+
event: ['s3'],
|
|
359
|
+
objType: {
|
|
360
|
+
objectType: 'feedChunk',
|
|
361
|
+
serviceTag: 'FeedChunk'
|
|
362
|
+
},
|
|
363
|
+
bucketName: 'video-record-temp',
|
|
364
|
+
flowSteps: [
|
|
365
|
+
{ stepName: 'Start', event: ['wbs'] },
|
|
366
|
+
{ stepName: 'ProcessAfterUpload', event: ['s3'] },
|
|
367
|
+
{ stepName: 'ConfirmComplete', event: ['extTopic'] }
|
|
368
|
+
]
|
|
369
|
+
};
|
|
370
|
+
|
|
371
|
+
const source = 'app/src/generatedCode/FlowSchemas/FeedChunkRecordUrl/source';
|
|
372
|
+
const handlerRoot = 'src/generatedCode/FlowSchemas/FeedChunkRecordUrl/source';
|
|
373
|
+
|
|
374
|
+
const result = await emitS3Flow(mockFlow, source, handlerRoot);
|
|
375
|
+
|
|
376
|
+
assert.equal(result.functions.length, 3);
|
|
377
|
+
const functionNames = result.functions.map((fn) => fn.split(':')[0].trim());
|
|
378
|
+
assert.deepEqual(functionNames, [
|
|
379
|
+
'FeedChunkRecordUrlHdrWbs',
|
|
380
|
+
'FeedChunkRecordUrlProcessAfterUploadHdrS3',
|
|
381
|
+
'FeedChunkRecordUrlConfirmCompleteHdrSqs'
|
|
382
|
+
]);
|
|
383
|
+
|
|
384
|
+
const artifactPaths = result.artifacts.map((a) => a.path);
|
|
385
|
+
assert.ok(artifactPaths.includes(`${source}/FeedChunkRecordUrl_Main.js`));
|
|
386
|
+
assert.ok(artifactPaths.includes(`${source}/FeedChunkRecordUrl_HdrWbs.js`));
|
|
387
|
+
assert.ok(artifactPaths.includes(`${source}/FeedChunkRecordUrlProcessAfterUpload_Main.js`));
|
|
388
|
+
assert.ok(artifactPaths.includes(`${source}/FeedChunkRecordUrlProcessAfterUpload_HdrS3.js`));
|
|
389
|
+
assert.ok(artifactPaths.includes(`${source}/FeedChunkRecordUrlConfirmComplete_Main.js`));
|
|
390
|
+
assert.ok(artifactPaths.includes(`${source}/FeedChunkRecordUrlConfirmComplete_HdrSqs.js`));
|
|
391
|
+
|
|
392
|
+
const sqsFunctionYaml = result.functions.find((fn) =>
|
|
393
|
+
fn.startsWith('FeedChunkRecordUrlConfirmCompleteHdrSqs:')
|
|
394
|
+
);
|
|
395
|
+
assert.ok(
|
|
396
|
+
sqsFunctionYaml.includes(
|
|
397
|
+
'${self:custom.iz_resourcePrefix}FeedChunkRecordUrlConfirmCompleteHdrSqs'
|
|
398
|
+
)
|
|
399
|
+
);
|
|
400
|
+
|
|
401
|
+
const procMainArtifact = result.artifacts.find(
|
|
402
|
+
(a) => a.path === `${source}/FeedChunkRecordUrlProcessAfterUpload_Main.js`
|
|
403
|
+
);
|
|
404
|
+
assert.ok(
|
|
405
|
+
procMainArtifact.content.includes('FeedChunkRecordUrlConfirmCompleteHdrSqs'),
|
|
406
|
+
'ProcessAfterUpload_Main must target ConfirmCompleteHdrSqs for callback'
|
|
407
|
+
);
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
test('emitS3Flow generates auto RBAC check, parameter groups hook, and file parameter extraction in Start_Main.js', async () => {
|
|
411
|
+
const mockFlow = {
|
|
412
|
+
flowTag: 'FeedChunkRecordUrl',
|
|
413
|
+
initiateBy: 'user',
|
|
414
|
+
handleObj: 'one',
|
|
415
|
+
statusType: 'statusField',
|
|
416
|
+
event: ['s3'],
|
|
417
|
+
objType: {
|
|
418
|
+
objectType: 'feedChunk',
|
|
419
|
+
serviceTag: 'FeedChunk'
|
|
420
|
+
},
|
|
421
|
+
bucketName: 'video-record-temp',
|
|
422
|
+
contentTypePrefix: 'video/',
|
|
423
|
+
maxFileSizeMb: 50,
|
|
424
|
+
s3KeyPrefix: 'uploads/',
|
|
425
|
+
createBucket: false,
|
|
426
|
+
flowSteps: [
|
|
427
|
+
{ stepName: 'Start', event: ['wbs'] },
|
|
428
|
+
{ stepName: 'ProcessAfterUpload', event: ['s3'] },
|
|
429
|
+
{ stepName: 'ConfirmComplete', event: ['extTopic'] }
|
|
430
|
+
]
|
|
431
|
+
};
|
|
432
|
+
|
|
433
|
+
const mockObjects = [
|
|
434
|
+
{
|
|
435
|
+
objectType: 'feedChunk',
|
|
436
|
+
belongTo: { serviceTag: 'vidFeedOperation', objectType: 'operation' }
|
|
437
|
+
}
|
|
438
|
+
];
|
|
439
|
+
|
|
440
|
+
const source = 'app/src/generatedCode/FlowSchemas/FeedChunkRecordUrl/source';
|
|
441
|
+
const handlerRoot = 'src/generatedCode/FlowSchemas/FeedChunkRecordUrl/source';
|
|
442
|
+
|
|
443
|
+
const result = await emitS3Flow(mockFlow, source, handlerRoot, {
|
|
444
|
+
objects: mockObjects
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
const startMainArtifact = result.artifacts.find(
|
|
448
|
+
(a) => a.path === `${source}/FeedChunkRecordUrl_Main.js`
|
|
449
|
+
);
|
|
450
|
+
assert.ok(startMainArtifact, 'FeedChunkRecordUrl_Main.js should be generated');
|
|
451
|
+
|
|
452
|
+
// RBAC checks
|
|
453
|
+
assert.ok(
|
|
454
|
+
startMainArtifact.content.includes('checkRbacPermission'),
|
|
455
|
+
'Start_Main must auto-generate checkRbacPermission'
|
|
456
|
+
);
|
|
457
|
+
assert.ok(
|
|
458
|
+
startMainArtifact.content.includes('//(<rbacParameterGroups_FeedChunkRecordUrl>)'),
|
|
459
|
+
'Start_Main must include rbacParameterGroups hook'
|
|
460
|
+
);
|
|
461
|
+
assert.ok(
|
|
462
|
+
startMainArtifact.content.includes('targetIdentifiers: requestParams.targetIdentifiers,'),
|
|
463
|
+
'Start_Main must use targetIdentifiers: requestParams.targetIdentifiers'
|
|
464
|
+
);
|
|
465
|
+
assert.ok(
|
|
466
|
+
startMainArtifact.content.includes("serviceTag: 'vidFeedOperation'"),
|
|
467
|
+
'Start_Main must target parent serviceTag for RBAC'
|
|
468
|
+
);
|
|
469
|
+
assert.ok(
|
|
470
|
+
startMainArtifact.content.includes("objectType: 'operation'"),
|
|
471
|
+
'Start_Main must target parent objectType for RBAC'
|
|
472
|
+
);
|
|
473
|
+
|
|
474
|
+
// File parameter extraction
|
|
475
|
+
assert.ok(
|
|
476
|
+
startMainArtifact.content.includes("let importType = 'mp4'"),
|
|
477
|
+
'Start_Main should infer default importType mp4 from video/ prefix'
|
|
478
|
+
);
|
|
479
|
+
assert.ok(
|
|
480
|
+
startMainArtifact.content.includes('requestParams.file?.size'),
|
|
481
|
+
'Start_Main should extract maxFileSizeMb from file.size'
|
|
482
|
+
);
|
|
483
|
+
assert.ok(
|
|
484
|
+
startMainArtifact.content.includes("let uploadPath = '';"),
|
|
485
|
+
'Start_Main should initialize uploadPath to empty string inside createKeyForUploadFile hook'
|
|
486
|
+
);
|
|
487
|
+
|
|
488
|
+
// Conditions & Fields
|
|
489
|
+
assert.ok(
|
|
490
|
+
startMainArtifact.content.includes("['starts-with', '$Content-Type', 'video/']"),
|
|
491
|
+
'Start_Main must enforce starts-with video/ condition'
|
|
492
|
+
);
|
|
493
|
+
assert.ok(
|
|
494
|
+
!startMainArtifact.content.includes("'Content-Type': `video/${importType}`"),
|
|
495
|
+
'Start_Main must omit fixed Content-Type from Fields to allow dynamic client Content-Type'
|
|
496
|
+
);
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
test('emitS3Flow supports composite identifiers with 2 keys', async () => {
|
|
500
|
+
const flow = {
|
|
501
|
+
flowTag: 'FeedChunkRecordUrl',
|
|
502
|
+
flowType: 'custom',
|
|
503
|
+
event: ['s3'],
|
|
504
|
+
bucketName: 'video-record-temp',
|
|
505
|
+
contentTypePrefix: 'video/',
|
|
506
|
+
maxFileSizeMb: 50,
|
|
507
|
+
objType: {
|
|
508
|
+
objectType: 'feedChunk',
|
|
509
|
+
serviceTag: 'FeedChunk'
|
|
510
|
+
},
|
|
511
|
+
belongTo: {
|
|
512
|
+
serviceTag: 'vidFeedOperation',
|
|
513
|
+
objectType: 'operation',
|
|
514
|
+
identifiers: ['operationId', 'subId']
|
|
515
|
+
},
|
|
516
|
+
flowSteps: [
|
|
517
|
+
{ stepName: 'Start', event: ['wbs'] },
|
|
518
|
+
{ stepName: 'ProcessAfterUpload', event: ['s3'] },
|
|
519
|
+
{ stepName: 'ConfirmAfterUpload', event: ['sqs'] }
|
|
520
|
+
]
|
|
521
|
+
};
|
|
522
|
+
|
|
523
|
+
const objects = [
|
|
524
|
+
{
|
|
525
|
+
objectType: 'feedChunk',
|
|
526
|
+
belongTo: {
|
|
527
|
+
serviceTag: 'vidFeedOperation',
|
|
528
|
+
objectType: 'operation',
|
|
529
|
+
identifiers: ['operationId', 'subId']
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
];
|
|
533
|
+
|
|
534
|
+
const source = 'app/src/generatedCode/FlowSchemas/FeedChunkRecordUrl/source';
|
|
535
|
+
const handlerRoot = 'src/generatedCode/FlowSchemas/FeedChunkRecordUrl/source';
|
|
536
|
+
|
|
537
|
+
const result = await emitS3Flow(flow, source, handlerRoot, {
|
|
538
|
+
objects
|
|
539
|
+
});
|
|
540
|
+
const startMain = result.artifacts.find(
|
|
541
|
+
(a) => a.path === `${source}/FeedChunkRecordUrl_Main.js`
|
|
542
|
+
);
|
|
543
|
+
|
|
544
|
+
assert.ok(
|
|
545
|
+
startMain.content.includes('requestParams.targetIdentifiers?.operationId'),
|
|
546
|
+
'Start_Main should extract operationId'
|
|
547
|
+
);
|
|
548
|
+
assert.ok(
|
|
549
|
+
startMain.content.includes('requestParams.targetIdentifiers?.subId'),
|
|
550
|
+
'Start_Main should extract subId'
|
|
551
|
+
);
|
|
552
|
+
assert.ok(
|
|
553
|
+
startMain.content.includes("throw new Error('targetIdentifiers.operationId is required')"),
|
|
554
|
+
'Start_Main should validate operationId'
|
|
555
|
+
);
|
|
556
|
+
assert.ok(
|
|
557
|
+
startMain.content.includes("throw new Error('targetIdentifiers.subId is required')"),
|
|
558
|
+
'Start_Main should validate subId'
|
|
559
|
+
);
|
|
560
|
+
assert.ok(
|
|
561
|
+
startMain.content.includes("['eq', '$x-amz-meta-operationId', targetOperationId]"),
|
|
562
|
+
'Start_Main should include condition for operationId'
|
|
563
|
+
);
|
|
564
|
+
assert.ok(
|
|
565
|
+
startMain.content.includes("['eq', '$x-amz-meta-subId', targetSubId]"),
|
|
566
|
+
'Start_Main should include condition for subId'
|
|
567
|
+
);
|
|
568
|
+
assert.ok(
|
|
569
|
+
startMain.content.includes("'x-amz-meta-operationId': targetOperationId"),
|
|
570
|
+
'Start_Main should include field for operationId'
|
|
571
|
+
);
|
|
572
|
+
assert.ok(
|
|
573
|
+
startMain.content.includes("'x-amz-meta-subId': targetSubId"),
|
|
574
|
+
'Start_Main should include field for subId'
|
|
575
|
+
);
|
|
576
|
+
|
|
577
|
+
const procMain = result.artifacts.find(
|
|
578
|
+
(a) => a.path === `${source}/FeedChunkRecordUrlProcessAfterUpload_Main.js`
|
|
579
|
+
);
|
|
580
|
+
assert.ok(
|
|
581
|
+
procMain.content.includes('operationId: parentId,'),
|
|
582
|
+
'ProcessAfterUpload_Main should include first parentId key'
|
|
583
|
+
);
|
|
584
|
+
assert.ok(
|
|
585
|
+
procMain.content.includes('subId: subId'),
|
|
586
|
+
'ProcessAfterUpload_Main should include second parentId key'
|
|
587
|
+
);
|
|
588
|
+
});
|
|
589
|
+
|
|
590
|
+
test('extractIdentifierFieldNames correctly parses partitionKey, sortKey, and composite fieldNames', async () => {
|
|
591
|
+
const { extractIdentifierFieldNames } = await import(
|
|
592
|
+
'../../../src/emit/application/custom/customFlowS3.js'
|
|
593
|
+
);
|
|
594
|
+
|
|
595
|
+
const dynamoIdentifiers = [
|
|
596
|
+
{
|
|
597
|
+
type: 'partitionKey',
|
|
598
|
+
fieldNames: ['companyId', 'deptId'],
|
|
599
|
+
deliminator: '_',
|
|
600
|
+
name: 'pk'
|
|
601
|
+
},
|
|
602
|
+
{
|
|
603
|
+
type: 'sortKey',
|
|
604
|
+
fieldName: 'operationId',
|
|
605
|
+
name: 'sk'
|
|
606
|
+
}
|
|
607
|
+
];
|
|
608
|
+
assert.deepEqual(extractIdentifierFieldNames(dynamoIdentifiers), [
|
|
609
|
+
'companyId',
|
|
610
|
+
'deptId',
|
|
611
|
+
'operationId'
|
|
612
|
+
]);
|
|
613
|
+
|
|
614
|
+
const graphIdentifiers = [
|
|
615
|
+
{
|
|
616
|
+
type: 'identifier',
|
|
617
|
+
fieldName: 'operationId'
|
|
618
|
+
}
|
|
619
|
+
];
|
|
620
|
+
assert.deepEqual(extractIdentifierFieldNames(graphIdentifiers), [
|
|
621
|
+
'operationId'
|
|
622
|
+
]);
|
|
623
|
+
});
|
|
624
|
+
|
|
625
|
+
test('emitS3Flow automatically resolves partitionKey and sortKey from parent schema in objects', async () => {
|
|
626
|
+
const flow = {
|
|
627
|
+
flowTag: 'FeedChunkRecordUrl',
|
|
628
|
+
flowType: 'custom',
|
|
629
|
+
event: ['s3'],
|
|
630
|
+
bucketName: 'video-record-temp',
|
|
631
|
+
contentTypePrefix: 'video/',
|
|
632
|
+
maxFileSizeMb: 50,
|
|
633
|
+
objType: {
|
|
634
|
+
objectType: 'feedChunk',
|
|
635
|
+
serviceTag: 'FeedChunk'
|
|
636
|
+
},
|
|
637
|
+
flowSteps: [
|
|
638
|
+
{ stepName: 'Start', event: ['wbs'] },
|
|
639
|
+
{ stepName: 'ProcessAfterUpload', event: ['s3'] },
|
|
640
|
+
{ stepName: 'ConfirmAfterUpload', event: ['sqs'] }
|
|
641
|
+
]
|
|
642
|
+
};
|
|
643
|
+
|
|
644
|
+
const parentOperationObj = {
|
|
645
|
+
objectType: 'operation',
|
|
646
|
+
identifiers: [
|
|
647
|
+
{
|
|
648
|
+
type: 'partitionKey',
|
|
649
|
+
fieldName: 'operationId'
|
|
650
|
+
},
|
|
651
|
+
{
|
|
652
|
+
type: 'sortKey',
|
|
653
|
+
fieldName: 'versionId'
|
|
654
|
+
}
|
|
655
|
+
]
|
|
656
|
+
};
|
|
657
|
+
|
|
658
|
+
const childFeedChunkObj = {
|
|
659
|
+
objectType: 'feedChunk',
|
|
660
|
+
belongTo: {
|
|
661
|
+
serviceTag: 'vidFeedOperation',
|
|
662
|
+
objectType: 'operation'
|
|
663
|
+
}
|
|
664
|
+
};
|
|
665
|
+
|
|
666
|
+
const source = 'app/src/generatedCode/FlowSchemas/FeedChunkRecordUrl/source';
|
|
667
|
+
const handlerRoot = 'src/generatedCode/FlowSchemas/FeedChunkRecordUrl/source';
|
|
668
|
+
|
|
669
|
+
const result = await emitS3Flow(flow, source, handlerRoot, {
|
|
670
|
+
objects: [childFeedChunkObj, parentOperationObj]
|
|
671
|
+
});
|
|
672
|
+
|
|
673
|
+
const startMain = result.artifacts.find(
|
|
674
|
+
(a) => a.path === `${source}/FeedChunkRecordUrl_Main.js`
|
|
675
|
+
);
|
|
676
|
+
|
|
677
|
+
assert.ok(
|
|
678
|
+
startMain.content.includes("throw new Error('targetIdentifiers.operationId is required')"),
|
|
679
|
+
'Start_Main should validate partitionKey operationId from parent schema'
|
|
680
|
+
);
|
|
681
|
+
assert.ok(
|
|
682
|
+
startMain.content.includes("throw new Error('targetIdentifiers.versionId is required')"),
|
|
683
|
+
'Start_Main should validate sortKey versionId from parent schema'
|
|
684
|
+
);
|
|
685
|
+
assert.ok(
|
|
686
|
+
startMain.content.includes("['eq', '$x-amz-meta-operationId', targetOperationId]"),
|
|
687
|
+
'Start_Main should include condition for partitionKey'
|
|
688
|
+
);
|
|
689
|
+
assert.ok(
|
|
690
|
+
startMain.content.includes("['eq', '$x-amz-meta-versionId', targetVersionId]"),
|
|
691
|
+
'Start_Main should include condition for sortKey'
|
|
692
|
+
);
|
|
693
|
+
});
|
|
694
|
+
|
|
695
|
+
test('emitS3Flow with ADR-016 pipeline without step-level event enum resolves correctly', async () => {
|
|
696
|
+
const mockFlow = {
|
|
697
|
+
flowTag: 'FeedChunkRecordUrl',
|
|
698
|
+
initiateBy: 'user',
|
|
699
|
+
handleObj: 'one',
|
|
700
|
+
statusType: 'statusField',
|
|
701
|
+
event: ['s3'],
|
|
702
|
+
objType: {
|
|
703
|
+
objectType: 'feedChunk',
|
|
704
|
+
serviceTag: 'FeedChunk'
|
|
705
|
+
},
|
|
706
|
+
bucketName: 'video-record-temp',
|
|
707
|
+
flowSteps: [
|
|
708
|
+
{ stepName: 'Start' },
|
|
709
|
+
{ stepName: 'ProcessAfterUpload' },
|
|
710
|
+
{ stepName: 'ConfirmComplete', event: ['extTopic'] }
|
|
711
|
+
]
|
|
712
|
+
};
|
|
713
|
+
|
|
714
|
+
const source = 'app/src/generatedCode/FlowSchemas/FeedChunkRecordUrl/source';
|
|
715
|
+
const handlerRoot = 'src/generatedCode/FlowSchemas/FeedChunkRecordUrl/source';
|
|
716
|
+
|
|
717
|
+
const result = await emitS3Flow(mockFlow, source, handlerRoot);
|
|
718
|
+
|
|
719
|
+
assert.equal(result.functions.length, 3);
|
|
720
|
+
const functionNames = result.functions.map((fn) => fn.split(':')[0].trim());
|
|
721
|
+
assert.deepEqual(functionNames, [
|
|
722
|
+
'FeedChunkRecordUrlHdrWbs',
|
|
723
|
+
'FeedChunkRecordUrlProcessAfterUploadHdrS3',
|
|
724
|
+
'FeedChunkRecordUrlConfirmCompleteHdrSqs'
|
|
725
|
+
]);
|
|
726
|
+
});
|
|
727
|
+
|
|
728
|
+
|
|
729
|
+
|
|
730
|
+
|