@izara_project/izara-core-generate-service-code 1.0.98 → 1.0.99

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 (34) hide show
  1. package/package.json +1 -1
  2. package/src/compile/flowValidator.js +45 -8
  3. package/src/compile/loadSchemas.js +0 -3
  4. package/src/emit/application/coreFunction/emitCoreFunctionArtifacts.js +15 -1
  5. package/src/emit/application/coreFunction/templates/InitialSetupConfig.ejs +8 -4
  6. package/src/emit/application/crud/templates/CreateEndpoint_Main.ejs +4 -4
  7. package/src/emit/application/crud/templates/CreateLogicalLib.ejs +60 -8
  8. package/src/emit/application/crud/templates/DeleteEndpoint_Main.ejs +4 -4
  9. package/src/emit/application/crud/templates/UpdateEndpoint_Main.ejs +4 -4
  10. package/src/emit/application/custom/customFlowS3.js +4 -4
  11. package/src/emit/application/custom/templates/FlowEndpoint_Main.ejs +2 -2
  12. package/src/emit/application/rbac/emitDynamicRbacFlowSchemas.js +4 -1
  13. package/src/emit/application/rbac/emitRbacApplicationArtifacts.js +76 -1
  14. package/src/emit/application/rbac/templates/ReservedDynamicUsage_Main.ejs +2 -0
  15. package/src/emit/application/rbac/templates/ReservedUsageTtlExpired_HdrDss.ejs +176 -0
  16. package/src/emit/infrastructure/functions/functionYamlArtifact.js +28 -0
  17. package/src/emit/infrastructure/functions/templates/DssFunction_Yaml.ejs +21 -0
  18. package/src/emit/infrastructure/resources/iamResourceArtifacts.js +45 -5
  19. package/src/emit/infrastructure/resources/templates/GeneratedLambdaRoleServiceConfig.ejs +5 -3
  20. package/src/emit/schemas/attributeTree/emitAttributeTreeSchemas.js +62 -1
  21. package/src/generate.js +2 -6
  22. package/src/output/mergeHooks.js +105 -5
  23. package/src/plan/domainRegistry.js +0 -10
  24. package/task.md +40 -96
  25. package/test/integration/graphDrivenFlowSchema.test.js +58 -0
  26. package/test/unit/application/createTargetRoleHandler.test.js +70 -1
  27. package/test/unit/application/crudApplicationArtifacts.test.js +4 -0
  28. package/test/unit/infrastructure/resourceArtifacts.test.js +52 -0
  29. package/test/unit/output/mergeHooks.test.js +137 -0
  30. package/test/unit/schemas/attributeTreeSchemas.test.js +32 -3
  31. package/test/unit/schemas/attributeTreeSingleService.test.js +175 -0
  32. package/test/unit/templates/hookTagsValidation.test.js +83 -0
  33. package/src/emit/schemas/attributeTree/emitAttributeTreeTargetValueStandard.js +0 -132
  34. package/test/unit/schemas/attributeTreeDirectives.test.js +0 -177
package/task.md CHANGED
@@ -1,132 +1,76 @@
1
- # Task Checklist: Attribute Tree Per-Repo Code Generation Refactoring
1
+ # Task Checklist: Attribute Tree Single-Service Code Generation Consolidation
2
2
 
3
3
  > **Canonical Architecture Plan**: [[10.Systems/GenerateServiceCode/30-Generation/Task Roadmap - Attribute Tree Per-Repo Generation Refactoring|Task Roadmap in Obsidian]]
4
- > **Selected Design**: **Option C (`GenerateDirectives/`)** + **Style 1 (`generatedCode/<domain>/`)**
4
+ > **Selected Design**: **Single Main Service (Self-Repo Consolidation)** + **Style 1 (`generatedCode/<domain>/`)**
5
5
  > **Repository**: `IzaraCore-SupportServices-GenerateServiceCode-Seagame`
6
6
 
7
7
  ---
8
8
 
9
- ## 🎯 Target Architecture: Style 1 (`generatedCode/<domain>/`)
9
+ ## 🎯 Target Architecture: Single Unified Service (Self-Repo)
10
+
11
+ All schemas for an `attributeTree` (Tree nodes, Standard Value Handler node, and cascading PropertyValue nodes) are emitted directly inside the **Main Service** workspace:
10
12
 
