@izara_project/izara-core-generate-service-code 1.0.66 → 1.0.68

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 (90) hide show
  1. package/.prettierrc +11 -0
  2. package/docs/TriggeredCache Flow.drawio +193 -0
  3. package/docs/TriggeredCacheFlow.mmd +47 -0
  4. package/package.json +16 -12
  5. package/scripts/format-ejs.js +105 -0
  6. package/src/compile/compileProject.js +16 -1
  7. package/src/compile/loadSchemas.js +108 -1
  8. package/src/emit/crud/emitCrudApplicationArtifacts.js +9 -5
  9. package/src/emit/crud/emitDynamicCrudFlowSchemas.js +1 -2
  10. package/src/emit/crud/templates/CreateEndpoint_Main.ejs +300 -109
  11. package/src/emit/crud/templates/DeleteEndpoint_Main.ejs +221 -143
  12. package/src/emit/crud/templates/FlowEndpointBeforeLogical_Main.ejs +22 -6
  13. package/src/emit/crud/templates/FlowEndpointComplete_Main.ejs +150 -38
  14. package/src/emit/crud/templates/FlowEndpoint_HdrWbs.ejs +51 -31
  15. package/src/emit/crud/templates/FlowHandler_HdrApi.ejs +57 -31
  16. package/src/emit/crud/templates/FlowHandler_HdrInv.ejs +28 -26
  17. package/src/emit/crud/templates/FlowHandler_HdrSqs.ejs +15 -11
  18. package/src/emit/crud/templates/FlowMain_Wbs.ejs +53 -38
  19. package/src/emit/crud/templates/GetEndpoint_Main.ejs +6 -3
  20. package/src/emit/crud/templates/GetRangeEndpoint_Main.ejs +2 -2
  21. package/src/emit/crud/templates/UpdateEndpoint_Main.ejs +407 -252
  22. package/src/emit/custom/customFlows.js +112 -31
  23. package/src/emit/custom/emitCustomFlowApplicationArtifacts.js +89 -77
  24. package/src/emit/custom/templates/FlowEndpoint_Main.ejs +49 -37
  25. package/src/emit/custom/templates/FlowStep_Main.ejs +7 -9
  26. package/src/emit/custom/templates/s3/ConfirmReserved_HdrSqs.ejs +59 -54
  27. package/src/emit/custom/templates/s3/ConfirmReserved_Main.ejs +33 -23
  28. package/src/emit/custom/templates/s3/CreateObject_HdrWbs.ejs +30 -23
  29. package/src/emit/custom/templates/s3/CreateObject_Main.ejs +56 -39
  30. package/src/emit/custom/templates/s3/CreatePreSignUrl_HdrSqs.ejs +56 -51
  31. package/src/emit/custom/templates/s3/CreatePreSignUrl_Main.ejs +112 -75
  32. package/src/emit/custom/templates/s3/FlowHandler_HdrS3.ejs +12 -7
  33. package/src/emit/custom/templates/s3/ProcessS3File_Main.ejs +11 -8
  34. package/src/emit/custom/templates/s3/ReservedLimit_HdrSqs.ejs +64 -55
  35. package/src/emit/custom/templates/s3/ReservedLimit_Main.ejs +74 -52
  36. package/src/emit/custom/templates/triggerCache/CheckTriggerCache_HdrSqs.ejs +58 -14
  37. package/src/emit/custom/templates/triggerCache/CheckTriggerCache_Main.ejs +12 -17
  38. package/src/emit/custom/templates/triggerCache/TriggerCacheComplete_Main.ejs +20 -13
  39. package/src/emit/custom/templates/triggerCache/TriggerCache_Main.ejs +4 -1
  40. package/src/emit/function/functionYamlArtifact.js +46 -12
  41. package/src/emit/rbac/emitDynamicRbacFlowSchemas.js +28 -15
  42. package/src/emit/rbac/emitRbacApplicationArtifacts.js +179 -66
  43. package/src/emit/rbac/templates/CreateTargetRole_HdrSqs.ejs +41 -18
  44. package/src/emit/rbac/templates/CreateTargetRole_Main.ejs +23 -3
  45. package/src/emit/rbac/templates/ListRbac_HdrApi.ejs +27 -5
  46. package/src/emit/rbac/templates/ListRbac_HdrSqs.ejs +41 -8
  47. package/src/emit/rbac/templates/ListRbac_Main.ejs +26 -6
  48. package/src/emit/rbac/templates/RbacMutation_Main.ejs +23 -3
  49. package/src/emit/rbac/templates/UserRbacFlow_HdrSqs.ejs +46 -0
  50. package/src/emit/rbac/templates/UserRbacFlow_Main.ejs +152 -33
  51. package/src/emit/rbac/templates/UserRoleMutation_Main.ejs +24 -4
  52. package/src/emit/relationship/emitDynamicRelationshipFlowSchemas.js +1 -2
  53. package/src/emit/relationship/emitRelationshipApplicationArtifacts.js +6 -5
  54. package/src/emit/relationship/templates/ChangeRelationshipComplete_Main.ejs +64 -37
  55. package/src/emit/relationship/templates/ChangeRelationship_Main.ejs +260 -144
  56. package/src/emit/relationship/templates/CreateRelationshipComplete_Main.ejs +68 -43
  57. package/src/emit/relationship/templates/CreateRelationship_Main.ejs +315 -171
  58. package/src/emit/relationship/templates/DeleteRelationshipComplete_Main.ejs +67 -41
  59. package/src/emit/relationship/templates/DeleteRelationship_Main.ejs +226 -115
  60. package/src/emit/relationship/templates/FlowHandler_HdrApi.ejs +41 -33
  61. package/src/emit/relationship/templates/FlowHandler_HdrDsq.ejs +15 -11
  62. package/src/emit/relationship/templates/FlowHandler_HdrInv.ejs +28 -26
  63. package/src/emit/relationship/templates/FlowHandler_HdrSqs.ejs +15 -11
  64. package/src/emit/relationship/templates/GetRelationshipComplete_Main.ejs +46 -26
  65. package/src/emit/relationship/templates/GetRelationship_Main.ejs +233 -126
  66. package/src/emit/relationship/templates/MoveRelationshipComplete_Main.ejs +72 -44
  67. package/src/emit/relationship/templates/MoveRelationship_Main.ejs +266 -150
  68. package/src/emit/relationship/templates/UpdateRelationshipComplete_Main.ejs +74 -45
  69. package/src/emit/relationship/templates/UpdateRelationship_Main.ejs +285 -152
  70. package/src/emit/resource/emitResourceArtifacts.js +499 -108
  71. package/src/emit/resource/templates/DynamoDBTable_Yaml.ejs +18 -18
  72. package/src/emit/resource/templates/FlowOut_Yaml.ejs +5 -5
  73. package/src/emit/resource/templates/GeneratedLambdaRoleServiceConfig.ejs +138 -0
  74. package/src/emit/resource/templates/ResourceBeforeLogicalYaml.ejs +44 -44
  75. package/src/emit/resource/templates/ResourceSqsYaml.ejs +36 -36
  76. package/src/emit/resource/templates/ResourceYaml.ejs +48 -48
  77. package/src/emit/resource/templates/S3Bucket_Yaml.ejs +19 -0
  78. package/src/emit/resource/templates/SharedResource_Yaml.ejs +11 -11
  79. package/src/emit/resource/templates/SystemDynamoDB_Yaml.ejs +105 -105
  80. package/src/emit/system/emitSystemWebSocketArtifacts.js +2 -1
  81. package/src/generate.js +8 -4
  82. package/test/createTargetRoleHandler.test.js +63 -14
  83. package/test/crudApplicationArtifacts.test.js +1 -1
  84. package/test/customFlowApplicationArtifacts.test.js +71 -43
  85. package/test/dynamicCrudFlowSchemas.test.js +6 -6
  86. package/test/dynamicRbacFlowSchemas.test.js +8 -12
  87. package/test/dynamicRelationshipFlowSchemas.test.js +3 -3
  88. package/test/publicApi.test.js +11 -13
  89. package/test/resourceArtifacts.test.js +13 -3
  90. package/src/emit/rbac/templates/UserRbacFlow_HdrInv.ejs +0 -10
