@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@izara_project/izara-core-generate-service-code",
3
- "version": "1.0.89",
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 { sns } from '@izara_project/izara-core-library-external-request';
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 (!targetIdentifiers && submittedByUserId) {
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
- if (!targetIdentifiers.submittedByUserId && submittedByUserId) {
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 'Completed';
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 'Completed';
292
+ return cacheResult;
293
293
  }
294
294
 
295
295
  async function _publicMessageOut(_izContext, message) {
296
- const topicOut = snsSharedLib.snsTopicArnByFlowSchema(
297
- _izContext,
298
- process.env.iz_flowTag + '_Out'
299
- );
296
+ try {
297
+ const topicOut = snsSharedLib.snsTopicArnByFlowSchema(
298
+ _izContext,
299
+ process.env.iz_flowTag + '_Out'
300
+ );
300
301
 
301
- await sns.publishAsync(_izContext, {
302
- TopicArn: topicOut,
303
- Message: JSON.stringify(message)
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(hasSelfRbacAddOn);
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 (rbac) {
247
+ if (selfRbac) {
248
248
  const flowTags = [
249
- ...new Set(objects.filter(hasRbacAddOn).map(rbacFlowTag))
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.rbac
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.rbac) roles.push('FlowRbacRole');
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
- if (obj.belongTo?.serviceTag && obj.belongTo.serviceTag !== 'Rbac') {
672
- tags.add(obj.belongTo.serviceTag);
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.rbac;
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.rbac
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
- return hasSelfRbacAddOn(object);
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(hasRbacAddOn).map(rbacFlowTag))
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
- assert.match(
110
- await readFile(
111
- join(
112
- rootPath,
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
- /Fast-path triggered: user self-action/
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');