@izara_project/izara-core-generate-service-code 1.0.88 → 1.0.89

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.
Files changed (26) hide show
  1. package/package.json +1 -1
  2. package/src/compile/flowValidator.js +25 -0
  3. package/src/compile/loadSchemas.js +154 -0
  4. package/src/emit/coreFunction/templates/InitialSetupConfig.ejs +3 -7
  5. package/src/emit/crud/crudActionPlan.js +67 -9
  6. package/src/emit/crud/emitDynamicCrudFlowSchemas.js +15 -32
  7. package/src/emit/crud/templates/CreateEndpoint_Main.ejs +33 -27
  8. package/src/emit/crud/templates/CreateLogicalLib.ejs +247 -89
  9. package/src/emit/crud/templates/CreateNodeCallback_Main.ejs +30 -20
  10. package/src/emit/crud/templates/DeleteAfterHookCallback_Main.ejs +118 -0
  11. package/src/emit/crud/templates/DeleteComplete_Main.ejs +124 -10
  12. package/src/emit/crud/templates/DeleteEndpoint_Main.ejs +26 -16
  13. package/src/emit/crud/templates/DeleteLogicalLib.ejs +112 -1
  14. package/src/emit/crud/templates/UpdateAfterHookCallback_Main.ejs +120 -0
  15. package/src/emit/crud/templates/UpdateComplete_Main.ejs +104 -10
  16. package/src/emit/crud/templates/UpdateEndpoint_Main.ejs +34 -22
  17. package/src/emit/crud/templates/UpdateLogicalLib.ejs +121 -1
  18. package/src/emit/rbac/emitDynamicRbacFlowSchemas.js +8 -2
  19. package/src/emit/rbac/templates/CancelUsage_Main.ejs +34 -14
  20. package/src/emit/rbac/templates/ConfirmUsage_Main.ejs +34 -14
  21. package/src/emit/resource/emitResourceArtifacts.js +9 -2
  22. package/test/crudApplicationArtifacts.test.js +345 -0
  23. package/test/dynamicCrudFlowSchemas.test.js +10 -5
  24. package/test/dynamicRbacFlowSchemas.test.js +24 -2
  25. package/test/graphDrivenFlowSchema.test.js +36 -0
  26. package/test/resourceArtifacts.test.js +71 -0
@@ -46,6 +46,26 @@ test('emits CRUD handlers and function YAML from schema data without reading gen
46
46
  createMain,
47
47
  /validateFieldNames\(_izContext, requestParams, objectSchemas, errorsFound\)/
48
48
  );
49
+ assert.doesNotMatch(
50
+ createMain,
51
+ /const \{ createDataDetailsLib \} = dataDetailsLib;/
52
+ );
53
+ const createLogicalLib = await readFile(
54
+ join(
55
+ rootPath,
56
+ 'app/src/generatedCode/FlowObjects/Create/source/CreateLogicalLib.js'
57
+ ),
58
+ 'utf8'
59
+ );
60
+ assert.match(
61
+ createLogicalLib,
62
+ /export async function resolveAndGenerateIdentifiers\(\s*_izContext,\s*requestParams,\s*objectSchema,\s*errorsFound\s*\)/
63
+ );
64
+ assert.match(createLogicalLib, /return \{\s*objectSchemas\s*\};/);
65
+ assert.match(
66
+ createLogicalLib,
67
+ /export function validateFieldNames\(\s*_izContext,\s*requestParams,\s*objectSchemas,\s*errorsFound\s*\)/
68
+ );
49
69
  assert.doesNotMatch(createMain, /FlowHook|beforeLogical|codeHookTag/);
50
70
  const yaml = await readFile(
51
71
  join(rootPath, 'app/sls_yaml/generatedCode/source/flow-objects.yml'),
@@ -277,6 +297,26 @@ test('emits Complete handlers and inlines sync RBAC for Update with Graph', asyn
277
297
  );
278
298
  });
279
299
 
