@izara_project/izara-core-generate-service-code 1.0.67 → 1.0.69

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