@izara_project/izara-core-generate-service-code 1.0.82 → 1.0.84

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 (60) hide show
  1. package/package.json +4 -4
  2. package/src/compile/flowValidator.js +34 -0
  3. package/src/compile/loadSchemas.js +121 -7
  4. package/src/emit/attributeTree/emitAttributeTreeSchemas.js +388 -0
  5. package/src/emit/coreFunction/emitCoreFunctionArtifacts.js +29 -1
  6. package/src/emit/coreFunction/templates/InitialSetupConfig.ejs +16 -3
  7. package/src/emit/crud/crudActionPlan.js +43 -7
  8. package/src/emit/crud/emitCrudApplicationArtifacts.js +19 -4
  9. package/src/emit/crud/emitDynamicCrudFlowSchemas.js +27 -6
  10. package/src/emit/crud/generateCrudApplicationArtifacts.js +3 -2
  11. package/src/emit/crud/generateDynamicCrudFlowSchemas.js +3 -2
  12. package/src/emit/crud/loadLocalSchemaArrays.js +3 -3
  13. package/src/emit/crud/templates/CreateComplete_Main.ejs +1 -1
  14. package/src/emit/crud/templates/CreateEndpoint_Main.ejs +75 -21
  15. package/src/emit/crud/templates/CreateLogicalLib.ejs +73 -8
  16. package/src/emit/crud/templates/DeleteComplete_Main.ejs +1 -1
  17. package/src/emit/crud/templates/DeleteEndpoint_Main.ejs +39 -11
  18. package/src/emit/crud/templates/DeleteLogicalLib.ejs +6 -3
  19. package/src/emit/crud/templates/FlowEndpointRbacCallback_Main.ejs +2 -2
  20. package/src/emit/crud/templates/FlowEndpoint_HdrWbs.ejs +7 -0
  21. package/src/emit/crud/templates/FlowMain_Wbs.ejs +5 -2
  22. package/src/emit/crud/templates/GetEndpoint_Main.ejs +13 -0
  23. package/src/emit/crud/templates/UpdateComplete_Main.ejs +1 -1
  24. package/src/emit/crud/templates/UpdateEndpoint_Main.ejs +53 -4
  25. package/src/emit/crud/templates/UpdateLogicalLib.ejs +4 -3
  26. package/src/emit/custom/customFlows.js +49 -0
  27. package/src/emit/custom/emitCustomFlowApplicationArtifacts.js +168 -49
  28. package/src/emit/custom/generateCustomFlowApplicationArtifacts.js +1 -1
  29. package/src/emit/custom/templates/FlowEndpointRbacCallback_Main.ejs +194 -0
  30. package/src/emit/custom/templates/FlowEndpoint_Main.ejs +14 -22
  31. package/src/emit/custom/templates/FlowStep_Main.ejs +1 -1
  32. package/src/emit/function/templates/WbsFunction_Yaml.ejs +0 -2
  33. package/src/emit/rbac/emitDynamicRbacFlowSchemas.js +33 -8
  34. package/src/emit/rbac/emitRbacApplicationArtifacts.js +204 -7
  35. package/src/emit/rbac/templates/CancelUsage_Main.ejs +103 -0
  36. package/src/emit/rbac/templates/ConfirmUsage_Main.ejs +71 -0
  37. package/src/emit/rbac/templates/CreateTargetRole_Main.ejs +49 -6
  38. package/src/emit/rbac/templates/ListRbac_Main.ejs +9 -3
  39. package/src/emit/rbac/templates/RbacMutation_Main.ejs +1 -1
  40. package/src/emit/rbac/templates/ReservedDynamicUsage_Main.ejs +147 -0
  41. package/src/emit/rbac/templates/UserRbacFlow_Main.ejs +59 -23
  42. package/src/emit/rbac/templates/UserRoleMutation_Main.ejs +1 -1
  43. package/src/emit/relationship/emitDynamicRelationshipFlowSchemas.js +1 -1
  44. package/src/emit/relationship/templates/ChangeRelationship_Main.ejs +10 -3
  45. package/src/emit/resource/emitResourceArtifacts.js +350 -112
  46. package/src/emit/resource/templates/SystemDynamoDB_Yaml.ejs +53 -2
  47. package/src/emit/system/emitSystemWebSocketArtifacts.js +6 -4
  48. package/src/emit/system/templates/WebSocketConnect.ejs +71 -12
  49. package/src/emit/system/templates/WebSocketConnect_Yaml.ejs +7 -0
  50. package/src/emit/systemText/emitSystemTextSchemas.js +3 -2
  51. package/src/generate.js +41 -10
  52. package/test/attributeTreeSchemas.test.js +219 -0
  53. package/test/createTargetRoleHandler.test.js +59 -4
  54. package/test/crudApplicationArtifacts.test.js +23 -6
  55. package/test/customFlowApplicationArtifacts.test.js +5 -5
  56. package/test/dynamicCrudFlowSchemas.test.js +110 -13
  57. package/test/dynamicRbacFlowSchemas.test.js +15 -2
  58. package/test/publicApi.test.js +7 -7
  59. package/test/resourceArtifacts.test.js +70 -29
  60. package/test/systemTextSchemas.test.js +1 -1
@@ -184,7 +184,7 @@ test('preserves function-setting hooks across CRUD regeneration', async () => {
184
184
  assert.equal(await readFile(yamlPath, 'utf8'), yaml);
185
185
  });
