@izara_project/izara-core-generate-service-code 1.0.99 → 1.0.100

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 (50) hide show
  1. package/package.json +1 -1
  2. package/src/compile/compileProject.js +16 -5
  3. package/src/compile/flowValidator.js +3 -1
  4. package/src/compile/loadSchemas.js +18 -4
  5. package/src/compile/schemaValidatorSetup.js +16 -0
  6. package/src/emit/application/coreFunction/emitCoreFunctionArtifacts.js +108 -108
  7. package/src/emit/application/coreFunction/templates/InitialSetupRunner.ejs +2 -2
  8. package/src/emit/application/crud/templates/CreateComplete_Main.ejs +2 -0
  9. package/src/emit/application/crud/templates/CreateLogicalLib.ejs +14 -7
  10. package/src/emit/application/crud/templates/DeleteComplete_Main.ejs +2 -0
  11. package/src/emit/application/crud/templates/FlowEndpoint_HdrWbs.ejs +7 -5
  12. package/src/emit/application/crud/templates/UpdateComplete_Main.ejs +2 -0
  13. package/src/emit/application/custom/customFlowPaths.js +13 -16
  14. package/src/emit/application/custom/customFlowS3.js +274 -63
  15. package/src/emit/application/custom/customFlows.js +131 -0
  16. package/src/emit/application/custom/emitCustomFlowApplicationArtifacts.js +3 -1
  17. package/src/emit/application/custom/templates/s3/ConfirmAfterUpload_HdrSqs.ejs +110 -0
  18. package/src/emit/application/custom/templates/s3/ConfirmAfterUpload_Main.ejs +89 -0
  19. package/src/emit/application/custom/templates/s3/ConfirmComplete_HdrSqs.ejs +110 -0
  20. package/src/emit/application/custom/templates/s3/ConfirmComplete_Main.ejs +89 -0
  21. package/src/emit/application/custom/templates/s3/CreateObject_HdrWbs.ejs +2 -1
  22. package/src/emit/application/custom/templates/s3/CreateObject_Main.ejs +4 -0
  23. package/src/emit/application/custom/templates/s3/CreatePreSignUrl_HdrSqs.ejs +42 -0
  24. package/src/emit/application/custom/templates/s3/CreatePreSignUrl_Main.ejs +23 -0
  25. package/src/emit/application/custom/templates/s3/ProcessS3File_Main.ejs +135 -27
  26. package/src/emit/application/custom/templates/s3/Start_Main.ejs +197 -0
  27. package/src/emit/application/rbac/emitDynamicRbacFlowSchemas.js +5 -1
  28. package/src/emit/application/rbac/emitRbacApplicationArtifacts.js +67 -60
  29. package/src/emit/application/rbac/templates/ReservedDynamicUsage_Main.ejs +27 -9
  30. package/src/emit/application/rbac/templates/UserRbacFlow_Main.ejs +4 -3
  31. package/src/emit/application/system/templates/Register_HdrWbs.ejs +2 -1
  32. package/src/emit/infrastructure/functions/functionYamlArtifact.js +3 -8
  33. package/src/emit/infrastructure/functions/templates/S3Function_Yaml.ejs +2 -3
  34. package/src/emit/infrastructure/resources/iamResourceArtifacts.js +22 -0
  35. package/src/emit/infrastructure/resources/resourceState.js +15 -5
  36. package/src/emit/infrastructure/resources/s3ResourceArtifacts.js +19 -3
  37. package/src/emit/infrastructure/resources/templates/S3Bucket_Yaml.ejs +1 -1
  38. package/src/emit/infrastructure/resources/templates/SystemDynamoDB_Yaml.ejs +2 -2
  39. package/src/emit/schemas/error/emitErrorSchemas.js +120 -10
  40. package/src/emit/schemas/error/templates/errorConsts.ejs +12 -0
  41. package/src/generate.js +32 -30
  42. package/test/integration/publicApi.test.js +33 -24
  43. package/test/unit/application/createTargetRoleHandler.test.js +33 -10
  44. package/test/unit/application/customFlowApplicationArtifacts.test.js +6 -6
  45. package/test/unit/application/customFlowS3.test.js +730 -0
  46. package/test/unit/infrastructure/functionYamlArtifact.test.js +6 -4
  47. package/test/unit/infrastructure/resourceArtifacts.test.js +51 -0
  48. package/test/unit/schemas/dynamicRbacFlowSchemas.test.js +8 -6
  49. package/test/unit/schemas/dynamicRelationshipFlowSchemas.test.js +51 -0
  50. package/test/unit/schemas/errorSchemas.test.js +84 -5