@@ -1,37 +1,122 @@
1
1
  import ejs from 'ejs';
2
2
  import { readFile } from 'node:fs/promises';
3
3
 
4
- import { customFlowRoleName, flowExtTopicSteps, generatedOwnTopicFlows, supportedCustomFlows, triggerCacheFlows, triggerCacheQueueNames } from '../custom/customFlows.js';
4
+ import {
5
+ customFlowRoleName,
6
+ flowDsqSteps,
7
+ flowExtTopicSteps,
8
+ generatedOwnTopicFlows,
9
+ supportedCustomFlows
10
+ } from '../custom/customFlows.js';
5
11
  import { hasSystemWebSocket } from '../system/emitSystemWebSocketArtifacts.js';
6
12
 
7
13
  const TEMPLATE_URL = new URL('./templates/', import.meta.url);
8
14
  const RESOURCE_ROOT = 'resource/sls_yaml/generatedCode/source';
9
15
  const APP_ROOT = 'app/sls_yaml/generatedCode/source';
16
+ const INITIAL_SETUP_ROOT = 'app/initial_setup/generatedCode/source';
10
17
  const CRUD_ACTIONS = ['Create', 'Update', 'Delete', 'Get'];
11
- const RELATIONSHIP_ACTIONS = ['CreateRelationship', 'UpdateRelationship', 'DeleteRelationship', 'GetRelationship', 'ChangeRelationship', 'MoveRelationship'];
12
- const RBAC_ACTIONS = ['CreateTargetRole', 'ListTargetRole', 'DeleteTargetRole', 'CreateRolePermissions', 'ListRolePermissions', 'DeleteRolePermissions', 'CreateUserRole', 'ListUserInRoles', 'DeleteUserFromRole'];
13
- const SYSTEM_TABLES = ['WebSocketTask', 'UserUploadRecords', 'RegisterRecords', 'AwaitingMultipleSteps', 'AwaitingMultipleStepByPending', 'FindDataMain', 'LogicalResultsMain', 'LogicalResultsData', 'AwaitingStep', 'TargetRoles', 'RolePermissions', 'RoleUsers', 'UserRoles'];
14
- const RBAC_TABLES = ['TargetRoles', 'RolePermissions', 'RoleUsers', 'UserRoles', 'WebSocketTask'];
15
- const LEGACY_PROCESS_QUEUES = ['FindDataHdrSqs', 'ProcessLogicalHdrSqs', 'PaginateProcessLogicalHdrDsq', 'PaginateProcessLogicalHdrSqs'];
16
- const DYNAMO_ACTIONS = ['dynamodb:DeleteItem', 'dynamodb:GetItem', 'dynamodb:PutItem', 'dynamodb:Query', 'dynamodb:UpdateItem'];
17
- const SQS_CONSUMER_ACTIONS = ['sqs:DeleteMessage', 'sqs:GetQueueAttributes', 'sqs:ReceiveMessage'];
18
+ const RELATIONSHIP_ACTIONS = [
19
+ 'CreateRelationship',
20
+ 'UpdateRelationship',
21
+ 'DeleteRelationship',
22
+ 'GetRelationship',
23
+ 'ChangeRelationship',
24
+ 'MoveRelationship'
25
+ ];
26
+ const RBAC_ACTIONS = [
27
+ 'CreateTargetRole',
28
+ 'ListTargetRole',
29
+ 'DeleteTargetRole',
30
+ 'CreateRolePermissions',
31
+ 'ListRolePermissions',
32
+ 'DeleteRolePermissions',
33
+ 'CreateUserRole',
34
+ 'ListUserInRoles',
35
+ 'DeleteUserFromRole'
36
+ ];
37
+ const SYSTEM_TABLES = [
38
+ 'WebSocketTask',
39
+ 'UserUploadRecords',
40
+ 'RegisterRecords',
41
+ 'AwaitingMultipleSteps',
42
+ 'AwaitingMultipleStepByPending',
43
+ 'FindDataMain',
44
+ 'LogicalResultsMain',
45
+ 'LogicalResultsData',
46
+ 'AwaitingStep',
47
+ 'TargetRoles',
48
+ 'RolePermissions',
49
+ 'RoleUsers',
50
+ 'UserRoles'
51
+ ];
52
+ const RBAC_TABLES = [
53
+ 'TargetRoles',
54
+ 'RolePermissions',
55
+ 'RoleUsers',
56
+ 'UserRoles',
57
+ 'WebSocketTask'
58
+ ];
59
+ const LEGACY_PROCESS_QUEUES = [
60
+ 'FindDataHdrSqs',
61
+ 'ProcessLogicalHdrSqs',
62
+ 'PaginateProcessLogicalHdrDsq',
63
+ 'PaginateProcessLogicalHdrSqs'
64
+ ];
65
+ const DYNAMO_ACTIONS = [
66
+ 'dynamodb:DeleteItem',
67
+ 'dynamodb:GetItem',
68
+ 'dynamodb:PutItem',
69
+ 'dynamodb:Query',
70
+ 'dynamodb:UpdateItem'
71
+ ];
72
+ const SQS_CONSUMER_ACTIONS = [
73
+ 'sqs:DeleteMessage',
74
+ 'sqs:GetQueueAttributes',
75
+ 'sqs:ReceiveMessage'
76
+ ];
18
77
 