11
- ### Target Service (e.g. `SellOfferTermValueStandard`)
12
13
  ```text
13
14
  app/src/schemas/
14
- ├── GenerateDirectives/
15
- │ └── sellOfferTermValueStandard.js <-- [Directive] Developer creates this
16
- │
17
15
  ├── ObjectSchemas/
16
+ │ ├── sellOffer.js <-- Source ObjectSchema
18
17
  │ └── generatedCode/
19
18
  │ ├── attributeTree/
20
- │ │ └── sellOfferTermValueStandard.js <-- [Emitted by TASK-02]
21
- │ └── propertyValue/
22
- │ ├── sellOfferTermValueStandardPropertyLabel.js <-- [Emitted by PropertyValue addOn]
23
- │ └── sellOfferTermValueStandardPropertyValue.js <-- [Emitted by PropertyValue addOn]
19
+ │ │ ├── sellOfferTermLink.js <-- Link Node
20
+ │ │ ├── sellOfferTermValue.js <-- Value Node
21
+ │ │ ├── sellOfferTermLabel.js <-- Label Node
22
+ │ │ └── sellOfferTermValueStandard.js <-- Handler Node (Self-Repo)
23
+ │ │
24
+ │ └── propertyValue/ <-- Cascading from ValueStandard
25
+ │ ├── sellOfferTermValueStandardPropertyLabel.js
26
+ │ └── sellOfferTermValueStandardPropertyValue.js
24
27
  │
25
28
  └── RelationshipSchemas/
26
29
  └── generatedCode/
30
+ ├── attributeTree/
31
+ │ ├── hasSellOfferTermLink.js
32
+ │ ├── disabledSellOfferTermLink.js
33
+ │ ├── isSellOfferTerm.js
34
+ │ └── isSellOfferTermLabel.js
35
+ │
27
36
  └── propertyValue/
28
37
  ├── hasSellOfferTermValueStandardPropertyValue.js
29
38
  ├── disabledSellOfferTermValueStandardPropertyValue.js
30
39
  └── isSellOfferTermValueStandardPropertyLabel.js
31
40
  ```
32
41
 
33
- ### Source Service (Self-Repo)
34
- ```text
35
- app/src/schemas/
36
- ├── ObjectSchemas/
37
- │ ├── sellOffer.js <-- Source ObjectSchema
38
- │ └── generatedCode/
39
- │ └── attributeTree/
40
- │ ├── sellOfferTermLink.js
41
- │ ├── sellOfferTermValue.js
42
- │ └── sellOfferTermLabel.js
43
- │
44
- └── RelationshipSchemas/
45
- └── generatedCode/
46
- └── attributeTree/
47
- ├── hasSellOfferTermLink.js
48
- ├── disabledSellOfferTermLink.js
49
- ├── isSellOfferTerm.js
50
- └── isSellOfferTermLabel.js
51
- ```
52
-
53
42
  ---
54
43
 
55
44
  ## 📋 Implementation Checklist
56
45
 
57
46
  - [x] **TASK-01: Recursive Schema Loading & Directives Loader**
58
47
  - **File**: `src/compile/loadSchemas.js`
59
- - **Action**:
60
- 1. Update `loadLocalSchemaDirectory(project, directoryName, options)` to recursively find all `.js` files within subdirectories (supporting `ObjectSchemas/generatedCode/**` and `RelationshipSchemas/generatedCode/**`).
61
- 2. Export `loadGenerateDirectives(project, options)` calling `loadLocalSchemaDirectory(project, 'GenerateDirectives', options)`.
62
- - **Acceptance Criteria**: `loadLocalSchemaDirectory(project, 'ObjectSchemas')` discovers and validates schemas nested inside `generatedCode/attributeTree/` and `generatedCode/propertyValue/`. Returns `[]` when `GenerateDirectives` folder is missing.
48
+ - **Acceptance Criteria**: `loadLocalSchemaDirectory(project, 'ObjectSchemas')` discovers and validates schemas nested inside `generatedCode/**`.
63
49
 
64
- - [x] **TASK-02: Target ValueStandard Schema Emitter**
65
- - **File**: `src/emit/schemas/attributeTree/emitAttributeTreeTargetValueStandard.js` (NEW)
66
- - **Action**:
67
- 1. Filter `directives` for `directive.type === 'attributeTreeTarget'`.
68
- 2. Emit `{tag}ValueStandard.js` to `app/src/schemas/ObjectSchemas/generatedCode/attributeTree/${tag}ValueStandard.js`.
69
- 3. Include preservation hook `//(<addOnDataStructure${tag}ValueStandard>)` with commented `propertyValue` addOn template.
70
- 4. Export `attributeTreeTargetValueStandardManagedPaths(directives)`.
71
- - **Acceptance Criteria**: Emitted file adheres strictly to Izara backend syntax rules and matches Style 1 output path.
72
-
73
- - [x] **TASK-03: Align AttributeTree Self-Repo Emitter to Style 1**
50
+ - [x] **TASK-02: Consolidate ValueStandard into `emitAttributeTreeSchemas.js`**
74
51
  - **File**: `src/emit/schemas/attributeTree/emitAttributeTreeSchemas.js`