@@ -0,0 +1,197 @@
1
+ /*
2
+ Copyright (C) 2026 Sven Mason <http://izara.io>
3
+
4
+ This program is free software: you can redistribute it and/or modify
5
+ it under the terms of the GNU Affero General Public License as
6
+ published by the Free Software Foundation, either version 3 of the
7
+ License, or (at your option) any later version.
8
+
9
+ This program is distributed in the hope that it will be useful,
10
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ GNU Affero General Public License for more details.
13
+
14
+ You should have received a copy of the GNU Affero General Public License
15
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
16
+ */
17
+
18
+ import { v7 as uuidv7 } from 'uuid';
19
+ import { createPresignedPost } from '@aws-sdk/s3-presigned-post';
20
+ import { S3Client } from '@aws-sdk/client-s3';
21
+ import { consts } from '@izara_project/izara-middleware';
22
+ import dynamodbSharedLib from '@izara_project/izara-core-library-dynamodb';
23
+ import { webSocket } from '@izara_project/izara-core-library-external-request';
24
+ const { postToConnection } = webSocket;
25
+ import { utils } from '@izara_project/izara-core-library-service-schemas';
26
+ const { createFlowTypeConcat } = utils;
27
+
28
+ const client = new S3Client({ region: process.env.AWS_REGION });
29
+
30
+ //(<optionalRequired>)
31
+ //(</optionalRequired>)
32
+
33
+ /**
34
+ * Step 1: Start (Deferred-Node S3 Pipeline - ADR-016)
35
+ * Generates UUIDv7, computes S3 Presigned POST in-memory, writes ephemeral WebSocketTask,
36
+ * and returns upload parameters to client without writing entity records to the database.
37
+ *
38
+ * @param {Object} _izContext
39
+ * @param {Object} requestParams
40
+ * @param {Object} callingFlowConfig
41
+ * @returns {Promise<Object>}
42
+ */
43
+ export default async function <%- functionName %>(
44
+ _izContext,
45
+ requestParams,
46
+ callingFlowConfig = {}
47
+ //(<additionalParams>)
48
+ //(</additionalParams>)
49
+ ) {
50
+ try {
51
+ _izContext.logger.debug('<%- functionName %> started', {
52
+ requestParams,
53
+ callingFlowConfig
54
+ });
55
+
56
+ const connectionId = _izContext.correlationIds.get(consts.CONNECTION_ID);
57
+ const submittedByUserId = _izContext.correlationIds?.get(
58
+ consts.SUBMITTED_BY_USER_ID
59
+ );
60
+ if (!submittedByUserId) {
61
+ throw new Error('Submitted by user id is required');
62
+ }
63
+
64
+ //(<beforeCreateObjectHook>)
65
+ //(</beforeCreateObjectHook>)
66
+
67
+ <% if (isRbac) { -%>
68
+ const { checkRbacPermission } =
69
+ await import('@izara_project/izara-authorization');
70
+
71
+ //(<rbacParameterGroups_<%- flowTag %>>)
72
+ const parameterGroups = {};
73
+ //(</rbacParameterGroups_<%- flowTag %>>)
74
+
75
+ const resultPermissionCheck = await checkRbacPermission(
76
+ _izContext,
77
+ {
78
+ flowTag: requestParams.flowTag || '<%- flowTag %>',
79
+ serviceTag: process.env.iz_serviceTag
80
+ },
81
+ parameterGroups,
82
+ {
83
+ targetIdentifiers: requestParams.targetIdentifiers,
84
+ rbacObjType: {
85
+ serviceTag: '<%- parentServiceTag || rbacServiceTag %>',
86
+ objectType: '<%- parentObjectType || objectType %>'
87
+ },
88
+ sync: true
89
+ }
90
+ );
91
+
92
+ _izContext.logger.debug(
93
+ `[${process.env.iz_serviceTag}:<%- flowTag %>] checkRbacPermission result:`,
94
+ resultPermissionCheck
95
+ );
96
+
97
+ if (resultPermissionCheck && resultPermissionCheck.permission !== true) {
98
+ throw new Error('Permission denied');
99
+ }
100
+ <% } -%>
101
+
102
+ const <%- objectType %>Id = uuidv7();
103
+
104
+ <% if (typeof parentIdKeys !== 'undefined' && parentIdKeys && parentIdKeys.length > 0) { -%>
105
+ const targetParentId = requestParams.targetIdentifiers?.<%- parentIdKeys[0] %>;
106
+ <% for (const key of parentIdKeys) { -%>
107
+ const target<%- key.charAt(0).toUpperCase() + key.slice(1) %> = requestParams.targetIdentifiers?.<%- key %>;
108
+ if (!target<%- key.charAt(0).toUpperCase() + key.slice(1) %>) {
109
+ throw new Error('targetIdentifiers.<%- key %> is required');
110
+ }
111
+ <% } -%>
112
+ <% } -%>
113
+
114
+ let maxFileSizeMb = <%- maxFileSizeMb %>;
115
+ if (requestParams.file?.size) {
116
+ maxFileSizeMb = Math.ceil(requestParams.file.size / 1048576);
117
+ } else if (requestParams.file?.fileSize) {
118
+ maxFileSizeMb = requestParams.file.fileSize;
119
+ } else if (requestParams.fieldNames?.fileSize) {
120
+ maxFileSizeMb = requestParams.fieldNames.fileSize;
121
+ }
122
+
123
+ //(<createKeyForUploadFile>)
124
+ let importType = '<%- defaultImportType %>';
125
+ let uploadPath = '';
126
+ //(</createKeyForUploadFile>)
127
+ const Key = `<%- s3KeyPrefix %>${uploadPath}`;
128
+ const maximumSize = maxFileSizeMb * 1048576;
129
+
130
+ let createPreSignUrlParam = {
131
+ Bucket: '<%- bucketName %>',
132
+ Key: Key,
133
+ Conditions: [
134
+ ['content-length-range', 0, maximumSize],
135
+ ['eq', '$x-amz-meta-<%- objectType %>Id', <%- objectType %>Id],
136
+ <% if (typeof parentIdKeys !== 'undefined' && parentIdKeys && parentIdKeys.length > 0) { -%>
137
+ <% for (const key of parentIdKeys) { -%>
138
+ ['eq', '$x-amz-meta-<%- key %>', target<%- key.charAt(0).toUpperCase() + key.slice(1) %>],
139
+ <% } -%>
140
+ <% } -%>
141
+ ['starts-with', '$x-amz-meta-submittedByUserId', ''],
142
+ ['starts-with', '$Content-Type', '<%- contentTypePrefix %>']
143
+ //(<conditionCreateUrlLink>)
144
+ //(</conditionCreateUrlLink>)
145
+ ],
146
+ Fields: {
147
+ 'x-amz-meta-<%- objectType %>Id': <%- objectType %>Id,
148
+ <% if (typeof parentIdKeys !== 'undefined' && parentIdKeys && parentIdKeys.length > 0) { -%>
149
+ <% for (const key of parentIdKeys) { -%>
150
+ 'x-amz-meta-<%- key %>': target<%- key.charAt(0).toUpperCase() + key.slice(1) %>,
151
+ <% } -%>
152
+ <% } -%>
153
+ 'x-amz-meta-submittedByUserId': submittedByUserId,
154
+ //(<fieldCreateUrlLink>)
155
+ //(</fieldCreateUrlLink>)
156
+ },
157
+ Expires: <%- expires %>
158
+ };
159
+
160
+ //(<modifyPreSignUrlParam>)
161
+ //(</modifyPreSignUrlParam>)
162
+
163
+ const uploadUrl = await createPresignedPost(client, createPreSignUrlParam);
164
+
165
+ if (connectionId) {
166
+ await dynamodbSharedLib.putItem(
167
+ _izContext,
168
+ await dynamodbSharedLib.tableName(_izContext, 'WebSocketTask'),
169
+ {
170
+ taskKey: `${createFlowTypeConcat(_izContext, { flowTag: '<%- flowTag %>', serviceTag: process.env.iz_serviceTag })}${<%- objectType %>Id}`,
171
+ connectionId: connectionId,
172
+ expirationTime: Math.floor(Date.now() / 1000) + 7200
173
+ }
174
+ );
175
+
176
+ await postToConnection(
177
+ _izContext,
178
+ {
179
+ status: 'success',
180
+ data: {
181
+ uploadUrl: uploadUrl,
182
+ <%- objectType %>Id: <%- objectType %>Id
183
+ }
184
+ },
185
+ connectionId
186
+ );
187
+ }
188
+
189
+ return {
190
+ uploadUrl: uploadUrl,
191
+ <%- objectType %>Id: <%- objectType %>Id
192
+ };
193
+ } catch (err) {
194
+ _izContext.logger.error('Error in <%- functionName %>:', err);
195
+ throw err;
196
+ }
197
+ }
@@ -20,7 +20,7 @@ const RBAC_CORE_FLOW_DEFINITIONS = [
20
20
  const LIMIT_FLOW_DEFINITIONS = [
21
21
  {
22
22
  flowTag: 'ReservedDynamicUsage',
23
- event: ['ownTopic', 'lambdaSyncInv', 'lambdaSyncApi']
23
+ event: ['lambdaSyncInv', 'lambdaSyncApi']
24
24
  },
25
25
  {
26
26
  flowTag: 'ConfirmUsage',
@@ -29,6 +29,10 @@ const LIMIT_FLOW_DEFINITIONS = [
29
29
  {
30
30
  flowTag: 'CancelUsage',
31
31
  event: ['ownTopic', 'lambdaSyncInv', 'lambdaSyncApi']
32
+ },
33
+ {
34
+ flowTag: 'ReservedUsageTtlExpired',
35
+ event: ['dss']
32
36
  }
33
37
  ];
34
38
 
@@ -174,14 +174,21 @@ const STATIC_RBAC_APPLICATION_PATHS = [
174
174
  ];
175
175
 
176
176
  const STATIC_LIMIT_APPLICATION_PATHS = [
177
- ...ACCOUNT_LIMIT_HANDLERS.flatMap((flowTag) => [
178
- `app/src/generatedCode/FlowLimit/${flowTag}/source/${flowTag}_Main.js`,
179
- `app/src/generatedCode/FlowLimit/${flowTag}/source/${flowTag}_HdrSqs.js`,
180
- `app/src/generatedCode/FlowLimit/${flowTag}/source/${flowTag}_HdrInv.js`,
181
- `app/src/generatedCode/FlowLimit/${flowTag}/source/${flowTag}_HdrApi.js`,
182
- `app/src/generatedCode/FlowLimit/${flowTag}/source/${flowTag}_WbsMain.js`,
183
- `app/src/generatedCode/FlowLimit/${flowTag}/source/${flowTag}_HdrWbs.js`
184
- ]),
177
+ ...ACCOUNT_LIMIT_HANDLERS.flatMap((flowTag) => {
178
+ const paths = [
179
+ `app/src/generatedCode/FlowLimit/${flowTag}/source/${flowTag}_Main.js`,
180
+ `app/src/generatedCode/FlowLimit/${flowTag}/source/${flowTag}_HdrInv.js`,
181
+ `app/src/generatedCode/FlowLimit/${flowTag}/source/${flowTag}_HdrApi.js`
182
+ ];
183
+ if (flowTag !== 'ReservedDynamicUsage') {
184
+ paths.splice(
185
+ 1,
186
+ 0,
187
+ `app/src/generatedCode/FlowLimit/${flowTag}/source/${flowTag}_HdrSqs.js`
188
+ );
189
+ }
190
+ return paths;
191
+ }),
185
192
  'app/src/generatedCode/FlowLimit/ReservedUsageTtlExpired/source/ReservedUsageTtlExpired_HdrDss.js',
186
193
  FLOW_LIMITS_YAML_PATH
187
194
  ];
@@ -212,65 +219,65 @@ export function rbacApplicationManagedPaths(objects = []) {
212
219
  async function createAccountLimitHandler(flowTag) {
213
220
  const sourcePath = `app/src/generatedCode/FlowLimit/${flowTag}/source`;
214
221
  const handlerPath = `src/generatedCode/FlowLimit/${flowTag}/source`;
215
- const webSocket = await createWebSocketEntry(
216
- flowTag,
217
- sourcePath,
218
- handlerPath
219
- );
220
222
  const shared = {
221
223
  flowTag,
222
224
  flowStepName: '',
223
225
  mainFileName: `${flowTag}_Main`,
224
226
  functionName: flowTag
225
227
  };
228
+ const artifacts = [
229
+ {
230
+ path: `${sourcePath}/${flowTag}_Main.js`,
231
+ content: await renderRbacTemplate(`${flowTag}_Main.ejs`, { flowTag })
232
+ }
233
+ ];
234
+ const functions = [];
235
+ if (flowTag !== 'ReservedDynamicUsage') {
236
+ artifacts.push({
237
+ path: `${sourcePath}/${flowTag}_HdrSqs.js`,
238
+ content: await renderRbacTemplate('ListRbac_HdrSqs.ejs', { flowTag })
239
+ });
240
+ functions.push({
241
+ type: 'sqs',
242
+ functionName: `${flowTag}HdrSqs`,
243
+ handlerPath: `${handlerPath}/${flowTag}_HdrSqs.main`,
244
+ roleName: 'FlowRbacRole',
245
+ queueName: `${flowTag}HdrSqs`,
246
+ flowTag: flowTag,
247
+ flowStepName: ''
248
+ });
249
+ }
250
+ artifacts.push(
251
+ {
252
+ path: `${sourcePath}/${flowTag}_HdrInv.js`,
253
+ content: await renderSharedTemplate('FlowHandler_HdrInv.ejs', shared)
254
+ },
255
+ {
256
+ path: `${sourcePath}/${flowTag}_HdrApi.js`,
257
+ content: await renderSharedTemplate('FlowHandler_HdrApi.ejs', shared)
258
+ }
259
+ );
260
+ functions.push(
261
+ {
262
+ type: 'inv',
263
+ functionName: `${flowTag}HdrInv`,
264
+ handlerPath: `${handlerPath}/${flowTag}_HdrInv.main`,
265
+ roleName: 'FlowRbacRole',
266
+ flowTag: flowTag,
267
+ flowStepName: ''
268
+ },
269
+ {
270
+ type: 'api',
271
+ functionName: `${flowTag}HdrApi`,
272
+ handlerPath: `${handlerPath}/${flowTag}_HdrApi.main`,
273
+ roleName: 'FlowRbacRole',
274
+ flowTag: flowTag,
275
+ flowStepName: ''
276
+ }
277
+ );
226
278
  return {
227
- artifacts: [
228
- {
229
- path: `${sourcePath}/${flowTag}_Main.js`,
230
- content: await renderRbacTemplate(`${flowTag}_Main.ejs`, { flowTag })
231
- },
232
- {
233
- path: `${sourcePath}/${flowTag}_HdrSqs.js`,
234
- content: await renderRbacTemplate('ListRbac_HdrSqs.ejs', { flowTag })
235
- },
236
- {
237
- path: `${sourcePath}/${flowTag}_HdrInv.js`,
238
- content: await renderSharedTemplate('FlowHandler_HdrInv.ejs', shared)
239
- },
240
- {
241
- path: `${sourcePath}/${flowTag}_HdrApi.js`,
242
- content: await renderSharedTemplate('FlowHandler_HdrApi.ejs', shared)
243
- },
244
- ...webSocket.artifacts
245
- ],
246
- functions: [
247
- {
248
- type: 'sqs',
249
- functionName: `${flowTag}HdrSqs`,
250
- handlerPath: `${handlerPath}/${flowTag}_HdrSqs.main`,
251
- roleName: 'FlowRbacRole',
252
- queueName: `${flowTag}HdrSqs`,
253
- flowTag: flowTag,
254
- flowStepName: ''
255
- },
256
- {
257
- type: 'inv',
258
- functionName: `${flowTag}HdrInv`,
259
- handlerPath: `${handlerPath}/${flowTag}_HdrInv.main`,
260
- roleName: 'FlowRbacRole',
261
- flowTag: flowTag,
262
- flowStepName: ''
263
- },
264
- {
265
- type: 'api',
266
- functionName: `${flowTag}HdrApi`,
267
- handlerPath: `${handlerPath}/${flowTag}_HdrApi.main`,
268
- roleName: 'FlowRbacRole',
269
- flowTag: flowTag,
270
- flowStepName: ''
271
- },
272
- ...webSocket.functions
273
- ]
279
+ artifacts,
280
+ functions
274
281
  };
275
282
  }
276
283
 
@@ -161,7 +161,7 @@ export default async function ReservedDynamicUsage(_izContext, requestParams) {
161
161
  let passStatus = false;
162
162
  if (currentTotal + value <= maxLimit) {
163
163
  passStatus = true;
164
- const ttlTimestamp = Math.floor(Date.now() / 1000) + 3600;
164
+ const expirationTime = Math.floor(Date.now() / 1000) + 3600;
165
165
 
166
166
  await dynamodbSharedLib.putItem(
167
167
  _izContext,
@@ -173,7 +173,7 @@ export default async function ReservedDynamicUsage(_izContext, requestParams) {
173
173
  flowTypeConcat,
174
174
  usageCount: value,
175
175
  runningTotalUsageStatus: 'reserved',
176
- ttlTimestamp
176
+ expirationTime
177
177
  }
178
178
  );
179
179
 
@@ -196,13 +196,31 @@ export default async function ReservedDynamicUsage(_izContext, requestParams) {
196
196
  identifiersConcat
197
197
  };
198
198
 
199
- await sns.publishMessage(_izContext, {
200
- message: { returnResult },
201
- TopicArn: snsSharedLib.snsTopicArnByFlowSchema(
202
- _izContext,
203
- 'ReservedDynamicUsage_Out'
204
- )
205
- });
199
+ const eventSourceTag =
200
+ _izContext.izEventSource?.eventSourceTag ||
201
+ requestParams?.izEventSource?.eventSourceTag;
202
+
203
+ let isSync = false;
204
+ if (
205
+ eventSourceTag === 'DirectInvoke' ||
206
+ eventSourceTag === 'ApiGateway' ||
207
+ eventSourceTag === 'ApiGatewayV2' ||
208
+ requestParams?.hasOwnProperty('__context__')
209
+ ) {
210
+ isSync = true;
211
+ }
212
+
213
+ if (!isSync) {
214
+ await sns.publishMessage(_izContext, {
215
+ message: { returnResult },
216
+ TopicArn: snsSharedLib.snsTopicArnByFlowSchema(
217
+ _izContext,
218
+ 'ReservedDynamicUsage_Out'
219
+ )
220
+ });
221
+ }
222
+
223
+ return returnResult;
206
224
  } catch (error) {
207
225
  _izContext.logger.error(`[${process.env.iz_serviceTag}:ReservedDynamicUsage] error:`, error);
208
226
  throw error;
@@ -55,7 +55,8 @@ async function _getL2Cache(_izContext, submittedByUserId, cacheSubKey) {
55
55
  userId: submittedByUserId,
56
56
  cacheKey: cacheSubKey
57
57
  });
58
- if (result && result.ttlTimestamp && result.ttlTimestamp > Math.floor(Date.now() / 1000)) {
58
+ const expTime = result && (result.expirationTime || result.ttlTimestamp);
59
+ if (expTime && expTime > Math.floor(Date.now() / 1000)) {
59
60
  return {
60
61
  returnValue: {
61
62
  permission: Boolean(result.permission),
@@ -75,14 +76,14 @@ async function _getL2Cache(_izContext, submittedByUserId, cacheSubKey) {
75
76
  async function _setL2Cache(_izContext, submittedByUserId, cacheSubKey, value) {
76
77
  try {
77
78
  const tableName = dynamodbSharedLib.tableName(_izContext, 'RbacPermissionCaches');
78
- const ttlTimestamp = Math.floor(Date.now() / 1000) + L2_TTL_SECONDS;
79
+ const expirationTime = Math.floor(Date.now() / 1000) + L2_TTL_SECONDS;
79
80
  await dynamodbSharedLib.putItem(_izContext, tableName, {
80
81
  userId: submittedByUserId,
81
82
  cacheKey: cacheSubKey,
82
83
  permission: value.returnValue ? value.returnValue.permission : value.permission,
83
84
  roleIdKey: value.returnValue ? value.returnValue.roleIdKey : value.roleIdKey,
84
85
  rbacType: 'target',
85
- ttlTimestamp
86
+ expirationTime
86
87
  });
87
88
  } catch (error) {
88
89
  // Safely ignore L2 write errors if table is not configured
@@ -27,7 +27,8 @@ export const main = middleware.wrap(async (event, context) => {
27
27
  await dynamodbSharedLib.tableName(event._izContext, 'WebSocketTask'),
28
28
  {
29
29
  taskKey: correlationId,
30
- connectionId: connectionId
30
+ connectionId: connectionId,
31
+ expirationTime: Math.floor(Date.now() / 1000) + 7200
31
32
  }
32
33
  );
33
34
 
@@ -181,6 +181,7 @@ export function renderS3FunctionYaml({
181
181
  handlerPath,
182
182
  roleName,
183
183
  bucketName,
184
+ s3KeyPrefix,
184
185
  flowTag,
185
186
  flowStepName
186
187
  }) {
@@ -189,6 +190,7 @@ export function renderS3FunctionYaml({
189
190
  handlerPath,
190
191
  roleName,
191
192
  bucketName,
193
+ s3KeyPrefix,
192
194
  environmentSetting: _environmentSetting(
193
195
  functionName,
194
196
  flowTag,
@@ -342,15 +344,8 @@ function _sqsFilterPatterns(
342
344
  - body:
343
345
  MessageAttributes:
344
346
  callingFlow:
345
- Value:
347
+ StringValue:
346
348
  - \${self:custom.iz_resourcePrefix}${callingFlowName}
347
- - body:
348
- MessageAttributes:
349
- callingFlow:
350
- - exists: false
351
- - body:
352
- MessageAttributes:
353
- - exists: false
354
349
  #<#/${hookFunctionName}FilterPatterns#>
355
350
  `;
356
351
  }
@@ -8,9 +8,8 @@
8
8
  bucket: <%= bucketName %>
9
9
  event: s3:ObjectCreated:*
10
10
  existing: true
11
- rules:
11
+ rules:<% if (s3KeyPrefix) { %>
12
+ - prefix: <%- s3KeyPrefix %><% } %>
12
13
  #<#<%= functionName %>rules#>
13
14
  #<#/<%= functionName %>rules#>
14
- - prefix: uploads/
15
- - suffix: .csv
16
15
  forceDeploy: true
@@ -139,6 +139,25 @@ export function roleStatements(state, roleName) {
139
139
  resource: ['arn:aws:s3:::${self:custom.iz_serviceSchemaBucketName}']
140
140
  });
141
141
  }
142
+ const s3FlowsForRole = (state.flows || []).filter((candidate) => {
143
+ const roleCandidate = customFlowRoleName(candidate?.flowTag ?? '');
144
+ return (
145
+ roleCandidate === roleName &&
146
+ includesEvent(candidate, 's3') &&
147
+ Boolean(candidate.bucketName)
148
+ );
149
+ });
150
+ if (s3FlowsForRole.length > 0) {
151
+ const bucketResources = [
152
+ ...new Set(
153
+ s3FlowsForRole.map((flow) => `arn:aws:s3:::${flow.bucketName}/*`)
154
+ )
155
+ ].sort();
156
+ statements.push({
157
+ action: ['s3:GetObject'],
158
+ resource: bucketResources
159
+ });
160
+ }
142
161
  addStatement(statements, dynamoActionsForRole(roleName), [
143
162
  ...dynamoArns(roleTableNames(state, roleName)),
144
163
  ...externalDynamoArns(roleExternalDynamoTables(state, roleName))
@@ -479,6 +498,9 @@ export function roleTopicNames(state, roleName) {
479
498
  if (flowHasOwnTopic(flow)) {
480
499
  topics.push(`${name}_In`);
481
500
  }
501
+ if (includesEvent(flow, 's3')) {
502
+ topics.push('Create_In');
503
+ }
482
504
  return topics;
483
505
  }
484
506
  return [];
@@ -4,7 +4,8 @@ import {
4
4
  flowExtTopicSteps,
5
5
  generatedDsqFlows,
6
6
  generatedOwnTopicFlows,
7
- supportedCustomFlows
7
+ supportedCustomFlows,
8
+ resolveS3Parts
8
9
  } from '../../application/custom/customFlows.js';
9
10
  import { hasSystemWebSocket } from '../../application/system/emitSystemWebSocketArtifacts.js';
10
11
  import { rbacFlowTag } from '../../application/rbac/rbacFlowTag.js';
@@ -161,9 +162,12 @@ export function createResourceState({ objects, relationships, flows }) {
161
162
 
162
163
  for (const flow of flows.filter((flow) => includesEvent(flow, 's3'))) {
163
164
  const tag = upperFirst(flow.flowTag);
164
- customSqsQueues.push(`${tag}CreatePreSignUrlHdrSqs`);
165
- customSqsQueues.push(`${tag}ReservedLimitHdrSqs`);
166
- customSqsQueues.push(`${tag}ConfirmAfterUploadHdrSqs`);
165
+ const parts = resolveS3Parts(flow);
166
+ for (const part of parts) {
167
+ if (part.type === 'sqs') {
168
+ customSqsQueues.push(`${tag}${part.action}HdrSqs`);
169
+ }
170
+ }
167
171
  }
168
172
 
169
173
  const customDsqQueues = flows
@@ -281,8 +285,14 @@ export function createResourceState({ objects, relationships, flows }) {
281
285
  topicName = `Out${upperFirst(flow.outputTopic)}`;
282
286
  }
283
287
  }
288
+ let targetAction = 'ReservedLimit';
289
+ const parts = resolveS3Parts(flow);
290
+ const firstSqsPart = parts.find((p) => p.type === 'sqs');
291
+ if (firstSqsPart && firstSqsPart.action) {
292
+ targetAction = firstSqsPart.action;
293
+ }
284
294
  const queueName =
285
- flow.s3Subscription?.queueName || `${tag}ReservedLimitHdrSqs`;
295
+ flow.s3Subscription?.queueName || `${tag}${targetAction}HdrSqs`;
286
296
  const subscriptionName =
287
297
  flow.s3Subscription?.subscriptionName ||
288
298
  `Subscription${topicName}To${queueName}`;
@@ -1,13 +1,29 @@
1
1
  import { indentYaml, renderResourceTemplate } from './resourceTemplate.js';
2
2
 
3
+ export function toAlphanumericPascalCase(str) {
4
+ if (!str) {
5
+ return '';
6
+ }
7
+ const parts = str.split(/[^a-zA-Z0-9]+/);
8
+ const validParts = parts.filter(Boolean);
9
+ const capitalizedParts = validParts.map((word) => {
10
+ return word.charAt(0).toUpperCase() + word.slice(1);
11
+ });
12
+ return capitalizedParts.join('');
13
+ }
14
+
3
15
  export async function renderS3(state) {
4
16
  if (state.s3Buckets.length === 0) {
5
17
  return '';
6
18
  }
7
19
  const renderedBuckets = await Promise.all(
8
- state.s3Buckets.map((bucketName) =>
9
- renderResourceTemplate('S3Bucket_Yaml.ejs', { bucketName })
10
- )
20
+ state.s3Buckets.map((bucketName) => {
21
+ const bucketLogicalId = `${toAlphanumericPascalCase(bucketName)}S3Bucket`;
22
+ return renderResourceTemplate('S3Bucket_Yaml.ejs', {
23
+ bucketName,
24
+ bucketLogicalId
25
+ });
26
+ })
11
27
  );
12
28
  const body = renderedBuckets.join('\n');
13
29
  return `Resources:\n${indentYaml(body, 2)}`;
@@ -1,5 +1,5 @@
1
1
  ##===== [S3 Bucket: <%- bucketName %>]
2
- <%- bucketName %>S3Bucket:
2
+ <%- bucketLogicalId %>:
3
3
  Type: AWS::S3::Bucket
4
4
  Properties:
5
5
  BucketName: <%- bucketName %>
@@ -230,7 +230,7 @@ Resources:
230
230
  - AttributeName: cacheKey
231
231
  KeyType: RANGE
232
232
  TimeToLiveSpecification:
233
- AttributeName: ttlTimestamp
233
+ AttributeName: expirationTime
234
234
  Enabled: true
235
235
 
236
236
  RunningTotalUsageTable:
@@ -265,7 +265,7 @@ Resources:
265
265
  - AttributeName: usedTimestampId
266
266
  KeyType: RANGE
267
267
  TimeToLiveSpecification:
268
- AttributeName: ttlTimestamp
268
+ AttributeName: expirationTime
269
269
  Enabled: true
270
270
  StreamSpecification:
271
271
  StreamViewType: NEW_AND_OLD_IMAGES