@izara_project/izara-core-generate-service-code 1.0.98 → 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.
Files changed (70) hide show
  1. package/package.json +1 -1
  2. package/src/compile/compileProject.js +16 -5
  3. package/src/compile/flowValidator.js +47 -8
  4. package/src/compile/loadSchemas.js +18 -7
  5. package/src/compile/schemaValidatorSetup.js +16 -0
  6. package/src/emit/application/coreFunction/emitCoreFunctionArtifacts.js +123 -109
  7. package/src/emit/application/coreFunction/templates/InitialSetupConfig.ejs +8 -4
  8. package/src/emit/application/coreFunction/templates/InitialSetupRunner.ejs +2 -2
  9. package/src/emit/application/crud/templates/CreateComplete_Main.ejs +2 -0
  10. package/src/emit/application/crud/templates/CreateEndpoint_Main.ejs +4 -4
  11. package/src/emit/application/crud/templates/CreateLogicalLib.ejs +74 -15
  12. package/src/emit/application/crud/templates/DeleteComplete_Main.ejs +2 -0
  13. package/src/emit/application/crud/templates/DeleteEndpoint_Main.ejs +4 -4
  14. package/src/emit/application/crud/templates/FlowEndpoint_HdrWbs.ejs +7 -5
  15. package/src/emit/application/crud/templates/UpdateComplete_Main.ejs +2 -0
  16. package/src/emit/application/crud/templates/UpdateEndpoint_Main.ejs +4 -4
  17. package/src/emit/application/custom/customFlowPaths.js +13 -16
  18. package/src/emit/application/custom/customFlowS3.js +277 -66
  19. package/src/emit/application/custom/customFlows.js +131 -0
  20. package/src/emit/application/custom/emitCustomFlowApplicationArtifacts.js +3 -1
  21. package/src/emit/application/custom/templates/FlowEndpoint_Main.ejs +2 -2
  22. package/src/emit/application/custom/templates/s3/ConfirmAfterUpload_HdrSqs.ejs +110 -0
  23. package/src/emit/application/custom/templates/s3/ConfirmAfterUpload_Main.ejs +89 -0
  24. package/src/emit/application/custom/templates/s3/ConfirmComplete_HdrSqs.ejs +110 -0
  25. package/src/emit/application/custom/templates/s3/ConfirmComplete_Main.ejs +89 -0
  26. package/src/emit/application/custom/templates/s3/CreateObject_HdrWbs.ejs +2 -1
  27. package/src/emit/application/custom/templates/s3/CreateObject_Main.ejs +4 -0
  28. package/src/emit/application/custom/templates/s3/CreatePreSignUrl_HdrSqs.ejs +42 -0
  29. package/src/emit/application/custom/templates/s3/CreatePreSignUrl_Main.ejs +23 -0
  30. package/src/emit/application/custom/templates/s3/ProcessS3File_Main.ejs +135 -27
  31. package/src/emit/application/custom/templates/s3/Start_Main.ejs +197 -0
  32. package/src/emit/application/rbac/emitDynamicRbacFlowSchemas.js +8 -1
  33. package/src/emit/application/rbac/emitRbacApplicationArtifacts.js +107 -25
  34. package/src/emit/application/rbac/templates/ReservedDynamicUsage_Main.ejs +29 -9
  35. package/src/emit/application/rbac/templates/ReservedUsageTtlExpired_HdrDss.ejs +176 -0
  36. package/src/emit/application/rbac/templates/UserRbacFlow_Main.ejs +4 -3
  37. package/src/emit/application/system/templates/Register_HdrWbs.ejs +2 -1
  38. package/src/emit/infrastructure/functions/functionYamlArtifact.js +31 -8
  39. package/src/emit/infrastructure/functions/templates/DssFunction_Yaml.ejs +21 -0
  40. package/src/emit/infrastructure/functions/templates/S3Function_Yaml.ejs +2 -3
  41. package/src/emit/infrastructure/resources/iamResourceArtifacts.js +67 -5
  42. package/src/emit/infrastructure/resources/resourceState.js +15 -5
  43. package/src/emit/infrastructure/resources/s3ResourceArtifacts.js +19 -3
  44. package/src/emit/infrastructure/resources/templates/GeneratedLambdaRoleServiceConfig.ejs +5 -3
  45. package/src/emit/infrastructure/resources/templates/S3Bucket_Yaml.ejs +1 -1
  46. package/src/emit/infrastructure/resources/templates/SystemDynamoDB_Yaml.ejs +2 -2
  47. package/src/emit/schemas/attributeTree/emitAttributeTreeSchemas.js +62 -1
  48. package/src/emit/schemas/error/emitErrorSchemas.js +120 -10
  49. package/src/emit/schemas/error/templates/errorConsts.ejs +12 -0
  50. package/src/generate.js +32 -34
  51. package/src/output/mergeHooks.js +105 -5
  52. package/src/plan/domainRegistry.js +0 -10
  53. package/task.md +40 -96
  54. package/test/integration/graphDrivenFlowSchema.test.js +58 -0
  55. package/test/integration/publicApi.test.js +33 -24
  56. package/test/unit/application/createTargetRoleHandler.test.js +101 -9
  57. package/test/unit/application/crudApplicationArtifacts.test.js +4 -0
  58. package/test/unit/application/customFlowApplicationArtifacts.test.js +6 -6
  59. package/test/unit/application/customFlowS3.test.js +730 -0
  60. package/test/unit/infrastructure/functionYamlArtifact.test.js +6 -4
  61. package/test/unit/infrastructure/resourceArtifacts.test.js +103 -0
  62. package/test/unit/output/mergeHooks.test.js +137 -0
  63. package/test/unit/schemas/attributeTreeSchemas.test.js +32 -3
  64. package/test/unit/schemas/attributeTreeSingleService.test.js +175 -0
  65. package/test/unit/schemas/dynamicRbacFlowSchemas.test.js +8 -6
  66. package/test/unit/schemas/dynamicRelationshipFlowSchemas.test.js +51 -0
  67. package/test/unit/schemas/errorSchemas.test.js +84 -5
  68. package/test/unit/templates/hookTagsValidation.test.js +83 -0
  69. package/src/emit/schemas/attributeTree/emitAttributeTreeTargetValueStandard.js +0 -132
  70. package/test/unit/schemas/attributeTreeDirectives.test.js +0 -177