75
52
  - **Action**:
76
- 1. Update ObjectSchema emission paths from `generatedAttributeTree/` to `generatedCode/attributeTree/`.
77
- 2. Update RelationshipSchema emission paths from `generatedAttributeTree/` to `generatedCode/attributeTree/`.
78
- 3. Update `attributeTreeObjectSchemaManagedPaths` and `attributeTreeRelationshipSchemaManagedPaths`.
79
- - **Acceptance Criteria**: Self-repo attributeTree schemas output cleanly into `generatedCode/attributeTree/`.
80
-
81
- - [x] **TASK-04: Align PropertyValue Emitter to Style 1**
82
- - **File**: `src/emit/schemas/propertyValue/emitPropertyValueSchemas.js`
83
- - **Action**:
84
- 1. Update ObjectSchema emission paths from `generatedPropertyValue/` to `generatedCode/propertyValue/`.
85
- 2. Update RelationshipSchema emission paths from `generatedPropertyValue/` to `generatedCode/propertyValue/`.
86
- 3. Update `propertyValueObjectSchemaManagedPaths` and `propertyValueRelationshipSchemaManagedPaths`.
87
- - **Acceptance Criteria**: PropertyValue schemas output cleanly into `generatedCode/propertyValue/`.
88
-
89
- - [x] **TASK-05: Register Managed Scopes in Domain Registry**
90
- - **File**: `src/plan/domainRegistry.js`
91
- - **Action**:
92
- 1. Import `emitAttributeTreeTargetValueStandard` and `attributeTreeTargetValueStandardManagedPaths`.
93
- 2. Register new scope `attribute-tree-target-value-standard`.
94
- 3. Pass `directives` into `resolveEmissionGroups(context)`.
95
- - **Acceptance Criteria**: All 5 dynamic schema scopes registered and tracked in `.generate-service-code-v2-managed.json`.
96
-
97
- - [x] **TASK-06: Decouple Generation Pipeline in `src/generate.js`**
98
- - **File**: `src/generate.js`
99
- - **Action**:
100
- 1. Load directives: `const directives = await loadGenerateDirectives(project, schemaLoadOptions);`.
101
- 2. Pass `directives` to `resolveEmissionGroups`.
102
- 3. Delete lines 104-111 (`targetFiles = await _emitTargetServiceAttributeTreeSchemas(...)`).
103
- 4. Delete lines 129-186 (`_emitTargetServiceAttributeTreeSchemas` function definition).
104
- - **Acceptance Criteria**: Zero cross-repo filesystem writes. Generator operates strictly within `workspaceOutputRoot`.
105
-
106
- - [x] **TASK-07: Delete Legacy Cross-Repo Files**
107
- - **Files to Delete**:
108
- - `src/emit/schemas/attributeTree/resolveTargetServicePath.js`
109
- - `src/emit/schemas/attributeTree/emitAttributeTreeTargetSchemas.js`
110
- - **Action**: Remove dead files and any unused imports.
111
- - **Acceptance Criteria**: `git status` shows clean file deletions.
53
+ 1. Emit `${tag}ValueStandard.js` directly within `emitAttributeTreeObjectSchemas`.
54
+ 2. Add `${tag}ValueStandard.js` to `attributeTreeObjectSchemaManagedPaths`.
55
+ 3. Include preservation hook `//(<addOnDataStructure${tag}ValueStandard>)` with commented `propertyValue` addOn template.
56
+ - **Acceptance Criteria**: Single-service attributeTree generates all 4 ObjectSchemas (`Link`, `Value`, `Label`, `ValueStandard`).
112
57
 
113
- - [x] **TASK-08: Unit & Integration Test Verification**
58
+ - [x] **TASK-03: Remove Target Directive Scope & Dead Code**
114
59
  - **Files**:
115
- - `test/unit/schemas/attributeTreeDirectives.test.js` (NEW)
116
- - `test/unit/schemas/attributeTreeSchemas.test.js` (Update paths)
117
- - `test/unit/schemas/propertyValueSchemas.test.js` (Update paths)
118
- - **Verification Command**: `npm test`
119
- - **Acceptance Criteria**: All 198+ tests pass with zero regression.
120
-
121
- - [x] **TASK-09: End-to-End Cascading Verification**
122
- - **Action**: Verify two-phase generation locally:
123
- 1. Target service with directive -> generates `sellOfferTermValueStandard.js`.
124
- 2. Target service uncomments `propertyValue` -> generates PropertyLabel, PropertyValue, and 3 Relationship schemas.
125
- - **Acceptance Criteria**: Zero cross-repo dependency required.
126
-
127
- - [x] **TASK-10: Synchronize Vault & Taiga Documentation**
60
+ - `src/plan/domainRegistry.js` (Removed `attribute-tree-target-value-standard` scope & unused `directives` param)
61
+ - `src/emit/schemas/attributeTree/emitAttributeTreeTargetValueStandard.js` (Deleted dead file)
62
+ - `src/compile/loadSchemas.js` (Removed obsolete `loadGenerateDirectives`)
63
+ - `src/generate.js` (Removed `loadGenerateDirectives` call & `directives` passing)
64
+ - `test/unit/schemas/attributeTreeSingleService.test.js` (Removed obsolete directive loader tests)
65
+ - **Acceptance Criteria**: Clean scope tracking under `attribute-tree-object-schemas` with zero dead directive loading.
66
+
67
+ - [x] **TASK-04: Single-Service & Cascading Integration Verification**
68
+ - **File**: `test/unit/schemas/attributeTreeSingleService.test.js`
69
+ - **Action**: Verifies single-service generation and cascading generation to `propertyValue` within the same repository.
70
+ - **Acceptance Criteria**: 100% passing tests with zero cross-repo write or external directive needed.
71
+
72
+ - [x] **TASK-05: Synchronize Vault & Documentation**
128
73
  - **Vault Files**:
129
74
  - `10.Systems/GenerateServiceCode/20-Schema/Dynamic Schema.md`
130
75
  - `40.Vault/Operations/timeline-log.md`
131
76
  - `40.Vault/Memory/active-context.md`
132
- - **Taiga Wiki**: Update `generatecode` wiki page.
@@ -544,6 +544,64 @@ test('validateFlowSchema passes for s3 event with valid bucketName', () => {
544
544
  );
545
545
  });
546
546
 