186
186
 
187
- test('does not add RBAC handlers to Create', async () => {
187
+ test('emits a single Complete RBAC handler for Create without Graph', async () => {
188
188
  const rootPath = await createRbacProject();
189
189
  const result = await generateCrudApplicationArtifacts(rootPath, {
190
190
  serviceTag: 'orders'
@@ -196,13 +196,13 @@ test('does not add RBAC handlers to Create', async () => {
196
196
  assert.ok(createPaths.includes(
197
197
  'app/src/generatedCode/FlowObjects/Create/source/Create_Main.js'
198
198
  ));
199
- assert.ok(!createPaths.includes(
199
+ assert.ok(createPaths.includes(
200
200
  'app/src/generatedCode/FlowObjects/Create/source/CreateComplete_Main.js'
201
201
  ));
202
202
  assert.ok(
203
203
  !createPaths.some((path) => path.includes('CreateRbacCallback'))
204
204
  );
205
- assert.doesNotMatch(
205
+ assert.match(
206
206
  await readFile(
207
207
  join(
208
208
  rootPath,
@@ -280,7 +280,7 @@ test('emits GetRangeHdrApi when an object defines a sortKey identifier', async (
280
280
  const rootPath = await createProject(true);
281
281
  await writeFile(
282
282
  join(rootPath, 'app/src/schemas/ObjectSchemas/Order.js'),
283
- "export default { objectType: 'order', generatedBy: 'userGenerated', identifiers: [{ type: 'identifier', fieldName: 'orderId' }, { type: 'sortKey', fieldName: 'createdAt' }], storageResources: { graph: { storageType: 'graph' } } };\n"
283
+ "export default { objectType: 'order', generatedBy: 'user', identifiers: [{ type: 'identifier', fieldName: 'orderId' }, { type: 'sortKey', fieldName: 'createdAt' }], storageResources: { graph: { storageType: 'graph' } } };\n"
284
284
  );
285
285
  const result = await generateCrudApplicationArtifacts(rootPath, {
286
286
  serviceTag: 'orders'
@@ -341,6 +341,23 @@ test('removes managed CRUD artifacts when no applicable object remains', async (
341
341
  );
342
342
  });
343
343
 
344
+ test('logs warning when ObjectSchema has invalid or non-CRUD generatedBy in crudApplicationArtifacts', async () => {
345
+ const rootPath = await createProject(false);
346
+ const warnings = [];
347
+ const logger = {
348
+ warn: (msg) => warnings.push(msg)
349
+ };
350
+
351
+ const result = await generateCrudApplicationArtifacts(rootPath, { logger });
352
+ assert.deepEqual(result.files, []);
353
+ assert.equal(warnings.length, 1);
354
+ assert.match(
355
+ warnings[0],
356
+ /\[WARN\] \[CRUD Application\] Skipped FlowObjects application artifacts generation/
357
+ );
358
+ });
359
+
360
+
344
361
  async function createProject(withCrudObject) {
345
362
  const rootPath = await mkdtemp(
346
363
  join(tmpdir(), 'generate-service-code-v2-crud-application-')
@@ -352,7 +369,7 @@ async function createProject(withCrudObject) {
352
369
  await writeFile(
353
370
  join(schemaRoot, 'ObjectSchemas/Order.js'),
354
371
  withCrudObject
355
- ? "export default { objectType: 'order', generatedBy: 'userGenerated', storageResources: { graph: { storageType: 'graph' } } };\n"
372
+ ? "export default { objectType: 'order', generatedBy: 'user', storageResources: { graph: { storageType: 'graph' } } };\n"
356
373
  : "export default { objectType: 'order' };\n"
357
374
  );
358
375
  return rootPath;
@@ -368,7 +385,7 @@ async function createRbacProject(withHook = false, withGraph = false) {
368
385
  await mkdir(join(schemaRoot, 'FlowSchemas'), { recursive: true });
369
386
  await writeFile(
370
387
  join(schemaRoot, 'ObjectSchemas/Order.js'),
371
- `export default { objectType: 'order', generatedBy: 'userGenerated', belongTo: { serviceTag: 'UserAccount', objectType: 'user' }${withGraph ? ", storageResources: { graph: { storageType: 'graph' } }" : ''} };\n`
388
+ `export default { objectType: 'order', generatedBy: 'user', belongTo: { serviceTag: 'UserAccount', objectType: 'user' }${withGraph ? ", storageResources: { graph: { storageType: 'graph' } }" : ''} };\n`
372
389
  );
373
390
  if (withHook) {
374
391
  await writeFile(
@@ -340,7 +340,7 @@ test('emits and removes ext-topic handlers declared by custom flow steps', async
340
340
  await readFile(
341
341
  join(
342
342
  rootPath,
343
- 'resource/sls_yaml/generatedCode/source/generated-sns-in-sqs.yml'
343
+ 'resource/sls_yaml/generatedCode/source/generated-sns-sqs-custom-flows.yml'
344
344
  ),
345
345
  'utf8'
346
346
  ),
@@ -350,7 +350,7 @@ test('emits and removes ext-topic handlers declared by custom flow steps', async
350
350
  await readFile(
351
351
  join(
352
352
  rootPath,
353
- 'resource/sls_yaml/generatedCode/source/generated-sns-in-sqs.yml'
353
+ 'resource/sls_yaml/generatedCode/source/generated-sns-sqs-custom-flows.yml'
354
354
  ),
355
355
  'utf8'
356
356
  ),
@@ -398,7 +398,7 @@ test('emits and removes ext-topic handlers declared by custom flow steps', async
398
398
  await readFile(
399
399
  join(
400
400
  rootPath,
401
- 'resource/sls_yaml/generatedCode/source/generated-sns-in-sqs.yml'
401
+ 'resource/sls_yaml/generatedCode/source/generated-sns-sqs-custom-flows.yml'
402
402
  ),
403
403
  'utf8'
404
404
  ),
@@ -437,7 +437,7 @@ test('emits and removes the trigger-cache artifact suite when triggerFlowTypes i
437
437
  const resources = await readFile(
438
438
  join(
439
439
  rootPath,
440
- 'resource/sls_yaml/generatedCode/source/generated-sns-in-sqs.yml'
440
+ 'resource/sls_yaml/generatedCode/source/generated-sns-sqs-custom-flows.yml'
441
441
  ),
442
442
  'utf8'
443
443
  );
@@ -864,7 +864,7 @@ test('registers SQS queue, SNS subscription, and SNS out topic for custom flowSc
864
864
  const snsInSqs = await readFile(
865
865
  join(
866
866
  rootPath,
867
- 'resource/sls_yaml/generatedCode/source/generated-sns-in-sqs.yml'
867
+ 'resource/sls_yaml/generatedCode/source/generated-sns-sqs-custom-flows.yml'
868
868
  ),
869
869
  'utf8'
870
870
  );
@@ -6,10 +6,11 @@ import test from 'node:test';
6
6
  import { pathToFileURL } from 'node:url';
7
7
 
8
8
  import { generateDynamicCrudFlowSchemas } from '../src/emit/crud/generateDynamicCrudFlowSchemas.js';
9
+ import { createCrudActionPlans } from '../src/emit/crud/crudActionPlan.js';
9
10
 
10
11
  test('generates dynamic CRUD schemas from local schema modules', async () => {
11
12
  const rootPath = await createProject({
12
- generatedBy: 'userGenerated'
13
+ generatedBy: 'user'
13
14
  });
14
15
 
15
16
  const result = await generateDynamicCrudFlowSchemas(rootPath);
@@ -41,7 +42,7 @@ test('generates dynamic CRUD schemas from local schema modules', async () => {
41
42
 
42
43
  test('accepts singleton object exports used by existing schema files', async () => {
43
44
  const rootPath = await createProject({
44
- generatedBy: 'userGenerated',
45
+ generatedBy: 'user',
45
46
  singletonExports: true
46
47
  });
47
48
 
@@ -57,7 +58,7 @@ test('accepts singleton object exports used by existing schema files', async ()
57
58
  });
58
59
 
59
60
  test('does not generate CRUD schemas without an applicable object', async () => {
60
- const rootPath = await createProject({ generatedBy: 'userGenerated' });
61
+ const rootPath = await createProject({ generatedBy: 'user' });
61
62
  await generateDynamicCrudFlowSchemas(rootPath);
62
63
  await writeFile(
63
64
  join(rootPath, 'app/src/schemas/ObjectSchemas/Order.js'),
@@ -71,7 +72,7 @@ test('does not generate CRUD schemas without an applicable object', async () =>
71
72
  });
72
73
 
73
74
  test('does not allow options to override the rootPath argument', async () => {
74
- const rootPath = await createProject({ generatedBy: 'userGenerated' });
75
+ const rootPath = await createProject({ generatedBy: 'user' });
75
76
  const ignoredRootPath = await createProject({ generatedBy: 'external' });
76
77
 
77
78
  const result = await generateDynamicCrudFlowSchemas(rootPath, {
@@ -101,7 +102,7 @@ test('rejects source modules that do not default-export schema objects or arrays
101
102
 
102
103
  test('rejects deprecated FlowHook schemas before generating CRUD schemas', async () => {
103
104
  const rootPath = await createProject({
104
- generatedBy: 'systemGenerated',
105
+ generatedBy: 'systemWithNoUser',
105
106
  flow: createHookFlow(['ownTopic'])
106
107
  });
107
108
 
@@ -121,7 +122,7 @@ test('generates a direct Complete step for RBAC-only Update flows', async () =>
121
122
  join(objectDirectory, 'Order.js'),
122
123
  `export default [{
123
124
  objectType: 'order',
124
- generatedBy: 'userGenerated',
125
+ generatedBy: 'user',
125
126
  belongTo: { serviceTag: 'UserAccount', objectType: 'user' }
126
127
  }];\n`
127
128
  );
@@ -141,15 +142,15 @@ test('generates a direct Complete step for RBAC-only Update flows', async () =>
141
142
  assert.equal(update.flowSteps[1].properties.length, 0);
142
143
  });
143
144
 
144
- test('ignores RBAC for Create even when Graph storage is present', async () => {
145
+ test('generates RbacCallback and Complete for Create when RBAC and Graph are present', async () => {
145
146
  const rootPath = await createProject({
146
- generatedBy: 'userGenerated',
147
+ generatedBy: 'user',
147
148
  });
148
149
  await writeFile(
149
150
  join(rootPath, 'app/src/schemas/ObjectSchemas/Order.js'),
150
151
  `export default [{
151
152
  objectType: 'order',
152
- generatedBy: 'userGenerated',
153
+ generatedBy: 'user',
153
154
  belongTo: { serviceTag: 'UserAccount', objectType: 'user' }
154
155
  , storageResources: { graph: { storageType: 'graph' } }
155
156
  }];\n`
@@ -159,6 +160,7 @@ test('ignores RBAC for Create even when Graph storage is present', async () => {
159
160
  const create = await readGeneratedFlow(rootPath, 'Create');
160
161
  assert.deepEqual(create.flowSteps.map((step) => step.stepName), [
161
162
  'Start',
163
+ 'RbacCallback',
162
164
  'Complete'
163
165
  ]);
164
166
  });
@@ -173,7 +175,7 @@ test('uses the renamed RBAC callback step when Graph storage is present', async
173
175
  join(objectDirectory, 'Order.js'),
174
176
  `export default [{
175
177
  objectType: 'order',
176
- generatedBy: 'userGenerated',
178
+ generatedBy: 'user',
177
179
  belongTo: { serviceTag: 'UserAccount', objectType: 'user' },
178
180
  storageResources: { graph: { storageType: 'graph' } }
179
181
  }];\n`
@@ -183,9 +185,10 @@ test('uses the renamed RBAC callback step when Graph storage is present', async
183
185
  const create = await readGeneratedFlow(rootPath, 'Create');
184
186
  assert.deepEqual(create.flowSteps.map((step) => step.stepName), [
185
187
  'Start',
188
+ 'RbacCallback',
186
189
  'Complete'
187
190
  ]);
188
- assert.deepEqual(create.flowSteps[1].properties, [
191
+ assert.deepEqual(create.flowSteps[2].properties, [
189
192
  'returnValue',
190
193
  'parentFlowId',
191
194
  'status',
@@ -204,7 +207,7 @@ test('chains RbacCallback and Complete for Update with RBAC and Graph', async ()
204
207
  join(objectDirectory, 'Order.js'),
205
208
  `export default [{
206
209
  objectType: 'order',
207
- generatedBy: 'userGenerated',
210
+ generatedBy: 'user',
208
211
  belongTo: { serviceTag: 'UserAccount', objectType: 'user' },
209
212
  storageResources: { graph: { storageType: 'graph' } }
210
213
  }];\n`
@@ -242,7 +245,7 @@ test('omits Complete step when no graph storage resources exist', async () => {
242
245
  join(objectDirectory, 'Order.js'),
243
246
  `export default [{
244
247
  objectType: 'order',
245
- generatedBy: 'userGenerated',
248
+ generatedBy: 'user',
246
249
  storageResources: {
247
250
  myTable: { storageType: 'dynamoDB', tableName: 'OrderTable' }
248
251
  }
@@ -258,6 +261,57 @@ test('omits Complete step when no graph storage resources exist', async () => {
258
261
  assert.equal(create.flowSteps[0].nextSteps, undefined);
259
262
  });
260
263
 
264
+ test('evaluates hasQuotaReservation based on billable flag in belongTo and addOnDataStructure', () => {
265
+ const billableObj = {
266
+ objectType: 'unitType',
267
+ generatedBy: 'user',
268
+ addOnDataStructure: [{ type: 'rbac', billable: true }]
269
+ };
270
+ const plans1 = createCrudActionPlans([billableObj]);
271
+ assert.equal(plans1.Create.hasQuotaReservation, true);
272
+
273
+ const nonBillableObj1 = {
274
+ objectType: 'internalTag',
275
+ generatedBy: 'user',
276
+ addOnDataStructure: [{ type: 'rbac', billable: false }]
277
+ };
278
+ const plans2 = createCrudActionPlans([nonBillableObj1]);
279
+ assert.equal(plans2.Create.hasQuotaReservation, false);
280
+
281
+ const nonBillableObj2 = {
282
+ objectType: 'deliveryMethod',
283
+ generatedBy: 'user',
284
+ belongTo: {
285
+ objectType: 'supplyNetwork',
286
+ serviceTag: 'SupplyNetwork',
287
+ billable: false
288
+ },
289
+ belongToSchema: {
290
+ objectType: 'supplyNetwork',
291
+ addOnDataStructure: [{ type: 'rbac' }]
292
+ }
293
+ };
294
+ const plans3 = createCrudActionPlans([nonBillableObj2]);
295
+ assert.equal(plans3.Create.hasQuotaReservation, false);
296
+ assert.equal(plans3.Update.hasRbac, true);
297
+
298
+ const billableObj2 = {
299
+ objectType: 'unitType',
300
+ generatedBy: 'user',
301
+ belongTo: {
302
+ objectType: 'supplyNetwork',
303
+ serviceTag: 'SupplyNetwork',
304
+ billable: true
305
+ },
306
+ belongToSchema: {
307
+ objectType: 'supplyNetwork',
308
+ addOnDataStructure: [{ type: 'rbac' }]
309
+ }
310
+ };
311
+ const plans4 = createCrudActionPlans([billableObj2]);
312
+ assert.equal(plans4.Create.hasQuotaReservation, true);
313
+ });
314
+
261
315
  async function createProject({ generatedBy, flow, singletonExports = false }) {
262
316
  const rootPath = await mkdtemp(
263
317
  join(tmpdir(), 'generate-service-code-v2-crud-')
@@ -318,3 +372,46 @@ function readGeneratedFile(rootPath, action) {
318
372
  'utf8'
319
373
  );
320
374
  }
375
+
376
+ test('supports standard generatedBy values (user, systemWithUser, systemNoUser)', async () => {
377
+ for (const generatedBy of ['user', 'systemWithUser', 'systemNoUser']) {
378
+ const rootPath = await createProject({ generatedBy });
379
+ await generateDynamicCrudFlowSchemas(rootPath);
380
+ const create = await readGeneratedFlow(rootPath, 'Create');
381
+ assert.ok(create, `Create flow should be generated for generatedBy: ${generatedBy}`);
382
+ assert.deepEqual(create.flowSteps.map((s) => s.stepName), ['Start', 'Complete']);
383
+ }
384
+ });
385
+
386
+ test('logs warning when ObjectSchema has invalid or non-CRUD generatedBy', async () => {
387
+ const rootPath = await createProject({ generatedBy: 'user' });
388
+ await writeFile(
389
+ join(rootPath, 'app/src/schemas/ObjectSchemas/Order.js'),
390
+ "export default [{ objectType: 'order', generatedBy: 'invalidGeneratedBy' }];\n"
391
+ );
392
+
393
+ const warnings = [];
394
+ const logger = {
395
+ warn: (msg) => warnings.push(msg)
396
+ };
397
+
398
+ const result = await generateDynamicCrudFlowSchemas(rootPath, { logger });
399
+ assert.deepEqual(result.files, []);
400
+ assert.equal(warnings.length, 1);
401
+ assert.match(warnings[0], /\[WARN\] \[CRUD FlowSchemas\] Skipped FlowObjects FlowSchemas generation/);
402
+ assert.match(warnings[0], /invalidGeneratedBy/);
403
+ });
404
+
405
+ test('throws error when ObjectSchema validation fails with validateSchemas: true', async () => {
406
+ const rootPath = await createProject({ generatedBy: 'user' });
407
+ await writeFile(
408
+ join(rootPath, 'app/src/schemas/ObjectSchemas/Order.js'),
409
+ "export default [{ objectType: 'order', fieldNames: {}, identifiers: [] }];\n"
410
+ );
411
+
412
+ await assert.rejects(
413
+ generateDynamicCrudFlowSchemas(rootPath, { validateSchemas: true }),
414
+ /\[SCHEMA_VALIDATION_ERROR\] Schema validation failed for ObjectSchemas\/Order\.js/
415
+ );
416
+ });
417
+
@@ -8,7 +8,7 @@ import { pathToFileURL } from 'node:url';
8
8
  import { generateDynamicRbacFlowSchemas } from '../src/emit/rbac/generateDynamicRbacFlowSchemas.js';
9
9
 
10
10
  const projectRoots = new Set();
11
- const STATIC_ACTIONS = [
11
+ const STATIC_RBAC_ACTIONS = [
12
12
  'CreateTargetRole',
13
13
  'ListTargetRole',
14
14
  'DeleteTargetRole',
@@ -20,6 +20,15 @@ const STATIC_ACTIONS = [
20
20
  'DeleteUserFromRole'
21
21
  ];
22
22
 
23
+ const STATIC_LIMIT_ACTIONS = [
24
+ 'CreateRoleLimit',
25
+ 'ListRoleLimits',
26
+ 'DeleteRoleLimit',
27
+ 'ReservedDynamicUsage',
28
+ 'ConfirmUsage',
29
+ 'CancelUsage'
30
+ ];
31
+
23
32
  after(async () => {
24
33
  await Promise.all(
25
34
  [...projectRoots].map((rootPath) =>
@@ -38,10 +47,14 @@ test('generates shared and object-specific RBAC flow schemas', async () => {
38
47
  assert.deepEqual(
39
48
  result.files.map((file) => file.path),
40
49
  [
41
- ...STATIC_ACTIONS.map(
50
+ ...STATIC_RBAC_ACTIONS.map(
42
51
  (action) =>
43
52
  `app/src/schemas/FlowSchemas/generateCode/FlowRbac/${action}.js`
44
53
  ),
54
+ ...STATIC_LIMIT_ACTIONS.map(
55
+ (action) =>
56
+ `app/src/schemas/FlowSchemas/generateCode/FlowLimit/${action}.js`
57
+ ),
45
58
  'app/src/schemas/FlowSchemas/generateCode/FlowRbac/ValidatorRbac.js'
46
59
  ]
47
60
  );
@@ -23,7 +23,7 @@ test('generate commits schemas, application artifacts, and resources in one publ
23
23
  const rootPath = await createCrudProject();
24
24
  const result = await generate(rootPath);
25
25
 
26
- assert.strictEqual(result.files.length, 65);
26
+ assert.strictEqual(result.files.length, 74);
27
27
  assert.equal(result.project.rootPath, rootPath);
28
28
  assert.match(
29
29
  await readFile(
@@ -155,7 +155,7 @@ test('generate commits the system WebSocket completion path for own-topic flows'
155
155
  await readFile(
156
156
  join(
157
157
  rootPath,
158
- 'resource/sls_yaml/generatedCode/source/generated-sns-in-sqs.yml'
158
+ 'resource/sls_yaml/generatedCode/source/generated-sns-sqs-system.yml'
159
159
  ),
160
160
  'utf8'
161
161
  ),
@@ -165,7 +165,7 @@ test('generate commits the system WebSocket completion path for own-topic flows'
165
165
  await readFile(
166
166
  join(
167
167
  rootPath,
168
- 'resource/sls_yaml/generatedCode/source/generated-sns-in-sqs.yml'
168
+ 'resource/sls_yaml/generatedCode/source/generated-sns-sqs-system.yml'
169
169
  ),
170
170
  'utf8'
171
171
  ),
@@ -235,7 +235,7 @@ test('generate commits the trigger-cache suite with its queues when triggerFlowT
235
235
  await readFile(
236
236
  join(
237
237
  rootPath,
238
- 'resource/sls_yaml/generatedCode/source/generated-sns-in-sqs.yml'
238
+ 'resource/sls_yaml/generatedCode/source/generated-sns-sqs-custom-flows.yml'
239
239
  ),
240
240
  'utf8'
241
241
  ),
@@ -273,7 +273,7 @@ test('generate removes the system WebSocket path when the last own-topic flow is
273
273
  await readFile(
274
274
  join(
275
275
  rootPath,
276
- 'resource/sls_yaml/generatedCode/source/generated-sns-in-sqs.yml'
276
+ 'resource/sls_yaml/generatedCode/source/generated-sns-sqs-system.yml'
277
277
  ),
278
278
  'utf8'
279
279
  ),
@@ -309,7 +309,7 @@ test('generate removes stale CRUD schemas and application artifacts', async () =
309
309
 
310
310
  const result = await generate(rootPath);
311
311
 
312
- assert.equal(result.files.length, 29);
312
+ assert.equal(result.files.length, 38);
313
313
  await assert.rejects(
314
314
  readFile(
315
315
  join(
@@ -346,7 +346,7 @@ async function createCrudProject() {
346
346
  await mkdir(objectDirectory, { recursive: true });
347
347
  await writeFile(
348
348
  join(objectDirectory, 'Order.js'),
349
- "export default { objectType: 'order', generatedBy: 'userGenerated', storageResources: { graph: { storageType: 'graph' } } };\n"
349
+ "export default { objectType: 'order', generatedBy: 'user', storageResources: { graph: { storageType: 'graph' } } };\n"
350
350
  );
351
351
  return rootPath;
352
352
  }
@@ -19,28 +19,52 @@ test('emits queues, topics, tables, and IAM roles from schema data', async () =>
19
19
  const rootPath = await createProject();
20
20
  const result = await generateResourceArtifacts(rootPath);
21
21
 
22
- assert.equal(result.files.length, 6);
23
- const resources = await readFile(
22
+ assert.equal(result.files.length, 10);
23
+ const crudResources = await readFile(
24
24
  join(
25
25
  rootPath,
26
- 'resource/sls_yaml/generatedCode/source/generated-sns-in-sqs.yml'
26
+ 'resource/sls_yaml/generatedCode/source/generated-sns-sqs-crud.yml'
27
27
  ),
28
28
  'utf8'
29
29
  );
30
- assert.match(resources, /SubscriptionOutCreateToUploadReservedLimitHdrSqs:/);
31
- assert.match(resources, /TopicArn: !Ref OutCreate/);
30
+ assert.match(crudResources, /InCreate:/);
31
+ assert.match(crudResources, /^ CreateCompleteHdrSqs:/m);
32
+ assert.match(crudResources, /^ UpdateCompleteHdrSqs:/m);
33
+ assert.match(crudResources, /^ DeleteCompleteHdrSqs:/m);
34
+ assert.doesNotMatch(crudResources, /RbacCallbackHdrSqs/);
35
+ assert.doesNotMatch(crudResources, /FlowHook|beforeLogical/);
36
+
37
+ const customResources = await readFile(
38
+ join(
39
+ rootPath,
40
+ 'resource/sls_yaml/generatedCode/source/generated-sns-sqs-custom-flows.yml'
41
+ ),
42
+ 'utf8'
43
+ );
44
+ assert.match(
45
+ customResources,
46
+ /SubscriptionOutCreateToUploadReservedLimitHdrSqs:/
47
+ );
48
+ assert.match(customResources, /TopicArn: !Ref OutCreate/);
32
49
  assert.match(
33
- resources,
50
+ customResources,
34
51
  /Endpoint: 'arn:aws:sqs:\${self:custom\.iz_region}:\${self:custom\.iz_accountId}:\${self:custom\.iz_resourcePrefix}UploadReservedLimitHdrSqs'/
35
52
  );
36
- assert.match(resources, /InCreate:/);
53
+
54
+ const systemResources = await readFile(
55
+ join(
56
+ rootPath,
57
+ 'resource/sls_yaml/generatedCode/source/generated-sns-sqs-system.yml'
58
+ ),
59
+ 'utf8'
60
+ );
37
61
  for (const queueName of [
38
62
  'FindDataHdrSqs',
39
63
  'ProcessLogicalHdrSqs',
40
64
  'PaginateProcessLogicalHdrDsq',
41
65
  'PaginateProcessLogicalHdrSqs'
42
66
  ]) {
43
- assert.match(resources, new RegExp(`^ ${queueName}:`, 'm'));
67
+ assert.match(systemResources, new RegExp(`^ ${queueName}:`, 'm'));
44
68
  }
45
69
  const dynamoYaml = await readFile(
46
70
  join(
@@ -52,6 +76,10 @@ test('emits queues, topics, tables, and IAM roles from schema data', async () =>
52
76
  assert.match(dynamoYaml, /OrdersTable:/);
53
77
  assert.match(dynamoYaml, /#<#OrdersTableSetting#>/);
54
78
  assert.match(dynamoYaml, /#<#\/OrdersTableSetting#>/);
79
+ assert.match(dynamoYaml, /TargetRolesTable:\s+Type: 'AWS::DynamoDB::Table'[\s\S]+AttributeName: identifiersConcat/);
80
+ assert.match(dynamoYaml, /RoleLimitTable:\s+Type: 'AWS::DynamoDB::Table'[\s\S]+TableName: \${self:custom\.iz_resourcePrefix}RoleLimit/);
81
+ assert.match(dynamoYaml, /RunningTotalUsageTable:\s+Type: 'AWS::DynamoDB::Table'[\s\S]+TableName: \${self:custom\.iz_resourcePrefix}RunningTotalUsage/);
82
+ assert.match(dynamoYaml, /RunningTotalReservedUsageTable:\s+Type: 'AWS::DynamoDB::Table'[\s\S]+TableName: \${self:custom\.iz_resourcePrefix}RunningTotalReservedUsage/);
55
83
  const iam = await readFile(
56
84
  join(
57
85
  rootPath,
@@ -80,22 +108,28 @@ test('emits queues, topics, tables, and IAM roles from schema data', async () =>
80
108
  roleBlock(iam, 'FlowRbacRole'),
81
109
  /RolePermissions|ProcessCreateTargetRoleHdrSqs/
82
110
  );
111
+ assert.match(roleBlock(iam, 'FlowRbacRole'), /RoleLimit/);
112
+ assert.match(roleBlock(iam, 'FlowRbacRole'), /RunningTotalUsage/);
113
+ assert.match(roleBlock(iam, 'FlowObjectsRole'), /RoleLimit/);
83
114
  // Per-object RBAC flow queue (e.g. ValidatorRbacHdrSqs from Order with rbac add-on)
84
115
  assert.match(roleBlock(iam, 'FlowRbacRole'), /ValidatorRbacHdrSqs/);
85
- assert.match(resources, /^ CreateRelationshipHdrSqs:/m);
86
- assert.doesNotMatch(resources, /^ CreateCompleteHdrSqs:/m);
87
- assert.match(resources, /^ UpdateCompleteHdrSqs:/m);
88
- assert.match(resources, /^ DeleteCompleteHdrSqs:/m);
89
- assert.doesNotMatch(resources, /RbacCallbackHdrSqs/);
90
- assert.doesNotMatch(resources, /FlowHook|beforeLogical/);
116
+
117
+ const relResources = await readFile(
118
+ join(
119
+ rootPath,
120
+ 'resource/sls_yaml/generatedCode/source/generated-sns-sqs-relationship.yml'
121
+ ),
122
+ 'utf8'
123
+ );
124
+ assert.match(relResources, /^ CreateRelationshipHdrSqs:/m);
91
125
  assert.equal(
92
- (resources.match(/^ CreateRelationshipHdrSqs:/gm) ?? []).length,
126
+ (relResources.match(/^ CreateRelationshipHdrSqs:/gm) ?? []).length,
93
127
  1
94
128
  );
95
- assert.match(resources, /^ CreateRelationshipCompleteHdrSqs:/m);
96
- assert.match(resources, /^ GetRelationshipHdrDsq:/m);
129
+ assert.match(relResources, /^ CreateRelationshipCompleteHdrSqs:/m);
130
+ assert.match(relResources, /^ GetRelationshipHdrDsq:/m);
97
131
  assert.doesNotMatch(
98
- resources,
132
+ relResources,
99
133
  /Process(?:Create|Update|Delete|Get|Change|Move)Relationship/
100
134
  );
101
135
  const relationshipRole = roleBlock(iam, 'FlowRelationshipRole');
@@ -107,9 +141,12 @@ test('emits queues, topics, tables, and IAM roles from schema data', async () =>
107
141
  /Process(?:Create|Update|Delete|Get|Change|Move)Relationship/
108
142
  );
109
143
  assert.match(roleBlock(iam, 'RegisterRole'), /s3:GetObject/);
144
+ assert.match(roleBlock(iam, 'RegisterRole'), /s3:ListBucket/);
110
145
  assert.match(roleBlock(iam, 'RegisterRole'), /RegisterComplete/);
111
146
  assert.match(roleBlock(iam, 'RegisterRole'), /dynamodb:/);
112
147
  assert.match(roleBlock(iam, 'RegisterRole'), /execute-api:ManageConnections/);
148
+ assert.match(roleBlock(iam, 'FlowObjectsRole'), /s3:GetObject/);
149
+ assert.match(roleBlock(iam, 'FlowObjectsRole'), /s3:ListBucket/);
113
150
  const procFindDataRole = roleBlock(iam, 'ProcFindDataRole');
114
151
  assert.match(procFindDataRole, /dynamodb:UpdateItem/);
115
152
  assert.match(procFindDataRole, /dynamodb:GetItem/);
@@ -118,12 +155,16 @@ test('emits queues, topics, tables, and IAM roles from schema data', async () =>
118
155
  assert.match(procFindDataRole, /FindData_Out/);
119
156
  assert.doesNotMatch(
120
157
  iam,
121
- /s3:GetObjectVersion|s3:ListBucket|sqs:DeleteMessageBatch|sqs:GetQueueUrl/
158
+ /s3:GetObjectVersion|sqs:DeleteMessageBatch|sqs:GetQueueUrl/
122
159
  );
123
160
  assert.match(iam, /#<#\/\w+StatementHookCode#>\n\n \w+Role:/);
124
161
 
125
162
  for (const filePath of [
126
- 'resource/sls_yaml/generatedCode/source/generated-sns-in-sqs.yml',
163
+ 'resource/sls_yaml/generatedCode/source/generated-sns-sqs-crud.yml',
164
+ 'resource/sls_yaml/generatedCode/source/generated-sns-sqs-relationship.yml',
165
+ 'resource/sls_yaml/generatedCode/source/generated-sns-sqs-rbac.yml',
166
+ 'resource/sls_yaml/generatedCode/source/generated-sns-sqs-custom-flows.yml',
167
+ 'resource/sls_yaml/generatedCode/source/generated-sns-sqs-system.yml',
127
168
  'resource/sls_yaml/generatedCode/source/generated-sns-out.yml',
128
169
  'resource/sls_yaml/generatedCode/source/generated-dynamoDB-table.yml',
129
170
  'app/sls_yaml/generatedCode/source/generate-shared-resource.yml'
@@ -153,16 +194,16 @@ test('emits the shared WebSocket completion queue and output-topic subscriptions
153
194
  const rootPath = await createProject();
154
195
  await generateResourceArtifacts(rootPath);
155
196
 
156
- const resources = await readFile(
197
+ const systemResources = await readFile(
157
198
  join(
158
199
  rootPath,
159
- 'resource/sls_yaml/generatedCode/source/generated-sns-in-sqs.yml'
200
+ 'resource/sls_yaml/generatedCode/source/generated-sns-sqs-system.yml'
160
201
  ),
161
202
  'utf8'
162
203
  );
163
- assert.match(resources, /^ WebSocketCompleteHdrSqs:/m);
164
- assert.match(resources, /SubscriptionToOutCustomWebSocketComplete:/);
165
- assert.match(resources, /SubscriptionToOutCustomRegisterComplete:/);
204
+ assert.match(systemResources, /^ WebSocketCompleteHdrSqs:/m);
205
+ assert.match(systemResources, /SubscriptionToOutCustomWebSocketComplete:/);
206
+ assert.match(systemResources, /SubscriptionToOutCustomRegisterComplete:/);
166
207
  const iam = await readFile(
167
208
  join(
168
209
  rootPath,
@@ -186,11 +227,11 @@ test('emits the shared WebSocket completion queue and output-topic subscriptions
186
227
  );
187
228
  }
188
229
  assert.match(
189
- resources,
230
+ systemResources,
190
231
  /SubscriptionToOutCreateRelationshipWebSocketComplete:/
191
232
  );
192
233
  assert.match(
193
- resources,
234
+ systemResources,
194
235
  /SubscriptionToOutCreateRelationshipRegisterComplete:/
195
236
  );
196
237
  });
@@ -250,7 +291,7 @@ test('emits DynamoDB table with number sortKey attribute type when field type is
250
291
  join(rootPath, 'app/src/schemas/ObjectSchemas/CartOrder.js'),
251
292
  "export default { " +
252
293
  "objectType: 'cartOrder', " +
253
- "generatedBy: 'userGenerated', " +
294
+ "generatedBy: 'user', " +
254
295
  "fieldNames: { " +
255
296
  " cartId: { type: 'string' }, " +
256
297
  " sortId: { type: 'number' } " +
@@ -329,7 +370,7 @@ async function createProject() {
329
370
  );
330
371
  await writeFile(
331
372
  join(schemas, 'ObjectSchemas/Order.js'),
332
- "export default { objectType: 'order', generatedBy: 'userGenerated', addOnDataStructure: [{ type: 'rbac' }], identifiers: [{ type: 'partitionKey', fieldName: 'orderId' }], storageResources: { main: { storageType: 'dynamoDB', tableName: 'Orders' } } };\n"
373
+ "export default { objectType: 'order', generatedBy: 'user', addOnDataStructure: [{ type: 'rbac' }], identifiers: [{ type: 'partitionKey', fieldName: 'orderId' }], storageResources: { main: { storageType: 'dynamoDB', tableName: 'Orders' } } };\n"
333
374
  );
334
375
  await writeFile(
335
376
  join(schemas, 'RelationshipSchemas/OrderCustomer.js'),
@@ -123,7 +123,7 @@ test('generate preserves user hooks inside SystemTextSchemas.js across regenerat
123
123
  );
124
124
  await writeFile(
125
125
  join(rootPath, 'app/src/schemas/ObjectSchemas/Item.js'),
126
- "export default { objectType: 'item', fieldNames: { itemId: { type: 'string' } }, generatedBy: 'userGenerated' };\n"
126
+ "export default { objectType: 'item', fieldNames: { itemId: { type: 'string' } }, generatedBy: 'user' };\n"
127
127
  );
128
128
 
129
129
  // First generate