19
- export async function emitResourceArtifacts({ objects, relationships, flows = [] }) {
78
+ export async function emitResourceArtifacts({
79
+ objects,
80
+ relationships,
81
+ flows = []
82
+ }) {
20
83
  if (![objects, relationships, flows].every(Array.isArray)) {
21
84
  throw new TypeError('objects, relationships, and flows must be arrays');
22
85
  }
23
86
  const state = createResourceState({ objects, relationships, flows });
24
- const [inSqs, out, dynamo, iam] = await Promise.all([
87
+ const [inSqs, out, dynamo, iam, lambdaRoleConfig, s3] = await Promise.all([
25
88
  renderInSqs(state),
26
89
  renderOut(state),
27
90
  renderDynamo(state),
28
- renderIam(state)
91
+ renderIam(state),
92
+ renderLambdaRoleConfig(state),
93
+ renderS3(state)
29
94
  ]);
30
95
  return [
31
- { path: `${RESOURCE_ROOT}/generated-sns-in-sqs.yml`, content: normalizeYaml(inSqs) },
32
- { path: `${RESOURCE_ROOT}/generated-sns-out.yml`, content: normalizeYaml(out) },
33
- { path: `${RESOURCE_ROOT}/generated-dynamoDB-table.yml`, content: normalizeYaml(dynamo) },
34
- { path: `${APP_ROOT}/generate-shared-resource.yml`, content: normalizeYaml(iam) }
96
+ {
97
+ path: `${RESOURCE_ROOT}/generated-sns-in-sqs.yml`,
98
+ content: normalizeYaml(inSqs)
99
+ },
100
+ {
101
+ path: `${RESOURCE_ROOT}/generated-sns-out.yml`,
102
+ content: normalizeYaml(out)
103
+ },
104
+ {
105
+ path: `${RESOURCE_ROOT}/generated-dynamoDB-table.yml`,
106
+ content: normalizeYaml(dynamo)
107
+ },
108
+ {
109
+ path: `${RESOURCE_ROOT}/generated-s3.yml`,
110
+ content: normalizeYaml(s3)
111
+ },
112
+ {
113
+ path: `${APP_ROOT}/generate-shared-resource.yml`,
114
+ content: normalizeYaml(iam)
115
+ },
116
+ {
117
+ path: `${INITIAL_SETUP_ROOT}/generatedLambdaRoleServiceConfig.js`,
118
+ content: lambdaRoleConfig
119
+ }
35
120
  ];
36
121
  }
37
122
 
@@ -39,33 +124,49 @@ export const resourceArtifactPaths = [
39
124
  `${RESOURCE_ROOT}/generated-sns-in-sqs.yml`,
40
125
  `${RESOURCE_ROOT}/generated-sns-out.yml`,
41
126
  `${RESOURCE_ROOT}/generated-dynamoDB-table.yml`,
42
- `${APP_ROOT}/generate-shared-resource.yml`
127
+ `${RESOURCE_ROOT}/generated-s3.yml`,
128
+ `${APP_ROOT}/generate-shared-resource.yml`,
129
+ `${INITIAL_SETUP_ROOT}/generatedLambdaRoleServiceConfig.js`
43
130
  ];
44
131
 
45
132
  function createResourceState({ objects, relationships, flows }) {
46
133
  const crud = objects.some(isCrudObject);
47
134
  const rbac = objects.some(hasRbacAddOn);
48
- const ownTopics = new Set(flows.filter(flow => includesEvent(flow, 'ownTopic')).map(flow => upperFirst(flow.flowTag)));
135
+ const ownTopics = new Set(
136
+ flows
137
+ .filter(flow => includesEvent(flow, 'ownTopic'))
138
+ .map(flow => upperFirst(flow.flowTag))
139
+ );
49
140
  const sqsQueues = new Set([
50
141
  ...LEGACY_PROCESS_QUEUES,
51
- ...flows.filter(flow => includesEvent(flow, 'extTopic')).map(flow => `Process${upperFirst(flow.flowTag)}HdrSqs`)
142
+ ...flows
143
+ .filter(flow => includesEvent(flow, 'extTopic'))
144
+ .map(flow => `Process${upperFirst(flow.flowTag)}HdrSqs`)
52
145
  ]);
53
- const triggerCache = triggerCacheFlows(flows);
54
- const triggerCacheQueues = [...new Set(triggerCache.flatMap(triggerCacheQueueNames))].map(queueName => ({
55
- queueName,
56
- resourceQueueName: queueName.replaceAll('_', '')
57
- }));
58
- flowExtTopicSteps(flows).forEach(({ flowTag, stepName }) => sqsQueues.add(`${flowTag}${stepName}HdrSqs`));
59
- flows.filter(flow => includesEvent(flow, 's3')).forEach(flow => {
60
- const tag = upperFirst(flow.flowTag);
61
- sqsQueues.add(`${tag}CreatePreSignUrlHdrSqs`);
62
- sqsQueues.add(`${tag}ReservedLimitHdrSqs`);
63
- sqsQueues.add(`${tag}ConfirmAfterUploadHdrSqs`);
64
- });
65
- const dsqQueues = new Set(flows.filter(flow => includesEvent(flow, 'dsq')).map(flow => `Process${upperFirst(flow.flowTag)}HdrDsq`));
146
+ flowExtTopicSteps(flows).forEach(({ flowTag, stepName }) =>
147
+ sqsQueues.add(`${flowTag}${stepName}HdrSqs`)
148
+ );
149
+ flows
150
+ .filter(flow => includesEvent(flow, 's3'))
151
+ .forEach(flow => {
152
+ const tag = upperFirst(flow.flowTag);
153
+ sqsQueues.add(`${tag}CreatePreSignUrlHdrSqs`);
154
+ sqsQueues.add(`${tag}ReservedLimitHdrSqs`);
155
+ sqsQueues.add(`${tag}ConfirmAfterUploadHdrSqs`);
156
+ });
157
+ const dsqQueues = new Set(
158
+ flows
159
+ .filter(flow => includesEvent(flow, 'dsq'))
160
+ .map(flow => `${upperFirst(flow.flowTag)}HdrDsq`)
161
+ );
162
+ flowDsqSteps(flows).forEach(({ flowTag, stepName }) =>
163
+ dsqQueues.add(`${flowTag}${stepName}HdrDsq`)
164
+ );
66
165
  if (crud) {
67
166
  CRUD_ACTIONS.forEach(action => ownTopics.add(action));
68
- ['Create', 'Update', 'Delete'].forEach(action => sqsQueues.add(`${action}CompleteHdrSqs`));
167
+ ['Create', 'Update', 'Delete'].forEach(action =>
168
+ sqsQueues.add(`${action}CompleteHdrSqs`)
169
+ );
69
170
  }
70
171
  if (relationships.length > 0) {
71
172
  RELATIONSHIP_ACTIONS.forEach(action => {
@@ -79,75 +180,157 @@ function createResourceState({ objects, relationships, flows }) {
79
180
  ownTopics.add(action);
80
181
  sqsQueues.add(`Process${action}HdrSqs`);
81
182
  });
82
- objects.filter(hasRbacAddOn).forEach(object => sqsQueues.add(`Process${upperFirst(object.objectType)}RbacFlowHdrInv`));
183
+ objects
184
+ .filter(hasRbacAddOn)
185
+ .forEach(object => {
186
+ const flowTag = upperFirst(object.objectType) + 'RbacFlow';
187
+ ownTopics.add(flowTag);
188
+ });
83
189
  }
84
190
  const beforeLogicalByAction = new Map();
85
- for (const flow of flows.filter(flow => flow?.generatedCodeConfig?.codeHookTag === 'beforeLogical')) {
191
+ for (const flow of flows.filter(
192
+ flow => flow?.generatedCodeConfig?.codeHookTag === 'beforeLogical'
193
+ )) {
86
194
  const action = upperFirst(flow.generatedCodeConfig.generatedCodeTag);
87
195
  if (!action || !flow.flowTag) continue;
88
196
  const flowTags = beforeLogicalByAction.get(action) ?? new Set();
89
197
  flowTags.add(flow.flowTag);
90
198
  beforeLogicalByAction.set(action, flowTags);
91
199
  }
92
- const beforeLogical = [...beforeLogicalByAction].map(([action, flowTags]) => ({ action, flowTags: [...flowTags].sort() }));
93
- const customRoles = [...new Set([
94
- ...supportedCustomFlows(flows).map(flow => customFlowRoleName(flow.flowTag)),
95
- ...generatedOwnTopicFlows(flows).map(flow => customFlowRoleName(flow.flowTag)),
96
- ...flowExtTopicSteps(flows).map(({ flow }) => customFlowRoleName(flow.flowTag)),
97
- ...triggerCache.map(flow => customFlowRoleName(flow.flowTag))
98
- ])];
99
- const websocket = hasSystemWebSocket(flows, { includeRelationshipOwnTopic: relationships.length > 0 });
200
+ const beforeLogical = [...beforeLogicalByAction].map(
201
+ ([action, flowTags]) => ({ action, flowTags: [...flowTags].sort() })
202
+ );
203
+ const customRoles = [
204
+ ...new Set([
205
+ ...supportedCustomFlows(flows).map(flow =>
206
+ customFlowRoleName(flow.flowTag)
207
+ ),
208
+ ...generatedOwnTopicFlows(flows).map(flow =>
209
+ customFlowRoleName(flow.flowTag)
210
+ ),
211
+ ...flowExtTopicSteps(flows).map(({ flow }) =>
212
+ customFlowRoleName(flow.flowTag)
213
+ ),
214
+ ...flowDsqSteps(flows).map(({ flow }) => customFlowRoleName(flow.flowTag))
215
+ ])
216
+ ];
217
+ const websocket = hasSystemWebSocket(flows, {
218
+ includeRelationshipOwnTopic: true
219
+ });
100
220
  const webSocketRoles = new Set(websocket ? ['WebSocketMainRole'] : []);
101
221
  if (crud) webSocketRoles.add('FlowObjectsRole');
102
222
  if (relationships.length > 0) webSocketRoles.add('FlowRelationshipRole');
103
223
  if (rbac) webSocketRoles.add('FlowRbacRole');
104
224
  [...supportedCustomFlows(flows), ...generatedOwnTopicFlows(flows)]
105
- .filter(flow => includesEvent(flow, 'ownTopic') || includesEvent(flow, 's3'))
225
+ .filter(
226
+ flow => includesEvent(flow, 'ownTopic') || includesEvent(flow, 's3')
227
+ )
106
228
  .forEach(flow => webSocketRoles.add(customFlowRoleName(flow.flowTag)));
107
229
  const outTopics = new Set([
108
230
  ...ownTopics,
109
231
  ...beforeLogical.map(({ action }) => action),
110
- ...flows.filter(flow => flow?.outputTopic && upperFirst(flow.flowTag)).map(flow => upperFirst(flow.flowTag))
232
+ ...flows
233
+ .filter(flow => flow?.outputTopic && upperFirst(flow.flowTag))
234
+ .map(flow => upperFirst(flow.flowTag))
111
235
  ]);
112
236
  const objectTableNames = [...collectTables(objects).keys()];
113
237
  const relationshipTableNames = [...collectTables(relationships).keys()];
114
- const tableMap = new Map([...collectTables(objects), ...collectTables(relationships)]);
238
+ const tableMap = new Map([
239
+ ...collectTables(objects),
240
+ ...collectTables(relationships)
241
+ ]);
242
+ const externalDynamoTables = objects.flatMap(
243
+ object => object.externalDynamoResources ?? []
244
+ );
245
+ const s3Buckets = flows
246
+ .filter(flow => includesEvent(flow, 's3') && flow.createBucket === true)
247
+ .map(flow => flow.bucketName)
248
+ .filter(Boolean);
115
249
  return {
116
250
  crud,
117
251
  rbac,
118
252
  relationships: relationships.length > 0,
119
253
  ownTopics: [...ownTopics].filter(Boolean).sort(),
120
254
  sqsQueues: [...sqsQueues].filter(Boolean).sort(),
121
- triggerCacheQueues,
122
255
  dsqQueues: [...dsqQueues].filter(Boolean).sort(),
123
256
  beforeLogical,
124
257
  customRoles,
125
258
  websocket,
126
259
  webSocketRoles,
127
260
  webSocketSubscriptions: websocket
128
- ? [...outTopics].filter(topic => (
129
- flows.some(flow => flow?.outputTopic && upperFirst(flow.flowTag) === topic) ||
130
- (relationships.length > 0 && topic !== 'GetRelationship' && RELATIONSHIP_ACTIONS.includes(topic))
131
- )).sort()
261
+ ? [...outTopics]
262
+ .filter(
263
+ topic =>
264
+ flows.some(
265
+ flow =>
266
+ (flow?.outputTopic ||
267
+ (includesEvent(flow, 'ownTopic') &&
268
+ flow.initiateBy === 'user')) &&
269
+ upperFirst(flow.flowTag) === topic
270
+ ) ||
271
+ (relationships.length > 0 &&
272
+ topic !== 'GetRelationship' &&
273
+ RELATIONSHIP_ACTIONS.includes(topic))
274
+ )
275
+ .sort()
132
276
  : [],
133
277
  outTopics: [...outTopics].filter(Boolean).sort(),
134
- tables: [...tableMap.entries()].map(([tableName, keys]) => ({ tableName, ...keys })),
278
+ tables: [...tableMap.entries()].map(([tableName, keys]) => ({
279
+ tableName,
280
+ ...keys
281
+ })),
135
282
  allTableNames: [...new Set([...SYSTEM_TABLES, ...tableMap.keys()])].sort(),
136
283
  objectTableNames,
137
284
  relationshipTableNames,
138
- objectTablesByType: new Map(objects.map(object => [object.objectType, Object.values(object.storageResources ?? {}).find(resource => resource?.storageType === 'dynamoDB')?.tableName]).filter(([, tableName]) => tableName)),
285
+ externalDynamoTables,
286
+ objectTablesByType: new Map(
287
+ objects
288
+ .map(object => [
289
+ object.objectType,
290
+ Object.values(object.storageResources ?? {}).find(
291
+ resource => resource?.storageType === 'dynamoDB'
292
+ )?.tableName
293
+ ])
294
+ .filter(([, tableName]) => tableName)
295
+ ),
296
+ s3Buckets,
139
297
  flows
140
298
  };
141
299
  }
142
300
 
301
+ async function renderS3(state) {
302
+ if (state.s3Buckets.length === 0) return 'Resources:';
303
+ const template = await readFile(new URL('S3Bucket_Yaml.ejs', TEMPLATE_URL), 'utf8');
304
+ const chunks = await Promise.all(
305
+ state.s3Buckets.map(bucketName => ejs.render(template, { bucketName }))
306
+ );
307
+ return `Resources:\n${chunks.join('\n')}`;
308
+ }
309
+
143
310
  async function renderInSqs(state) {
144
311
  const chunks = await Promise.all([
145
- ...state.ownTopics.map(queueName => render('ResourceYaml.ejs', { queueName })),
146
- ...state.sqsQueues.map(queueName => render('ResourceSqsYaml.ejs', { queueName })),
147
- ...state.triggerCacheQueues.map(queue => render('ResourceSqsYaml.ejs', queue)),
148
- ...state.dsqQueues.map(queueName => render('ResourceSqsYaml.ejs', { queueName })),
149
- ...state.beforeLogical.map(({ action, flowTags }) => render('ResourceBeforeLogicalYaml.ejs', { queueName: `${action}BeforeLogical`, flowTags })),
150
- ...(state.websocket ? [render('ResourceSqsYaml.ejs', { queueName: 'WebSocketCompleteHdrSqs' })] : [])
312
+ ...state.ownTopics.map(queueName =>
313
+ render('ResourceYaml.ejs', { queueName })
314
+ ),
315
+ ...state.sqsQueues.map(queueName =>
316
+ render('ResourceSqsYaml.ejs', { queueName })
317
+ ),
318
+ ...state.dsqQueues.map(queueName =>
319
+ render('ResourceSqsYaml.ejs', { queueName })
320
+ ),
321
+ ...state.beforeLogical.map(({ action, flowTags }) =>
322
+ render('ResourceBeforeLogicalYaml.ejs', {
323
+ queueName: `${action}BeforeLogical`,
324
+ flowTags
325
+ })
326
+ ),
327
+ ...(state.websocket
328
+ ? [
329
+ render('ResourceSqsYaml.ejs', {
330
+ queueName: 'WebSocketCompleteHdrSqs'
331
+ })
332
+ ]
333
+ : [])
151
334
  ]);
152
335
  return `Resources:\n${chunks.join('\n')}\n${state.webSocketSubscriptions.map(renderWebSocketSubscription).join('\n')}`;
153
336
  }
@@ -171,19 +354,62 @@ async function renderIam(state) {
171
354
  if (state.rbac) roles.push('FlowRbacRole');
172
355
  if (state.websocket) roles.push('WebSocketMainRole');
173
356
  roles.push(...state.customRoles);
174
- return render('SharedResource_Yaml.ejs', { roles: roles.sort().map(roleName => ({ roleName, statements: roleStatements(state, roleName) })) });
357
+ return render('SharedResource_Yaml.ejs', {
358
+ roles: roles.sort().map(roleName => ({
359
+ roleName,
360
+ statements: roleStatements(state, roleName)
361
+ }))
362
+ });
363
+ }
364
+
365
+ async function renderLambdaRoleConfig(state) {
366
+ const s3Flows = state.flows
367
+ .filter(flow => includesEvent(flow, 's3'))
368
+ .map(flow => ({
369
+ roleName: upperFirst(flow.flowTag)
370
+ }));
371
+ return render('GeneratedLambdaRoleServiceConfig.ejs', {
372
+ crud: state.crud,
373
+ relationships: state.relationships,
374
+ s3Flows
375
+ });
175
376
  }
176
377
 
177
378
  function roleStatements(state, roleName) {
178
379
  const statements = [];
179
380
  if (roleNeedsSchemaRead(state, roleName)) {
180
- statements.push({ action: ['s3:GetObject'], resource: ['arn:aws:s3:::${self:custom.iz_serviceSchemaBucketName}/perServiceSchemas/*'] });
381
+ statements.push({
382
+ action: ['s3:GetObject'],
383
+ resource: [
384
+ 'arn:aws:s3:::${self:custom.iz_serviceSchemaBucketName}/perServiceSchemas/*'
385
+ ]
386
+ });
181
387
  }
182
- addStatement(statements, dynamoActionsForRole(roleName), dynamoArns(roleTableNames(state, roleName)));
183
- addStatement(statements, SQS_CONSUMER_ACTIONS, queueArns(roleQueueNames(state, roleName)));
184
- addStatement(statements, ['sns:Publish'], topicArns(roleTopicNames(state, roleName)));
388
+ addStatement(
389
+ statements,
390
+ dynamoActionsForRole(roleName),
391
+ [
392
+ ...dynamoArns(roleTableNames(state, roleName)),
393
+ ...externalDynamoArns(roleExternalDynamoTables(state, roleName))
394
+ ]
395
+ );
396
+ addStatement(
397
+ statements,
398
+ SQS_CONSUMER_ACTIONS,
399
+ queueArns(roleQueueNames(state, roleName))
400
+ );
401
+ addStatement(
402
+ statements,
403
+ ['sns:Publish'],
404
+ topicArns(roleTopicNames(state, roleName))
405
+ );
185
406
  if (state.webSocketRoles.has(roleName)) {
186
- statements.push({ action: ['execute-api:ManageConnections'], resource: ['arn:aws:execute-api:${self:custom.iz_region}:${self:custom.iz_accountId}:${self:custom.iz_webSocketHostId}/*'] });
407
+ statements.push({
408
+ action: ['execute-api:ManageConnections'],
409
+ resource: [
410
+ 'arn:aws:execute-api:${self:custom.iz_region}:${self:custom.iz_accountId}:*/*'
411
+ ]
412
+ });
187
413
  }
188
414
  return statements;
189
415
  }
@@ -199,15 +425,39 @@ function roleNeedsSchemaRead(state, roleName) {
199
425
 
200
426
  function roleTableNames(state, roleName) {
201
427
  if (roleName === 'RegisterRole') return [];
202
- if (roleName === 'WebSocketMainRole') return ['UserUploadRecords', 'WebSocketTask'];
203
- if (roleName === 'FlowObjectsRole') return [...state.objectTableNames, 'WebSocketTask'];
204
- if (roleName === 'FlowRelationshipRole') return [...state.objectTableNames, ...state.relationshipTableNames, 'AwaitingMultipleSteps', 'WebSocketTask'];
428
+ if (roleName === 'WebSocketMainRole')
429
+ return ['UserUploadRecords', 'WebSocketTask'];
430
+ if (roleName === 'FlowObjectsRole')
431
+ return [
432
+ ...state.objectTableNames,
433
+ 'AwaitingMultipleSteps',
434
+ 'AwaitingMultipleStepByPending',
435
+ 'WebSocketTask'
436
+ ];
437
+ if (roleName === 'FlowRelationshipRole')
438
+ return [
439
+ ...state.objectTableNames,
440
+ ...state.relationshipTableNames,
441
+ 'AwaitingMultipleSteps',
442
+ 'AwaitingMultipleStepByPending',
443
+ 'WebSocketTask'
444
+ ];
205
445
  if (roleName === 'FlowRbacRole') return RBAC_TABLES;
206
- if (roleName === 'ProcFindDataRole') return [...state.objectTableNames, ...state.relationshipTableNames, 'FindDataMain', 'LogicalResultsMain', 'LogicalResultsData'];
207
- const flow = state.flows.find(candidate => customFlowRoleName(candidate?.flowTag ?? '') === roleName);
446
+ if (roleName === 'ProcFindDataRole')
447
+ return [
448
+ ...state.objectTableNames,
449
+ ...state.relationshipTableNames,
450
+ 'FindDataMain',
451
+ 'LogicalResultsMain',
452
+ 'LogicalResultsData'
453
+ ];
454
+ const flow = state.flows.find(
455
+ candidate => customFlowRoleName(candidate?.flowTag ?? '') === roleName
456
+ );
208
457
  if (!flow) return [];
209
458
  const tables = [];
210
- if (includesEvent(flow, 'ownTopic') || includesEvent(flow, 's3')) tables.push('WebSocketTask');
459
+ if (includesEvent(flow, 'ownTopic') || includesEvent(flow, 's3'))
460
+ tables.push('WebSocketTask');
211
461
  if (includesEvent(flow, 's3')) tables.push('UserUploadRecords');
212
462
  if (flow.statusType === 'storedCache') {
213
463
  const object = state.objectTablesByType.get(flow.objType?.objectType);
@@ -217,56 +467,152 @@ function roleTableNames(state, roleName) {
217
467
  }
218
468
 
219
469
  function dynamoActionsForRole(roleName) {
220
- if (roleName === 'ProcFindDataRole') return ['dynamodb:GetItem', 'dynamodb:Query'];
221
- if (roleName === 'WebSocketMainRole') return ['dynamodb:DeleteItem', 'dynamodb:PutItem', 'dynamodb:Query'];
222
- if (roleName === 'FlowRbacRole') return [...DYNAMO_ACTIONS, 'dynamodb:TransactWriteItems'];
470
+ if (roleName === 'ProcFindDataRole')
471
+ return ['dynamodb:GetItem', 'dynamodb:Query'];
472
+ if (roleName === 'WebSocketMainRole')
473
+ return ['dynamodb:DeleteItem', 'dynamodb:PutItem', 'dynamodb:Query'];
474
+ if (roleName === 'FlowRbacRole')
475
+ return [...DYNAMO_ACTIONS, 'dynamodb:TransactWriteItems'];
223
476
  return DYNAMO_ACTIONS;
224
477
  }
225
478
 
226
479
  function roleQueueNames(state, roleName) {
227
480
  if (roleName === 'ProcFindDataRole') return LEGACY_PROCESS_QUEUES;
228
- if (roleName === 'WebSocketMainRole') return state.websocket ? ['WebSocketCompleteHdrSqs'] : [];
229
- if (roleName === 'FlowObjectsRole') return [
230
- ...CRUD_ACTIONS.map(action => `${action}HdrSqs`),
231
- ...CRUD_ACTIONS.filter(action => action !== 'Get').map(action => `${action}CompleteHdrSqs`),
232
- ...state.beforeLogical.filter(({ action }) => CRUD_ACTIONS.includes(action)).map(({ action }) => `${action}BeforeLogicalHdrSqs`)
233
- ];
234
- if (roleName === 'FlowRelationshipRole') return [
235
- ...RELATIONSHIP_ACTIONS.flatMap(action => [`${action}HdrSqs`, `${action}CompleteHdrSqs`]),
236
- 'GetRelationshipHdrDsq'
237
- ];
238
- if (roleName === 'FlowRbacRole') return RBAC_ACTIONS.flatMap(action => [`${action}HdrSqs`, `Process${action}HdrSqs`]);
239
- const flow = state.flows.find(candidate => customFlowRoleName(candidate?.flowTag ?? '') === roleName);
481
+ if (roleName === 'WebSocketMainRole')
482
+ return state.websocket ? ['WebSocketCompleteHdrSqs'] : [];
483
+ if (roleName === 'FlowObjectsRole')
484
+ return [
485
+ ...CRUD_ACTIONS.map(action => `${action}HdrSqs`),
486
+ ...CRUD_ACTIONS.filter(action => action !== 'Get').map(
487
+ action => `${action}CompleteHdrSqs`
488
+ ),
489
+ ...state.beforeLogical
490
+ .filter(({ action }) => CRUD_ACTIONS.includes(action))
491
+ .map(({ action }) => `${action}BeforeLogicalHdrSqs`)
492
+ ];
493
+ if (roleName === 'FlowRelationshipRole')
494
+ return [
495
+ ...RELATIONSHIP_ACTIONS.flatMap(action => [
496
+ `${action}HdrSqs`,
497
+ `${action}CompleteHdrSqs`
498
+ ]),
499
+ 'GetRelationshipHdrDsq'
500
+ ];
501
+ if (roleName === 'FlowRbacRole')
502
+ return RBAC_ACTIONS.flatMap(action => [
503
+ `${action}HdrSqs`,
504
+ `Process${action}HdrSqs`
505
+ ]);
506
+ const flow = state.flows.find(
507
+ candidate => customFlowRoleName(candidate?.flowTag ?? '') === roleName
508
+ );
240
509
  if (!flow) return [];
241
510
  const tag = upperFirst(flow.flowTag);
242
511
  return [
243
- ...(includesEvent(flow, 'extTopic') ? [`Process${tag}HdrSqs`] : []),
512
+ ...(includesEvent(flow, 'extTopic') ? [`${tag}HdrSqs`] : []),
244
513
  ...(includesEvent(flow, 'ownTopic') ? [`${tag}HdrSqs`] : []),
245
- ...flowExtTopicSteps([flow]).map(({ flowTag, stepName }) => `${flowTag}${stepName}HdrSqs`),
246
- ...state.sqsQueues.filter(queueName => includesEvent(flow, 's3') && queueName.startsWith(tag)),
247
- ...(flow.statusType === 'triggerCache' ? triggerCacheQueueNames(flow) : [])
514
+ ...(includesEvent(flow, 'dsq') ? [`${tag}HdrDsq`] : []),
515
+ ...flowExtTopicSteps([flow]).map(
516
+ ({ flowTag, stepName }) => `${flowTag}${stepName}HdrSqs`
517
+ ),
518
+ ...flowDsqSteps([flow]).map(
519
+ ({ flowTag, stepName }) => `${flowTag}${stepName}HdrDsq`
520
+ ),
521
+ ...state.sqsQueues.filter(
522
+ queueName => includesEvent(flow, 's3') && queueName.startsWith(tag)
523
+ )
248
524
  ];
249
525
  }
250
526
 
251
527
  function roleTopicNames(state, roleName) {
252
528
  if (roleName === 'ProcFindDataRole') return [];
253
- if (roleName === 'FlowObjectsRole') return state.outTopics.filter(topic => CRUD_ACTIONS.includes(topic));
254
- if (roleName === 'FlowRelationshipRole') return state.outTopics.filter(topic => RELATIONSHIP_ACTIONS.includes(topic));
255
- if (roleName === 'FlowRbacRole') return state.outTopics.filter(topic => RBAC_ACTIONS.includes(topic));
256
- const flow = state.flows.find(candidate => customFlowRoleName(candidate?.flowTag ?? '') === roleName);
257
- return flow ? state.outTopics.filter(topic => topic === upperFirst(flow.flowTag)) : [];
529
+ if (roleName === 'FlowObjectsRole')
530
+ return state.outTopics.filter(topic => CRUD_ACTIONS.includes(topic));
531
+ if (roleName === 'FlowRelationshipRole')
532
+ return state.outTopics.filter(topic =>
533
+ RELATIONSHIP_ACTIONS.includes(topic)
534
+ );
535
+ if (roleName === 'FlowRbacRole')
536
+ return state.outTopics.filter(topic => RBAC_ACTIONS.includes(topic));
537
+ const flow = state.flows.find(
538
+ candidate => customFlowRoleName(candidate?.flowTag ?? '') === roleName
539
+ );
540
+ return flow
541
+ ? state.outTopics.filter(topic => topic === upperFirst(flow.flowTag))
542
+ : [];
258
543
  }
259
544
 
260
545
  function dynamoArns(tableNames) {
261
- return [...new Set(tableNames)].sort().map(tableName => `arn:aws:dynamodb:${'${self:custom.iz_region}'}:${'${self:custom.iz_accountId}'}:table/${'${self:custom.iz_resourcePrefix}'}${tableName}`);
546
+ return [...new Set(tableNames)]
547
+ .sort()
548
+ .map(
549
+ tableName =>
550
+ `arn:aws:dynamodb:${'${self:custom.iz_region}'}:${'${self:custom.iz_accountId}'}:table/${'${self:custom.iz_resourcePrefix}'}${tableName}`
551
+ );
552
+ }
553
+
554
+ /**
555
+ * Generates DynamoDB ARNs for tables that belong to a parent service
556
+ * (via extendObjType). Uses the parent serviceTag instead of the
557
+ * current service's resourcePrefix, because those tables are owned
558
+ * by a different CloudFormation stack.
559
+ *
560
+ * ARN format:
561
+ * ...table/{parentServiceTag}_{iz_stage}_{tableName}
562
+ */
563
+ function externalDynamoArns(externalTables) {
564
+ const seen = new Set();
565
+ const unique = [];
566
+ for (const table of externalTables) {
567
+ const dedupeKey = `${table.serviceTag}::${table.tableName}`;
568
+ if (!seen.has(dedupeKey)) {
569
+ seen.add(dedupeKey);
570
+ unique.push(table);
571
+ }
572
+ }
573
+ return unique
574
+ .sort((a, b) =>
575
+ `${a.serviceTag}_${a.tableName}`.localeCompare(
576
+ `${b.serviceTag}_${b.tableName}`
577
+ )
578
+ )
579
+ .map(
580
+ ({ serviceTag, tableName }) =>
581
+ `arn:aws:dynamodb:${'${self:custom.iz_region}'}:${'${self:custom.iz_accountId}'}:table/${serviceTag}${'${self:custom.iz_stage}'}${tableName}`
582
+ );
583
+ }
584
+
585
+ /**
586
+ * Returns the external DynamoDB table references (cross-service)
587
+ * that should be accessible from a given IAM role.
588
+ */
589
+ function roleExternalDynamoTables(state, roleName) {
590
+ if (
591
+ roleName === 'FlowObjectsRole' ||
592
+ roleName === 'FlowRelationshipRole' ||
593
+ roleName === 'ProcFindDataRole'
594
+ ) {
595
+ return state.externalDynamoTables;
596
+ }
597
+ return [];
262
598
  }
263
599
 
264
600
  function queueArns(queueNames) {
265
- return [...new Set(queueNames)].sort().map(queueName => `arn:aws:sqs:${'${self:custom.iz_region}'}:${'${self:custom.iz_accountId}'}:${'${self:custom.iz_resourcePrefix}'}${queueName}`);
601
+ return [...new Set(queueNames)]
602
+ .sort()
603
+ .map(
604
+ queueName =>
605
+ `arn:aws:sqs:${'${self:custom.iz_region}'}:${'${self:custom.iz_accountId}'}:${'${self:custom.iz_resourcePrefix}'}${queueName}`
606
+ );
266
607
  }
267
608
 
268
609
  function topicArns(topicNames) {
269
- return [...new Set(topicNames)].sort().map(topic => `arn:aws:sns:${'${self:custom.iz_region}'}:${'${self:custom.iz_accountId}'}:${'${self:custom.iz_serviceTag}'}_${'${self:custom.iz_stage}'}_${topic}_Out`);
610
+ return [...new Set(topicNames)]
611
+ .sort()
612
+ .map(
613
+ topic =>
614
+ `arn:aws:sns:${'${self:custom.iz_region}'}:${'${self:custom.iz_accountId}'}:${'${self:custom.iz_serviceTag}'}_${'${self:custom.iz_stage}'}_${topic}_Out`
615
+ );
270
616
  }
271
617
 
272
618
  function renderWebSocketSubscription(topicName) {
@@ -277,11 +623,27 @@ function collectTables(schemas) {
277
623
  const tables = new Map();
278
624
  for (const schema of schemas) {
279
625
  if (!schema?.storageResources) continue;
280
- const identifiers = Array.isArray(schema.identifiers) ? schema.identifiers : [];
281
- const pk = keyName(identifiers.find(identifier => identifier.type === 'partitionKey' || identifier.type === 'identifier')) ?? 'PK';
282
- const sk = keyName(identifiers.find(identifier => identifier.type === 'sortKey'));
626
+ const identifiers = Array.isArray(schema.identifiers)
627
+ ? schema.identifiers
628
+ : [];
629
+ const pk =
630
+ keyName(
631
+ identifiers.find(
632
+ identifier =>
633
+ identifier.type === 'partitionKey' ||
634
+ identifier.type === 'identifier'
635
+ )
636
+ ) ?? 'PK';
637
+ const sk = keyName(
638
+ identifiers.find(identifier => identifier.type === 'sortKey')
639
+ );
283
640
  for (const resource of Object.values(schema.storageResources)) {
284
- if (resource?.storageType === 'dynamoDB' && resource.tableName && !tables.has(resource.tableName)) tables.set(resource.tableName, { pk, sk });
641
+ if (
642
+ resource?.storageType === 'dynamoDB' &&
643
+ resource.tableName &&
644
+ !tables.has(resource.tableName)
645
+ )
646
+ tables.set(resource.tableName, { pk, sk });
285
647
  }
286
648
  }
287
649
  return tables;
@@ -289,12 +651,41 @@ function collectTables(schemas) {
289
651
 
290
652
  function keyName(identifier) {
291
653
  if (!identifier) return undefined;
292
- return identifier.name ?? identifier.fieldName ?? (Array.isArray(identifier.fieldNames) ? identifier.fieldNames.join(identifier.deliminator ?? '_') : undefined);
654
+ return (
655
+ identifier.name ??
656
+ identifier.fieldName ??
657
+ (Array.isArray(identifier.fieldNames)
658
+ ? identifier.fieldNames.join(identifier.deliminator ?? '_')
659
+ : undefined)
660
+ );
293
661
  }
294
662
 
295
- function isCrudObject(object) { return object?.generatedBy === 'userGenerated' || object?.generatedBy === 'systemGenerated'; }
296
- function hasRbacAddOn(object) { return Array.isArray(object?.addOnDataStructure) && object.addOnDataStructure.some(addOn => addOn?.type === 'rbac'); }
297
- function includesEvent(flow, event) { return Array.isArray(flow?.event) && flow.event.includes(event); }
298
- function upperFirst(value) { return typeof value === 'string' && value ? value[0].toUpperCase() + value.slice(1) : ''; }
299
- function normalizeYaml(content) { return `${content.trimEnd()}\n`; }
300
- async function render(templateName, data) { return ejs.render(await readFile(new URL(templateName, TEMPLATE_URL), 'utf8'), { resourceQueueName: data.queueName, ...data }); }
663
+ function isCrudObject(object) {
664
+ return (
665
+ object?.generatedBy === 'userGenerated' ||
666
+ object?.generatedBy === 'systemGenerated'
667
+ );
668
+ }
669
+ function hasRbacAddOn(object) {
670
+ return (
671
+ Array.isArray(object?.addOnDataStructure) &&
672
+ object.addOnDataStructure.some(addOn => addOn?.type === 'rbac')
673
+ );
674
+ }
675
+ function includesEvent(flow, event) {
676
+ return Array.isArray(flow?.event) && flow.event.includes(event);
677
+ }
678
+ function upperFirst(value) {
679
+ return typeof value === 'string' && value
680
+ ? value[0].toUpperCase() + value.slice(1)
681
+ : '';
682
+ }
683
+ function normalizeYaml(content) {
684
+ return `${content.trimEnd()}\n`;
685
+ }
686
+ async function render(templateName, data) {
687
+ return ejs.render(
688
+ await readFile(new URL(templateName, TEMPLATE_URL), 'utf8'),
689
+ { resourceQueueName: data.queueName, ...data }
690
+ );
691
+ }