547
+ test('validateFlowSchema passes for s3 event with explicit S3 flowSteps', () => {
548
+ assert.equal(
549
+ validateFlowSchema({
550
+ flowTag: 'uploadFlow',
551
+ event: ['s3'],
552
+ bucketName: 'MyUploadBucket',
553
+ flowSteps: [
554
+ { stepName: 'CreateObject', event: ['wbs'] },
555
+ { stepName: 'ReservedLimit', event: ['sqs'] },
556
+ { stepName: 'CreatePreSignUrl', event: ['sqs'] },
557
+ { stepName: 'ProcessAfterUpload', event: ['s3'] },
558
+ { stepName: 'ConfirmAfterUpload', event: ['sqs'] }
559
+ ]
560
+ }),
561
+ true
562
+ );
563
+ });
564
+
565
+ test('validateFlowSchema passes for s3 event without flowSteps', () => {
566
+ assert.equal(
567
+ validateFlowSchema({
568
+ flowTag: 'uploadFlow',
569
+ event: ['s3'],
570
+ bucketName: 'MyUploadBucket'
571
+ }),
572
+ true
573
+ );
574
+ });
575
+
576
+ test('validateFlowSchema rejects s3 event with invalid entry stepName', () => {
577
+ assert.throws(
578
+ () =>
579
+ validateFlowSchema({
580
+ flowTag: 'uploadFlow',
581
+ event: ['s3'],
582
+ bucketName: 'MyUploadBucket',
583
+ flowSteps: [{ stepName: 'InvalidEntry' }]
584
+ }),
585
+ /Entry step flowSteps\[0\] for s3 flow must have stepName 'CreateObject' or 'Start'/
586
+ );
587
+ });
588
+
589
+ test('validateFlowSchema rejects s3 event with invalid non-entry stepName', () => {
590
+ assert.throws(
591
+ () =>
592
+ validateFlowSchema({
593
+ flowTag: 'uploadFlow',
594
+ event: ['s3'],
595
+ bucketName: 'MyUploadBucket',
596
+ flowSteps: [
597
+ { stepName: 'CreateObject' },
598
+ { stepName: 'InvalidStep' }
599
+ ]
600
+ }),
601
+ /is invalid for s3 flow/
602
+ );
603
+ });
604
+
547
605
  test('validateFlowSchema rejects event with storedCache or triggerCache as invalid event', () => {
548
606
  assert.throws(
549
607
  () =>
@@ -36,7 +36,7 @@ test('generates RBAC own-topic WebSocket entries alongside their SQS handlers',
36
36
  const rootPath = await createProject(true);
37
37
  const result = await generateCreateTargetRoleHandler(rootPath);
38
38
 
39
- assert.equal(result.files.length, 60);
39
+ assert.equal(result.files.length, 67);
40
40
  assert.match(
41
41
  await readFile(join(rootPath, result.files[0].path), 'utf8'),
42
42
  /CreateTargetRole/
@@ -211,6 +211,16 @@ test('generates RBAC own-topic WebSocket entries alongside their SQS handlers',
211
211
  );
212
212
  assert.match(yaml, new RegExp(`${flowTag}HdrWbs:\\n[\\s\\S]*websocket:`));
213
213
  }
214
+
215
+ const ttlExpiredFile = await readFile(
216
+ join(
217
+ rootPath,
218
+ 'app/src/generatedCode/FlowLimit/ReservedUsageTtlExpired/source/ReservedUsageTtlExpired_HdrDss.js'
219
+ ),
220
+ 'utf8'
221
+ );
222
+ assert.match(ttlExpiredFile, /Auto-refunded expired reservation/);
223
+ assert.match(ttlExpiredFile, /RunningTotalUsage/);
214
224
  });
215
225
 
216
226
  test('removes CreateTargetRole handler when RBAC is removed', async () => {
@@ -244,6 +254,65 @@ test('removes CreateTargetRole handler when RBAC is removed', async () => {
244
254
  );
245
255
  });
246
256
 
257
+ test('generates synchronous HdrInv and HdrApi handlers for FlowLimit flows', async () => {
258
+ const rootPath = await createProject(true);
259
+ await generateCreateTargetRoleHandler(rootPath);
260
+
261
+ const limitYaml = await readFile(
262
+ join(rootPath, 'app/sls_yaml/generatedCode/source/flow-limits.yml'),
263
+ 'utf8'
264
+ );
265
+
266
+ for (const flowTag of ['ReservedDynamicUsage', 'ConfirmUsage', 'CancelUsage']) {
267
+ assert.match(
268
+ limitYaml,
269
+ new RegExp(
270
+ `${flowTag}HdrInv:\\n\\s+handler: src/generatedCode/FlowLimit/${flowTag}/source/${flowTag}_HdrInv\\.main`
271
+ )
272
+ );
273
+ assert.match(
274
+ limitYaml,
275
+ new RegExp(
276
+ `${flowTag}HdrApi:\\n\\s+handler: src/generatedCode/FlowLimit/${flowTag}/source/${flowTag}_HdrApi\\.main`
277
+ )
278
+ );
279
+
280
+ const invFile = await readFile(
281
+ join(
282
+ rootPath,
283
+ `app/src/generatedCode/FlowLimit/${flowTag}/source/${flowTag}_HdrInv.js`
284
+ ),
285
+ 'utf8'
286
+ );
287
+ assert.match(invFile, /export const main = middleware\.wrap/);
288
+ assert.match(
289
+ invFile,
290
+ new RegExp(`import ${flowTag} from '\\./${flowTag}_Main\\.js'`)
291
+ );
292
+
293
+ const apiFile = await readFile(
294
+ join(
295
+ rootPath,
296
+ `app/src/generatedCode/FlowLimit/${flowTag}/source/${flowTag}_HdrApi.js`
297
+ ),
298
+ 'utf8'
299
+ );
300
+ assert.match(apiFile, /export const main = middleware\.wrap/);
301
+ assert.match(
302
+ apiFile,
303
+ new RegExp(`import ${flowTag} from '\\./${flowTag}_Main\\.js'`)
304
+ );
305
+ }
306
+
307
+ assert.match(
308
+ limitYaml,
309
+ /ReservedUsageTtlExpiredHdrDss:\n\s+handler: src\/generatedCode\/FlowLimit\/ReservedUsageTtlExpired\/source\/ReservedUsageTtlExpired_HdrDss\.main/
310
+ );
311
+ assert.match(limitYaml, /type: dynamodb/);
312
+ assert.match(limitYaml, /RunningTotalReservedUsageTable/);
313
+ assert.match(limitYaml, /eventName: \["REMOVE"\]/);
314
+ });
315
+
247
316
  async function createProject(hasRbac) {
248
317
  const rootPath = await mkdtemp(
249
318
  join(tmpdir(), 'generate-service-code-v2-rbac-handler-')
@@ -1119,6 +1119,10 @@ test('emits synchronous ConfirmUsage and CancelUsage invocation in CreateLogical
1119
1119
  assert.match(createLogicalLib, /CancelUsageHdrInv/);
1120
1120
  assert.match(createLogicalLib, /lambda\.invokeSync\(/);
1121
1121
  assert.match(createLogicalLib, /publishCompletionMessage\(/);
1122
+ assert.match(createLogicalLib, /OBJECT_QUOTA_SERVICE_TAGS/);
1123
+ assert.match(createLogicalLib, /"order":"UserAccount"/);
1124
+ assert.match(createLogicalLib, /targetFunction,\s*quotaServiceTag/);
1125
+ assert.doesNotMatch(createLogicalLib, /targetFunction,\s*['"]Rbac['"]/);
1122
1126
  });
1123
1127
 
1124
1128
  test('emits synchronous AfterHook invocation in Update and Delete handlers', async () => {
@@ -816,6 +816,58 @@ test('emits lambda:InvokeFunction in FlowObjectsRole when flow has event generat
816
816
  assert.match(role, /GenSyncHook/);
817
817
  });
818
818
 
819
+ test('emits DynamoDB stream permissions in FlowRbacRole and multi-tenant quota topics in FlowObjectsRole', async () => {
820
+ const rootPath = await createProject();
821
+ const schemas = join(rootPath, 'app/src/schemas');
822
+ await writeFile(
823
+ join(schemas, 'ObjectSchemas/Order.js'),
824
+ "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"
825
+ );
826
+ await generateResourceArtifacts(rootPath);
827
+
828
+ const iam = await readFile(
829
+ join(
830
+ rootPath,
831
+ 'app/sls_yaml/generatedCode/source/generate-shared-resource.yml'
832
+ ),
833
+ 'utf8'
834
+ );
835
+ const rbacRole = roleBlock(iam, 'FlowRbacRole');
836
+ assert.match(rbacRole, /dynamodb:DescribeStream/);
837
+ assert.match(rbacRole, /RunningTotalReservedUsage\/stream\/\*/);
838
+
839
+ const flowObjectsRole = roleBlock(iam, 'FlowObjectsRole');
840
+ assert.match(flowObjectsRole, /SupplyNetwork_\${self:custom\.iz_stage}_ConfirmUsage_In/);
841
+ assert.match(flowObjectsRole, /SupplyNetwork_\${self:custom\.iz_stage}_CancelUsage_In/);
842
+ assert.match(flowObjectsRole, /SupplyNetwork\${self:custom\.iz_stage}ConfirmUsageHdrInv/);
843
+ assert.match(flowObjectsRole, /SupplyNetwork\${self:custom\.iz_stage}CancelUsageHdrInv/);
844
+
845
+ const { createResourceState } = await import(
846
+ '../../../src/emit/infrastructure/resources/resourceState.js'
847
+ );
848
+ const { renderLambdaRoleConfig } = await import(
849
+ '../../../src/emit/infrastructure/resources/iamResourceArtifacts.js'
850
+ );
851
+ const state = createResourceState({
852
+ objects: [
853
+ {
854
+ objectType: 'order',
855
+ generatedBy: 'user',
856
+ belongTo: { serviceTag: 'SupplyNetwork', objectType: 'tenant' },
857
+ addOnDataStructure: [{ type: 'rbac' }]
858
+ }
859
+ ],
860
+ relationships: [],
861
+ flows: [
862
+ { flowTag: 'upload', event: ['s3'], quotaServiceTag: 'SupplyNetwork' }
863
+ ]
864
+ });
865
+ const lambdaRoleConfig = await renderLambdaRoleConfig(state);
866
+ assert.match(lambdaRoleConfig, /ConfirmUsage_In', 'SupplyNetwork'/);
867
+ assert.match(lambdaRoleConfig, /CancelUsage_In', 'SupplyNetwork'/);
868
+ assert.match(lambdaRoleConfig, /ReservedDynamicUsage_In', 'SupplyNetwork'/);
869
+ assert.doesNotMatch(lambdaRoleConfig, /'AccountLimits'/);
870
+ });
819
871
 
820
872
  function roleBlock(iam, roleName) {
821
873
  const match = iam.match(
@@ -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
  });