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

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 (29) 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/CreateComplete_Main.ejs +135 -10
  8. package/src/emit/crud/templates/CreateEndpoint_Main.ejs +33 -27
  9. package/src/emit/crud/templates/CreateLogicalLib.ejs +282 -95
  10. package/src/emit/crud/templates/CreateNodeCallback_Main.ejs +38 -21
  11. package/src/emit/crud/templates/DeleteAfterHookCallback_Main.ejs +118 -0
  12. package/src/emit/crud/templates/DeleteComplete_Main.ejs +124 -10
  13. package/src/emit/crud/templates/DeleteEndpoint_Main.ejs +26 -16
  14. package/src/emit/crud/templates/DeleteLogicalLib.ejs +113 -4
  15. package/src/emit/crud/templates/UpdateAfterHookCallback_Main.ejs +120 -0
  16. package/src/emit/crud/templates/UpdateComplete_Main.ejs +104 -10
  17. package/src/emit/crud/templates/UpdateEndpoint_Main.ejs +34 -22
  18. package/src/emit/crud/templates/UpdateLogicalLib.ejs +121 -4
  19. package/src/emit/rbac/emitDynamicRbacFlowSchemas.js +8 -2
  20. package/src/emit/rbac/templates/CancelUsage_Main.ejs +34 -14
  21. package/src/emit/rbac/templates/ConfirmUsage_Main.ejs +34 -14
  22. package/src/emit/rbac/templates/UserRbacFlow_Main.ejs +20 -13
  23. package/src/emit/resource/emitResourceArtifacts.js +55 -14
  24. package/test/createTargetRoleHandler.test.js +11 -8
  25. package/test/crudApplicationArtifacts.test.js +440 -0
  26. package/test/dynamicCrudFlowSchemas.test.js +10 -5
  27. package/test/dynamicRbacFlowSchemas.test.js +24 -2
  28. package/test/graphDrivenFlowSchema.test.js +36 -0
  29. package/test/resourceArtifacts.test.js +113 -0
@@ -46,6 +46,30 @@ 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
+ );
69
+ assert.doesNotMatch(
70
+ createLogicalLib,
71
+ /targetIdentifiers\.submittedByUserId\s*=/
72
+ );
49
73
  assert.doesNotMatch(createMain, /FlowHook|beforeLogical|codeHookTag/);
50
74
  const yaml = await readFile(
51
75
  join(rootPath, 'app/sls_yaml/generatedCode/source/flow-objects.yml'),
@@ -277,6 +301,26 @@ test('emits Complete handlers and inlines sync RBAC for Update with Graph', asyn
277
301
  );
278
302
  });
279
303
 
