@izara_project/izara-core-generate-service-code 1.0.98 → 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.
- package/package.json +1 -1
- package/src/compile/compileProject.js +16 -5
- package/src/compile/flowValidator.js +47 -8
- package/src/compile/loadSchemas.js +18 -7
- package/src/compile/schemaValidatorSetup.js +16 -0
- package/src/emit/application/coreFunction/emitCoreFunctionArtifacts.js +123 -109
- package/src/emit/application/coreFunction/templates/InitialSetupConfig.ejs +8 -4
- package/src/emit/application/coreFunction/templates/InitialSetupRunner.ejs +2 -2
- package/src/emit/application/crud/templates/CreateComplete_Main.ejs +2 -0
- package/src/emit/application/crud/templates/CreateEndpoint_Main.ejs +4 -4
- package/src/emit/application/crud/templates/CreateLogicalLib.ejs +74 -15
- package/src/emit/application/crud/templates/DeleteComplete_Main.ejs +2 -0
- package/src/emit/application/crud/templates/DeleteEndpoint_Main.ejs +4 -4
- package/src/emit/application/crud/templates/FlowEndpoint_HdrWbs.ejs +7 -5
- package/src/emit/application/crud/templates/UpdateComplete_Main.ejs +2 -0
- package/src/emit/application/crud/templates/UpdateEndpoint_Main.ejs +4 -4
- package/src/emit/application/custom/customFlowPaths.js +13 -16
- package/src/emit/application/custom/customFlowS3.js +277 -66
- package/src/emit/application/custom/customFlows.js +131 -0
- package/src/emit/application/custom/emitCustomFlowApplicationArtifacts.js +3 -1
- package/src/emit/application/custom/templates/FlowEndpoint_Main.ejs +2 -2
- package/src/emit/application/custom/templates/s3/ConfirmAfterUpload_HdrSqs.ejs +110 -0
- package/src/emit/application/custom/templates/s3/ConfirmAfterUpload_Main.ejs +89 -0
- package/src/emit/application/custom/templates/s3/ConfirmComplete_HdrSqs.ejs +110 -0
- package/src/emit/application/custom/templates/s3/ConfirmComplete_Main.ejs +89 -0
- package/src/emit/application/custom/templates/s3/CreateObject_HdrWbs.ejs +2 -1
- package/src/emit/application/custom/templates/s3/CreateObject_Main.ejs +4 -0
- package/src/emit/application/custom/templates/s3/CreatePreSignUrl_HdrSqs.ejs +42 -0
- package/src/emit/application/custom/templates/s3/CreatePreSignUrl_Main.ejs +23 -0
- package/src/emit/application/custom/templates/s3/ProcessS3File_Main.ejs +135 -27
- package/src/emit/application/custom/templates/s3/Start_Main.ejs +197 -0
- package/src/emit/application/rbac/emitDynamicRbacFlowSchemas.js +8 -1
- package/src/emit/application/rbac/emitRbacApplicationArtifacts.js +107 -25
- package/src/emit/application/rbac/templates/ReservedDynamicUsage_Main.ejs +29 -9
- package/src/emit/application/rbac/templates/ReservedUsageTtlExpired_HdrDss.ejs +176 -0
- package/src/emit/application/rbac/templates/UserRbacFlow_Main.ejs +4 -3
- package/src/emit/application/system/templates/Register_HdrWbs.ejs +2 -1
- package/src/emit/infrastructure/functions/functionYamlArtifact.js +31 -8
- package/src/emit/infrastructure/functions/templates/DssFunction_Yaml.ejs +21 -0
- package/src/emit/infrastructure/functions/templates/S3Function_Yaml.ejs +2 -3
- package/src/emit/infrastructure/resources/iamResourceArtifacts.js +67 -5
- package/src/emit/infrastructure/resources/resourceState.js +15 -5
- package/src/emit/infrastructure/resources/s3ResourceArtifacts.js +19 -3
- package/src/emit/infrastructure/resources/templates/GeneratedLambdaRoleServiceConfig.ejs +5 -3
- package/src/emit/infrastructure/resources/templates/S3Bucket_Yaml.ejs +1 -1
- package/src/emit/infrastructure/resources/templates/SystemDynamoDB_Yaml.ejs +2 -2
- package/src/emit/schemas/attributeTree/emitAttributeTreeSchemas.js +62 -1
- package/src/emit/schemas/error/emitErrorSchemas.js +120 -10
- package/src/emit/schemas/error/templates/errorConsts.ejs +12 -0
- package/src/generate.js +32 -34
- package/src/output/mergeHooks.js +105 -5
- package/src/plan/domainRegistry.js +0 -10
- package/task.md +40 -96
- package/test/integration/graphDrivenFlowSchema.test.js +58 -0
- package/test/integration/publicApi.test.js +33 -24
- package/test/unit/application/createTargetRoleHandler.test.js +101 -9
- package/test/unit/application/crudApplicationArtifacts.test.js +4 -0
- package/test/unit/application/customFlowApplicationArtifacts.test.js +6 -6
- package/test/unit/application/customFlowS3.test.js +730 -0
- package/test/unit/infrastructure/functionYamlArtifact.test.js +6 -4
- package/test/unit/infrastructure/resourceArtifacts.test.js +103 -0
- package/test/unit/output/mergeHooks.test.js +137 -0
- package/test/unit/schemas/attributeTreeSchemas.test.js +32 -3
- package/test/unit/schemas/attributeTreeSingleService.test.js +175 -0
- package/test/unit/schemas/dynamicRbacFlowSchemas.test.js +8 -6
- package/test/unit/schemas/dynamicRelationshipFlowSchemas.test.js +51 -0
- package/test/unit/schemas/errorSchemas.test.js +84 -5
- package/test/unit/templates/hookTagsValidation.test.js +83 -0
- package/src/emit/schemas/attributeTree/emitAttributeTreeTargetValueStandard.js +0 -132
- package/test/unit/schemas/attributeTreeDirectives.test.js +0 -177
|
@@ -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
|
+
}
|
|
@@ -18,7 +18,10 @@ const RBAC_CORE_FLOW_DEFINITIONS = [
|
|
|
18
18
|
];
|
|
19
19
|
|
|
20
20
|
const LIMIT_FLOW_DEFINITIONS = [
|
|
21
|
-
{
|
|
21
|
+
{
|
|
22
|
+
flowTag: 'ReservedDynamicUsage',
|
|
23
|
+
event: ['lambdaSyncInv', 'lambdaSyncApi']
|
|
24
|
+
},
|
|
22
25
|
{
|
|
23
26
|
flowTag: 'ConfirmUsage',
|
|
24
27
|
event: ['ownTopic', 'lambdaSyncInv', 'lambdaSyncApi']
|
|
@@ -26,6 +29,10 @@ const LIMIT_FLOW_DEFINITIONS = [
|
|
|
26
29
|
{
|
|
27
30
|
flowTag: 'CancelUsage',
|
|
28
31
|
event: ['ownTopic', 'lambdaSyncInv', 'lambdaSyncApi']
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
flowTag: 'ReservedUsageTtlExpired',
|
|
35
|
+
event: ['dss']
|
|
29
36
|
}
|
|
30
37
|
];
|
|
31
38
|
|
|
@@ -3,6 +3,7 @@ import { readFile } from 'node:fs/promises';
|
|
|
3
3
|
|
|
4
4
|
import {
|
|
5
5
|
renderApiFunctionYaml,
|
|
6
|
+
renderDssFunctionYaml,
|
|
6
7
|
renderInvFunctionYaml,
|
|
7
8
|
renderSqsFunctionYaml,
|
|
8
9
|
renderWbsFunctionYaml
|
|
@@ -53,7 +54,10 @@ export async function emitRbacApplicationArtifacts(objects) {
|
|
|
53
54
|
|
|
54
55
|
const limitHandlers = await Promise.all(
|
|
55
56
|
hasSelfRbac
|
|
56
|
-
?
|
|
57
|
+
? [
|
|
58
|
+
...ACCOUNT_LIMIT_HANDLERS.map(createAccountLimitHandler),
|
|
59
|
+
createReservedUsageTtlExpiredHandler()
|
|
60
|
+
]
|
|
57
61
|
: []
|
|
58
62
|
);
|
|
59
63
|
|
|
@@ -170,12 +174,22 @@ const STATIC_RBAC_APPLICATION_PATHS = [
|
|
|
170
174
|
];
|
|
171
175
|
|
|
172
176
|
const STATIC_LIMIT_APPLICATION_PATHS = [
|
|
173
|
-
...ACCOUNT_LIMIT_HANDLERS.flatMap((flowTag) =>
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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
|
+
}),
|
|
192
|
+
'app/src/generatedCode/FlowLimit/ReservedUsageTtlExpired/source/ReservedUsageTtlExpired_HdrDss.js',
|
|
179
193
|
FLOW_LIMITS_YAML_PATH
|
|
180
194
|
];
|
|
181
195
|
|
|
@@ -205,34 +219,101 @@ export function rbacApplicationManagedPaths(objects = []) {
|
|
|
205
219
|
async function createAccountLimitHandler(flowTag) {
|
|
206
220
|
const sourcePath = `app/src/generatedCode/FlowLimit/${flowTag}/source`;
|
|
207
221
|
const handlerPath = `src/generatedCode/FlowLimit/${flowTag}/source`;
|
|
208
|
-
const
|
|
222
|
+
const shared = {
|
|
209
223
|
flowTag,
|
|
210
|
-
|
|
211
|
-
|
|
224
|
+
flowStepName: '',
|
|
225
|
+
mainFileName: `${flowTag}_Main`,
|
|
226
|
+
functionName: flowTag
|
|
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
|
+
}
|
|
212
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
|
+
);
|
|
278
|
+
return {
|
|
279
|
+
artifacts,
|
|
280
|
+
functions
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
async function createReservedUsageTtlExpiredHandler() {
|
|
285
|
+
const sourcePath =
|
|
286
|
+
'app/src/generatedCode/FlowLimit/ReservedUsageTtlExpired/source';
|
|
287
|
+
const handlerPath =
|
|
288
|
+
'src/generatedCode/FlowLimit/ReservedUsageTtlExpired/source';
|
|
213
289
|
return {
|
|
214
290
|
artifacts: [
|
|
215
291
|
{
|
|
216
|
-
path: `${sourcePath}
|
|
217
|
-
content: await renderRbacTemplate(
|
|
218
|
-
}
|
|
219
|
-
{
|
|
220
|
-
path: `${sourcePath}/${flowTag}_HdrSqs.js`,
|
|
221
|
-
content: await renderRbacTemplate('ListRbac_HdrSqs.ejs', { flowTag })
|
|
222
|
-
},
|
|
223
|
-
...webSocket.artifacts
|
|
292
|
+
path: `${sourcePath}/ReservedUsageTtlExpired_HdrDss.js`,
|
|
293
|
+
content: await renderRbacTemplate('ReservedUsageTtlExpired_HdrDss.ejs')
|
|
294
|
+
}
|
|
224
295
|
],
|
|
225
296
|
functions: [
|
|
226
297
|
{
|
|
227
|
-
type: '
|
|
228
|
-
functionName:
|
|
229
|
-
handlerPath: `${handlerPath}
|
|
298
|
+
type: 'dss',
|
|
299
|
+
functionName: 'ReservedUsageTtlExpiredHdrDss',
|
|
300
|
+
handlerPath: `${handlerPath}/ReservedUsageTtlExpired_HdrDss.main`,
|
|
230
301
|
roleName: 'FlowRbacRole',
|
|
231
|
-
|
|
232
|
-
|
|
302
|
+
tableName: 'RunningTotalReservedUsage',
|
|
303
|
+
batchSize: 100,
|
|
304
|
+
startingPosition: 'TRIM_HORIZON',
|
|
305
|
+
filterPatterns: [
|
|
306
|
+
{
|
|
307
|
+
eventName: ['REMOVE'],
|
|
308
|
+
userIdentity: {
|
|
309
|
+
type: ['Service'],
|
|
310
|
+
principalId: ['dynamodb.amazonaws.com']
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
],
|
|
314
|
+
flowTag: 'ReservedUsageTtlExpired',
|
|
233
315
|
flowStepName: ''
|
|
234
|
-
}
|
|
235
|
-
...webSocket.functions
|
|
316
|
+
}
|
|
236
317
|
]
|
|
237
318
|
};
|
|
238
319
|
}
|
|
@@ -523,6 +604,7 @@ function renderFunctionYaml(descriptor) {
|
|
|
523
604
|
if (descriptor.type === 'api') return renderApiFunctionYaml(descriptor);
|
|
524
605
|
if (descriptor.type === 'inv') return renderInvFunctionYaml(descriptor);
|
|
525
606
|
if (descriptor.type === 'wbs') return renderWbsFunctionYaml(descriptor);
|
|
607
|
+
if (descriptor.type === 'dss') return renderDssFunctionYaml(descriptor);
|
|
526
608
|
return renderSqsFunctionYaml(descriptor);
|
|
527
609
|
}
|
|
528
610
|
|
|
@@ -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
|
|
164
|
+
const expirationTime = Math.floor(Date.now() / 1000) + 3600;
|
|
165
165
|
|
|
166
166
|
await dynamodbSharedLib.putItem(
|
|
167
167
|
_izContext,
|
|
@@ -169,9 +169,11 @@ export default async function ReservedDynamicUsage(_izContext, requestParams) {
|
|
|
169
169
|
{
|
|
170
170
|
runningTotalReservedId: `${identifiersConcat}_${flowTypeConcat}`,
|
|
171
171
|
usedTimestampId: uniqueIdentify,
|
|
172
|
+
identifiersConcat,
|
|
173
|
+
flowTypeConcat,
|
|
172
174
|
usageCount: value,
|
|
173
175
|
runningTotalUsageStatus: 'reserved',
|
|
174
|
-
|
|
176
|
+
expirationTime
|
|
175
177
|
}
|
|
176
178
|
);
|
|
177
179
|
|
|
@@ -194,13 +196,31 @@ export default async function ReservedDynamicUsage(_izContext, requestParams) {
|
|
|
194
196
|
identifiersConcat
|
|
195
197
|
};
|
|
196
198
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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;
|
|
204
224
|
} catch (error) {
|
|
205
225
|
_izContext.logger.error(`[${process.env.iz_serviceTag}:ReservedDynamicUsage] error:`, error);
|
|
206
226
|
throw error;
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright (C) 2020 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 { middlewareHandler as middleware } from '@izara_project/izara-middleware';
|
|
19
|
+
import Logger from '@izara_project/izara-core-library-logger';
|
|
20
|
+
import dynamodbSharedLib from '@izara_project/izara-core-library-dynamodb';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* DynamoDB Stream handler for TTL expired records in RunningTotalReservedUsage.
|
|
24
|
+
* Automatically refunds abandoned reservations with atomic decrement.
|
|
25
|
+
*/
|
|
26
|
+
export const main = middleware.wrap(async (event, context) => {
|
|
27
|
+
try {
|
|
28
|
+
if (!event || !Array.isArray(event.Records)) {
|
|
29
|
+
Logger.debug('No records in DynamoDB stream event');
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
let _izContext = event._izContext;
|
|
34
|
+
if (!_izContext) {
|
|
35
|
+
_izContext = {
|
|
36
|
+
logger: Logger,
|
|
37
|
+
correlationIds: new Map()
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
for (const record of event.Records) {
|
|
42
|
+
let recordContext = record._izContext;
|
|
43
|
+
if (!recordContext) {
|
|
44
|
+
recordContext = _izContext;
|
|
45
|
+
}
|
|
46
|
+
let recordLogger = recordContext.logger;
|
|
47
|
+
if (!recordLogger) {
|
|
48
|
+
recordLogger = Logger;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const isTtlDelete =
|
|
52
|
+
record.eventName === 'REMOVE' &&
|
|
53
|
+
record.userIdentity &&
|
|
54
|
+
record.userIdentity.type === 'Service' &&
|
|
55
|
+
record.userIdentity.principalId === 'dynamodb.amazonaws.com';
|
|
56
|
+
|
|
57
|
+
if (!isTtlDelete) {
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const oldImage = record.dynamodb && record.dynamodb.OldImage;
|
|
62
|
+
if (!oldImage) {
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
let oldStatus = null;
|
|
67
|
+
if (oldImage.runningTotalUsageStatus && oldImage.runningTotalUsageStatus.S) {
|
|
68
|
+
oldStatus = oldImage.runningTotalUsageStatus.S;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (oldStatus !== 'reserved') {
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
let identifiersConcat = null;
|
|
76
|
+
if (oldImage.identifiersConcat && oldImage.identifiersConcat.S) {
|
|
77
|
+
identifiersConcat = oldImage.identifiersConcat.S;
|
|
78
|
+
}
|
|
79
|
+
let flowTypeConcat = null;
|
|
80
|
+
if (oldImage.flowTypeConcat && oldImage.flowTypeConcat.S) {
|
|
81
|
+
flowTypeConcat = oldImage.flowTypeConcat.S;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if ((!identifiersConcat || !flowTypeConcat) && oldImage.runningTotalReservedId && oldImage.runningTotalReservedId.S) {
|
|
85
|
+
const reservedId = oldImage.runningTotalReservedId.S;
|
|
86
|
+
const lastUnderscoreIndex = reservedId.lastIndexOf('_');
|
|
87
|
+
if (lastUnderscoreIndex > 0) {
|
|
88
|
+
if (!identifiersConcat) {
|
|
89
|
+
identifiersConcat = reservedId.substring(0, lastUnderscoreIndex);
|
|
90
|
+
}
|
|
91
|
+
if (!flowTypeConcat) {
|
|
92
|
+
flowTypeConcat = reservedId.substring(lastUnderscoreIndex + 1);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
let usageCount = 1;
|
|
98
|
+
if (oldImage.usageCount && oldImage.usageCount.N) {
|
|
99
|
+
usageCount = Number(oldImage.usageCount.N);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (identifiersConcat && flowTypeConcat && usageCount > 0) {
|
|
103
|
+
const usageTable = dynamodbSharedLib.tableName(
|
|
104
|
+
recordContext,
|
|
105
|
+
'RunningTotalUsage'
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
const usageRecord = await dynamodbSharedLib.getItem(
|
|
109
|
+
recordContext,
|
|
110
|
+
usageTable,
|
|
111
|
+
{
|
|
112
|
+
identifiersConcat: identifiersConcat,
|
|
113
|
+
flowTypeConcat: flowTypeConcat
|
|
114
|
+
}
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
if (usageRecord) {
|
|
118
|
+
let currentTotal = 0;
|
|
119
|
+
if (typeof usageRecord.totalCount === 'number') {
|
|
120
|
+
currentTotal = usageRecord.totalCount;
|
|
121
|
+
}
|
|
122
|
+
const newTotal = Math.max(0, currentTotal - usageCount);
|
|
123
|
+
const updateUuid = recordContext.uniqueRequestId || `${Date.now()}`;
|
|
124
|
+
|
|
125
|
+
const queryElementsWhenUpdate = {
|
|
126
|
+
additionalAttributes: { currentUuid: usageRecord.updateUuid },
|
|
127
|
+
logicalElements: [
|
|
128
|
+
{
|
|
129
|
+
type: 'logical',
|
|
130
|
+
comparison: 'equals',
|
|
131
|
+
lhsAttribute: 'updateUuid',
|
|
132
|
+
rhsReference: 'currentUuid'
|
|
133
|
+
}
|
|
134
|
+
],
|
|
135
|
+
returnValues: 'ALL_NEW'
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
const updateAttributes = [
|
|
139
|
+
{ attributeName: 'totalCount', action: 'set', value: newTotal },
|
|
140
|
+
{ attributeName: 'updateUuid', action: 'set', value: updateUuid }
|
|
141
|
+
];
|
|
142
|
+
|
|
143
|
+
await dynamodbSharedLib.updateItem(
|
|
144
|
+
recordContext,
|
|
145
|
+
usageTable,
|
|
146
|
+
{
|
|
147
|
+
identifiersConcat: identifiersConcat,
|
|
148
|
+
flowTypeConcat: flowTypeConcat
|
|
149
|
+
},
|
|
150
|
+
updateAttributes,
|
|
151
|
+
queryElementsWhenUpdate,
|
|
152
|
+
{
|
|
153
|
+
complexAttributes: true,
|
|
154
|
+
errorOnConditionalExpNotPass: false
|
|
155
|
+
}
|
|
156
|
+
).catch(() => null);
|
|
157
|
+
|
|
158
|
+
recordLogger.info({
|
|
159
|
+
message: '[ReservedUsageTtlExpired] Auto-refunded expired reservation',
|
|
160
|
+
identifiersConcat: identifiersConcat,
|
|
161
|
+
flowTypeConcat: flowTypeConcat,
|
|
162
|
+
usageCount: usageCount,
|
|
163
|
+
previousTotal: currentTotal,
|
|
164
|
+
newTotal: newTotal
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
} catch (err) {
|
|
170
|
+
Logger.error({
|
|
171
|
+
message: 'Unhandled error in ReservedUsageTtlExpired_HdrDss',
|
|
172
|
+
error: err
|
|
173
|
+
});
|
|
174
|
+
throw err;
|
|
175
|
+
}
|
|
176
|
+
});
|
|
@@ -55,7 +55,8 @@ async function _getL2Cache(_izContext, submittedByUserId, cacheSubKey) {
|
|
|
55
55
|
userId: submittedByUserId,
|
|
56
56
|
cacheKey: cacheSubKey
|
|
57
57
|
});
|
|
58
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|