@izara_project/izara-core-generate-service-code 1.0.64 → 1.0.65

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 (21) hide show
  1. package/package.json +1 -1
  2. package/src/codeGenerators/app/sls_yaml/FunctionYamlGenerator.js +10 -0
  3. package/src/codeGenerators/app/sls_yaml/SharedResourceYamlGenerator.js +3 -4
  4. package/src/codeGenerators/app/sls_yaml/_policy/PolicyRegistry.js +1 -39
  5. package/src/codeGenerators/app/sls_yaml/_policy/__tests__/PolicyRegistry.test.js +1 -16
  6. package/src/codeGenerators/app/src/generatedCode/Flow/_shared/events/BaseDsqHandler.js +10 -2
  7. package/src/codeGenerators/app/src/generatedCode/Flow/_shared/events/__tests__/BaseDsqHandler.test.js +5 -4
  8. package/src/codeGenerators/app/src/generatedCode/Flow/_shared/events/eventBridge.js +12 -4
  9. package/src/codeGenerators/app/src/generatedCode/Flow/_shared/events/lambdaSyncApi.js +10 -2
  10. package/src/codeGenerators/app/src/generatedCode/Flow/_shared/events/lambdaSyncInv.js +10 -2
  11. package/src/codeGenerators/app/src/generatedCode/Flow/_shared/shared/flowValidator.js +33 -2
  12. package/src/codeGenerators/app/src/generatedCode/Flow/_shared/shared/propertyNormalizer.js +11 -8
  13. package/src/generateCode.js +0 -1
  14. package/src/schemaGenerators/app/src/schemas/RelationshipSchemas/AttributeTreeRelationshipSchemaGenerator.js +1 -41
  15. package/src/schemaGenerators/app/src/schemas/RelationshipSchemas/PropertyValueRelationshipSchemaGenerator.js +1 -41
  16. package/src/schemaGenerators/app/src/schemas/RelationshipSchemas/__tests__/relationshipSchemaHelpers.test.js +38 -0
  17. package/src/schemaGenerators/app/src/schemas/RelationshipSchemas/relationshipSchemaHelpers.js +40 -0
  18. package/src/codeGenerators/app/sls_yaml/_policy/PolicyEmitter.js +0 -46
  19. package/src/codeGenerators/app/sls_yaml/_policy/__tests__/PolicyEmitter.test.js +0 -55
  20. package/src/codeGenerators/app/sls_yaml/_policy/templates/ManagedPolicy_Yaml.ejs +0 -22
  21. package/test_schema.js +0 -2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@izara_project/izara-core-generate-service-code",
3
- "version": "1.0.64",
3
+ "version": "1.0.65",
4
4
  "description": "Code for locally generating per service files",
5
5
  "homepage": "https://bitbucket.org/izara-core-support-services/izara-core-support-services-generate-service-code#readme",