@@ -83,14 +83,16 @@ test('filters all SQS functions by their calling flow', () => {
83
83
  /#<#CreateCompleteHdrSqsFilterPatterns#>[\s\S]*filterPatterns:[\s\S]*\$\{self:custom\.iz_resourcePrefix\}CreateCompleteHdrSqs/
84
84
  );
85
85
  assert.match(completion, /#<#\/CreateCompleteHdrSqsFilterPatterns#>/);
86
- assert.match(completion, /callingFlow:\s*\n\s*- exists: false/);
87
- assert.match(completion, /MessageAttributes:\s*\n\s*- exists: false/);
86
+ assert.match(completion, /callingFlow:\s*\n\s*StringValue:/);
87
+ assert.doesNotMatch(completion, /callingFlow:\s*\n\s*- exists: false/);
88
+ assert.doesNotMatch(completion, /MessageAttributes:\s*\n\s*- exists: false/);
88
89
  assert.match(
89
90
  ordinary,
90
91
  /#<#CreateHdrSqsFilterPatterns#>[\s\S]*filterPatterns:[\s\S]*\$\{self:custom\.iz_resourcePrefix\}CreateHdrSqs/
91
92
  );
92
93
  assert.match(ordinary, /#<#\/CreateHdrSqsFilterPatterns#>/);
93
- assert.match(ordinary, /callingFlow:\s*\n\s*- exists: false/);
94
- assert.match(ordinary, /MessageAttributes:\s*\n\s*- exists: false/);
94
+ assert.match(ordinary, /callingFlow:\s*\n\s*StringValue:/);
95
+ assert.doesNotMatch(ordinary, /callingFlow:\s*\n\s*- exists: false/);
96
+ assert.doesNotMatch(ordinary, /MessageAttributes:\s*\n\s*- exists: false/);
95
97
  });
96
98
 
@@ -7,6 +7,10 @@ import test, { after } from 'node:test';
7
7
  process.env.NODE_ENV = 'test';
8
8
 
9
9
  import { generateResourceArtifacts } from '../../../src/emit/infrastructure/resources/generateResourceArtifacts.js';
10
+ import {
11
+ renderS3,
12
+ toAlphanumericPascalCase
13
+ } from '../../../src/emit/infrastructure/resources/s3ResourceArtifacts.js';
10
14
 
11
15
  const roots = new Set();
12
16
  after(() =>
@@ -816,6 +820,58 @@ test('emits lambda:InvokeFunction in FlowObjectsRole when flow has event generat
816
820
  assert.match(role, /GenSyncHook/);
817
821
  });
818
822
 
823
+ test('emits DynamoDB stream permissions in FlowRbacRole and multi-tenant quota topics in FlowObjectsRole', async () => {
824
+ const rootPath = await createProject();
825
+ const schemas = join(rootPath, 'app/src/schemas');
826
+ await writeFile(
827
+ join(schemas, 'ObjectSchemas/Order.js'),
828
+ "export default { objectType: 'order', generatedBy: 'user', belongTo: { serviceTag: 'SupplyNetwork', objectType: 'tenant' }, addOnDataStructure: [{ type: 'rbac' }], identifiers: [{ type: 'partitionKey', fieldName: 'orderId' }], storageResources: { main: { storageType: 'dynamoDB', tableName: 'Orders' } } };\n"
829
+ );
830
+ await generateResourceArtifacts(rootPath);
831
+
832
+ const iam = await readFile(
833
+ join(
834
+ rootPath,
835
+ 'app/sls_yaml/generatedCode/source/generate-shared-resource.yml'
836
+ ),
837
+ 'utf8'
838
+ );
839
+ const rbacRole = roleBlock(iam, 'FlowRbacRole');
840
+ assert.match(rbacRole, /dynamodb:DescribeStream/);
841
+ assert.match(rbacRole, /RunningTotalReservedUsage\/stream\/\*/);
842
+
843
+ const flowObjectsRole = roleBlock(iam, 'FlowObjectsRole');
844
+ assert.match(flowObjectsRole, /SupplyNetwork_\${self:custom\.iz_stage}_ConfirmUsage_In/);
845
+ assert.match(flowObjectsRole, /SupplyNetwork_\${self:custom\.iz_stage}_CancelUsage_In/);
846
+ assert.match(flowObjectsRole, /SupplyNetwork\${self:custom\.iz_stage}ConfirmUsageHdrInv/);
847
+ assert.match(flowObjectsRole, /SupplyNetwork\${self:custom\.iz_stage}CancelUsageHdrInv/);
848
+
849
+ const { createResourceState } = await import(
850
+ '../../../src/emit/infrastructure/resources/resourceState.js'
851
+ );
852
+ const { renderLambdaRoleConfig } = await import(
853
+ '../../../src/emit/infrastructure/resources/iamResourceArtifacts.js'
854
+ );
855
+ const state = createResourceState({
856
+ objects: [
857
+ {
858
+ objectType: 'order',
859
+ generatedBy: 'user',
860
+ belongTo: { serviceTag: 'SupplyNetwork', objectType: 'tenant' },
861
+ addOnDataStructure: [{ type: 'rbac' }]
862
+ }
863
+ ],
864
+ relationships: [],
865
+ flows: [
866
+ { flowTag: 'upload', event: ['s3'], quotaServiceTag: 'SupplyNetwork' }
867
+ ]
868
+ });
869
+ const lambdaRoleConfig = await renderLambdaRoleConfig(state);
870
+ assert.match(lambdaRoleConfig, /ConfirmUsage_In', 'SupplyNetwork'/);
871
+ assert.match(lambdaRoleConfig, /CancelUsage_In', 'SupplyNetwork'/);
872
+ assert.match(lambdaRoleConfig, /ReservedDynamicUsage_In', 'SupplyNetwork'/);
873
+ assert.doesNotMatch(lambdaRoleConfig, /'AccountLimits'/);
874
+ });
819
875
 
820
876
  function roleBlock(iam, roleName) {
821
877
  const match = iam.match(
@@ -857,3 +913,50 @@ async function createProject() {
857
913
  );
858
914
  return rootPath;
859
915
  }
916
+
917
+ test('toAlphanumericPascalCase converts non-alphanumeric strings to PascalCase', () => {
918
+ assert.equal(
919
+ toAlphanumericPascalCase('video-record-temp-test'),
920
+ 'VideoRecordTempTest'
921
+ );
922
+ assert.equal(
923
+ toAlphanumericPascalCase('video.record_temp-test'),
924
+ 'VideoRecordTempTest'
925
+ );
926
+ assert.equal(toAlphanumericPascalCase('AlreadyPascal'), 'AlreadyPascal');
927
+ assert.equal(toAlphanumericPascalCase(''), '');
928
+ });
929
+
930
+ test('renderS3 generates alphanumeric PascalCase Logical ID for CloudFormation', async () => {
931
+ const yaml = await renderS3({
932
+ s3Buckets: ['video-record-temp-test']
933
+ });
934
+ assert.ok(
935
+ yaml.includes('VideoRecordTempTestS3Bucket:'),
936
+ 'Logical ID must be alphanumeric PascalCase without hyphens'
937
+ );
938
+ assert.ok(
939
+ !yaml.includes('video-record-temp-testS3Bucket:'),
940
+ 'Logical ID must not contain hyphens'
941
+ );
942
+ assert.ok(
943
+ yaml.includes('BucketName: video-record-temp-test'),
944
+ 'BucketName property must preserve original bucket name'
945
+ );
946
+ });
947
+
948
+ test('roleStatements emits s3:GetObject for custom S3 flow bucket', async () => {
949
+ const rootPath = await createProject();
950
+ await generateResourceArtifacts(rootPath);
951
+ const iam = await readFile(
952
+ join(
953
+ rootPath,
954
+ 'app/sls_yaml/generatedCode/source/generate-shared-resource.yml'
955
+ ),
956
+ 'utf8'
957
+ );
958
+ const uploadRole = roleBlock(iam, 'UploadRole');
959
+ assert.match(uploadRole, /s3:GetObject/);
960
+ assert.match(uploadRole, /arn:aws:s3:::UploadBucket\/\*/);
961
+ });
962
+
@@ -266,3 +266,140 @@ test('respects writeWhenFileExist false flag by returning disk content unchanged
266
266
  await rm(tmpDir, { recursive: true, force: true });
267
267
  }
268
268
  });
269
+
270
+ test('merges hook when disk content has legacy tag without closing angle bracket', async () => {
271
+ const tmpDir = await mkdtemp(path.join(os.tmpdir(), 'mergeHooks-test-'));
272
+ try {
273
+ const filePath = 'app/src/Flow_Main.js';
274
+ const diskContent = [
275
+ '//(<rbacPublish_CreatePlugInConn)',
276
+ 'let plugInTag = requestParams.plugInTag;',
277
+ '//(</rbacPublish_CreatePlugInConn)'
278
+ ].join('\n') + '\n';
279
+ await writeTestFile(tmpDir, filePath, diskContent);
280
+
281
+ const artifacts = [
282
+ {
283
+ path: filePath,
284
+ content: [
285
+ '//(<rbacPublish_CreatePlugInConn>)',
286
+ '//(</rbacPublish_CreatePlugInConn>)'
287
+ ].join('\n') + '\n'
288
+ }
289
+ ];
290
+
291
+ const result = await mergeHooks(tmpDir, artifacts);
292
+ assert.equal(
293
+ result[0].content,
294
+ [
295
+ '//(<rbacPublish_CreatePlugInConn>)',
296
+ 'let plugInTag = requestParams.plugInTag;',
297
+ '//(</rbacPublish_CreatePlugInConn>)'
298
+ ].join('\n') + '\n'
299
+ );
300
+ } finally {
301
+ await rm(tmpDir, { recursive: true, force: true });
302
+ }
303
+ });
304
+
305
+ test('merges hook when both template and disk content omit closing angle bracket', async () => {
306
+ const tmpDir = await mkdtemp(path.join(os.tmpdir(), 'mergeHooks-test-'));
307
+ try {
308
+ const filePath = 'app/src/Legacy_Main.js';
309
+ const diskContent = [
310
+ '//(<legacyHook)',
311
+ 'const legacyValue = 42;',
312
+ '//(</legacyHook)'
313
+ ].join('\n') + '\n';
314
+ await writeTestFile(tmpDir, filePath, diskContent);
315
+
316
+ const artifacts = [
317
+ {
318
+ path: filePath,
319
+ content: [
320
+ '//(<legacyHook)',
321
+ '//(</legacyHook)'
322
+ ].join('\n') + '\n'
323
+ }
324
+ ];
325
+
326
+ const result = await mergeHooks(tmpDir, artifacts);
327
+ assert.equal(
328
+ result[0].content,
329
+ [
330
+ '//(<legacyHook>)',
331
+ 'const legacyValue = 42;',
332
+ '//(</legacyHook>)'
333
+ ].join('\n') + '\n'
334
+ );
335
+ } finally {
336
+ await rm(tmpDir, { recursive: true, force: true });
337
+ }
338
+ });
339
+
340
+ test('throws error when source content has malformed hook tags (unclosed open tag)', async () => {
341
+ const tmpDir = await mkdtemp(path.join(os.tmpdir(), 'mergeHooks-test-'));
342
+ try {
343
+ const filePath = 'app/src/Broken_Main.js';
344
+ const diskContent = [
345
+ '//(<openHook>)',
346
+ 'const value = 1;',
347
+ '// without closing tag'
348
+ ].join('\n') + '\n';
349
+ await writeTestFile(tmpDir, filePath, diskContent);
350
+
351
+ const artifacts = [
352
+ {
353
+ path: filePath,
354
+ content: '//(<openHook>)\n//(</openHook>)\n'
355
+ }
356
+ ];
357
+
358
+ await assert.rejects(
359
+ async () => {
360
+ await mergeHooks(tmpDir, artifacts);
361
+ },
362
+ /\[mergeHooks\] Malformed hook tags in 'app\/src\/Broken_Main\.js'/
363
+ );
364
+ } finally {
365
+ await rm(tmpDir, { recursive: true, force: true });
366
+ }
367
+ });
368
+
369
+ test('warns when source content contains custom code in hook missing from template', async () => {
370
+ const tmpDir = await mkdtemp(path.join(os.tmpdir(), 'mergeHooks-test-'));
371
+ const originalWarn = console.warn;
372
+ const warnings = [];
373
+ console.warn = (...args) => warnings.push(args.join(' '));
374
+
375
+ try {
376
+ const filePath = 'app/src/Orphaned_Main.js';
377
+ const diskContent = [
378
+ '//(<removedHook>)',
379
+ 'const importantCustomCode = true;',
380
+ '//(</removedHook>)'
381
+ ].join('\n') + '\n';
382
+ await writeTestFile(tmpDir, filePath, diskContent);
383
+
384
+ const artifacts = [
385
+ {
386
+ path: filePath,
387
+ content: '//(<newHook>)\n//(</newHook>)\n'
388
+ }
389
+ ];
390
+
391
+ const result = await mergeHooks(tmpDir, artifacts);
392
+ assert.equal(result[0].content, '//(<newHook>)\n//(</newHook>)\n');
393
+ assert.ok(
394
+ warnings.some((w) =>
395
+ w.includes("Hook 'removedHook' in 'app/src/Orphaned_Main.js' contains custom code")
396
+ ),
397
+ 'Should log warning for orphaned hook with custom code'
398
+ );
399
+ } finally {
400
+ console.warn = originalWarn;
401
+ await rm(tmpDir, { recursive: true, force: true });
402
+ }
403
+ });
404
+
405
+
@@ -23,7 +23,7 @@ after(async () => {
23
23
  await cleanupTestProjects();
24
24
  });
25
25
 
26
- test('emitAttributeTreeObjectSchemas returns 3 object schemas per attributeTree tag', () => {
26
+ test('emitAttributeTreeObjectSchemas returns 4 object schemas per attributeTree tag', () => {
27
27
  const objects = [
28
28
  {
29
29
  objectType: 'sellOffer',
@@ -48,13 +48,14 @@ test('emitAttributeTreeObjectSchemas returns 3 object schemas per attributeTree
48
48
  ];
49
49
 
50
50
  const artifacts = emitAttributeTreeObjectSchemas({ objects });
51
- assert.equal(artifacts.length, 3);
51
+ assert.equal(artifacts.length, 4);
52
52
 
53
53
  const paths = artifacts.map((a) => a.path);
54
54
  assert.deepEqual(paths, [
55
55
  'app/src/schemas/ObjectSchemas/generatedCode/attributeTree/sellOfferTermLink.js',
56
56
  'app/src/schemas/ObjectSchemas/generatedCode/attributeTree/sellOfferTermValue.js',
57
- 'app/src/schemas/ObjectSchemas/generatedCode/attributeTree/sellOfferTermLabel.js'
57
+ 'app/src/schemas/ObjectSchemas/generatedCode/attributeTree/sellOfferTermLabel.js',
58
+ 'app/src/schemas/ObjectSchemas/generatedCode/attributeTree/sellOfferTermValueStandard.js'
58
59
  ]);
59
60
 
60
61
  const valueArtifact = artifacts.find((a) =>
@@ -63,6 +64,26 @@ test('emitAttributeTreeObjectSchemas returns 3 object schemas per attributeTree
63
64
  assert.match(valueArtifact.content, /"objectType": "sellOfferTermValue"/);
64
65
  assert.match(valueArtifact.content, /"sellOfferTermValueId"/);
65
66
 
67
+ const valueStandardArtifact = artifacts.find((a) =>
68
+ a.path.endsWith('sellOfferTermValueStandard.js')
69
+ );
70
+ assert.match(
71
+ valueStandardArtifact.content,
72
+ /objectType:\s*['"]sellOfferTermValueStandard['"]/
73
+ );
74
+ assert.match(
75
+ valueStandardArtifact.content,
76
+ /sellOfferTermValueStandardId/
77
+ );
78
+ assert.match(
79
+ valueStandardArtifact.content,
80
+ /\/\/\(<addOnDataStructuresellOfferTermValueStandard>\)/
81
+ );
82
+
83
+ const managedPaths = attributeTreeObjectSchemaManagedPaths(objects);
84
+ assert.equal(managedPaths.length, 4);
85
+ assert.deepEqual(managedPaths, paths);
86
+
66
87
  for (const artifact of artifacts) {
67
88
  assert.doesNotMatch(artifact.content, /dynamoDB/);
68
89
  assert.match(artifact.content, /GraphHandler/);
@@ -152,6 +173,8 @@ test('generate commits and cleans up attributeTree dynamic schemas', async () =>
152
173
 
153
174
  const objectRelPath =
154
175
  'app/src/schemas/ObjectSchemas/generatedCode/attributeTree/sellOfferTermLink.js';
176
+ const valueStandardRelPath =
177
+ 'app/src/schemas/ObjectSchemas/generatedCode/attributeTree/sellOfferTermValueStandard.js';
155
178
  const relRelPath =
156
179
  'app/src/schemas/RelationshipSchemas/generatedCode/attributeTree/hasSellOfferTermLink.js';
157
180
 
@@ -160,6 +183,9 @@ test('generate commits and cleans up attributeTree dynamic schemas', async () =>
160
183
  assert.match(objectContent, /GraphHandler/);
161
184
  assert.doesNotMatch(objectContent, /dynamoDB/);
162
185
 
186
+ const valueStandardContent = await readProjectFile(rootPath, valueStandardRelPath);
187
+ assert.match(valueStandardContent, /sellOfferTermValueStandard/);
188
+
163
189
  const relContent = await readProjectFile(rootPath, relRelPath);
164
190
  assert.match(relContent, /hasSellOfferTermLink/);
165
191
  assert.match(relContent, /sellOfferTermLink/);
@@ -171,6 +197,9 @@ test('generate commits and cleans up attributeTree dynamic schemas', async () =>
171
197
  await assert.rejects(readProjectFile(rootPath, objectRelPath), {
172
198
  code: 'ENOENT'
173
199
  });
200
+ await assert.rejects(readProjectFile(rootPath, valueStandardRelPath), {
201
+ code: 'ENOENT'
202
+ });
174
203
  await assert.rejects(readProjectFile(rootPath, relRelPath), {
175
204
  code: 'ENOENT'
176
205
  });
@@ -0,0 +1,175 @@
1
+ import assert from 'node:assert/strict';
2
+ import test, { after } from 'node:test';
3
+ import { generate } from '../../../src/generate.js';
4
+ import {
5
+ createTestProject,
6
+ readProjectFile,
7
+ writeProjectFile,
8
+ cleanupTestProjects
9
+ } from '../../helpers/testProject.js';
10
+
11
+ after(async () => {
12
+ await cleanupTestProjects();
13
+ });
14
+
15
+ test('single-service attributeTree generation and propertyValue cascading', async () => {
16
+ const initialSchema = `export default [
17
+ {
18
+ objectType: 'sellOffer',
19
+ canDelete: false,
20
+ generatedBy: 'user',
21
+ addOnDataStructure: [
22
+ {
23
+ type: 'attributeTree',
24
+ attributeTreeTag: 'sellOfferTerm'
25
+ }
26
+ ],
27
+ storageResources: {
28
+ dynamoDB: {
29
+ storageType: 'dynamoDB',
30
+ tableName: 'SellOfferRecords',
31
+ serviceTag: 'SellOfferManager'
32
+ },
33
+ myGraph: {
34
+ storageType: 'graph',
35
+ graphServerTag: 'GraphHandler'
36
+ }
37
+ },
38
+ fieldNames: {
39
+ sellOfferId: {
40
+ type: 'string',
41
+ randomOnCreate: true,
42
+ canUpdate: false,
43
+ storageResourceTags: ['dynamoDB', 'myGraph']
44
+ }
45
+ },
46
+ identifiers: [
47
+ {
48
+ type: 'partitionKey',
49
+ fieldName: 'sellOfferId'
50
+ }
51
+ ]
52
+ }
53
+ ];
54
+ `;
55
+
56
+ const rootPath = await createTestProject({
57
+ prefix: 'izara-single-service-attr-tree-',
58
+ serviceTag: 'SellOfferManager',
59
+ schemas: {
60
+ 'ObjectSchemas/SellOffer.js': initialSchema
61
+ }
62
+ });
63
+
64
+ // Phase 1: generate produces all 4 ObjectSchemas and 4 RelationshipSchemas
65
+ await generate(rootPath);
66
+
67
+ const linkPath =
68
+ 'app/src/schemas/ObjectSchemas/generatedCode/attributeTree/sellOfferTermLink.js';
69
+ const valuePath =
70
+ 'app/src/schemas/ObjectSchemas/generatedCode/attributeTree/sellOfferTermValue.js';
71
+ const labelPath =
72
+ 'app/src/schemas/ObjectSchemas/generatedCode/attributeTree/sellOfferTermLabel.js';
73
+ const valueStandardPath =
74
+ 'app/src/schemas/ObjectSchemas/generatedCode/attributeTree/sellOfferTermValueStandard.js';
75
+
76
+ const linkContent = await readProjectFile(rootPath, linkPath);
77
+ assert.match(linkContent, /sellOfferTermLink/);
78
+
79
+ const valueContent = await readProjectFile(rootPath, valuePath);
80
+ assert.match(valueContent, /sellOfferTermValue/);
81
+
82
+ const labelContent = await readProjectFile(rootPath, labelPath);
83
+ assert.match(labelContent, /sellOfferTermLabel/);
84
+
85
+ const valueStandardContent = await readProjectFile(
86
+ rootPath,
87
+ valueStandardPath
88
+ );
89
+ assert.match(valueStandardContent, /sellOfferTermValueStandard/);
90
+ assert.match(
91
+ valueStandardContent,
92
+ /\/\/\(<addOnDataStructuresellOfferTermValueStandard>\)/
93
+ );
94
+
95
+ // Check relationship schemas
96
+ const hasLinkRelPath =
97
+ 'app/src/schemas/RelationshipSchemas/generatedCode/attributeTree/hasSellOfferTermLink.js';
98
+ const hasLinkRel = await readProjectFile(rootPath, hasLinkRelPath);
99
+ assert.match(hasLinkRel, /hasSellOfferTermLink/);
100
+
101
+ // Phase 2: developer uncomments propertyValue addOn in ValueStandard
102
+ const modifiedValueStandard = valueStandardContent.replace(
103
+ '//(<addOnDataStructuresellOfferTermValueStandard>)\n // {\n // type: \'propertyValue\',\n // propertyValuePrefix: \'sellOfferTermValueStandard\'\n // }\n //(</addOnDataStructuresellOfferTermValueStandard>)',
104
+ '//(<addOnDataStructuresellOfferTermValueStandard>)\n {\n type: \'propertyValue\',\n propertyValuePrefix: \'sellOfferTermValueStandard\'\n }\n //(</addOnDataStructuresellOfferTermValueStandard>)'
105
+ );
106
+ await writeProjectFile(rootPath, valueStandardPath, modifiedValueStandard);
107
+
108
+ await generate(rootPath);
109
+
110
+ // Verify hook preservation
111
+ const reloadedValueStandard = await readProjectFile(
112
+ rootPath,
113
+ valueStandardPath
114
+ );
115
+ assert.match(reloadedValueStandard, /type:\s*'propertyValue'/);
116
+
117
+ // Verify cascading generation: propertyValue domain in the same repo
118
+ const propLabelPath =
119
+ 'app/src/schemas/ObjectSchemas/generatedCode/propertyValue/sellOfferTermValueStandardPropertyLabel.js';
120
+ const propValuePath =
121
+ 'app/src/schemas/ObjectSchemas/generatedCode/propertyValue/sellOfferTermValueStandardPropertyValue.js';
122
+ const hasPropRelPath =
123
+ 'app/src/schemas/RelationshipSchemas/generatedCode/propertyValue/hasSellOfferTermValueStandardPropertyValue.js';
124
+ const disPropRelPath =
125
+ 'app/src/schemas/RelationshipSchemas/generatedCode/propertyValue/disabledSellOfferTermValueStandardPropertyValue.js';
126
+ const isPropRelPath =
127
+ 'app/src/schemas/RelationshipSchemas/generatedCode/propertyValue/isSellOfferTermValueStandardPropertyLabel.js';
128
+
129
+ const propLabel = await readProjectFile(rootPath, propLabelPath);
130
+ assert.match(propLabel, /sellOfferTermValueStandardPropertyLabel/);
131
+
132
+ const propValue = await readProjectFile(rootPath, propValuePath);
133
+ assert.match(propValue, /sellOfferTermValueStandardPropertyValue/);
134
+
135
+ const hasPropRel = await readProjectFile(rootPath, hasPropRelPath);
136
+ assert.match(hasPropRel, /hasSellOfferTermValueStandardPropertyValue/);
137
+
138
+ const disPropRel = await readProjectFile(rootPath, disPropRelPath);
139
+ assert.match(disPropRel, /disabledSellOfferTermValueStandardPropertyValue/);
140
+
141
+ const isPropRel = await readProjectFile(rootPath, isPropRelPath);
142
+ assert.match(isPropRel, /isSellOfferTermValueStandardPropertyLabel/);
143
+
144
+ // Cleanup check: remove attributeTree addOn from base schema
145
+ const cleanSchema = `export default [
146
+ {
147
+ objectType: 'sellOffer',
148
+ canDelete: false,
149
+ generatedBy: 'user',
150
+ addOnDataStructure: [],
151
+ storageResources: {
152
+ myGraph: { storageType: 'graph', graphServerTag: 'GraphHandler' }
153
+ },
154
+ fieldNames: {
155
+ sellOfferId: { type: 'string', storageResourceTags: ['myGraph'] }
156
+ },
157
+ identifiers: [{ type: 'partitionKey', fieldName: 'sellOfferId' }]
158
+ }
159
+ ];
160
+ `;
161
+ await writeProjectFile(
162
+ rootPath,
163
+ 'app/src/schemas/ObjectSchemas/SellOffer.js',
164
+ cleanSchema
165
+ );
166
+ await generate(rootPath);
167
+
168
+ // All attributeTree schemas should be removed
169
+ await assert.rejects(readProjectFile(rootPath, linkPath), { code: 'ENOENT' });
170
+ await assert.rejects(readProjectFile(rootPath, valuePath), { code: 'ENOENT' });
171
+ await assert.rejects(readProjectFile(rootPath, labelPath), { code: 'ENOENT' });
172
+ await assert.rejects(readProjectFile(rootPath, valueStandardPath), {
173
+ code: 'ENOENT'
174
+ });
175
+ });
@@ -26,7 +26,8 @@ const STATIC_RBAC_ACTIONS = [
26
26
  const STATIC_LIMIT_ACTIONS = [
27
27
  'ReservedDynamicUsage',
28
28
  'ConfirmUsage',
29
- 'CancelUsage'
29
+ 'CancelUsage',
30
+ 'ReservedUsageTtlExpired'
30
31
  ];
31
32
 
32
33
  after(async () => {
@@ -113,11 +114,12 @@ test('generates shared and object-specific RBAC flow schemas', async () => {
113
114
  'lambdaSyncInv',
114
115
  'lambdaSyncApi'
115
116
  ]);
116
- assert.deepEqual(cancelUsage.event, [
117
- 'ownTopic',
118
- 'lambdaSyncInv',
119
- 'lambdaSyncApi'
120
- ]);
117
+ const reservedUsageTtlExpired = await readGeneratedFlow(
118
+ rootPath,
119
+ 'ReservedUsageTtlExpired',
120
+ 'FlowLimit'
121
+ );
122
+ assert.deepEqual(reservedUsageTtlExpired.event, ['dss']);
121
123
  });
122
124
 
123
125
  test('removes RBAC schemas when the last RBAC object is removed', async () => {
@@ -6,6 +6,7 @@ import test, { after } from 'node:test';
6
6
  import { pathToFileURL } from 'node:url';
7
7
 
8
8
  import { generateDynamicRelationshipFlowSchemas } from '../../../src/emit/application/relationship/generateDynamicRelationshipFlowSchemas.js';
9
+ import { loadLocalSchemaDirectory } from '../../../src/compile/loadSchemas.js';
9
10
 
10
11
  const RELATIONSHIP_ACTIONS = [
11
12
  'CreateRelationship',
@@ -136,6 +137,56 @@ test('does not allow options to override the rootPath argument', async () => {
136
137
  );
137
138
  });
138
139
 
140
+ test('validates relationship schemas with systemNoUser and systemWithUser generatedBy', async () => {
141
+ const rootPath = await mkdtemp(
142
+ join(tmpdir(), 'generate-service-code-v2-rel-val-')
143
+ );
144
+ projectRoots.add(rootPath);
145
+ const relationshipDirectory = join(
146
+ rootPath,
147
+ 'app/src/schemas/RelationshipSchemas'
148
+ );
149
+ await mkdir(relationshipDirectory, { recursive: true });
150
+ const relSchema = [
151
+ {
152
+ isItemLabel: {
153
+ fieldNames: {},
154
+ storageResources: {
155
+ myGraph: { storageType: 'graph', graphServerTag: 'GraphHandler' }
156
+ },
157
+ generatedBy: 'systemNoUser',
158
+ links: [
159
+ {
160
+ storageResourceTags: ['myGraph'],
161
+ from: {
162
+ objType: { serviceTag: 'ItemSvc', objectType: 'itemValue' },
163
+ linkType: 'many',
164
+ requiredOnCreate: true
165
+ },
166
+ to: {
167
+ objType: { serviceTag: 'ItemSvc', objectType: 'itemLabel' },
168
+ linkType: 'one'
169
+ }
170
+ }
171
+ ]
172
+ }
173
+ }
174
+ ];
175
+ await writeFile(
176
+ join(relationshipDirectory, 'isItemLabel.js'),
177
+ `export default ${JSON.stringify(relSchema, null, 2)};\n`
178
+ );
179
+
180
+ const schemas = await loadLocalSchemaDirectory(
181
+ { schemaRoot: join(rootPath, 'app/src/schemas') },
182
+ 'RelationshipSchemas',
183
+ { validateSchemas: true }
184
+ );
185
+
186
+ assert.equal(schemas.length, 1);
187
+ assert.equal(schemas[0].isItemLabel.generatedBy, 'systemNoUser');
188
+ });
189
+
139
190
  async function createProject({ singletonExport = false } = {}) {
140
191
  const rootPath = await mkdtemp(
141
192
  join(tmpdir(), 'generate-service-code-v2-relationship-')