@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.
- package/package.json +1 -1
- package/src/compile/flowValidator.js +45 -8
- package/src/compile/loadSchemas.js +0 -3
- package/src/emit/application/coreFunction/emitCoreFunctionArtifacts.js +15 -1
- package/src/emit/application/coreFunction/templates/InitialSetupConfig.ejs +8 -4
- package/src/emit/application/crud/templates/CreateEndpoint_Main.ejs +4 -4
- package/src/emit/application/crud/templates/CreateLogicalLib.ejs +60 -8
- package/src/emit/application/crud/templates/DeleteEndpoint_Main.ejs +4 -4
- package/src/emit/application/crud/templates/UpdateEndpoint_Main.ejs +4 -4
- package/src/emit/application/custom/customFlowS3.js +4 -4
- package/src/emit/application/custom/templates/FlowEndpoint_Main.ejs +2 -2
- package/src/emit/application/rbac/emitDynamicRbacFlowSchemas.js +4 -1
- package/src/emit/application/rbac/emitRbacApplicationArtifacts.js +76 -1
- package/src/emit/application/rbac/templates/ReservedDynamicUsage_Main.ejs +2 -0
- package/src/emit/application/rbac/templates/ReservedUsageTtlExpired_HdrDss.ejs +176 -0
- package/src/emit/infrastructure/functions/functionYamlArtifact.js +28 -0
- package/src/emit/infrastructure/functions/templates/DssFunction_Yaml.ejs +21 -0
- package/src/emit/infrastructure/resources/iamResourceArtifacts.js +45 -5
- package/src/emit/infrastructure/resources/templates/GeneratedLambdaRoleServiceConfig.ejs +5 -3
- package/src/emit/schemas/attributeTree/emitAttributeTreeSchemas.js +62 -1
- package/src/generate.js +2 -6
- package/src/output/mergeHooks.js +105 -5
- package/src/plan/domainRegistry.js +0 -10
- package/task.md +40 -96
- package/test/integration/graphDrivenFlowSchema.test.js +58 -0
- package/test/unit/application/createTargetRoleHandler.test.js +70 -1
- package/test/unit/application/crudApplicationArtifacts.test.js +4 -0
- package/test/unit/infrastructure/resourceArtifacts.test.js +52 -0
- package/test/unit/output/mergeHooks.test.js +137 -0
- package/test/unit/schemas/attributeTreeSchemas.test.js +32 -3
- package/test/unit/schemas/attributeTreeSingleService.test.js +175 -0
- package/test/unit/templates/hookTagsValidation.test.js +83 -0
- package/src/emit/schemas/attributeTree/emitAttributeTreeTargetValueStandard.js +0 -132
- package/test/unit/schemas/attributeTreeDirectives.test.js +0 -177
package/task.md
CHANGED
|
@@ -1,132 +1,76 @@
|
|
|
1
|
-
# Task Checklist: Attribute Tree
|
|
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**: **
|
|
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:
|
|
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
|
-
│ │
|
|
21
|
-
│
|
|
22
|
-
│
|
|
23
|
-
│
|
|
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
|
-
- **
|
|
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:
|
|
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.
|
|
77
|
-
2.
|
|
78
|
-
3.
|
|
79
|
-
- **Acceptance Criteria**:
|
|
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-
|
|
58
|
+
- [x] **TASK-03: Remove Target Directive Scope & Dead Code**
|
|
114
59
|
- **Files**:
|
|
115
|
-
- `
|
|
116
|
-
- `
|
|
117
|
-
- `
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
- **Acceptance Criteria**:
|
|
126
|
-
|
|
127
|
-
- [x] **TASK-
|
|
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,
|
|
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
|
|
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,
|
|
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
|
});
|