300
+ test('omits CreateComplete_Main and CreateCompleteHdrSqs when hasCreateNodeCallback is true', async () => {
301
+ const rootPath = await createRbacProject(false, true);
302
+ const result = await generateCrudApplicationArtifacts(rootPath, {
303
+ serviceTag: 'orders'
304
+ });
305
+ const createPaths = result.files
306
+ .map((file) => file.path)
307
+ .filter((path) => path.includes('/FlowObjects/Create/source/'));
308
+
309
+ assert.ok(createPaths.includes(
310
+ 'app/src/generatedCode/FlowObjects/Create/source/CreateNodeCallback_Main.js'
311
+ ));
312
+ assert.ok(!createPaths.includes(
313
+ 'app/src/generatedCode/FlowObjects/Create/source/CreateComplete_Main.js'
314
+ ));
315
+ assert.ok(!createPaths.includes(
316
+ 'app/src/generatedCode/FlowObjects/Create/source/CreateComplete_HdrSqs.js'
317
+ ));
318
+ });
319
+
280
320
  test('rejects deprecated FlowHook schemas', async () => {
281
321
  const rootPath = await createRbacProject(true);
282
322
 
@@ -402,6 +442,22 @@ test('emits targetIdentifiers passed directly to checkRbacPermission options in
402
442
  serviceTag: 'orders'
403
443
  });
404
444
 
445
+ const createMain = await readFile(
446
+ join(
447
+ rootPath,
448
+ 'app/src/generatedCode/FlowObjects/Create/source/Create_Main.js'
449
+ ),
450
+ 'utf8'
451
+ );
452
+ assert.match(
453
+ createMain,
454
+ /targetIdentifiers:\s*requestParams\.rbacTargetIdentifiers\s*\|\|\s*requestParams\.targetIdentifiers/
455
+ );
456
+ assert.doesNotMatch(
457
+ createMain,
458
+ /const\s*\{[^}]*targetIdentifiers[^}]*\}\s*=\s*await\s*resolveSchemaAndIdentity/
459
+ );
460
+
405
461
  const updateMain = await readFile(
406
462
  join(
407
463
  rootPath,
@@ -948,6 +1004,295 @@ test('emits sync before-hook in Delete_Main with abort check', async () => {
948
1004
  );
949
1005
  });
950
1006
 
1007
+ test('emits hooks across multiple endpoints when generateCodeTag is an array', async () => {
1008
+ const rootPath = await createProject(true);
1009
+ const schemas = join(rootPath, 'app/src/schemas');
1010
+ await writeFile(
1011
+ join(schemas, 'FlowSchemas/CheckOrderMulti.js'),
1012
+ "export default { flowTag: 'checkOrderMulti', event: ['lambdaSyncInv'], " +
1013
+ "generatedCodeConfig: { generateCodeTag: ['create', 'update'], " +
1014
+ "codeHookTag: 'before', invocationType: 'lambdaSyncInv' }, " +
1015
+ "objType: { objectType: 'order' } };\n"
1016
+ );
1017
+ await generateCrudApplicationArtifacts(rootPath, {
1018
+ serviceTag: 'orders'
1019
+ });
1020
+
1021
+ const createMain = await readFile(
1022
+ join(
1023
+ rootPath,
1024
+ 'app/src/generatedCode/FlowObjects/Create/source/Create_Main.js'
1025
+ ),
1026
+ 'utf8'
1027
+ );
1028
+ const updateMain = await readFile(
1029
+ join(
1030
+ rootPath,
1031
+ 'app/src/generatedCode/FlowObjects/Update/source/Update_Main.js'
1032
+ ),
1033
+ 'utf8'
1034
+ );
1035
+ const deleteMain = await readFile(
1036
+ join(
1037
+ rootPath,
1038
+ 'app/src/generatedCode/FlowObjects/Delete/source/Delete_Main.js'
1039
+ ),
1040
+ 'utf8'
1041
+ );
1042
+
1043
+ assert.match(createMain, /'checkOrderMulti'/);
1044
+ assert.match(updateMain, /'checkOrderMulti'/);
1045
+ assert.doesNotMatch(deleteMain, /'checkOrderMulti'/);
1046
+ });
1047
+
1048
+ test('emits ownTopic before-hook callbacks across multiple endpoints when generateCodeTag is an array', async () => {
1049
+ const rootPath = await createProject(true);
1050
+ const schemas = join(rootPath, 'app/src/schemas');
1051
+ await writeFile(
1052
+ join(schemas, 'FlowSchemas/ValidateOrderMulti.js'),
1053
+ "export default { flowTag: 'validateOrderMulti', event: ['ownTopic'], " +
1054
+ "generatedCodeConfig: { generateCodeTag: ['update', 'delete'], " +
1055
+ "codeHookTag: 'before', invocationType: 'ownTopic' }, " +
1056
+ "objType: { objectType: 'order' } };\n"
1057
+ );
1058
+ const result = await generateCrudApplicationArtifacts(rootPath, {
1059
+ serviceTag: 'orders'
1060
+ });
1061
+
1062
+ const updateCallbackExists = result.files.some((f) =>
1063
+ f.path.includes('UpdateBeforeHookCallback_Main.js')
1064
+ );
1065
+ const deleteCallbackExists = result.files.some((f) =>
1066
+ f.path.includes('DeleteBeforeHookCallback_Main.js')
1067
+ );
1068
+ const createCallbackExists = result.files.some((f) =>
1069
+ f.path.includes('CreateBeforeHookCallback_Main.js')
1070
+ );
1071
+
1072
+ assert.strictEqual(updateCallbackExists, true);
1073
+ assert.strictEqual(deleteCallbackExists, true);
1074
+ assert.strictEqual(createCallbackExists, false);
1075
+ });
1076
+
1077
+ test('emits synchronous ConfirmUsage and CancelUsage invocation in CreateLogicalLib when object has rbac and graph', async () => {
1078
+ const rootPath = await createRbacProject(false, true);
1079
+ await generateCrudApplicationArtifacts(rootPath, { serviceTag: 'orders' });
1080
+
1081
+ const createLogicalLib = await readFile(
1082
+ join(
1083
+ rootPath,
1084
+ 'app/src/generatedCode/FlowObjects/Create/source/CreateLogicalLib.js'
1085
+ ),
1086
+ 'utf8'
1087
+ );
1088
+
1089
+ assert.match(createLogicalLib, /ConfirmUsageHdrInv/);
1090
+ assert.match(createLogicalLib, /CancelUsageHdrInv/);
1091
+ assert.match(createLogicalLib, /lambda\.invokeSync\(/);
1092
+ assert.match(createLogicalLib, /publishCompletionMessage\(/);
1093
+ });
1094
+
1095
+ test('emits synchronous AfterHook invocation in Update and Delete handlers', async () => {
1096
+ const rootPath = await createProject(true);
1097
+ const schemas = join(rootPath, 'app/src/schemas');
1098
+ await writeFile(
1099
+ join(schemas, 'FlowSchemas/AuditOrderMulti.js'),
1100
+ "export default { flowTag: 'auditOrderMulti', event: ['lambdaSyncInv'], " +
1101
+ "generatedCodeConfig: { generateCodeTag: ['update', 'delete'], " +
1102
+ "codeHookTag: 'after', invocationType: 'lambdaSyncInv' }, " +
1103
+ "objType: { objectType: 'order' } };\n"
1104
+ );
1105
+ await generateCrudApplicationArtifacts(rootPath, {
1106
+ serviceTag: 'orders'
1107
+ });
1108
+
1109
+ const updateComplete = await readFile(
1110
+ join(
1111
+ rootPath,
1112
+ 'app/src/generatedCode/FlowObjects/Update/source/UpdateComplete_Main.js'
1113
+ ),
1114
+ 'utf8'
1115
+ );
1116
+ const updateLogicalLib = await readFile(
1117
+ join(
1118
+ rootPath,
1119
+ 'app/src/generatedCode/FlowObjects/Update/source/UpdateLogicalLib.js'
1120
+ ),
1121
+ 'utf8'
1122
+ );
1123
+ const deleteComplete = await readFile(
1124
+ join(
1125
+ rootPath,
1126
+ 'app/src/generatedCode/FlowObjects/Delete/source/DeleteComplete_Main.js'
1127
+ ),
1128
+ 'utf8'
1129
+ );
1130
+ const deleteLogicalLib = await readFile(
1131
+ join(
1132
+ rootPath,
1133
+ 'app/src/generatedCode/FlowObjects/Delete/source/DeleteLogicalLib.js'
1134
+ ),
1135
+ 'utf8'
1136
+ );
1137
+
1138
+ assert.match(updateComplete, /'auditOrderMulti'/);
1139
+ assert.match(updateComplete, /lambda\.invokeSync\(/);
1140
+ assert.match(updateLogicalLib, /'auditOrderMulti'/);
1141
+ assert.match(updateLogicalLib, /lambda\.invokeSync\(/);
1142
+ assert.match(deleteComplete, /'auditOrderMulti'/);
1143
+ assert.match(deleteComplete, /lambda\.invokeSync\(/);
1144
+ assert.match(deleteLogicalLib, /'auditOrderMulti'/);
1145
+ assert.match(deleteLogicalLib, /lambda\.invokeSync\(/);
1146
+ });
1147
+
1148
+ test('emits ownTopic after-hook callbacks and dispatch for update and delete', async () => {
1149
+ const rootPath = await createProject(true);
1150
+ const schemas = join(rootPath, 'app/src/schemas');
1151
+ await writeFile(
1152
+ join(schemas, 'FlowSchemas/NotifyOrderMulti.js'),
1153
+ "export default { flowTag: 'notifyOrderMulti', event: ['ownTopic'], " +
1154
+ "generatedCodeConfig: { generateCodeTag: ['update', 'delete'], " +
1155
+ "codeHookTag: 'after', invocationType: 'ownTopic' }, " +
1156
+ "objType: { objectType: 'order' } };\n"
1157
+ );
1158
+ const result = await generateCrudApplicationArtifacts(rootPath, {
1159
+ serviceTag: 'orders'
1160
+ });
1161
+
1162
+ const updateCallbackExists = result.files.some((f) =>
1163
+ f.path.includes('UpdateAfterHookCallback_Main.js')
1164
+ );
1165
+ const deleteCallbackExists = result.files.some((f) =>
1166
+ f.path.includes('DeleteAfterHookCallback_Main.js')
1167
+ );
1168
+ assert.strictEqual(updateCallbackExists, true);
1169
+ assert.strictEqual(deleteCallbackExists, true);
1170
+
1171
+ const updateComplete = await readFile(
1172
+ join(
1173
+ rootPath,
1174
+ 'app/src/generatedCode/FlowObjects/Update/source/UpdateComplete_Main.js'
1175
+ ),
1176
+ 'utf8'
1177
+ );
1178
+ const updateLogicalLib = await readFile(
1179
+ join(
1180
+ rootPath,
1181
+ 'app/src/generatedCode/FlowObjects/Update/source/UpdateLogicalLib.js'
1182
+ ),
1183
+ 'utf8'
1184
+ );
1185
+ const deleteComplete = await readFile(
1186
+ join(
1187
+ rootPath,
1188
+ 'app/src/generatedCode/FlowObjects/Delete/source/DeleteComplete_Main.js'
1189
+ ),
1190
+ 'utf8'
1191
+ );
1192
+ const deleteLogicalLib = await readFile(
1193
+ join(
1194
+ rootPath,
1195
+ 'app/src/generatedCode/FlowObjects/Delete/source/DeleteLogicalLib.js'
1196
+ ),
1197
+ 'utf8'
1198
+ );
1199
+
1200
+ assert.match(updateComplete, /'notifyOrderMulti_In'/);
1201
+ assert.match(updateComplete, /UpdateAfterHookCallbackHdrSqs/);
1202
+ assert.match(updateLogicalLib, /'notifyOrderMulti_In'/);
1203
+ assert.match(updateLogicalLib, /UpdateAfterHookCallbackHdrSqs/);
1204
+ assert.match(deleteComplete, /'notifyOrderMulti_In'/);
1205
+ assert.match(deleteComplete, /DeleteAfterHookCallbackHdrSqs/);
1206
+ assert.match(deleteLogicalLib, /'notifyOrderMulti_In'/);
1207
+ assert.match(deleteLogicalLib, /DeleteAfterHookCallbackHdrSqs/);
1208
+ });
1209
+
1210
+ test('supports objType in generatedCodeConfig with string, array, and global hook', async () => {
1211
+ const rootPath = await createProject(true);
1212
+ const schemaRoot = join(rootPath, 'app/src/schemas');
1213
+ await writeFile(
1214
+ join(schemaRoot, 'ObjectSchemas/Cart.js'),
1215
+ "export default { objectType: 'cart', generatedBy: 'user' };\n"
1216
+ );
1217
+ await writeFile(
1218
+ join(schemaRoot, 'FlowSchemas/HookWithCustomObjType.js'),
1219
+ `export default {
1220
+ flowTag: 'hookWithCustomObj',
1221
+ objType: { objectType: 'rootAudit' },
1222
+ event: ['lambdaSyncInv'],
1223
+ generatedCodeConfig: {
1224
+ generateCodeTag: 'create',
1225
+ codeHookTag: 'before',
1226
+ invocationType: 'lambdaSyncInv',
1227
+ objType: 'cart'
1228
+ }
1229
+ };\n`
1230
+ );
1231
+ await writeFile(
1232
+ join(schemaRoot, 'FlowSchemas/HookWithMultiObjType.js'),
1233
+ `export default {
1234
+ flowTag: 'hookWithMultiObj',
1235
+ objType: { objectType: 'rootAudit' },
1236
+ event: ['ownTopic'],
1237
+ outputTopic: true,
1238
+ generatedCodeConfig: {
1239
+ generateCodeTag: 'update',
1240
+ codeHookTag: 'after',
1241
+ invocationType: 'ownTopic',
1242
+ objType: ['order', 'cart']
1243
+ }
1244
+ };\n`
1245
+ );
1246
+ await writeFile(
1247
+ join(schemaRoot, 'FlowSchemas/GlobalHook.js'),
1248
+ `export default {
1249
+ flowTag: 'globalDeleteHook',
1250
+ event: ['lambdaSyncInv'],
1251
+ generatedCodeConfig: {
1252
+ generateCodeTag: 'delete',
1253
+ codeHookTag: 'before',
1254
+ invocationType: 'lambdaSyncInv'
1255
+ }
1256
+ };\n`
1257
+ );
1258
+
1259
+ await generateCrudApplicationArtifacts(rootPath, {
1260
+ serviceTag: 'orders'
1261
+ });
1262
+
1263
+ // 1. Before hook with custom objType 'cart' should be attached to cart, not rootAudit
1264
+ const createMain = await readFile(
1265
+ join(
1266
+ rootPath,
1267
+ 'app/src/generatedCode/FlowObjects/Create/source/Create_Main.js'
1268
+ ),
1269
+ 'utf8'
1270
+ );
1271
+ assert.match(createMain, /if \(requestParams\.objectType === 'cart'\)/);
1272
+ assert.match(createMain, /hookPayload_hookWithCustomObj/);
1273
+
1274
+ // 2. After hook with multi objType ['order', 'cart'] should check array
1275
+ const updateComplete = await readFile(
1276
+ join(
1277
+ rootPath,
1278
+ 'app/src/generatedCode/FlowObjects/Update/source/UpdateComplete_Main.js'
1279
+ ),
1280
+ 'utf8'
1281
+ );
1282
+ assert.match(updateComplete, /\["order","cart"\]\.includes/);
1283
+ assert.match(updateComplete, /hookPayload_hookWithMultiObj/);
1284
+
1285
+ // 3. Global delete hook without objType should be present
1286
+ const deleteMain = await readFile(
1287
+ join(
1288
+ rootPath,
1289
+ 'app/src/generatedCode/FlowObjects/Delete/source/Delete_Main.js'
1290
+ ),
1291
+ 'utf8'
1292
+ );
1293
+ assert.match(deleteMain, /hookPayload_globalDeleteHook/);
1294
+ });
1295
+
951
1296
  async function createProject(withCrudObject) {
952
1297
  const rootPath = await mkdtemp(
953
1298
  join(tmpdir(), 'generate-service-code-v2-crud-application-')
@@ -142,7 +142,7 @@ test('generates a direct Complete step for RBAC-only Update flows', async () =>
142
142
  assert.equal(update.flowSteps[1].properties.length, 0);
143
143
  });
144
144
 
145
- test('generates CreateNodeCallback and Complete for Create when RBAC and Graph are present', async () => {
145
+ test('generates CreateNodeCallback for Create when RBAC and Graph are present', async () => {
146
146
  const rootPath = await createProject({
147
147
  generatedBy: 'user',
148
148
  });
@@ -160,8 +160,14 @@ test('generates CreateNodeCallback and Complete for Create when RBAC and Graph a
160
160
  const create = await readGeneratedFlow(rootPath, 'Create');
161
161
  assert.deepEqual(create.flowSteps.map((step) => step.stepName), [
162
162
  'Start',
163
- 'CreateNodeCallback',
164
- 'Complete'
163
+ 'CreateNodeCallback'
164
+ ]);
165
+ assert.deepEqual(create.flowSteps[1].nextSteps, [
166
+ {
167
+ targetType: 'lambdaSyncInv',
168
+ serviceTag: 'Rbac',
169
+ functionName: 'ConfirmUsageHdrInv'
170
+ }
165
171
  ]);
166
172
  });
167
173
 
@@ -185,8 +191,7 @@ test('uses CreateNodeCallback step when Graph storage is present with sync RBAC'
185
191
  const create = await readGeneratedFlow(rootPath, 'Create');
186
192
  assert.deepEqual(create.flowSteps.map((step) => step.stepName), [
187
193
  'Start',
188
- 'CreateNodeCallback',
189
- 'Complete'
194
+ 'CreateNodeCallback'
190
195
  ]);
191
196
  assert.deepEqual(create.flowSteps[1].properties, [
192
197
  'returnValue',
@@ -69,6 +69,27 @@ test('generates shared and object-specific RBAC flow schemas', async () => {
69
69
  });
70
70
  assert.deepEqual(user.event, ['ownTopic']);
71
71
  assert.equal(user.steps, undefined);
72
+
73
+ const confirmUsage = await readGeneratedFlow(
74
+ rootPath,
75
+ 'ConfirmUsage',
76
+ 'FlowLimit'
77
+ );
78
+ const cancelUsage = await readGeneratedFlow(
79
+ rootPath,
80
+ 'CancelUsage',
81
+ 'FlowLimit'
82
+ );
83
+ assert.deepEqual(confirmUsage.event, [
84
+ 'ownTopic',
85
+ 'lambdaSyncInv',
86
+ 'lambdaSyncApi'
87
+ ]);
88
+ assert.deepEqual(cancelUsage.event, [
89
+ 'ownTopic',
90
+ 'lambdaSyncInv',
91
+ 'lambdaSyncApi'
92
+ ]);
72
93
  });
73
94
 
74
95
  test('removes RBAC schemas when the last RBAC object is removed', async () => {
@@ -144,10 +165,11 @@ async function createProject({ hasRbac = false } = {}) {
144
165
  return rootPath;
145
166
  }
146
167
 
147
- async function readGeneratedFlow(rootPath, flowTag) {
168
+ async function readGeneratedFlow(rootPath, flowTag, folder = 'FlowRbac') {
148
169
  const filePath = join(
149
170
  rootPath,
150
- 'app/src/schemas/FlowSchemas/generateCode/FlowRbac',
171
+ 'app/src/schemas/FlowSchemas/generateCode',
172
+ folder,
151
173
  `${flowTag}.js`
152
174
  );
153
175
  const module = await import(
@@ -255,3 +255,39 @@ test('validateFlowSchema rejects lambdaSyncInv nextSteps missing functionName',
255
255
  /lambdaSyncInv target requires non-empty 'functionName'/
256
256
  );
257
257
  });
258
+
259
+ test('validateFlowSchema passes for mixed sync and async events with single Start step', () => {
260
+ const mixedSingleStepSchema = {
261
+ flowTag: 'ConfirmUsage',
262
+ event: ['ownTopic', 'lambdaSyncInv', 'lambdaSyncApi'],
263
+ flowSteps: [
264
+ {
265
+ stepName: 'Start'
266
+ }
267
+ ]
268
+ };
269
+
270
+ assert.equal(validateFlowSchema(mixedSingleStepSchema), true);
271
+ });
272
+
273
+ test('validateFlowSchema rejects flow with synchronous events having multiple flowSteps', () => {
274
+ const mixedMultiStepSchema = {
275
+ flowTag: 'ConfirmUsage',
276
+ event: ['ownTopic', 'lambdaSyncInv'],
277
+ flowSteps: [
278
+ {
279
+ stepName: 'Start',
280
+ nextSteps: [{ targetType: 'ownTopic', stepName: 'StepTwo' }]
281
+ },
282
+ {
283
+ stepName: 'StepTwo',
284
+ previousSteps: [{ sourceType: 'ownTopic', stepName: 'Start' }]
285
+ }
286
+ ]
287
+ };
288
+
289
+ assert.throws(
290
+ () => validateFlowSchema(mixedMultiStepSchema),
291
+ /Flow 'ConfirmUsage' with synchronous events .* must only define a single entry step 'Start'/
292
+ );
293
+ });
@@ -204,6 +204,38 @@ test('rejects deprecated FlowHook schemas', async () => {
204
204
  );
205
205
  });
206
206
 
207
+ test('rejects invalid generateCodeTag configuration', async () => {
208
+ const rootPath = await createProject();
209
+ const flowDirectory = join(rootPath, 'app/src/schemas/FlowSchemas');
210
+ await writeFile(
211
+ join(flowDirectory, 'InvalidTag.js'),
212
+ "export default { flowTag: 'invalidHook', event: ['ownTopic'], " +
213
+ "generatedCodeConfig: { generateCodeTag: ['invalidAction'], " +
214
+ "codeHookTag: 'before' } };\n"
215
+ );
216
+
217
+ await assert.rejects(
218
+ generateResourceArtifacts(rootPath),
219
+ /generatedCodeConfig\.generateCodeTag contains invalid tag 'invalidAction'/
220
+ );
221
+ });
222
+
223
+ test('rejects empty generateCodeTag array configuration', async () => {
224
+ const rootPath = await createProject();
225
+ const flowDirectory = join(rootPath, 'app/src/schemas/FlowSchemas');
226
+ await writeFile(
227
+ join(flowDirectory, 'EmptyTag.js'),
228
+ "export default { flowTag: 'emptyHook', event: ['ownTopic'], " +
229
+ "generatedCodeConfig: { generateCodeTag: [], " +
230
+ "codeHookTag: 'before' } };\n"
231
+ );
232
+
233
+ await assert.rejects(
234
+ generateResourceArtifacts(rootPath),
235
+ /generatedCodeConfig\.generateCodeTag array must not be empty/
236
+ );
237
+ });
238
+
207
239
  test('emits the shared WebSocket completion queue and output-topic subscriptions', async () => {
208
240
  const rootPath = await createProject();
209
241
  await generateResourceArtifacts(rootPath);
@@ -382,6 +414,8 @@ test('emits CreateNodeCallbackHdrSqs in FlowObjectsRole when object has graph an
382
414
  assert.match(role, /RolePermissions/);
383
415
  assert.match(role, /ValidatorRbac_In/);
384
416
  assert.match(role, /Rbac_\$\{self:custom\.iz_stage\}_ValidatorRbac_In/);
417
+ assert.match(role, /ConfirmUsageHdrInv/);
418
+ assert.match(role, /CancelUsageHdrInv/);
385
419
  });
386
420
 
387
421
  test('emits CreateAfterHookCallbackHdrSqs in SQS resources and FlowObjectsRole when flow has after hook with ownTopic', async () => {
@@ -559,6 +593,43 @@ test('emits lambda:InvokeFunction in FlowObjectsRole when flow has after hook wi
559
593
  assert.match(role, /AuditAfterCreate/);
560
594
  });
561
595
 
596
+ test('emits SQS queues and deduplicated IAM policies when generateCodeTag is an array for multiple actions', async () => {
597
+ const rootPath = await createProject();
598
+ const schemas = join(rootPath, 'app/src/schemas');
599
+ await writeFile(
600
+ join(schemas, 'FlowSchemas/NotifyMultiAfter.js'),
601
+ "export default { flowTag: 'notifyMultiAfter', event: ['ownTopic'], " +
602
+ "generatedCodeConfig: { generateCodeTag: ['create', 'update'], " +
603
+ "codeHookTag: 'after', invocationType: 'ownTopic', " +
604
+ "objType: { objectType: 'order' } } };\n"
605
+ );
606
+ await generateResourceArtifacts(rootPath);
607
+
608
+ const crudResources = await readFile(
609
+ join(
610
+ rootPath,
611
+ 'resource/sls_yaml/generatedCode/source/generated-sns-sqs-crud.yml'
612
+ ),
613
+ 'utf8'
614
+ );
615
+ assert.match(crudResources, /^ CreateAfterHookCallbackHdrSqs:/m);
616
+ assert.match(crudResources, /^ UpdateAfterHookCallbackHdrSqs:/m);
617
+
618
+ const iam = await readFile(
619
+ join(
620
+ rootPath,
621
+ 'app/sls_yaml/generatedCode/source/generate-shared-resource.yml'
622
+ ),
623
+ 'utf8'
624
+ );
625
+ const role = roleBlock(iam, 'FlowObjectsRole');
626
+ assert.match(role, /CreateAfterHookCallbackHdrSqs/);
627
+ assert.match(role, /UpdateAfterHookCallbackHdrSqs/);
628
+
629
+ const notifyTopicMatches = role.match(/NotifyMultiAfter_In/g);
630
+ assert.strictEqual(notifyTopicMatches?.length, 1);
631
+ });
632
+
562
633
  function roleBlock(iam, roleName) {
563
634
  const match = iam.match(
564
635
  new RegExp(