@izara_project/izara-core-generate-service-code 1.0.89 → 1.0.90
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/emit/crud/templates/CreateComplete_Main.ejs +135 -10
- package/src/emit/crud/templates/CreateLogicalLib.ejs +36 -7
- package/src/emit/crud/templates/CreateNodeCallback_Main.ejs +8 -1
- package/src/emit/crud/templates/DeleteLogicalLib.ejs +1 -3
- package/src/emit/crud/templates/UpdateLogicalLib.ejs +0 -3
- package/src/emit/rbac/templates/UserRbacFlow_Main.ejs +20 -13
- package/src/emit/resource/emitResourceArtifacts.js +46 -12
- package/test/createTargetRoleHandler.test.js +11 -8
- package/test/crudApplicationArtifacts.test.js +95 -0
- package/test/resourceArtifacts.test.js +42 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@izara_project/izara-core-generate-service-code",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.90",
|
|
4
4
|
"description": "Code for locally generating per service files",
|
|
5
5
|
"homepage": "https://bitbucket.org/izara-core-support-services/izara-core-support-services-generate-service-code#readme",
|
|
6
6
|
"repository": {
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import asyncFlowSharedLib from '@izara_project/izara-core-library-asynchronous-flow';
|
|
2
2
|
import callingFlowSharedLib from '@izara_project/izara-core-library-calling-flow';
|
|
3
3
|
import dynamodbSharedLib from '@izara_project/izara-core-library-dynamodb';
|
|
4
|
+
import lambdaSharedLib from '@izara_project/izara-core-library-lambda';
|
|
4
5
|
import snsSharedLib from '@izara_project/izara-core-library-sns';
|
|
5
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
sns,
|
|
8
|
+
lambda
|
|
9
|
+
} from '@izara_project/izara-core-library-external-request';
|
|
6
10
|
import { consts } from '@izara_project/izara-core-library-service-schemas';
|
|
7
11
|
import { NoRetryError } from '@izara_project/izara-core-library-core';
|
|
8
12
|
|
|
@@ -142,18 +146,138 @@ export default async function CreateComplete(
|
|
|
142
146
|
const callingFlowConfig =
|
|
143
147
|
result?.additionalAttributes?.callingFlowConfig ||
|
|
144
148
|
passBackProperties;
|
|
149
|
+
const actionData = {
|
|
150
|
+
objType: returnValue.requestParams.objType,
|
|
151
|
+
objInstanceFull: node.objInstanceFull,
|
|
152
|
+
relationships: node.relationships,
|
|
153
|
+
parentFlowId: parentFlowId || returnValue?.parentFlowId,
|
|
154
|
+
existsNode,
|
|
155
|
+
status,
|
|
156
|
+
errorsFound
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
<%_ if (plan?.hasAfterHookSync) { _%>
|
|
160
|
+
// ── Synchronous After Hooks ──
|
|
161
|
+
<%_ plan.afterHooks.filter(h => h?.generatedCodeConfig?.invocationType === 'lambdaSyncInv').forEach(hook => { _%>
|
|
162
|
+
<%_ if (hook.targetObjectTypes && hook.targetObjectTypes.length === 1) { _%>
|
|
163
|
+
if (returnValue.requestParams?.objType?.objectType === '<%= hook.targetObjectTypes[0] %>') {
|
|
164
|
+
<%_ } else if (hook.targetObjectTypes && hook.targetObjectTypes.length > 1) { _%>
|
|
165
|
+
if (<%- JSON.stringify(hook.targetObjectTypes) %>.includes(returnValue.requestParams?.objType?.objectType)) {
|
|
166
|
+
<%_ } _%>
|
|
167
|
+
try {
|
|
168
|
+
let hookPayload_<%= hook.flowTag %> = {
|
|
169
|
+
objType: returnValue.requestParams.objType,
|
|
170
|
+
objInstanceFull: node.objInstanceFull,
|
|
171
|
+
relationships: node.relationships || [],
|
|
172
|
+
targetIdentifiers: requestParams.targetIdentifiers,
|
|
173
|
+
submittedByUserId: returnValue?.requestParams?.submittedByUserId || requestParams?.submittedByUserId
|
|
174
|
+
};
|
|
175
|
+
//(<beforeSendHook_<%= hook.flowTag %>>)
|
|
176
|
+
//(</beforeSendHook_<%= hook.flowTag %>>)
|
|
177
|
+
const hookResult = await lambda.invokeSync(
|
|
178
|
+
_izContext,
|
|
179
|
+
await lambdaSharedLib.lambdaFunctionName(
|
|
180
|
+
_izContext,
|
|
181
|
+
'<%= hook.flowTag %>'
|
|
182
|
+
),
|
|
183
|
+
{
|
|
184
|
+
requestParams: hookPayload_<%= hook.flowTag %>,
|
|
185
|
+
callingFlowConfig
|
|
186
|
+
}
|
|
187
|
+
);
|
|
188
|
+
if (hookResult?.errorsFound?.length > 0 || hookResult?.status === 'error') {
|
|
189
|
+
errorsFound.push(...(hookResult.errorsFound || ['Hook <%= hook.flowTag %> failed']));
|
|
190
|
+
}
|
|
191
|
+
} catch (hookErr) {
|
|
192
|
+
_izContext.logger.error('[CreateComplete] Sync hook <%= hook.flowTag %> error:', hookErr);
|
|
193
|
+
errorsFound.push(hookErr.message || 'Hook <%= hook.flowTag %> failed');
|
|
194
|
+
}
|
|
195
|
+
<%_ if (hook.targetObjectTypes && hook.targetObjectTypes.length > 0) { _%>
|
|
196
|
+
}
|
|
197
|
+
<%_ } _%>
|
|
198
|
+
<%_ }) _%>
|
|
199
|
+
<%_ } _%>
|
|
200
|
+
|
|
201
|
+
<%_ if (plan?.hasAfterHookAsync) { _%>
|
|
202
|
+
// ── Asynchronous After Hooks ──
|
|
203
|
+
let afterHookDispatched = false;
|
|
204
|
+
<%_ plan.afterHooks.filter(h => h?.generatedCodeConfig?.invocationType === 'ownTopic').forEach(hook => { _%>
|
|
205
|
+
<%_ if (hook.targetObjectTypes && hook.targetObjectTypes.length === 1) { _%>
|
|
206
|
+
if (returnValue.requestParams?.objType?.objectType === '<%= hook.targetObjectTypes[0] %>') {
|
|
207
|
+
<%_ } else if (hook.targetObjectTypes && hook.targetObjectTypes.length > 1) { _%>
|
|
208
|
+
if (<%- JSON.stringify(hook.targetObjectTypes) %>.includes(returnValue.requestParams?.objType?.objectType)) {
|
|
209
|
+
<%_ } _%>
|
|
210
|
+
afterHookDispatched = true;
|
|
211
|
+
let hookPayload_<%= hook.flowTag %> = {
|
|
212
|
+
objType: returnValue.requestParams.objType,
|
|
213
|
+
objInstanceFull: node.objInstanceFull,
|
|
214
|
+
relationships: node.relationships || [],
|
|
215
|
+
targetIdentifiers: requestParams.targetIdentifiers,
|
|
216
|
+
submittedByUserId: returnValue?.requestParams?.submittedByUserId || requestParams?.submittedByUserId
|
|
217
|
+
};
|
|
218
|
+
//(<beforeSendHook_<%= hook.flowTag %>>)
|
|
219
|
+
//(</beforeSendHook_<%= hook.flowTag %>>)
|
|
220
|
+
const hookMsg_<%= hook.flowTag %> = callingFlowSharedLib.addCallingFlowToSnsRequestMessageObject(
|
|
221
|
+
hookPayload_<%= hook.flowTag %>,
|
|
222
|
+
callingFlowSharedLib.addParentCallingFlowConfig(
|
|
223
|
+
callingFlowConfig,
|
|
224
|
+
callingFlowSharedLib.createCallingFlowConfig(
|
|
225
|
+
await lambdaSharedLib.lambdaFunctionName(
|
|
226
|
+
_izContext,
|
|
227
|
+
'CreateAfterHookCallbackHdrSqs'
|
|
228
|
+
),
|
|
229
|
+
{
|
|
230
|
+
actionData
|
|
231
|
+
}
|
|
232
|
+
)
|
|
233
|
+
)
|
|
234
|
+
);
|
|
235
|
+
|
|
236
|
+
await sns.publishAsync(_izContext, {
|
|
237
|
+
TopicArn: snsSharedLib.snsTopicArnByFlowSchema(
|
|
238
|
+
_izContext,
|
|
239
|
+
'<%= hook.flowTag %>_In'
|
|
240
|
+
),
|
|
241
|
+
Message: JSON.stringify(hookMsg_<%= hook.flowTag %>),
|
|
242
|
+
MessageAttributes: sns.createStringMessageAttributes(_izContext, {})
|
|
243
|
+
});
|
|
244
|
+
<%_ if (hook.targetObjectTypes && hook.targetObjectTypes.length > 0) { _%>
|
|
245
|
+
}
|
|
246
|
+
<%_ } _%>
|
|
247
|
+
<%_ }) _%>
|
|
248
|
+
if (!afterHookDispatched) {
|
|
249
|
+
const message =
|
|
250
|
+
callingFlowSharedLib.addParentPassBackPropertiesToSnsResponseMessageObject(
|
|
251
|
+
callingFlowConfig,
|
|
252
|
+
actionData
|
|
253
|
+
);
|
|
254
|
+
const messageAttributes =
|
|
255
|
+
callingFlowSharedLib.addParentPassBackCallingFlowToSnsResponseMessageAttributes(
|
|
256
|
+
callingFlowConfig,
|
|
257
|
+
{}
|
|
258
|
+
);
|
|
259
|
+
const topicArn = snsSharedLib.snsTopicArnByFlowSchema(
|
|
260
|
+
_izContext,
|
|
261
|
+
TOPIC_NAME_GENERATE_CODE.outCreateNodeComplete
|
|
262
|
+
);
|
|
263
|
+
_izContext.logger.debug('[CreateComplete] publishing to SNS', {
|
|
264
|
+
TopicArn: topicArn,
|
|
265
|
+
message
|
|
266
|
+
});
|
|
267
|
+
await sns.publishAsync(_izContext, {
|
|
268
|
+
Message: JSON.stringify(message),
|
|
269
|
+
MessageAttributes: sns.createStringMessageAttributes(
|
|
270
|
+
_izContext,
|
|
271
|
+
messageAttributes
|
|
272
|
+
),
|
|
273
|
+
TopicArn: topicArn
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
<%_ } else { _%>
|
|
145
277
|
const message =
|
|
146
278
|
callingFlowSharedLib.addParentPassBackPropertiesToSnsResponseMessageObject(
|
|
147
279
|
callingFlowConfig,
|
|
148
|
-
|
|
149
|
-
objType: returnValue.requestParams.objType,
|
|
150
|
-
objInstanceFull: node.objInstanceFull,
|
|
151
|
-
relationships: node.relationships,
|
|
152
|
-
parentFlowId: parentFlowId || returnValue?.parentFlowId,
|
|
153
|
-
existsNode,
|
|
154
|
-
status,
|
|
155
|
-
errorsFound
|
|
156
|
-
}
|
|
280
|
+
actionData
|
|
157
281
|
);
|
|
158
282
|
const messageAttributes =
|
|
159
283
|
callingFlowSharedLib.addParentPassBackCallingFlowToSnsResponseMessageAttributes(
|
|
@@ -176,6 +300,7 @@ export default async function CreateComplete(
|
|
|
176
300
|
),
|
|
177
301
|
TopicArn: topicArn
|
|
178
302
|
});
|
|
303
|
+
<%_ } _%>
|
|
179
304
|
//(<afterSendMessage>)
|
|
180
305
|
//(</afterSendMessage>)
|
|
181
306
|
}
|
|
@@ -89,7 +89,11 @@ export async function resolveSchemaAndIdentity(_izContext, requestParams, errors
|
|
|
89
89
|
|
|
90
90
|
targetIdentifiers = requestParams.targetIdentifiers;
|
|
91
91
|
|
|
92
|
-
if (
|
|
92
|
+
if (
|
|
93
|
+
!targetIdentifiers &&
|
|
94
|
+
objectSchema.belongTo?.objectType === 'user' &&
|
|
95
|
+
submittedByUserId
|
|
96
|
+
) {
|
|
93
97
|
targetIdentifiers = { submittedByUserId };
|
|
94
98
|
}
|
|
95
99
|
|
|
@@ -98,10 +102,6 @@ export async function resolveSchemaAndIdentity(_izContext, requestParams, errors
|
|
|
98
102
|
errorsFound.push('not have targetIdentifiers');
|
|
99
103
|
|
|
100
104
|
if (targetIdentifiers) {
|
|
101
|
-
if (!targetIdentifiers.submittedByUserId && submittedByUserId) {
|
|
102
|
-
targetIdentifiers.submittedByUserId = submittedByUserId;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
105
|
// if (Array.isArray(objectSchema.identifiers)) {
|
|
106
106
|
// for (const identifier of objectSchema.identifiers) {
|
|
107
107
|
// if (identifier.fieldName) {
|
|
@@ -126,7 +126,7 @@ export async function resolveSchemaAndIdentity(_izContext, requestParams, errors
|
|
|
126
126
|
}
|
|
127
127
|
|
|
128
128
|
if (requestParams.rbacTargetIdentifiers === undefined && targetIdentifiers) {
|
|
129
|
-
requestParams.rbacTargetIdentifiers = targetIdentifiers;
|
|
129
|
+
requestParams.rbacTargetIdentifiers = { ...targetIdentifiers };
|
|
130
130
|
}
|
|
131
131
|
|
|
132
132
|
_izContext.logger.debug(`[${process.env.iz_serviceTag}:<%- flowTag %>] Identity & Target Context Resolved:`, {
|
|
@@ -487,14 +487,43 @@ export async function saveDynamoDbRecords(_izContext, listOfRecords) {
|
|
|
487
487
|
);
|
|
488
488
|
}
|
|
489
489
|
|
|
490
|
+
<%_
|
|
491
|
+
const rootRbacObjects = (objects || []).filter(obj => {
|
|
492
|
+
const hasSelfRbac = Array.isArray(obj?.addOnDataStructure) && obj.addOnDataStructure.some(addOn => addOn?.type === 'rbac');
|
|
493
|
+
return hasSelfRbac && !obj?.belongTo && obj?.objectType !== 'user' && obj?.generatedBy !== 'systemNoUser';
|
|
494
|
+
});
|
|
495
|
+
_%>
|
|
496
|
+
const ROOT_RBAC_OBJECT_TYPES = new Set([
|
|
497
|
+
<%_ for (const obj of rootRbacObjects) { _%>
|
|
498
|
+
'<%- obj.objectType %>',
|
|
499
|
+
<%_ } _%>
|
|
500
|
+
]);
|
|
501
|
+
|
|
490
502
|
/**
|
|
491
503
|
* Auto-provisions owner TargetRole for Root RBAC object after Graph and DynamoDB write
|
|
492
504
|
* @param {Object} _izContext - Izara context object
|
|
493
505
|
* @param {Object} options - Target node and submitter details
|
|
494
506
|
* @returns {Promise<void>}
|
|
495
507
|
*/
|
|
496
|
-
export async function autoProvisionOwnerTargetRole(_izContext, { node, submittedByUserId }) {
|
|
508
|
+
export async function autoProvisionOwnerTargetRole(_izContext, { node, submittedByUserId, objType }) {
|
|
497
509
|
try {
|
|
510
|
+
let objectType = null;
|
|
511
|
+
if (typeof objType === 'string') {
|
|
512
|
+
objectType = objType;
|
|
513
|
+
} else if (objType && objType.objectType) {
|
|
514
|
+
objectType = objType.objectType;
|
|
515
|
+
} else if (typeof node?.objType === 'string') {
|
|
516
|
+
objectType = node.objType;
|
|
517
|
+
} else if (node?.objType?.objectType) {
|
|
518
|
+
objectType = node.objType.objectType;
|
|
519
|
+
} else if (node?.objectType) {
|
|
520
|
+
objectType = node.objectType;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
if (!objectType || !ROOT_RBAC_OBJECT_TYPES.has(objectType)) {
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
526
|
+
|
|
498
527
|
if (!submittedByUserId || !node?.objInstanceFull?.identifiers) {
|
|
499
528
|
return;
|
|
500
529
|
}
|
|
@@ -167,10 +167,17 @@ export default async function CreateNodeCallbackMain(
|
|
|
167
167
|
_izContext.correlationIds?.get(coreConsts.SUBMITTED_BY_USER_ID) ||
|
|
168
168
|
_izContext.correlationIds?.get('submittedByUserId');
|
|
169
169
|
|
|
170
|
+
<%_ const rootRbacObjs = (objects || []).filter(obj => {
|
|
171
|
+
const hasSelfRbac = Array.isArray(obj?.addOnDataStructure) && obj.addOnDataStructure.some(addOn => addOn?.type === 'rbac');
|
|
172
|
+
return hasSelfRbac && !obj?.belongTo && obj?.objectType !== 'user' && obj?.generatedBy !== 'systemNoUser';
|
|
173
|
+
}); _%>
|
|
174
|
+
<%_ if (rootRbacObjs.length > 0) { _%>
|
|
170
175
|
await autoProvisionOwnerTargetRole(_izContext, {
|
|
171
176
|
node,
|
|
172
|
-
submittedByUserId
|
|
177
|
+
submittedByUserId,
|
|
178
|
+
objType: returnValue?.requestParams?.objType
|
|
173
179
|
});
|
|
180
|
+
<%_ } _%>
|
|
174
181
|
|
|
175
182
|
<%_ if (plan?.hasAfterHookSync) { _%>
|
|
176
183
|
// ── Synchronous After Hooks ──
|
|
@@ -100,9 +100,7 @@ export async function resolveSchemaAndIdentity(
|
|
|
100
100
|
|
|
101
101
|
requestParams.submittedByUserId = submittedByUserId;
|
|
102
102
|
if (targetIdentifiers) {
|
|
103
|
-
|
|
104
|
-
targetIdentifiers.submittedByUserId = submittedByUserId;
|
|
105
|
-
}
|
|
103
|
+
requestParams.targetIdentifiers = targetIdentifiers;
|
|
106
104
|
}
|
|
107
105
|
|
|
108
106
|
_izContext.logger.debug(
|
|
@@ -105,9 +105,6 @@ export async function resolveSchemaAndIdentity(_izContext, requestParams, errors
|
|
|
105
105
|
|
|
106
106
|
requestParams.submittedByUserId = submittedByUserId;
|
|
107
107
|
if (targetIdentifiers) {
|
|
108
|
-
if (!targetIdentifiers.submittedByUserId && submittedByUserId) {
|
|
109
|
-
targetIdentifiers.submittedByUserId = submittedByUserId;
|
|
110
|
-
}
|
|
111
108
|
// requestParams.targetIdentifiers = targetIdentifiers;
|
|
112
109
|
}
|
|
113
110
|
|
|
@@ -172,7 +172,7 @@ export default async function <%- flowTag %>(_izContext, requestParams = {}) {
|
|
|
172
172
|
_setL1Cache(fullCacheKey, result);
|
|
173
173
|
await _setL2Cache(_izContext, submittedByUserId, cacheSubKey, result);
|
|
174
174
|
await _publicMessageOut(_izContext, { ...result, parentFlowId });
|
|
175
|
-
return
|
|
175
|
+
return result;
|
|
176
176
|
}
|
|
177
177
|
|
|
178
178
|
// 1. Check L1 In-Memory Cache
|
|
@@ -180,7 +180,7 @@ export default async function <%- flowTag %>(_izContext, requestParams = {}) {
|
|
|
180
180
|
if (l1Hit) {
|
|
181
181
|
_izContext.logger.debug(`[${process.env.iz_serviceTag}:<%- flowTag %>] L1 Cache hit:`, l1Hit);
|
|
182
182
|
await _publicMessageOut(_izContext, { ...l1Hit, parentFlowId });
|
|
183
|
-
return;
|
|
183
|
+
return l1Hit;
|
|
184
184
|
}
|
|
185
185
|
|
|
186
186
|
// 2. Check L2 Shared DynamoDB Cache
|
|
@@ -189,7 +189,7 @@ export default async function <%- flowTag %>(_izContext, requestParams = {}) {
|
|
|
189
189
|
_izContext.logger.debug(`[${process.env.iz_serviceTag}:<%- flowTag %>] L2 Cache hit:`, l2Hit);
|
|
190
190
|
_setL1Cache(fullCacheKey, l2Hit);
|
|
191
191
|
await _publicMessageOut(_izContext, { ...l2Hit, parentFlowId });
|
|
192
|
-
return;
|
|
192
|
+
return l2Hit;
|
|
193
193
|
}
|
|
194
194
|
|
|
195
195
|
_izContext.logger.debug(`[${process.env.iz_serviceTag}:<%- flowTag %>] Cache miss — querying DynamoDB`);
|
|
@@ -237,7 +237,7 @@ export default async function <%- flowTag %>(_izContext, requestParams = {}) {
|
|
|
237
237
|
_setL1Cache(fullCacheKey, result);
|
|
238
238
|
await _setL2Cache(_izContext, submittedByUserId, cacheSubKey, result);
|
|
239
239
|
await _publicMessageOut(_izContext, { ...result, parentFlowId });
|
|
240
|
-
return;
|
|
240
|
+
return result;
|
|
241
241
|
}
|
|
242
242
|
|
|
243
243
|
const rolePermissionsTable = dynamodbSharedLib.tableName(
|
|
@@ -289,19 +289,26 @@ export default async function <%- flowTag %>(_izContext, requestParams = {}) {
|
|
|
289
289
|
|
|
290
290
|
await _publicMessageOut(_izContext, { ...cacheResult, parentFlowId });
|
|
291
291
|
|
|
292
|
-
return
|
|
292
|
+
return cacheResult;
|
|
293
293
|
}
|
|
294
294
|
|
|
295
295
|
async function _publicMessageOut(_izContext, message) {
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
296
|
+
try {
|
|
297
|
+
const topicOut = snsSharedLib.snsTopicArnByFlowSchema(
|
|
298
|
+
_izContext,
|
|
299
|
+
process.env.iz_flowTag + '_Out'
|
|
300
|
+
);
|
|
300
301
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
302
|
+
await sns.publishAsync(_izContext, {
|
|
303
|
+
TopicArn: topicOut,
|
|
304
|
+
Message: JSON.stringify(message)
|
|
305
|
+
});
|
|
306
|
+
} catch (error) {
|
|
307
|
+
_izContext.logger.warn(
|
|
308
|
+
`[${process.env.iz_serviceTag}:_publicMessageOut] Failed to publish message out:`,
|
|
309
|
+
error
|
|
310
|
+
);
|
|
311
|
+
}
|
|
305
312
|
}
|
|
306
313
|
|
|
307
314
|
function _matchesParameterGroups(groups, input) {
|
|
@@ -204,7 +204,7 @@ export const resourceArtifactPaths = [
|
|
|
204
204
|
function createResourceState({ objects, relationships, flows }) {
|
|
205
205
|
const crud = objects.some(isCrudObject);
|
|
206
206
|
const selfRbac = objects.some(hasSelfRbacAddOn);
|
|
207
|
-
const rbac = objects.some(
|
|
207
|
+
const rbac = objects.some((obj) => hasRbacAddOn(obj, objects));
|
|
208
208
|
const crudActionPlans = createCrudActionPlans(objects, flows);
|
|
209
209
|
|
|
210
210
|
const crudOwnTopics = crud ? [...CRUD_ACTIONS] : [];
|
|
@@ -244,9 +244,9 @@ function createResourceState({ objects, relationships, flows }) {
|
|
|
244
244
|
rbacSqsQueues.push(`Process${action}HdrSqs`);
|
|
245
245
|
}
|
|
246
246
|
}
|
|
247
|
-
if (
|
|
247
|
+
if (selfRbac) {
|
|
248
248
|
const flowTags = [
|
|
249
|
-
...new Set(objects.filter(
|
|
249
|
+
...new Set(objects.filter(hasSelfRbacAddOn).map(rbacFlowTag))
|
|
250
250
|
];
|
|
251
251
|
for (const flowTag of flowTags) {
|
|
252
252
|
rbacOwnTopics.push(flowTag);
|
|
@@ -433,6 +433,7 @@ function createResourceState({ objects, relationships, flows }) {
|
|
|
433
433
|
return {
|
|
434
434
|
crud,
|
|
435
435
|
rbac,
|
|
436
|
+
selfRbac,
|
|
436
437
|
crudActionPlans,
|
|
437
438
|
crudOwnTopics: [...new Set(crudOwnTopics)].filter(Boolean).sort(),
|
|
438
439
|
crudSqsQueues: [...new Set(crudSqsQueues)].filter(Boolean).sort(),
|
|
@@ -638,7 +639,7 @@ async function renderDynamo(state) {
|
|
|
638
639
|
const [system, ...tables] = await Promise.all([
|
|
639
640
|
render('SystemDynamoDB_Yaml.ejs', {
|
|
640
641
|
hasS3: state.s3FlowTags.length > 0,
|
|
641
|
-
hasRbac: state.
|
|
642
|
+
hasRbac: state.selfRbac
|
|
642
643
|
}),
|
|
643
644
|
...state.tables.map((table) => render('DynamoDBTable_Yaml.ejs', table))
|
|
644
645
|
]);
|
|
@@ -650,7 +651,7 @@ async function renderIam(state) {
|
|
|
650
651
|
const roles = ['ProcFindDataRole', 'RegisterRole'];
|
|
651
652
|
if (state.crud) roles.push('FlowObjectsRole');
|
|
652
653
|
if (state.relationships) roles.push('FlowRelationshipRole');
|
|
653
|
-
if (state.
|
|
654
|
+
if (state.selfRbac) roles.push('FlowRbacRole');
|
|
654
655
|
if (state.websocket) roles.push('WebSocketMainRole');
|
|
655
656
|
roles.push(...state.customRoles);
|
|
656
657
|
const sortedRoles = roles.sort();
|
|
@@ -668,8 +669,21 @@ async function renderIam(state) {
|
|
|
668
669
|
function getRbacValidatorServiceTags(state) {
|
|
669
670
|
const tags = new Set();
|
|
670
671
|
for (const obj of state.objects || []) {
|
|
671
|
-
|
|
672
|
-
|
|
672
|
+
const targets = obj?.belongTo
|
|
673
|
+
? Array.isArray(obj.belongTo)
|
|
674
|
+
? obj.belongTo
|
|
675
|
+
: [obj.belongTo]
|
|
676
|
+
: [];
|
|
677
|
+
for (const target of targets) {
|
|
678
|
+
if (target?.serviceTag && target.serviceTag !== 'Rbac') {
|
|
679
|
+
tags.add(target.serviceTag);
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
if (
|
|
683
|
+
obj.belongToSchema?.serviceTag &&
|
|
684
|
+
obj.belongToSchema.serviceTag !== 'Rbac'
|
|
685
|
+
) {
|
|
686
|
+
tags.add(obj.belongToSchema.serviceTag);
|
|
673
687
|
}
|
|
674
688
|
}
|
|
675
689
|
return Array.from(tags).sort();
|
|
@@ -751,7 +765,7 @@ function addStatement(statements, action, resource) {
|
|
|
751
765
|
|
|
752
766
|
function roleNeedsSchemaRead(state, roleName) {
|
|
753
767
|
if (!roleName.endsWith('Role') || !roleName) return false;
|
|
754
|
-
return roleName !== 'FlowRbacRole' || state.
|
|
768
|
+
return roleName !== 'FlowRbacRole' || state.selfRbac;
|
|
755
769
|
}
|
|
756
770
|
|
|
757
771
|
function roleTableNames(state, roleName) {
|
|
@@ -767,7 +781,7 @@ function roleTableNames(state, roleName) {
|
|
|
767
781
|
'AwaitingMultipleSteps',
|
|
768
782
|
'AwaitingMultipleStepByPending',
|
|
769
783
|
'WebSocketTask',
|
|
770
|
-
...(state.
|
|
784
|
+
...(state.selfRbac
|
|
771
785
|
? [
|
|
772
786
|
'Roles',
|
|
773
787
|
'TargetRoles',
|
|
@@ -1288,8 +1302,28 @@ function hasSelfRbacAddOn(object) {
|
|
|
1288
1302
|
);
|
|
1289
1303
|
}
|
|
1290
1304
|
|
|
1291
|
-
function hasRbacAddOn(object) {
|
|
1292
|
-
|
|
1305
|
+
function hasRbacAddOn(object, objects = []) {
|
|
1306
|
+
const selfHasRbac = hasSelfRbacAddOn(object);
|
|
1307
|
+
const belongToTargets = object?.belongTo
|
|
1308
|
+
? Array.isArray(object.belongTo)
|
|
1309
|
+
? object.belongTo
|
|
1310
|
+
: [object.belongTo]
|
|
1311
|
+
: [];
|
|
1312
|
+
const belongToHasRbac =
|
|
1313
|
+
(Array.isArray(object?.belongToSchema?.addOnDataStructure) &&
|
|
1314
|
+
object.belongToSchema.addOnDataStructure.some(
|
|
1315
|
+
(addOn) => addOn?.type === 'rbac'
|
|
1316
|
+
)) ||
|
|
1317
|
+
belongToTargets.some((target) => {
|
|
1318
|
+
if (target?.serviceTag === 'UserAccount') return true;
|
|
1319
|
+
const matched = objects.find(
|
|
1320
|
+
(candidate) =>
|
|
1321
|
+
candidate?.serviceTag === target?.serviceTag &&
|
|
1322
|
+
candidate?.objectType === target?.objectType
|
|
1323
|
+
);
|
|
1324
|
+
return hasSelfRbacAddOn(matched);
|
|
1325
|
+
});
|
|
1326
|
+
return selfHasRbac || belongToHasRbac;
|
|
1293
1327
|
}
|
|
1294
1328
|
|
|
1295
1329
|
/**
|
|
@@ -1299,7 +1333,7 @@ function hasRbacAddOn(object) {
|
|
|
1299
1333
|
*/
|
|
1300
1334
|
function rbacFlowQueueNames(state) {
|
|
1301
1335
|
const flowTags = [
|
|
1302
|
-
...new Set((state.objects ?? []).filter(
|
|
1336
|
+
...new Set((state.objects ?? []).filter(hasSelfRbacAddOn).map(rbacFlowTag))
|
|
1303
1337
|
];
|
|
1304
1338
|
return flowTags.map((flowTag) => `${flowTag}HdrSqs`);
|
|
1305
1339
|
}
|
|
@@ -106,16 +106,19 @@ test('generates RBAC own-topic WebSocket entries alongside their SQS handlers',
|
|
|
106
106
|
),
|
|
107
107
|
/ListTargetRole/
|
|
108
108
|
);
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
'app/src/generatedCode/FlowRbac/ValidatorRbac/source/ValidatorRbac_Main.js'
|
|
114
|
-
),
|
|
115
|
-
'utf8'
|
|
109
|
+
const validatorRbac = await readFile(
|
|
110
|
+
join(
|
|
111
|
+
rootPath,
|
|
112
|
+
'app/src/generatedCode/FlowRbac/ValidatorRbac/source/ValidatorRbac_Main.js'
|
|
116
113
|
),
|
|
117
|
-
|
|
114
|
+
'utf8'
|
|
118
115
|
);
|
|
116
|
+
assert.match(validatorRbac, /Fast-path triggered: user self-action/);
|
|
117
|
+
assert.match(validatorRbac, /return l1Hit;/);
|
|
118
|
+
assert.match(validatorRbac, /return l2Hit;/);
|
|
119
|
+
assert.match(validatorRbac, /return cacheResult;/);
|
|
120
|
+
assert.doesNotMatch(validatorRbac, /L1 Cache hit:[^;]+;\s*return;/);
|
|
121
|
+
assert.doesNotMatch(validatorRbac, /L2 Cache hit:[^;]+;\s*return;/);
|
|
119
122
|
assert.match(
|
|
120
123
|
await readFile(
|
|
121
124
|
join(
|
|
@@ -66,6 +66,10 @@ test('emits CRUD handlers and function YAML from schema data without reading gen
|
|
|
66
66
|
createLogicalLib,
|
|
67
67
|
/export function validateFieldNames\(\s*_izContext,\s*requestParams,\s*objectSchemas,\s*errorsFound\s*\)/
|
|
68
68
|
);
|
|
69
|
+
assert.doesNotMatch(
|
|
70
|
+
createLogicalLib,
|
|
71
|
+
/targetIdentifiers\.submittedByUserId\s*=/
|
|
72
|
+
);
|
|
69
73
|
assert.doesNotMatch(createMain, /FlowHook|beforeLogical|codeHookTag/);
|
|
70
74
|
const yaml = await readFile(
|
|
71
75
|
join(rootPath, 'app/sls_yaml/generatedCode/source/flow-objects.yml'),
|
|
@@ -637,6 +641,31 @@ test('emits async after-hook callback and SNS publish in CreateLogicalLib and fl
|
|
|
637
641
|
{ stdio: 'inherit' }
|
|
638
642
|
);
|
|
639
643
|
|
|
644
|
+
const createCompleteMain = await readFile(
|
|
645
|
+
join(
|
|
646
|
+
rootPath,
|
|
647
|
+
'app/src/generatedCode/FlowObjects/Create/source/CreateComplete_Main.js'
|
|
648
|
+
),
|
|
649
|
+
'utf8'
|
|
650
|
+
);
|
|
651
|
+
assert.match(
|
|
652
|
+
createCompleteMain,
|
|
653
|
+
/snsTopicArnByFlowSchema\(\s*_izContext,\s*'notifyAfterCreate_In'\s*\)/
|
|
654
|
+
);
|
|
655
|
+
assert.match(createCompleteMain, /CreateAfterHookCallbackHdrSqs/);
|
|
656
|
+
|
|
657
|
+
execFileSync(
|
|
658
|
+
process.execPath,
|
|
659
|
+
[
|
|
660
|
+
'--check',
|
|
661
|
+
join(
|
|
662
|
+
rootPath,
|
|
663
|
+
'app/src/generatedCode/FlowObjects/Create/source/CreateComplete_Main.js'
|
|
664
|
+
)
|
|
665
|
+
],
|
|
666
|
+
{ stdio: 'inherit' }
|
|
667
|
+
);
|
|
668
|
+
|
|
640
669
|
const yaml = await readFile(
|
|
641
670
|
join(rootPath, 'app/sls_yaml/generatedCode/source/flow-objects.yml'),
|
|
642
671
|
'utf8'
|
|
@@ -1293,6 +1322,72 @@ test('supports objType in generatedCodeConfig with string, array, and global hoo
|
|
|
1293
1322
|
assert.match(deleteMain, /hookPayload_globalDeleteHook/);
|
|
1294
1323
|
});
|
|
1295
1324
|
|
|
1325
|
+
test('omits autoProvisionOwnerTargetRole in CreateNodeCallback when service only has child RBAC objects', async () => {
|
|
1326
|
+
const rootPath = await createRbacProject(false, true);
|
|
1327
|
+
await generateCrudApplicationArtifacts(rootPath, {
|
|
1328
|
+
serviceTag: 'orders'
|
|
1329
|
+
});
|
|
1330
|
+
|
|
1331
|
+
const callbackMain = await readFile(
|
|
1332
|
+
join(
|
|
1333
|
+
rootPath,
|
|
1334
|
+
'app/src/generatedCode/FlowObjects/Create/source/CreateNodeCallback_Main.js'
|
|
1335
|
+
),
|
|
1336
|
+
'utf8'
|
|
1337
|
+
);
|
|
1338
|
+
assert.doesNotMatch(callbackMain, /autoProvisionOwnerTargetRole\(_izContext/);
|
|
1339
|
+
|
|
1340
|
+
const logicalLib = await readFile(
|
|
1341
|
+
join(
|
|
1342
|
+
rootPath,
|
|
1343
|
+
'app/src/generatedCode/FlowObjects/Create/source/CreateLogicalLib.js'
|
|
1344
|
+
),
|
|
1345
|
+
'utf8'
|
|
1346
|
+
);
|
|
1347
|
+
assert.match(logicalLib, /const ROOT_RBAC_OBJECT_TYPES = new Set\(\[\s*\]\);/);
|
|
1348
|
+
});
|
|
1349
|
+
|
|
1350
|
+
test('emits autoProvisionOwnerTargetRole in CreateNodeCallback when service has root RBAC object', async () => {
|
|
1351
|
+
const rootPath = await mkdtemp(
|
|
1352
|
+
join(tmpdir(), 'generate-service-code-v2-crud-root-rbac-')
|
|
1353
|
+
);
|
|
1354
|
+
roots.add(rootPath);
|
|
1355
|
+
const schemaRoot = join(rootPath, 'app/src/schemas');
|
|
1356
|
+
await mkdir(join(schemaRoot, 'ObjectSchemas'), { recursive: true });
|
|
1357
|
+
await mkdir(join(schemaRoot, 'FlowSchemas'), { recursive: true });
|
|
1358
|
+
await writeFile(
|
|
1359
|
+
join(schemaRoot, 'ObjectSchemas/Workspace.js'),
|
|
1360
|
+
"export default { objectType: 'workspace', generatedBy: 'user', addOnDataStructure: [{ type: 'rbac' }], storageResources: { graph: { storageType: 'graph' } } };\n"
|
|
1361
|
+
);
|
|
1362
|
+
await generateCrudApplicationArtifacts(rootPath, {
|
|
1363
|
+
serviceTag: 'workspaces'
|
|
1364
|
+
});
|
|
1365
|
+
|
|
1366
|
+
const callbackMain = await readFile(
|
|
1367
|
+
join(
|
|
1368
|
+
rootPath,
|
|
1369
|
+
'app/src/generatedCode/FlowObjects/Create/source/CreateNodeCallback_Main.js'
|
|
1370
|
+
),
|
|
1371
|
+
'utf8'
|
|
1372
|
+
);
|
|
1373
|
+
assert.match(
|
|
1374
|
+
callbackMain,
|
|
1375
|
+
/await autoProvisionOwnerTargetRole\(_izContext,\s*\{\s*node,\s*submittedByUserId,\s*objType: returnValue\?\.requestParams\?\.objType\s*\}\);/
|
|
1376
|
+
);
|
|
1377
|
+
|
|
1378
|
+
const logicalLib = await readFile(
|
|
1379
|
+
join(
|
|
1380
|
+
rootPath,
|
|
1381
|
+
'app/src/generatedCode/FlowObjects/Create/source/CreateLogicalLib.js'
|
|
1382
|
+
),
|
|
1383
|
+
'utf8'
|
|
1384
|
+
);
|
|
1385
|
+
assert.match(
|
|
1386
|
+
logicalLib,
|
|
1387
|
+
/const ROOT_RBAC_OBJECT_TYPES = new Set\(\[\s*'workspace',\s*\]\);/
|
|
1388
|
+
);
|
|
1389
|
+
});
|
|
1390
|
+
|
|
1296
1391
|
async function createProject(withCrudObject) {
|
|
1297
1392
|
const rootPath = await mkdtemp(
|
|
1298
1393
|
join(tmpdir(), 'generate-service-code-v2-crud-application-')
|
|
@@ -418,6 +418,48 @@ test('emits CreateNodeCallbackHdrSqs in FlowObjectsRole when object has graph an
|
|
|
418
418
|
assert.match(role, /CancelUsageHdrInv/);
|
|
419
419
|
});
|
|
420
420
|
|
|
421
|
+
test('emits correct permissions in FlowObjectsRole when object is child RBAC node with graph', async () => {
|
|
422
|
+
const rootPath = await createProject();
|
|
423
|
+
const schemas = join(rootPath, 'app/src/schemas');
|
|
424
|
+
await rm(join(schemas, 'ObjectSchemas/Order.js'));
|
|
425
|
+
await writeFile(
|
|
426
|
+
join(schemas, 'ObjectSchemas/ChildNode.js'),
|
|
427
|
+
"export default { objectType: 'childNode', generatedBy: 'user', belongTo: [{ serviceTag: 'UserAccount', objectType: 'user' }], identifiers: [{ type: 'partitionKey', fieldName: 'childId' }], storageResources: { main: { storageType: 'graph', tableName: 'ChildNodes' } } };\n"
|
|
428
|
+
);
|
|
429
|
+
await generateResourceArtifacts(rootPath);
|
|
430
|
+
|
|
431
|
+
const iam = await readFile(
|
|
432
|
+
join(
|
|
433
|
+
rootPath,
|
|
434
|
+
'app/sls_yaml/generatedCode/source/generate-shared-resource.yml'
|
|
435
|
+
),
|
|
436
|
+
'utf8'
|
|
437
|
+
);
|
|
438
|
+
const role = roleBlock(iam, 'FlowObjectsRole');
|
|
439
|
+
assert.match(role, /CreateNodeCallbackHdrSqs/);
|
|
440
|
+
assert.match(role, /CreateNodeCallbackHdrSqsDLQ/);
|
|
441
|
+
assert.match(role, /ValidatorRbac_In/);
|
|
442
|
+
assert.match(role, /Rbac_\$\{self:custom\.iz_stage\}_ValidatorRbac_In/);
|
|
443
|
+
assert.match(role, /UserAccount_\$\{self:custom\.iz_stage\}_ValidatorRbac_In/);
|
|
444
|
+
assert.match(role, /ConfirmUsageHdrInv/);
|
|
445
|
+
assert.match(role, /CancelUsageHdrInv/);
|
|
446
|
+
assert.match(role, /UserAccount\$\{self:custom\.iz_stage\}ValidatorRbacHdrInv/);
|
|
447
|
+
// Child node service is not RBAC provider: no TargetRoles or Roles table in DynamoDB permissions
|
|
448
|
+
assert.doesNotMatch(role, /TargetRoles/);
|
|
449
|
+
assert.doesNotMatch(role, /RolePermissions/);
|
|
450
|
+
assert.doesNotMatch(iam, /FlowRbacRole:/);
|
|
451
|
+
|
|
452
|
+
const dynamo = await readFile(
|
|
453
|
+
join(
|
|
454
|
+
rootPath,
|
|
455
|
+
'resource/sls_yaml/generatedCode/source/generated-dynamoDB-table.yml'
|
|
456
|
+
),
|
|
457
|
+
'utf8'
|
|
458
|
+
);
|
|
459
|
+
assert.doesNotMatch(dynamo, /TargetRolesTable:/);
|
|
460
|
+
assert.doesNotMatch(dynamo, /RolesTable:/);
|
|
461
|
+
});
|
|
462
|
+
|
|
421
463
|
test('emits CreateAfterHookCallbackHdrSqs in SQS resources and FlowObjectsRole when flow has after hook with ownTopic', async () => {
|
|
422
464
|
const rootPath = await createProject();
|
|
423
465
|
const schemas = join(rootPath, 'app/src/schemas');
|