304
+ test('omits CreateComplete_Main and CreateCompleteHdrSqs when hasCreateNodeCallback is true', async () => {
305
+ const rootPath = await createRbacProject(false, true);
306
+ const result = await generateCrudApplicationArtifacts(rootPath, {
307
+ serviceTag: 'orders'
308
+ });
309
+ const createPaths = result.files
310
+ .map((file) => file.path)
311
+ .filter((path) => path.includes('/FlowObjects/Create/source/'));
312
+
313
+ assert.ok(createPaths.includes(
314
+ 'app/src/generatedCode/FlowObjects/Create/source/CreateNodeCallback_Main.js'
315
+ ));
316
+ assert.ok(!createPaths.includes(
317
+ 'app/src/generatedCode/FlowObjects/Create/source/CreateComplete_Main.js'
318
+ ));
319
+ assert.ok(!createPaths.includes(
320
+ 'app/src/generatedCode/FlowObjects/Create/source/CreateComplete_HdrSqs.js'
321
+ ));
322
+ });
323
+
280
324
  test('rejects deprecated FlowHook schemas', async () => {
281
325
  const rootPath = await createRbacProject(true);
282
326
 
@@ -402,6 +446,22 @@ test('emits targetIdentifiers passed directly to checkRbacPermission options in
402
446
  serviceTag: 'orders'
403
447
  });
404
448
 
449
+ const createMain = await readFile(
450
+ join(
451
+ rootPath,
452
+ 'app/src/generatedCode/FlowObjects/Create/source/Create_Main.js'
453
+ ),
454
+ 'utf8'
455
+ );
456
+ assert.match(
457
+ createMain,
458
+ /targetIdentifiers:\s*requestParams\.rbacTargetIdentifiers\s*\|\|\s*requestParams\.targetIdentifiers/
459
+ );
460
+ assert.doesNotMatch(
461
+ createMain,
462
+ /const\s*\{[^}]*targetIdentifiers[^}]*\}\s*=\s*await\s*resolveSchemaAndIdentity/
463
+ );
464
+
405
465
  const updateMain = await readFile(
406
466
  join(
407
467
  rootPath,
@@ -581,6 +641,31 @@ test('emits async after-hook callback and SNS publish in CreateLogicalLib and fl
581
641
  { stdio: 'inherit' }
582
642
  );
583
643
 
644
+ const createCompleteMain = await readFile(
645
+ join(
646
+ rootPath,
647
+ 'app/src/generatedCode/FlowObjects/Create/source/CreateComplete_Main.js'
648
+ ),
649
+ 'utf8'
650
+ );
651
+ assert.match(
652
+ createCompleteMain,
653
+ /snsTopicArnByFlowSchema\(\s*_izContext,\s*'notifyAfterCreate_In'\s*\)/
654
+ );
655
+ assert.match(createCompleteMain, /CreateAfterHookCallbackHdrSqs/);
656
+
657
+ execFileSync(
658
+ process.execPath,
659
+ [
660
+ '--check',
661
+ join(
662
+ rootPath,
663
+ 'app/src/generatedCode/FlowObjects/Create/source/CreateComplete_Main.js'
664
+ )
665
+ ],
666
+ { stdio: 'inherit' }
667
+ );
668
+
584
669
  const yaml = await readFile(
585
670
  join(rootPath, 'app/sls_yaml/generatedCode/source/flow-objects.yml'),
586
671
  'utf8'
@@ -948,6 +1033,361 @@ test('emits sync before-hook in Delete_Main with abort check', async () => {
948
1033
  );
949
1034
  });
950
1035
 
1036
+ test('emits hooks across multiple endpoints when generateCodeTag is an array', async () => {
1037
+ const rootPath = await createProject(true);
1038
+ const schemas = join(rootPath, 'app/src/schemas');
1039
+ await writeFile(
1040
+ join(schemas, 'FlowSchemas/CheckOrderMulti.js'),
1041
+ "export default { flowTag: 'checkOrderMulti', event: ['lambdaSyncInv'], " +
1042
+ "generatedCodeConfig: { generateCodeTag: ['create', 'update'], " +
1043
+ "codeHookTag: 'before', invocationType: 'lambdaSyncInv' }, " +
1044
+ "objType: { objectType: 'order' } };\n"
1045
+ );
1046
+ await generateCrudApplicationArtifacts(rootPath, {
1047
+ serviceTag: 'orders'
1048
+ });
1049
+
1050
+ const createMain = await readFile(
1051
+ join(
1052
+ rootPath,
1053
+ 'app/src/generatedCode/FlowObjects/Create/source/Create_Main.js'
1054
+ ),
1055
+ 'utf8'
1056
+ );
1057
+ const updateMain = await readFile(
1058
+ join(
1059
+ rootPath,
1060
+ 'app/src/generatedCode/FlowObjects/Update/source/Update_Main.js'
1061
+ ),
1062
+ 'utf8'
1063
+ );
1064
+ const deleteMain = await readFile(
1065
+ join(
1066
+ rootPath,
1067
+ 'app/src/generatedCode/FlowObjects/Delete/source/Delete_Main.js'
1068
+ ),
1069
+ 'utf8'
1070
+ );
1071
+
1072
+ assert.match(createMain, /'checkOrderMulti'/);
1073
+ assert.match(updateMain, /'checkOrderMulti'/);
1074
+ assert.doesNotMatch(deleteMain, /'checkOrderMulti'/);
1075
+ });
1076
+
1077
+ test('emits ownTopic before-hook callbacks across multiple endpoints when generateCodeTag is an array', async () => {
1078
+ const rootPath = await createProject(true);
1079
+ const schemas = join(rootPath, 'app/src/schemas');
1080
+ await writeFile(
1081
+ join(schemas, 'FlowSchemas/ValidateOrderMulti.js'),
1082
+ "export default { flowTag: 'validateOrderMulti', event: ['ownTopic'], " +
1083
+ "generatedCodeConfig: { generateCodeTag: ['update', 'delete'], " +
1084
+ "codeHookTag: 'before', invocationType: 'ownTopic' }, " +
1085
+ "objType: { objectType: 'order' } };\n"
1086
+ );
1087
+ const result = await generateCrudApplicationArtifacts(rootPath, {
1088
+ serviceTag: 'orders'
1089
+ });
1090
+
1091
+ const updateCallbackExists = result.files.some((f) =>
1092
+ f.path.includes('UpdateBeforeHookCallback_Main.js')
1093
+ );
1094
+ const deleteCallbackExists = result.files.some((f) =>
1095
+ f.path.includes('DeleteBeforeHookCallback_Main.js')
1096
+ );
1097
+ const createCallbackExists = result.files.some((f) =>
1098
+ f.path.includes('CreateBeforeHookCallback_Main.js')
1099
+ );
1100
+
1101
+ assert.strictEqual(updateCallbackExists, true);
1102
+ assert.strictEqual(deleteCallbackExists, true);
1103
+ assert.strictEqual(createCallbackExists, false);
1104
+ });
1105
+
1106
+ test('emits synchronous ConfirmUsage and CancelUsage invocation in CreateLogicalLib when object has rbac and graph', async () => {
1107
+ const rootPath = await createRbacProject(false, true);
1108
+ await generateCrudApplicationArtifacts(rootPath, { serviceTag: 'orders' });
1109
+
1110
+ const createLogicalLib = await readFile(
1111
+ join(
1112
+ rootPath,
1113
+ 'app/src/generatedCode/FlowObjects/Create/source/CreateLogicalLib.js'
1114
+ ),
1115
+ 'utf8'
1116
+ );
1117
+
1118
+ assert.match(createLogicalLib, /ConfirmUsageHdrInv/);
1119
+ assert.match(createLogicalLib, /CancelUsageHdrInv/);
1120
+ assert.match(createLogicalLib, /lambda\.invokeSync\(/);
1121
+ assert.match(createLogicalLib, /publishCompletionMessage\(/);
1122
+ });
1123
+
1124
+ test('emits synchronous AfterHook invocation in Update and Delete handlers', async () => {
1125
+ const rootPath = await createProject(true);
1126
+ const schemas = join(rootPath, 'app/src/schemas');
1127
+ await writeFile(
1128
+ join(schemas, 'FlowSchemas/AuditOrderMulti.js'),
1129
+ "export default { flowTag: 'auditOrderMulti', event: ['lambdaSyncInv'], " +
1130
+ "generatedCodeConfig: { generateCodeTag: ['update', 'delete'], " +
1131
+ "codeHookTag: 'after', invocationType: 'lambdaSyncInv' }, " +
1132
+ "objType: { objectType: 'order' } };\n"
1133
+ );
1134
+ await generateCrudApplicationArtifacts(rootPath, {
1135
+ serviceTag: 'orders'
1136
+ });
1137
+
1138
+ const updateComplete = await readFile(
1139
+ join(
1140
+ rootPath,
1141
+ 'app/src/generatedCode/FlowObjects/Update/source/UpdateComplete_Main.js'
1142
+ ),
1143
+ 'utf8'
1144
+ );
1145
+ const updateLogicalLib = await readFile(
1146
+ join(
1147
+ rootPath,
1148
+ 'app/src/generatedCode/FlowObjects/Update/source/UpdateLogicalLib.js'
1149
+ ),
1150
+ 'utf8'
1151
+ );
1152
+ const deleteComplete = await readFile(
1153
+ join(
1154
+ rootPath,
1155
+ 'app/src/generatedCode/FlowObjects/Delete/source/DeleteComplete_Main.js'
1156
+ ),
1157
+ 'utf8'
1158
+ );
1159
+ const deleteLogicalLib = await readFile(
1160
+ join(
1161
+ rootPath,
1162
+ 'app/src/generatedCode/FlowObjects/Delete/source/DeleteLogicalLib.js'
1163
+ ),
1164
+ 'utf8'
1165
+ );
1166
+
1167
+ assert.match(updateComplete, /'auditOrderMulti'/);
1168
+ assert.match(updateComplete, /lambda\.invokeSync\(/);
1169
+ assert.match(updateLogicalLib, /'auditOrderMulti'/);
1170
+ assert.match(updateLogicalLib, /lambda\.invokeSync\(/);
1171
+ assert.match(deleteComplete, /'auditOrderMulti'/);
1172
+ assert.match(deleteComplete, /lambda\.invokeSync\(/);
1173
+ assert.match(deleteLogicalLib, /'auditOrderMulti'/);
1174
+ assert.match(deleteLogicalLib, /lambda\.invokeSync\(/);
1175
+ });
1176
+
1177
+ test('emits ownTopic after-hook callbacks and dispatch for update and delete', async () => {
1178
+ const rootPath = await createProject(true);
1179
+ const schemas = join(rootPath, 'app/src/schemas');
1180
+ await writeFile(
1181
+ join(schemas, 'FlowSchemas/NotifyOrderMulti.js'),
1182
+ "export default { flowTag: 'notifyOrderMulti', event: ['ownTopic'], " +
1183
+ "generatedCodeConfig: { generateCodeTag: ['update', 'delete'], " +
1184
+ "codeHookTag: 'after', invocationType: 'ownTopic' }, " +
1185
+ "objType: { objectType: 'order' } };\n"
1186
+ );
1187
+ const result = await generateCrudApplicationArtifacts(rootPath, {
1188
+ serviceTag: 'orders'
1189
+ });
1190
+
1191
+ const updateCallbackExists = result.files.some((f) =>
1192
+ f.path.includes('UpdateAfterHookCallback_Main.js')
1193
+ );
1194
+ const deleteCallbackExists = result.files.some((f) =>
1195
+ f.path.includes('DeleteAfterHookCallback_Main.js')
1196
+ );
1197
+ assert.strictEqual(updateCallbackExists, true);
1198
+ assert.strictEqual(deleteCallbackExists, true);
1199
+
1200
+ const updateComplete = await readFile(
1201
+ join(
1202
+ rootPath,
1203
+ 'app/src/generatedCode/FlowObjects/Update/source/UpdateComplete_Main.js'
1204
+ ),
1205
+ 'utf8'
1206
+ );
1207
+ const updateLogicalLib = await readFile(
1208
+ join(
1209
+ rootPath,
1210
+ 'app/src/generatedCode/FlowObjects/Update/source/UpdateLogicalLib.js'
1211
+ ),
1212
+ 'utf8'
1213
+ );
1214
+ const deleteComplete = await readFile(
1215
+ join(
1216
+ rootPath,
1217
+ 'app/src/generatedCode/FlowObjects/Delete/source/DeleteComplete_Main.js'
1218
+ ),
1219
+ 'utf8'
1220
+ );
1221
+ const deleteLogicalLib = await readFile(
1222
+ join(
1223
+ rootPath,
1224
+ 'app/src/generatedCode/FlowObjects/Delete/source/DeleteLogicalLib.js'
1225
+ ),
1226
+ 'utf8'
1227
+ );
1228
+
1229
+ assert.match(updateComplete, /'notifyOrderMulti_In'/);
1230
+ assert.match(updateComplete, /UpdateAfterHookCallbackHdrSqs/);
1231
+ assert.match(updateLogicalLib, /'notifyOrderMulti_In'/);
1232
+ assert.match(updateLogicalLib, /UpdateAfterHookCallbackHdrSqs/);
1233
+ assert.match(deleteComplete, /'notifyOrderMulti_In'/);
1234
+ assert.match(deleteComplete, /DeleteAfterHookCallbackHdrSqs/);
1235
+ assert.match(deleteLogicalLib, /'notifyOrderMulti_In'/);
1236
+ assert.match(deleteLogicalLib, /DeleteAfterHookCallbackHdrSqs/);
1237
+ });
1238
+
1239
+ test('supports objType in generatedCodeConfig with string, array, and global hook', async () => {
1240
+ const rootPath = await createProject(true);
1241
+ const schemaRoot = join(rootPath, 'app/src/schemas');
1242
+ await writeFile(
1243
+ join(schemaRoot, 'ObjectSchemas/Cart.js'),
1244
+ "export default { objectType: 'cart', generatedBy: 'user' };\n"
1245
+ );
1246
+ await writeFile(
1247
+ join(schemaRoot, 'FlowSchemas/HookWithCustomObjType.js'),
1248
+ `export default {
1249
+ flowTag: 'hookWithCustomObj',
1250
+ objType: { objectType: 'rootAudit' },
1251
+ event: ['lambdaSyncInv'],
1252
+ generatedCodeConfig: {
1253
+ generateCodeTag: 'create',
1254
+ codeHookTag: 'before',
1255
+ invocationType: 'lambdaSyncInv',
1256
+ objType: 'cart'
1257
+ }
1258
+ };\n`
1259
+ );
1260
+ await writeFile(
1261
+ join(schemaRoot, 'FlowSchemas/HookWithMultiObjType.js'),
1262
+ `export default {
1263
+ flowTag: 'hookWithMultiObj',
1264
+ objType: { objectType: 'rootAudit' },
1265
+ event: ['ownTopic'],
1266
+ outputTopic: true,
1267
+ generatedCodeConfig: {
1268
+ generateCodeTag: 'update',
1269
+ codeHookTag: 'after',
1270
+ invocationType: 'ownTopic',
1271
+ objType: ['order', 'cart']
1272
+ }
1273
+ };\n`
1274
+ );
1275
+ await writeFile(
1276
+ join(schemaRoot, 'FlowSchemas/GlobalHook.js'),
1277
+ `export default {
1278
+ flowTag: 'globalDeleteHook',
1279
+ event: ['lambdaSyncInv'],
1280
+ generatedCodeConfig: {
1281
+ generateCodeTag: 'delete',
1282
+ codeHookTag: 'before',
1283
+ invocationType: 'lambdaSyncInv'
1284
+ }
1285
+ };\n`
1286
+ );
1287
+
1288
+ await generateCrudApplicationArtifacts(rootPath, {
1289
+ serviceTag: 'orders'
1290
+ });
1291
+
1292
+ // 1. Before hook with custom objType 'cart' should be attached to cart, not rootAudit
1293
+ const createMain = await readFile(
1294
+ join(
1295
+ rootPath,
1296
+ 'app/src/generatedCode/FlowObjects/Create/source/Create_Main.js'
1297
+ ),
1298
+ 'utf8'
1299
+ );
1300
+ assert.match(createMain, /if \(requestParams\.objectType === 'cart'\)/);
1301
+ assert.match(createMain, /hookPayload_hookWithCustomObj/);
1302
+
1303
+ // 2. After hook with multi objType ['order', 'cart'] should check array
1304
+ const updateComplete = await readFile(
1305
+ join(
1306
+ rootPath,
1307
+ 'app/src/generatedCode/FlowObjects/Update/source/UpdateComplete_Main.js'
1308
+ ),
1309
+ 'utf8'
1310
+ );
1311
+ assert.match(updateComplete, /\["order","cart"\]\.includes/);
1312
+ assert.match(updateComplete, /hookPayload_hookWithMultiObj/);
1313
+
1314
+ // 3. Global delete hook without objType should be present
1315
+ const deleteMain = await readFile(
1316
+ join(
1317
+ rootPath,
1318
+ 'app/src/generatedCode/FlowObjects/Delete/source/Delete_Main.js'
1319
+ ),
1320
+ 'utf8'
1321
+ );
1322
+ assert.match(deleteMain, /hookPayload_globalDeleteHook/);
1323
+ });
1324
+
1325
+ test('omits autoProvisionOwnerTargetRole in CreateNodeCallback when service only has child RBAC objects', async () => {
1326
+ const rootPath = await createRbacProject(false, true);
1327
+ await generateCrudApplicationArtifacts(rootPath, {
1328
+ serviceTag: 'orders'
1329
+ });
1330
+
1331
+ const callbackMain = await readFile(
1332
+ join(
1333
+ rootPath,
1334
+ 'app/src/generatedCode/FlowObjects/Create/source/CreateNodeCallback_Main.js'
1335
+ ),
1336
+ 'utf8'
1337
+ );
1338
+ assert.doesNotMatch(callbackMain, /autoProvisionOwnerTargetRole\(_izContext/);
1339
+
1340
+ const logicalLib = await readFile(
1341
+ join(
1342
+ rootPath,
1343
+ 'app/src/generatedCode/FlowObjects/Create/source/CreateLogicalLib.js'
1344
+ ),
1345
+ 'utf8'
1346
+ );
1347
+ assert.match(logicalLib, /const ROOT_RBAC_OBJECT_TYPES = new Set\(\[\s*\]\);/);
1348
+ });
1349
+
1350
+ test('emits autoProvisionOwnerTargetRole in CreateNodeCallback when service has root RBAC object', async () => {
1351
+ const rootPath = await mkdtemp(
1352
+ join(tmpdir(), 'generate-service-code-v2-crud-root-rbac-')
1353
+ );
1354
+ roots.add(rootPath);
1355
+ const schemaRoot = join(rootPath, 'app/src/schemas');
1356
+ await mkdir(join(schemaRoot, 'ObjectSchemas'), { recursive: true });
1357
+ await mkdir(join(schemaRoot, 'FlowSchemas'), { recursive: true });
1358
+ await writeFile(
1359
+ join(schemaRoot, 'ObjectSchemas/Workspace.js'),
1360
+ "export default { objectType: 'workspace', generatedBy: 'user', addOnDataStructure: [{ type: 'rbac' }], storageResources: { graph: { storageType: 'graph' } } };\n"
1361
+ );
1362
+ await generateCrudApplicationArtifacts(rootPath, {
1363
+ serviceTag: 'workspaces'
1364
+ });
1365
+
1366
+ const callbackMain = await readFile(
1367
+ join(
1368
+ rootPath,
1369
+ 'app/src/generatedCode/FlowObjects/Create/source/CreateNodeCallback_Main.js'
1370
+ ),
1371
+ 'utf8'
1372
+ );
1373
+ assert.match(
1374
+ callbackMain,
1375
+ /await autoProvisionOwnerTargetRole\(_izContext,\s*\{\s*node,\s*submittedByUserId,\s*objType: returnValue\?\.requestParams\?\.objType\s*\}\);/
1376
+ );
1377
+
1378
+ const logicalLib = await readFile(
1379
+ join(
1380
+ rootPath,
1381
+ 'app/src/generatedCode/FlowObjects/Create/source/CreateLogicalLib.js'
1382
+ ),
1383
+ 'utf8'
1384
+ );
1385
+ assert.match(
1386
+ logicalLib,
1387
+ /const ROOT_RBAC_OBJECT_TYPES = new Set\(\[\s*'workspace',\s*\]\);/
1388
+ );
1389
+ });
1390
+
951
1391
  async function createProject(withCrudObject) {
952
1392
  const rootPath = await mkdtemp(
953
1393
  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
+ });