@izara_project/izara-core-generate-service-code 1.0.63 → 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.
- package/package.json +1 -1
- package/src/codeGenerators/app/sls_yaml/FunctionYamlGenerator.js +10 -0
- package/src/codeGenerators/app/sls_yaml/SharedResourceYamlGenerator.js +3 -4
- package/src/codeGenerators/app/sls_yaml/_policy/PolicyRegistry.js +1 -39
- package/src/codeGenerators/app/sls_yaml/_policy/__tests__/PolicyRegistry.test.js +1 -16
- package/src/codeGenerators/app/src/generatedCode/Flow/_shared/events/BaseDsqHandler.js +10 -2
- package/src/codeGenerators/app/src/generatedCode/Flow/_shared/events/__tests__/BaseDsqHandler.test.js +5 -4
- package/src/codeGenerators/app/src/generatedCode/Flow/_shared/events/eventBridge.js +12 -4
- package/src/codeGenerators/app/src/generatedCode/Flow/_shared/events/lambdaSyncApi.js +10 -2
- package/src/codeGenerators/app/src/generatedCode/Flow/_shared/events/lambdaSyncInv.js +10 -2
- package/src/codeGenerators/app/src/generatedCode/Flow/_shared/shared/flowValidator.js +138 -1
- package/src/codeGenerators/app/src/generatedCode/Flow/_shared/shared/propertyNormalizer.js +15 -17
- package/src/generateCode.js +0 -1
- package/src/schemaGenerators/app/src/schemas/RelationshipSchemas/AttributeTreeRelationshipSchemaGenerator.js +1 -41
- package/src/schemaGenerators/app/src/schemas/RelationshipSchemas/PropertyValueRelationshipSchemaGenerator.js +1 -41
- package/src/schemaGenerators/app/src/schemas/RelationshipSchemas/__tests__/relationshipSchemaHelpers.test.js +38 -0
- package/src/schemaGenerators/app/src/schemas/RelationshipSchemas/relationshipSchemaHelpers.js +40 -0
- package/src/codeGenerators/app/sls_yaml/_policy/PolicyEmitter.js +0 -46
- package/src/codeGenerators/app/sls_yaml/_policy/__tests__/PolicyEmitter.test.js +0 -55
- package/src/codeGenerators/app/sls_yaml/_policy/templates/ManagedPolicy_Yaml.ejs +0 -22
- 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.
|
|
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
|
-
|
|
463
|
-
roleName,
|
|
464
|
-
|
|
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
|
|
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 {
|
|
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
|
|
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
|
|
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(
|
|
45
|
-
expect(generatedContent).toContain('
|
|
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(
|
|
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('
|
|
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
|
|
13
|
-
|
|
14
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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,3 +1,5 @@
|
|
|
1
|
+
import { TYPE_MAP } from './propertyNormalizer.js';
|
|
2
|
+
|
|
1
3
|
const ALLOWED_EVENTS = ['ownTopic', 'extTopic', 'lambdaSyncInv', 'lambdaSyncApi', 'eventBridge', 's3', 'dsq'];
|
|
2
4
|
const MAIN_ASYNC_EVENTS = ['ownTopic', 'extTopic', 's3'];
|
|
3
5
|
const RESERVED_SYSTEM_FLOW_TAGS = new Set([
|
|
@@ -48,6 +50,116 @@ function validateNamedRefs(flowTag, stepName, refs, defs, label, errors) {
|
|
|
48
50
|
}
|
|
49
51
|
}
|
|
50
52
|
|
|
53
|
+
function validateStepPropertyTypes(flowTag, propDefs, label, errors) {
|
|
54
|
+
if (!propDefs || typeof propDefs !== 'object') return;
|
|
55
|
+
const ALLOWED_TYPES = Object.keys(TYPE_MAP);
|
|
56
|
+
for (const [key, def] of Object.entries(propDefs)) {
|
|
57
|
+
if (!def || typeof def !== 'object') continue;
|
|
58
|
+
if (!def.type) {
|
|
59
|
+
errors.push(`Flow '${flowTag}' ${label} '${key}' is missing required field 'type'.`);
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
if (!ALLOWED_TYPES.includes(def.type)) {
|
|
63
|
+
errors.push(
|
|
64
|
+
`Flow '${flowTag}' ${label} '${key}' has unsupported type '${def.type}'. Allowed types are: ${ALLOWED_TYPES.join(', ')}.`
|
|
65
|
+
);
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
// enum: must supply enumValues array
|
|
69
|
+
if (def.type === 'enum') {
|
|
70
|
+
if (!Array.isArray(def.enumValues) || def.enumValues.length === 0) {
|
|
71
|
+
errors.push(
|
|
72
|
+
`Flow '${flowTag}' ${label} '${key}' has type 'enum' but is missing a non-empty 'enumValues' array.`
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
// default: must match resolved primitive type
|
|
77
|
+
// 'mixed' type accepts any value, so skip the type check
|
|
78
|
+
if (def.default !== undefined && def.type !== 'mixed') {
|
|
79
|
+
const resolvedPrimitive = TYPE_MAP[def.type];
|
|
80
|
+
const actualType = Array.isArray(def.default) ? 'array' : typeof def.default;
|
|
81
|
+
const expectedType = resolvedPrimitive === 'array' ? 'array' : resolvedPrimitive;
|
|
82
|
+
if (expectedType && actualType !== expectedType) {
|
|
83
|
+
errors.push(
|
|
84
|
+
`Flow '${flowTag}' ${label} '${key}' has a 'default' value of type '${actualType}' but expected '${expectedType}' (from schema type '${def.type}').`
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Warn about stepProperties / stepMessageAttributes keys that are declared
|
|
93
|
+
* but never referenced in any step's properties or messageAttributes arrays.
|
|
94
|
+
*/
|
|
95
|
+
function validateUnusedStepProperties(flowTag, flow, errors) {
|
|
96
|
+
const stepProps = flow.stepProperties;
|
|
97
|
+
const stepMsgAttrs = flow.stepMessageAttributes;
|
|
98
|
+
if (!stepProps && !stepMsgAttrs) return;
|
|
99
|
+
|
|
100
|
+
const usedProps = new Set();
|
|
101
|
+
const usedAttrs = new Set();
|
|
102
|
+
|
|
103
|
+
const flowSteps = Array.isArray(flow.flowSteps) ? flow.flowSteps : [];
|
|
104
|
+
for (const step of flowSteps) {
|
|
105
|
+
for (const ref of Array.isArray(step.properties) ? step.properties : []) {
|
|
106
|
+
if (typeof ref === 'string') usedProps.add(ref);
|
|
107
|
+
}
|
|
108
|
+
for (const ref of Array.isArray(step.messageAttributes) ? step.messageAttributes : []) {
|
|
109
|
+
if (typeof ref === 'string') usedAttrs.add(ref);
|
|
110
|
+
}
|
|
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
|
+
|
|
123
|
+
if (stepProps && typeof stepProps === 'object') {
|
|
124
|
+
for (const key of Object.keys(stepProps)) {
|
|
125
|
+
if (!usedProps.has(key)) {
|
|
126
|
+
errors.push(
|
|
127
|
+
`Flow '${flowTag}' stepProperties key '${key}' is declared but never referenced in any step's 'properties'.`
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (stepMsgAttrs && typeof stepMsgAttrs === 'object') {
|
|
134
|
+
for (const key of Object.keys(stepMsgAttrs)) {
|
|
135
|
+
if (!usedAttrs.has(key)) {
|
|
136
|
+
errors.push(
|
|
137
|
+
`Flow '${flowTag}' stepMessageAttributes key '${key}' is declared but never referenced in any step's 'messageAttributes'.`
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Detect keys that appear in both stepProperties and stepMessageAttributes
|
|
146
|
+
* to prevent ambiguous naming.
|
|
147
|
+
*/
|
|
148
|
+
function validateDuplicateStepPropertyKeys(flowTag, flow, errors) {
|
|
149
|
+
const stepProps = flow.stepProperties;
|
|
150
|
+
const stepMsgAttrs = flow.stepMessageAttributes;
|
|
151
|
+
if (!stepProps || !stepMsgAttrs) return;
|
|
152
|
+
|
|
153
|
+
const propKeys = new Set(Object.keys(stepProps));
|
|
154
|
+
for (const key of Object.keys(stepMsgAttrs)) {
|
|
155
|
+
if (propKeys.has(key)) {
|
|
156
|
+
errors.push(
|
|
157
|
+
`Flow '${flowTag}' key '${key}' is declared in both 'stepProperties' and 'stepMessageAttributes'. Keys must be unique across both groups.`
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
51
163
|
export function validateFlowsForGeneration(flows) {
|
|
52
164
|
const errors = [];
|
|
53
165
|
const seenCustomFlowTags = new Set();
|
|
@@ -86,7 +198,8 @@ export function validateFlowsForGeneration(flows) {
|
|
|
86
198
|
const seenStepNames = new Set();
|
|
87
199
|
let anyStepHasPlugInHooks = false;
|
|
88
200
|
|
|
89
|
-
for (
|
|
201
|
+
for (let i = 0; i < flowSteps.length; i++) {
|
|
202
|
+
const step = flowSteps[i];
|
|
90
203
|
const stepName = step?.stepName;
|
|
91
204
|
if (!stepName) {
|
|
92
205
|
errors.push(`Flow '${flowTag}' has a step without stepName.`);
|
|
@@ -110,6 +223,30 @@ export function validateFlowsForGeneration(flows) {
|
|
|
110
223
|
|
|
111
224
|
validateNamedRefs(flowTag, stepName, step.properties, flow.stepProperties, 'properties', errors);
|
|
112
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
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
validateStepPropertyTypes(flowTag, flow.stepProperties, 'stepProperties', errors);
|
|
239
|
+
validateStepPropertyTypes(flowTag, flow.stepMessageAttributes, 'stepMessageAttributes', errors);
|
|
240
|
+
validateDuplicateStepPropertyKeys(flowTag, flow, errors);
|
|
241
|
+
validateUnusedStepProperties(flowTag, flow, errors);
|
|
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
|
+
}
|
|
113
250
|
}
|
|
114
251
|
|
|
115
252
|
// ponytail: relaxed anyStepHasPlugInHooks requirement
|
|
@@ -1,20 +1,18 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
float: 'number', special: 'string', object: 'object',
|
|
5
|
-
boolean: 'boolean', array: 'array', arrayMixed: 'array',
|
|
6
|
-
arrayObject: 'array', arrayNumeric: 'array', arrayString: 'array',
|
|
7
|
-
timestamp: 'number'
|
|
8
|
-
};
|
|
1
|
+
import { consts } from '@izara_project/izara-core-library-service-schemas';
|
|
2
|
+
|
|
3
|
+
export const TYPE_MAP = consts.FIELDNAME_TYPE_MAPPING;
|
|
9
4
|
|
|
10
5
|
export function normalizePropertyType(p, lookupSource) {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
6
|
+
let prop =
|
|
7
|
+
typeof p === 'string'
|
|
8
|
+
? lookupSource && lookupSource[p]
|
|
9
|
+
? { propertyName: p, ...lookupSource[p] }
|
|
10
|
+
: { propertyName: p, type: 'string' }
|
|
11
|
+
: p;
|
|
12
|
+
|
|
13
|
+
if (prop && prop.type) {
|
|
14
|
+
prop = { ...prop, type: TYPE_MAP[prop.type] || prop.type };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return prop;
|
|
20
18
|
}
|
package/src/generateCode.js
CHANGED
|
@@ -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