6
6
  "repository": {
@@ -118,6 +118,16 @@ export async function generateFunctionYaml(allSchemas, options) {
118
118
  const handlerBaseName = splitFileName[0]; // e.g. ProcessCreate or CreateComplete
119
119
  const handlerSuffix = splitFileName[1]; // e.g. HdrSqs, HdrApi, HdrInv
120
120
 
121
+ // Skip custom handlers that self-register their own YAML to prevent duplicates
122
+ if (
123
+ handlerBaseName.startsWith('ProcessTriggerCache') ||
124
+ handlerBaseName.startsWith('CheckTriggerCache') ||
125
+ handlerBaseName.startsWith('TriggerCache') ||
126
+ handlerBaseName.startsWith('CompleteStatusField')
127
+ ) {
128
+ continue;
129
+ }
130
+
121
131
  const isCrud = isCrudFlow(flowTag);
122
132
  const isRel = isRelationshipFlow(flowTag);
123
133
  const isRbac =
@@ -459,10 +459,9 @@ export async function generateSharedResourceYaml(allSchemas, options) {
459
459
  );
460
460
 
461
461
  for (const roleName of allowedRoles) {
462
- policyRegistry.addMany(
463
- roleName,
464
- buildRoleStatements(roleName, resources, scopedResources)
465
- );
462
+ for (const statement of buildRoleStatements(roleName, resources, scopedResources)) {
463
+ policyRegistry.add(roleName, statement.action, statement.resource);
464
+ }
466
465
  }
467
466
 
468
467
  const groupedStatements = policyRegistry.toGroupedByRole();
@@ -1,9 +1,6 @@
1
1
  // src/codeGenerators/app/sls_yaml/_policy/PolicyRegistry.js
2
2
  //
3
- // In-memory IAM permission registry. Decoupled generators register
4
- // (roleName, action, resource) tuples at any point during generation.
5
- // PolicyEmitter reads the registry after all generators have run and
6
- // emits one AWS::IAM::ManagedPolicy per role with deduplicated statements.
3
+ // In-memory IAM permission registry for the shared-resource generator.
7
4
  //
8
5
  // Internal shape:
9
6
  // Map<roleName, Map<action, Set<resource>>>
@@ -15,48 +12,18 @@ export class PolicyRegistry {
15
12
  constructor() {
16
13
  /** @type {Map<string, Map<string, Set<string>>>} */
17
14
  this._roles = new Map();
18
- /** @type {Set<string>} Roles whose entries have been fully replaced via .set() */
19
- this._replaced = new Set();
20
15
  }
21
16
 
22
17
  add(roleName, action, resource) {
23
- if (this._replaced.has(roleName)) {
24
- throw new Error(
25
- `PolicyRegistry.add("${roleName}", ...): role was previously replaced via .set(); ` +
26
- `use .set() again or call .resetRole() first.`
27
- );
28
- }
29
18
  if (!this._roles.has(roleName)) this._roles.set(roleName, new Map());
30
19
  const actions = this._roles.get(roleName);
31
20
  if (!actions.has(action)) actions.set(action, new Set());
32
21
  actions.get(action).add(resource);
33
22
  }
34
23
 
35
- addMany(roleName, perms) {
36
- for (const p of perms) this.add(roleName, p.action, p.resource);
37
- }
38
-
39
- /** Fully replace a role's permission set (discards anything previously added). */
40
- set(roleName, perms) {
41
- this._roles.set(roleName, new Map());
42
- this._replaced.add(roleName);
43
- for (const p of perms) {
44
- const actions = this._roles.get(roleName);
45
- if (!actions.has(p.action)) actions.set(p.action, new Set());
46
- actions.get(p.action).add(p.resource);
47
- }
48
- }
49
-
50
- /** Forget all registrations for a role (allows re-add). */
51
- resetRole(roleName) {
52
- this._roles.delete(roleName);
53
- this._replaced.delete(roleName);
54
- }
55
-
56
24
  /** Forget every role. Used between test cases or if a caller wants a clean slate. */
57
25
  clear() {
58
26
  this._roles.clear();
59
- this._replaced.clear();
60
27
  }
61
28
 
62
29
  /**
@@ -79,11 +46,6 @@ export class PolicyRegistry {
79
46
  }
80
47
  return out;
81
48
  }
82
-
83
- /** List of role names currently registered (useful for the emitter to iterate). */
84
- registeredRoles() {
85
- return [...this._roles.keys()];
86
- }
87
49
  }
88
50
 
89
51
  /**
@@ -1,5 +1,5 @@
1
1
  import { describe, it, expect, beforeEach } from '@jest/globals';
2
- import { policyRegistry, PolicyRegistry } from '../PolicyRegistry.js';
2
+ import { PolicyRegistry } from '../PolicyRegistry.js';
3
3
 
4
4
  describe('PolicyRegistry', () => {
5
5
  let reg;
@@ -19,21 +19,6 @@ describe('PolicyRegistry', () => {
19
19
  expect(stmt.resource).toEqual(['arn:aws:s3:::a/*', 'arn:aws:s3:::b/*']);
20
20
  });
21
21
 
22
- it('addMany() inserts batch in one call', () => {
23
- reg.addMany('Role1', [
24
- { action: 's3:GetObject', resource: 'arn:aws:s3:::a/*' },
25
- { action: 's3:ListBucket', resource: 'arn:aws:s3:::a' }
26
- ]);
27
- expect(reg.toGroupedByRole().Role1.statements).toHaveLength(2);
28
- });
29
-
30
- it('set() replaces baseline for a role (full override)', () => {
31
- reg.add('Role1', 's3:GetObject', 'arn:aws:s3:::a/*'); // baseline
32
- reg.set('Role1', [{ action: 'dynamodb:GetItem', resource: 'arn:aws:dynamodb:*:*:table/x' }]);
33
- expect(reg.toGroupedByRole().Role1.statements).toHaveLength(1);
34
- expect(reg.toGroupedByRole().Role1.statements[0].action).toEqual(['dynamodb:GetItem']);
35
- });
36
-
37
22
  it('toGroupedByRole() returns empty object when nothing registered', () => {
38
23
  expect(reg.toGroupedByRole()).toEqual({});
39
24
  });
@@ -1,17 +1,25 @@
1
1
  import ejs from 'ejs';
2
2
  import fs from 'fs/promises';
3
3
  import path from 'path';
4
+ import { isCrudFlow, isRbacFlow, isRelationshipFlow } from '../shared/flowClassifier.js';
5
+ import { ownTopicFunctionName, processFunctionName } from '../shared/flowNaming.js';
4
6
  import { normalizePropertyType } from '../shared/propertyNormalizer.js';
5
7
 
6
8
  export async function generateDsqHandler(flow, stepName, suffix, flowOutputDir, mainFileName, templates) {
7
9
  const upperCase = (str) => str.charAt(0).toUpperCase() + str.slice(1);
8
10
  const flowTag = flow.flowTag;
9
11
 
12
+ const isOwnTopic = !stepName && !isCrudFlow(flowTag) && !isRbacFlow(flowTag) && !isRelationshipFlow(flowTag) && flow.event && flow.event.includes('ownTopic');
13
+
10
14
  // For entry point stepName is empty
11
- const handlerFileName = stepName ? `${flowTag}${stepName}_${suffix}` : `Process${upperCase(flowTag)}_${suffix}`;
15
+ const handlerFileName = stepName
16
+ ? `${flowTag}${stepName}_${suffix}`
17
+ : (isOwnTopic ? `${upperCase(flowTag)}_${suffix}` : `Process${upperCase(flowTag)}_${suffix}`);
12
18
 
13
19
  // Function name: matches default export in the referenced _Main.js
14
- const functionName = stepName ? `${flowTag}${stepName}` : `Process${upperCase(flowTag)}`;
20
+ const functionName = stepName
21
+ ? `${flowTag}${stepName}`
22
+ : (isOwnTopic ? ownTopicFunctionName(flowTag) : processFunctionName(flowTag));
15
23
 
16
24
  // Queue name: keep the full handler suffix so handler code matches
17
25
  const queueNameSuffix = suffix;
@@ -41,12 +41,13 @@ describe('generateDsqHandler', () => {
41
41
  const generatedContent = await fs.readFile(generatedFilePath, 'utf-8');
42
42
 
43
43
  // Verify it contains setValidatorSchema on line 39 (approx or exactly depending on formatting, let's verify substring)
44
- expect(generatedContent).toContain('middleware.setValidatorSchema(recordHandlerSharedLib.baseValidatorSchema(');
45
- expect(generatedContent).toContain('type: "object"');
44
+ expect(generatedContent).toContain('middleware.setValidatorSchema(');
45
+ expect(generatedContent).toContain('recordHandlerSharedLib.baseValidatorSchema({');
46
+ expect(generatedContent).toContain('type: \'object\'');
46
47
  expect(generatedContent).toContain('required: [\'body\', \'messageAttributes\']');
47
48
 
48
49
  // Verify it contains reformatDsqMessage
49
- expect(generatedContent).toContain('record = recordHandlerSharedLib.reformatDsqMessage(record._izContext, record);');
50
+ expect(generatedContent).toContain('record = recordHandlerSharedLib.reformatDsqMessage(');
50
51
  expect(generatedContent).toContain('record._izContext.logger.debug(\'record ReceiveMsgOutHdrSqs\', record);');
51
52
  });
52
53
 
@@ -88,6 +89,6 @@ describe('generateDsqHandler', () => {
88
89
  // Verify template rendering and function names
89
90
  expect(generatedContent).toContain('import createOrderStepOne from \'./CreateOrderStepOne_Main.js\';');
90
91
  expect(generatedContent).toContain('createOrderStepOne');
91
- expect(generatedContent).toContain('"createOrderStepOneHdrDsq"');
92
+ expect(generatedContent).toContain('\'createOrderStepOneHdrDsq\'');
92
93
  });
93
94
  });
@@ -1,17 +1,25 @@
1
1
  import ejs from 'ejs';
2
2
  import fs from 'fs/promises';
3
3
  import path from 'path';
4
+ import { isCrudFlow, isRbacFlow, isRelationshipFlow } from '../shared/flowClassifier.js';
5
+ import { ownTopicFunctionName, processFunctionName } from '../shared/flowNaming.js';
4
6
 
5
7
  export default async function generate(flow, stepName, flowOutputDir, mainFileName, templates) {
6
8
  const upperCase = (str) => str.charAt(0).toUpperCase() + str.slice(1);
7
9
  const flowTag = flow.flowTag;
8
10
 
9
-
11
+ const isOwnTopic = !stepName && !isCrudFlow(flowTag) && !isRbacFlow(flowTag) && !isRelationshipFlow(flowTag) && flow.event && flow.event.includes('ownTopic');
10
12
 
11
13
  const suffix = 'HdrInv';
12
- const handlerFileName = stepName ? `${flowTag}${stepName}_${suffix}` : `Process${upperCase(flowTag)}_${suffix}`;
13
- const queueName = stepName ? `${upperCase(flowTag)}${upperCase(stepName)}${upperCase(suffix)}` : `Process${upperCase(flowTag)}${upperCase(suffix)}`;
14
- const functionName = stepName ? `${flowTag}${stepName}` : `Process${upperCase(flowTag)}`;
14
+ const handlerFileName = stepName
15
+ ? `${flowTag}${stepName}_${suffix}`
16
+ : (isOwnTopic ? `${upperCase(flowTag)}_${suffix}` : `Process${upperCase(flowTag)}_${suffix}`);
17
+ const queueName = stepName
18
+ ? `${upperCase(flowTag)}${upperCase(stepName)}${upperCase(suffix)}`
19
+ : (isOwnTopic ? `${upperCase(flowTag)}${upperCase(suffix)}` : `Process${upperCase(flowTag)}${upperCase(suffix)}`);
20
+ const functionName = stepName
21
+ ? `${flowTag}${stepName}`
22
+ : (isOwnTopic ? ownTopicFunctionName(flowTag) : processFunctionName(flowTag));
15
23
 
16
24
  const handlerContent = ejs.render(templates.inv, {
17
25
  flowTag: flowTag,
@@ -1,18 +1,26 @@
1
1
  import ejs from 'ejs';
2
2
  import fs from 'fs/promises';
3
3
  import path from 'path';
4
+ import { isCrudFlow, isRbacFlow, isRelationshipFlow } from '../shared/flowClassifier.js';
5
+ import { ownTopicFunctionName, processFunctionName } from '../shared/flowNaming.js';
4
6
  import { normalizePropertyType } from '../shared/propertyNormalizer.js';
5
7
 
6
8
  export default async function generate(flow, stepName, flowOutputDir, mainFileName, templates) {
7
9
  const upperCase = (str) => str.charAt(0).toUpperCase() + str.slice(1);
8
10
  const flowTag = flow.flowTag;
9
11
 
12
+ const isOwnTopic = !stepName && !isCrudFlow(flowTag) && !isRbacFlow(flowTag) && !isRelationshipFlow(flowTag) && flow.event && flow.event.includes('ownTopic');
13
+
10
14
  const suffix = 'HdrApi';
11
- const handlerFileName = stepName ? `${flowTag}${stepName}_${suffix}` : `Process${upperCase(flowTag)}_${suffix}`;
15
+ const handlerFileName = stepName
16
+ ? `${flowTag}${stepName}_${suffix}`
17
+ : (isOwnTopic ? `${upperCase(flowTag)}_${suffix}` : `Process${upperCase(flowTag)}_${suffix}`);
12
18
  // Queue name: ${flowTag}${flowStepName}${HandlerType}
13
19
  const queueName = stepName ? `${flowTag}${stepName}${suffix}` : `${flowTag}${suffix}`;
14
20
  // Function name matches default export of the referenced _Main.js
15
- const functionName = stepName ? `${flowTag}${stepName}` : `Process${upperCase(flowTag)}`;
21
+ const functionName = stepName
22
+ ? `${flowTag}${stepName}`
23
+ : (isOwnTopic ? ownTopicFunctionName(flowTag) : processFunctionName(flowTag));
16
24
 
17
25
  let startConfig = null;
18
26
  if (!stepName && flow.flowSteps) {
@@ -1,18 +1,26 @@
1
1
  import ejs from 'ejs';
2
2
  import fs from 'fs/promises';
3
3
  import path from 'path';
4
+ import { isCrudFlow, isRbacFlow, isRelationshipFlow } from '../shared/flowClassifier.js';
5
+ import { ownTopicFunctionName, processFunctionName } from '../shared/flowNaming.js';
4
6
  import { normalizePropertyType } from '../shared/propertyNormalizer.js';
5
7
 
6
8
  export default async function generate(flow, stepName, flowOutputDir, mainFileName, templates) {
7
9
  const upperCase = (str) => str.charAt(0).toUpperCase() + str.slice(1);
8
10
  const flowTag = flow.flowTag;
9
11
 
12
+ const isOwnTopic = !stepName && !isCrudFlow(flowTag) && !isRbacFlow(flowTag) && !isRelationshipFlow(flowTag) && flow.event && flow.event.includes('ownTopic');
13
+
10
14
  const suffix = 'HdrInv';
11
- const handlerFileName = stepName ? `${flowTag}${stepName}_${suffix}` : `Process${upperCase(flowTag)}_${suffix}`;
15
+ const handlerFileName = stepName
16
+ ? `${flowTag}${stepName}_${suffix}`
17
+ : (isOwnTopic ? `${upperCase(flowTag)}_${suffix}` : `Process${upperCase(flowTag)}_${suffix}`);
12
18
  // Queue name: ${flowTag}${flowStepName}${HandlerType}
13
19
  const queueName = stepName ? `${flowTag}${stepName}${suffix}` : `${flowTag}${suffix}`;
14
20
  // Function name matches default export of the referenced _Main.js
15
- const functionName = stepName ? `${flowTag}${stepName}` : `Process${upperCase(flowTag)}`;
21
+ const functionName = stepName
22
+ ? `${flowTag}${stepName}`
23
+ : (isOwnTopic ? ownTopicFunctionName(flowTag) : processFunctionName(flowTag));
16
24
 
17
25
  let startConfig = null;
18
26
  if (!stepName && flow.flowSteps) {
@@ -100,7 +100,8 @@ function validateUnusedStepProperties(flowTag, flow, errors) {
100
100
  const usedProps = new Set();
101
101
  const usedAttrs = new Set();
102
102
 
103
- for (const step of Array.isArray(flow.flowSteps) ? flow.flowSteps : []) {
103
+ const flowSteps = Array.isArray(flow.flowSteps) ? flow.flowSteps : [];
104
+ for (const step of flowSteps) {
104
105
  for (const ref of Array.isArray(step.properties) ? step.properties : []) {
105
106
  if (typeof ref === 'string') usedProps.add(ref);
106
107
  }
@@ -109,6 +110,16 @@ function validateUnusedStepProperties(flowTag, flow, errors) {
109
110
  }
110
111
  }
111
112
 
113
+ const firstStep = flowSteps[0];
114
+ if (flow.outputTopic === true && firstStep && firstStep.returnValues) {
115
+ for (const ref of Array.isArray(firstStep.returnValues.properties) ? firstStep.returnValues.properties : []) {
116
+ if (typeof ref === 'string') usedProps.add(ref);
117
+ }
118
+ for (const ref of Array.isArray(firstStep.returnValues.messageAttributes) ? firstStep.returnValues.messageAttributes : []) {
119
+ if (typeof ref === 'string') usedAttrs.add(ref);
120
+ }
121
+ }
122
+
112
123
  if (stepProps && typeof stepProps === 'object') {
113
124
  for (const key of Object.keys(stepProps)) {
114
125
  if (!usedProps.has(key)) {
@@ -187,7 +198,8 @@ export function validateFlowsForGeneration(flows) {
187
198
  const seenStepNames = new Set();
188
199
  let anyStepHasPlugInHooks = false;
189
200
 
190
- for (const step of flowSteps) {
201
+ for (let i = 0; i < flowSteps.length; i++) {
202
+ const step = flowSteps[i];
191
203
  const stepName = step?.stepName;
192
204
  if (!stepName) {
193
205
  errors.push(`Flow '${flowTag}' has a step without stepName.`);
@@ -211,6 +223,16 @@ export function validateFlowsForGeneration(flows) {
211
223
 
212
224
  validateNamedRefs(flowTag, stepName, step.properties, flow.stepProperties, 'properties', errors);
213
225
  validateNamedRefs(flowTag, stepName, step.messageAttributes, flow.stepMessageAttributes, 'messageAttributes', errors);
226
+ if (step.returnValues) {
227
+ if (i === 0) {
228
+ if (flow.outputTopic === true) {
229
+ validateNamedRefs(flowTag, stepName, step.returnValues.properties, flow.stepProperties, 'returnValues.properties', errors);
230
+ validateNamedRefs(flowTag, stepName, step.returnValues.messageAttributes, flow.stepMessageAttributes, 'returnValues.messageAttributes', errors);
231
+ }
232
+ } else {
233
+ errors.push(`Flow '${flowTag}' step '${stepName}' must not have 'returnValues'. It is only allowed on the first step (index 0).`);
234
+ }
235
+ }
214
236
  }
215
237
 
216
238
  validateStepPropertyTypes(flowTag, flow.stepProperties, 'stepProperties', errors);
@@ -218,6 +240,15 @@ export function validateFlowsForGeneration(flows) {
218
240
  validateDuplicateStepPropertyKeys(flowTag, flow, errors);
219
241
  validateUnusedStepProperties(flowTag, flow, errors);
220
242
 
243
+ if (flow.outputTopic === true) {
244
+ const firstStep = flowSteps[0];
245
+ const hasReturnProps = firstStep?.returnValues?.properties && Array.isArray(firstStep.returnValues.properties) && firstStep.returnValues.properties.length > 0;
246
+ const hasReturnMsgAttrs = firstStep?.returnValues?.messageAttributes && Array.isArray(firstStep.returnValues.messageAttributes) && firstStep.returnValues.messageAttributes.length > 0;
247
+ if (!hasReturnProps && !hasReturnMsgAttrs) {
248
+ errors.push(`Flow '${flowTag}' has 'outputTopic: true' but its first step '${firstStep?.stepName || 'Start'}' does not define any non-empty 'returnValues' (properties or messageAttributes).`);
249
+ }
250
+ }
251
+
221
252
  // ponytail: relaxed anyStepHasPlugInHooks requirement
222
253
  // since unified entry points (step 0) can now handle plugins directly without start.
223
254
 
@@ -1,15 +1,18 @@
1
- import { consts } from '@izara_project/izara-shared-service-schemas';
1
+ import { consts } from '@izara_project/izara-core-library-service-schemas';
2
2
 
3
3
  export const TYPE_MAP = consts.FIELDNAME_TYPE_MAPPING;
4
4
 
5
5
  export function normalizePropertyType(p, lookupSource) {
6
- let prop = typeof p === 'string'
7
- ? ((lookupSource && lookupSource[p]) ? lookupSource[p] : { propertyName: p, type: 'string' })
8
- : p;
6
+ let prop =
7
+ typeof p === 'string'
8
+ ? lookupSource && lookupSource[p]
9
+ ? { propertyName: p, ...lookupSource[p] }
10
+ : { propertyName: p, type: 'string' }
11
+ : p;
9
12
 
10
- if (prop && prop.type) {
11
- prop = { ...prop, type: TYPE_MAP[prop.type] || prop.type };
12
- }
13
+ if (prop && prop.type) {
14
+ prop = { ...prop, type: TYPE_MAP[prop.type] || prop.type };
15
+ }
13
16
 
14
- return prop;
17
+ return prop;
15
18
  }
@@ -31,7 +31,6 @@ import { generateProcessLogicalYaml } from './codeGenerators/app/sls_yaml/Proces
31
31
  import { generateSharedResourceYaml } from './codeGenerators/app/sls_yaml/SharedResourceYamlGenerator.js';
32
32
  import { generateFindDataYaml } from './codeGenerators/app/sls_yaml/FindDataYamlGenerator.js';
33
33
  import { policyRegistry } from './codeGenerators/app/sls_yaml/_policy/PolicyRegistry.js';
34
- import { emitManagedPolicies } from './codeGenerators/app/sls_yaml/_policy/PolicyEmitter.js';
35
34
 
36
35
  function createGeneratorOptions(rootPath, options = {}) {
37
36
  return {
@@ -3,6 +3,7 @@ import path from 'path';
3
3
  import ejs from 'ejs';
4
4
  import { fileURLToPath } from 'url';
5
5
  import { attributeTreeLib } from '@izara_project/izara-attribute-tree';
6
+ import { createBasicRelationshipsProperties } from './relationshipSchemaHelpers.js';
6
7
 
7
8
  const __filename = fileURLToPath(import.meta.url);
8
9
  const __dirname = path.dirname(__filename);
@@ -12,47 +13,6 @@ function upperCase(str) {
12
13
  return str.charAt(0).toUpperCase() + str.slice(1);
13
14
  }
14
15
 
15
- function createBasicRelationshipsProperties(from, to, storageResources, baseDirection) {
16
- const otherDirection = baseDirection === 'to' ? 'from' : 'to';
17
- return {
18
- fieldNames: {
19
- originTimeStamp: {
20
- type: 'number',
21
- requiredOnCreate: true,
22
- canUpdate: false,
23
- validation: {
24
- minimum: 1111111111111,
25
- maximum: 9999999999999
26
- }
27
- }
28
- },
29
- storageResources: storageResources,
30
- links: [
31
- {
32
- storageResourceTags: Object.keys(storageResources),
33
- from: {
34
- objType: {
35
- objectType: from.objectType,
36
- serviceTag: from.serviceTag
37
- },
38
- linkType: from.linkType,
39
- direction: baseDirection,
40
- requiredOnCreate: from.requiredOnCreate
41
- },
42
- to: {
43
- objType: {
44
- objectType: to.objectType,
45
- serviceTag: to.serviceTag
46
- },
47
- linkType: to.linkType,
48
- direction: otherDirection,
49
- requiredOnCreate: to.requiredOnCreate
50
- }
51
- }
52
- ]
53
- };
54
- }
55
-
56
16
  export async function generateAttributeTreeRelationshipSchemas(hydratedObjectData, options) {
57
17
  console.log(' [AttributeTreeRelSchemaGenerator] Generating dynamic AttributeTree RelationshipSchemas...');
58
18
  const schemasOutputDir = path.join(
@@ -3,6 +3,7 @@ import path from 'path';
3
3
  import ejs from 'ejs';
4
4
  import { fileURLToPath } from 'url';
5
5
  import propertyNode from '@izara_project/izara-property-nodes';
6
+ import { createBasicRelationshipsProperties } from './relationshipSchemaHelpers.js';
6
7
 
7
8
  const __filename = fileURLToPath(import.meta.url);
8
9
  const __dirname = path.dirname(__filename);
@@ -12,47 +13,6 @@ function upperCase(str) {
12
13
  return str.charAt(0).toUpperCase() + str.slice(1);
13
14
  }
14
15
 
15
- function createBasicRelationshipsProperties(from, to, storageResources, baseDirection) {
16
- const otherDirection = baseDirection === 'to' ? 'from' : 'to';
17
- return {
18
- fieldNames: {
19
- originTimeStamp: {
20
- type: 'number',
21
- requiredOnCreate: true,
22
- canUpdate: false,
23
- validation: {
24
- minimum: 1111111111111,
25
- maximum: 9999999999999
26
- }
27
- }
28
- },
29
- storageResources: storageResources,
30
- links: [
31
- {
32
- storageResourceTags: Object.keys(storageResources),
33
- from: {
34
- objType: {
35
- objectType: from.objectType,
36
- serviceTag: from.serviceTag
37
- },
38
- linkType: from.linkType,
39
- direction: baseDirection,
40
- requiredOnCreate: from.requiredOnCreate
41
- },
42
- to: {
43
- objType: {
44
- objectType: to.objectType,
45
- serviceTag: to.serviceTag
46
- },
47
- linkType: to.linkType,
48
- direction: otherDirection,
49
- requiredOnCreate: to.requiredOnCreate
50
- }
51
- }
52
- ]
53
- };
54
- }
55
-
56
16
  export async function generatePropertyValueRelationshipSchemas(hydratedObjectData, options) {
57
17
  console.log(' [PropertyValueRelSchemaGenerator] Generating dynamic PropertyValue RelationshipSchemas...');
58
18
  const schemasOutputDir = path.join(
@@ -0,0 +1,38 @@
1
+ import { describe, expect, it } from '@jest/globals';
2
+ import { createBasicRelationshipsProperties } from '../relationshipSchemaHelpers.js';
3
+
4
+ describe('createBasicRelationshipsProperties', () => {
5
+ it('preserves the relationship schema shape and opposite direction', () => {
6
+ expect(createBasicRelationshipsProperties(
7
+ { serviceTag: 'Svc', objectType: 'Source', linkType: 'one', requiredOnCreate: false },
8
+ { serviceTag: 'Svc', objectType: 'Target', linkType: 'many', requiredOnCreate: true },
9
+ { graph: { storageType: 'graph' } },
10
+ 'from'
11
+ )).toEqual({
12
+ fieldNames: {
13
+ originTimeStamp: {
14
+ type: 'number',
15
+ requiredOnCreate: true,
16
+ canUpdate: false,
17
+ validation: { minimum: 1111111111111, maximum: 9999999999999 }
18
+ }
19
+ },
20
+ storageResources: { graph: { storageType: 'graph' } },
21
+ links: [{
22
+ storageResourceTags: ['graph'],
23
+ from: {
24
+ objType: { serviceTag: 'Svc', objectType: 'Source' },
25
+ linkType: 'one',
26
+ direction: 'from',
27
+ requiredOnCreate: false
28
+ },
29
+ to: {
30
+ objType: { serviceTag: 'Svc', objectType: 'Target' },
31
+ linkType: 'many',
32
+ direction: 'to',
33
+ requiredOnCreate: true
34
+ }
35
+ }]
36
+ });
37
+ });
38
+ });
@@ -0,0 +1,40 @@
1
+ export function createBasicRelationshipsProperties(from, to, storageResources, baseDirection) {
2
+ const otherDirection = baseDirection === 'to' ? 'from' : 'to';
3
+ return {
4
+ fieldNames: {
5
+ originTimeStamp: {
6
+ type: 'number',
7
+ requiredOnCreate: true,
8
+ canUpdate: false,
9
+ validation: {
10
+ minimum: 1111111111111,
11
+ maximum: 9999999999999
12
+ }
13
+ }
14
+ },
15
+ storageResources,
16
+ links: [
17
+ {
18
+ storageResourceTags: Object.keys(storageResources),
19
+ from: {
20
+ objType: {
21
+ objectType: from.objectType,
22
+ serviceTag: from.serviceTag
23
+ },
24
+ linkType: from.linkType,
25
+ direction: baseDirection,
26
+ requiredOnCreate: from.requiredOnCreate
27
+ },
28
+ to: {
29
+ objType: {
30
+ objectType: to.objectType,
31
+ serviceTag: to.serviceTag
32
+ },
33
+ linkType: to.linkType,
34
+ direction: otherDirection,
35
+ requiredOnCreate: to.requiredOnCreate
36
+ }
37
+ }
38
+ ]
39
+ };
40
+ }
@@ -1,46 +0,0 @@
1
- // src/codeGenerators/app/sls_yaml/_policy/PolicyEmitter.js
2
- //
3
- // Reads the PolicyRegistry at the end of the generation run and emits one
4
- // per-role .yml file containing a single AWS::IAM::ManagedPolicy. Each
5
- // managed policy attaches to its base role via `Roles: [ !Ref <RoleName> ]`.
6
- //
7
- // IMPORTANT: This emitter runs AFTER SharedResourceYamlGenerator so the
8
- // managed policies can reference the roles that generator just emitted.
9
- // See generateCode.js for the ordering.
10
-
11
- import fs from 'fs/promises';
12
- import path from 'path';
13
- import ejs from 'ejs';
14
- import { fileURLToPath } from 'url';
15
-
16
- const __filename = fileURLToPath(import.meta.url);
17
- const __dirname = path.dirname(__filename);
18
-
19
- /**
20
- * @param {import('./PolicyRegistry.js').PolicyRegistry} registry
21
- * @param {{ outputPath: string }} options
22
- */
23
- export async function emitManagedPolicies(registry, options) {
24
- const outDir = path.join(
25
- options.outputPath,
26
- 'app',
27
- 'sls_yaml',
28
- 'generatedCode',
29
- 'source',
30
- 'iam-managed-policies'
31
- );
32
- await fs.mkdir(outDir, { recursive: true });
33
-
34
- const grouped = registry.toGroupedByRole();
35
- const templatePath = path.join(__dirname, 'templates', 'ManagedPolicy_Yaml.ejs');
36
- const tpl = await fs.readFile(templatePath, 'utf8');
37
-
38
- for (const [roleName, { statements }] of Object.entries(grouped)) {
39
- const policyName = roleName.replace(/role$/i, '');
40
- const content = ejs.render(tpl, { roleName, policyName, statements });
41
- const outFile = path.join(outDir, `${policyName}Policy.yml`);
42
- await fs.writeFile(outFile, content, 'utf8');
43
- }
44
-
45
- console.log(` [PolicyEmitter] Wrote ${Object.keys(grouped).length} managed-policy file(s) to ${outDir}`);
46
- }
@@ -1,55 +0,0 @@
1
- import { describe, it, expect, beforeEach, afterEach } from '@jest/globals';
2
- import fs from 'fs/promises';
3
- import path from 'path';
4
- import os from 'os';
5
- import { PolicyRegistry } from '../PolicyRegistry.js';
6
- import { emitManagedPolicies } from '../PolicyEmitter.js';
7
-
8
- describe('emitManagedPolicies', () => {
9
- let tmpDir, reg;
10
- beforeEach(async () => {
11
- tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'iz-policy-emitter-'));
12
- reg = new PolicyRegistry();
13
- });
14
- afterEach(async () => { await fs.rm(tmpDir, { recursive: true, force: true }); });
15
-
16
- it('emits one .yml file per registered role', async () => {
17
- reg.add('FindDataRole', 's3:GetObject', 'arn:aws:s3:::a/*');
18
- reg.add('RegisterRole', 'dynamodb:GetItem', 'arn:aws:dynamodb:*:*:table/x');
19
- await emitManagedPolicies(reg, { outputPath: tmpDir });
20
- const files = await fs.readdir(path.join(tmpDir, 'app', 'sls_yaml', 'generatedCode', 'source', 'iam-managed-policies'));
21
- expect(files.sort()).toEqual(['FindDataPolicy.yml', 'RegisterPolicy.yml']);
22
- });
23
-
24
- it('emits empty dir + no files when registry is empty', async () => {
25
- await emitManagedPolicies(reg, { outputPath: tmpDir });
26
- const outDir = path.join(tmpDir, 'app', 'sls_yaml', 'generatedCode', 'source', 'iam-managed-policies');
27
- const stat = await fs.stat(outDir);
28
- expect(stat.isDirectory()).toBe(true);
29
- const files = await fs.readdir(outDir);
30
- expect(files).toEqual([]);
31
- });
32
-
33
- it('writes YAML that references the role via !Ref', async () => {
34
- reg.add('FindDataRole', 's3:GetObject', 'arn:aws:s3:::a/*');
35
- await emitManagedPolicies(reg, { outputPath: tmpDir });
36
- const content = await fs.readFile(
37
- path.join(tmpDir, 'app', 'sls_yaml', 'generatedCode', 'source', 'iam-managed-policies', 'FindDataPolicy.yml'),
38
- 'utf8'
39
- );
40
- expect(content).toContain('Type: AWS::IAM::ManagedPolicy');
41
- expect(content).toContain('Roles:');
42
- expect(content).toContain('!Ref FindDataRole');
43
- expect(content).toContain('s3:GetObject');
44
- });
45
-
46
- it('does NOT crash on roles with statements containing Serverless CFN variables', async () => {
47
- reg.add('FindDataRole', 's3:GetObject', 'arn:aws:s3:::${self:custom.iz_serviceSchemaBucketName}/*');
48
- await emitManagedPolicies(reg, { outputPath: tmpDir });
49
- const content = await fs.readFile(
50
- path.join(tmpDir, 'app', 'sls_yaml', 'generatedCode', 'source', 'iam-managed-policies', 'FindDataPolicy.yml'),
51
- 'utf8'
52
- );
53
- expect(content).toContain('${self:custom.iz_serviceSchemaBucketName}');
54
- });
55
- });
@@ -1,22 +0,0 @@
1
- Resources:
2
- <%- policyName %>Policy:
3
- Type: AWS::IAM::ManagedPolicy
4
- Properties:
5
- ManagedPolicyName: ${self:custom.iz_resourcePrefix}<%- policyName %>Policy
6
- Description: "Managed policy for <%- roleName %> (auto-generated by PolicyEmitter)"
7
- PolicyDocument:
8
- Version: "2012-10-17"
9
- Statement:
10
- <% statements.forEach((stmt) => { -%>
11
- - Effect: Allow
12
- Action:
13
- <% stmt.action.forEach((a) => { -%>
14
- - <%- a %>
15
- <% }) -%>
16
- Resource:
17
- <% stmt.resource.forEach((r) => { -%>
18
- - "<%- r %>"
19
- <% }) -%>
20
- <% }) -%>
21
- Roles:
22
- - !Ref <%- roleName %>
package/test_schema.js DELETED
@@ -1,2 +0,0 @@
1
- import { basicValidatorSchema } from '@izara_project/izara-shared-service-schemas';
2
- console.log(JSON.stringify(basicValidatorSchema.basicFlowSchema?.properties?.flowSteps, null, 2));