@izara_project/izara-core-generate-service-code 1.0.78 → 1.0.79

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 (61) hide show
  1. package/package.json +2 -2
  2. package/src/compile/flowValidator.js +163 -0
  3. package/src/compile/loadSchemas.js +15 -5
  4. package/src/emit/coreFunction/emitCoreFunctionArtifacts.js +137 -7
  5. package/src/emit/coreFunction/templates/InitialSetupConfig.ejs +132 -17
  6. package/src/emit/crud/crudActionPlan.js +82 -0
  7. package/src/emit/crud/emitCrudApplicationArtifacts.js +162 -112
  8. package/src/emit/crud/emitDynamicCrudFlowSchemas.js +43 -65
  9. package/src/emit/crud/generateDynamicCrudFlowSchemas.js +4 -1
  10. package/src/emit/crud/templates/CreateEndpoint_Main.ejs +84 -512
  11. package/src/emit/crud/templates/CreateLogicalLib.ejs +536 -0
  12. package/src/emit/crud/templates/DeleteEndpoint_Main.ejs +87 -202
  13. package/src/emit/crud/templates/DeleteLogicalLib.ejs +213 -0
  14. package/src/emit/crud/templates/FlowEndpointRbacCallback_Main.ejs +217 -0
  15. package/src/emit/crud/templates/GetEndpoint_Main.ejs +1 -1
  16. package/src/emit/crud/templates/GetRangeEndpoint_Main.ejs +9 -2
  17. package/src/emit/crud/templates/UpdateEndpoint_Main.ejs +87 -444
  18. package/src/emit/crud/templates/UpdateLogicalLib.ejs +472 -0
  19. package/src/emit/custom/customFlows.js +27 -2
  20. package/src/emit/custom/emitCustomFlowApplicationArtifacts.js +1 -2
  21. package/src/emit/custom/templates/FlowEndpoint_Main.ejs +10 -3
  22. package/src/emit/custom/templates/FlowStep_Main.ejs +1 -1
  23. package/src/emit/custom/templates/s3/CreateObject_Main.ejs +1 -1
  24. package/src/emit/custom/templates/triggerCache/CheckTriggerCache_HdrSqs.ejs +2 -2
  25. package/src/emit/rbac/emitDynamicRbacFlowSchemas.js +3 -9
  26. package/src/emit/rbac/emitRbacApplicationArtifacts.js +21 -16
  27. package/src/emit/rbac/rbacFlowTag.js +2 -7
  28. package/src/emit/rbac/templates/UserRbacFlow_Main.ejs +148 -21
  29. package/src/emit/relationship/emitDynamicRelationshipFlowSchemas.js +4 -2
  30. package/src/emit/relationship/templates/ChangeRelationship_Main.ejs +1 -1
  31. package/src/emit/relationship/templates/CreateRelationship_Main.ejs +1 -1
  32. package/src/emit/relationship/templates/DeleteRelationship_Main.ejs +1 -1
  33. package/src/emit/relationship/templates/GetRelationship_Main.ejs +1 -1
  34. package/src/emit/relationship/templates/MoveRelationship_Main.ejs +1 -1
  35. package/src/emit/relationship/templates/UpdateRelationship_Main.ejs +1 -1
  36. package/src/emit/resource/emitResourceArtifacts.js +83 -68
  37. package/src/emit/resource/templates/GeneratedLambdaRoleServiceConfig.ejs +31 -5
  38. package/src/emit/resource/templates/SystemDynamoDB_Yaml.ejs +21 -0
  39. package/src/emit/resource/templates/WebSocketSubscription_Yaml.ejs +7 -0
  40. package/src/emit/shared/templates/FlowHandler_HdrApi.ejs +1 -1
  41. package/src/emit/shared/templates/FlowHandler_HdrDsq.ejs +2 -2
  42. package/src/emit/shared/templates/FlowHandler_HdrInv.ejs +1 -1
  43. package/src/emit/shared/templates/FlowHandler_HdrSqs.ejs +2 -2
  44. package/src/emit/systemText/emitSystemTextSchemas.js +246 -0
  45. package/src/emit/systemText/templates/SystemTextSchemas.ejs +10 -0
  46. package/src/generate.js +55 -8
  47. package/test/crudApplicationArtifacts.test.js +147 -19
  48. package/test/customFlowApplicationArtifacts.test.js +308 -16
  49. package/test/dynamicCrudFlowSchemas.test.js +150 -14
  50. package/test/dynamicRbacFlowSchemas.test.js +2 -2
  51. package/test/dynamicRelationshipFlowSchemas.test.js +1 -1
  52. package/test/graphDrivenFlowSchema.test.js +257 -0
  53. package/test/publicApi.test.js +14 -5
  54. package/test/resourceArtifacts.test.js +25 -19
  55. package/test/systemTextSchemas.test.js +154 -0
  56. package/src/emit/crud/templates/CreateComplete_HdrSqs.ejs +0 -99
  57. package/src/emit/crud/templates/DeleteComplete_HdrSqs.ejs +0 -99
  58. package/src/emit/crud/templates/FlowEndpointBeforeLogical_Main.ejs +0 -174
  59. package/src/emit/crud/templates/FlowEndpointComplete_Main.ejs +0 -187
  60. package/src/emit/crud/templates/UpdateComplete_HdrSqs.ejs +0 -99
  61. package/src/emit/resource/templates/ResourceBeforeLogicalYaml.ejs +0 -47
@@ -0,0 +1,246 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import ejsLib from 'ejs';
3
+ import { rbacFlowTag } from '../rbac/rbacFlowTag.js';
4
+
5
+ const TEMPLATE_URL = new URL('./templates/', import.meta.url);
6
+ const SYSTEM_TEXT_SCHEMA_PATH =
7
+ 'app/src/schemas/SystemTextSchemas/generatedCode/SystemTextSchemas.js';
8
+
9
+ const CRUD_ACTIONS = ['Create', 'Update', 'Delete', 'Get'];
10
+ const RELATIONSHIP_ACTIONS = [
11
+ 'CreateRelationship',
12
+ 'UpdateRelationship',
13
+ 'DeleteRelationship',
14
+ 'GetRelationship',
15
+ 'ChangeRelationship',
16
+ 'MoveRelationship'
17
+ ];
18
+ const RBAC_SHARED_FLOW_TAGS = [
19
+ 'CreateTargetRole',
20
+ 'ListTargetRole',
21
+ 'DeleteTargetRole',
22
+ 'CreateRolePermissions',
23
+ 'ListRolePermissions',
24
+ 'DeleteRolePermissions',
25
+ 'CreateUserRole',
26
+ 'ListUserInRoles',
27
+ 'DeleteUserFromRole'
28
+ ];
29
+
30
+ export function systemTextSchemaManagedPaths() {
31
+ return [SYSTEM_TEXT_SCHEMA_PATH];
32
+ }
33
+
34
+ /**
35
+ * Emits SystemTextSchemas.js artifact based on objects, relationships, flows, and notifications
36
+ *
37
+ * @param {Object} params
38
+ * @param {Object} params.project
39
+ * @param {Array} [params.objects=[]]
40
+ * @param {Array} [params.relationships=[]]
41
+ * @param {Array} [params.flows=[]]
42
+ * @param {Array} [params.notifications=[]]
43
+ * @returns {Promise<Array<{path: string, content: string}>>}
44
+ */
45
+ export async function emitSystemTextSchemas({
46
+ project = {},
47
+ objects = [],
48
+ relationships = [],
49
+ flows = [],
50
+ notifications = []
51
+ } = {}) {
52
+ const safeObjects = Array.isArray(objects) ? objects : [];
53
+ const safeRelationships = Array.isArray(relationships) ? relationships : [];
54
+ const safeFlows = Array.isArray(flows) ? flows : [];
55
+ const safeNotifications = Array.isArray(notifications) ? notifications : [];
56
+
57
+ if (
58
+ safeObjects.length === 0 &&
59
+ safeRelationships.length === 0 &&
60
+ safeFlows.length === 0 &&
61
+ safeNotifications.length === 0
62
+ ) {
63
+ return [];
64
+ }
65
+
66
+ const serviceTag = project?.serviceTag || '';
67
+ const datas = [];
68
+
69
+ // 1. Process Objects
70
+ for (const object of safeObjects) {
71
+ const objectType = object?.objectType;
72
+ if (!objectType) {
73
+ continue;
74
+ }
75
+ const objServiceTag = object?.serviceTag || serviceTag;
76
+ const fieldNames = Object.keys(object?.fieldNames || {});
77
+
78
+ if (Array.isArray(object?.addOnDataStructure)) {
79
+ for (const addOn of object.addOnDataStructure) {
80
+ if (addOn?.type === 'versionedData' && addOn?.fieldNames) {
81
+ fieldNames.push(...Object.keys(addOn.fieldNames));
82
+ }
83
+ }
84
+ }
85
+
86
+ for (const fieldName of fieldNames) {
87
+ const nameSpace = `${objectType}_${objServiceTag}_description`;
88
+ const systemTextTag = `${fieldName}_${objectType}_${objServiceTag}`;
89
+ const openHook = `//(<${objectType}_${objServiceTag}${fieldName}Hook>)`;
90
+ const closeHook = `//(</${objectType}_${objServiceTag}${fieldName}Hook>)`;
91
+ datas.push({
92
+ nameSpace,
93
+ systemTextTag,
94
+ openHook,
95
+ closeHook
96
+ });
97
+ }
98
+ }
99
+
100
+ // 2. Process Notifications
101
+ for (const notification of safeNotifications) {
102
+ if (notification?.notificationGrouping && notification?.notificationTag) {
103
+ const nameSpace = `sysNotification_${notification.notificationGrouping}`;
104
+ const systemTextTag = notification.notificationTag;
105
+ const openHook = `//(<${notification.notificationTag}Hook>)`;
106
+ const closeHook = `//(</${notification.notificationTag}Hook>)`;
107
+ datas.push({
108
+ nameSpace,
109
+ systemTextTag,
110
+ openHook,
111
+ closeHook
112
+ });
113
+ }
114
+ }
115
+
116
+ // 3. Process Relationships
117
+ for (const rel of safeRelationships) {
118
+ let relTag = rel?.relationshipTag;
119
+ if (!relTag && typeof rel === 'object' && rel !== null) {
120
+ relTag = Object.keys(rel)[0];
121
+ }
122
+ if (!relTag) {
123
+ continue;
124
+ }
125
+ const relData = rel[relTag] || rel;
126
+ const relServiceTag = relData?.serviceTag || serviceTag;
127
+
128
+ if (relData?.fieldNames && Object.keys(relData.fieldNames).length > 0) {
129
+ for (const fieldName of Object.keys(relData.fieldNames)) {
130
+ const nameSpace = `${relTag}_${relServiceTag}_description`;
131
+ const systemTextTag = `${fieldName}_${relTag}_${relServiceTag}`;
132
+ const openHook = `//(<${relTag}_${relServiceTag}${fieldName}Hook>)`;
133
+ const closeHook = `//(</${relTag}_${relServiceTag}${fieldName}Hook>)`;
134
+ datas.push({
135
+ nameSpace,
136
+ systemTextTag,
137
+ openHook,
138
+ closeHook
139
+ });
140
+ }
141
+ }
142
+ }
143
+
144
+ // 4. Process Flows (Custom flows + Dynamic flows)
145
+ const flowTags = [];
146
+ const seenFlowTags = new Set();
147
+
148
+ for (const flow of safeFlows) {
149
+ const tag = flow?.flowTag || flow;
150
+ if (typeof tag === 'string' && tag.trim() !== '' && !seenFlowTags.has(tag)) {
151
+ seenFlowTags.add(tag);
152
+ flowTags.push(tag);
153
+ }
154
+ }
155
+
156
+ const dynamicFlowTags = [];
157
+
158
+ // Dynamic RBAC flows
159
+ const rbacObjects = safeObjects.filter(_hasRbacAddOn);
160
+ if (rbacObjects.length > 0) {
161
+ for (const rbacObj of rbacObjects) {
162
+ const tag = rbacFlowTag(rbacObj);
163
+ if (!seenFlowTags.has(tag) && !dynamicFlowTags.includes(tag)) {
164
+ dynamicFlowTags.push(tag);
165
+ }
166
+ }
167
+ const hasSelfRbac = safeObjects.some(_hasSelfRbacAddOn);
168
+ if (hasSelfRbac) {
169
+ for (const tag of RBAC_SHARED_FLOW_TAGS) {
170
+ if (!seenFlowTags.has(tag) && !dynamicFlowTags.includes(tag)) {
171
+ dynamicFlowTags.push(tag);
172
+ }
173
+ }
174
+ }
175
+ }
176
+
177
+ // Dynamic CRUD flows
178
+ const hasCrudObjects = safeObjects.some(
179
+ (object) =>
180
+ object?.generatedBy === 'userGenerated' ||
181
+ object?.generatedBy === 'systemGenerated'
182
+ );
183
+ if (hasCrudObjects) {
184
+ for (const action of CRUD_ACTIONS) {
185
+ if (!seenFlowTags.has(action) && !dynamicFlowTags.includes(action)) {
186
+ dynamicFlowTags.push(action);
187
+ }
188
+ }
189
+ }
190
+
191
+ // Dynamic Relationship flows
192
+ if (safeRelationships.length > 0) {
193
+ for (const action of RELATIONSHIP_ACTIONS) {
194
+ if (!seenFlowTags.has(action) && !dynamicFlowTags.includes(action)) {
195
+ dynamicFlowTags.push(action);
196
+ }
197
+ }
198
+ }
199
+
200
+ // Sort dynamic flows in descending alphabetical order to match legacy contract
201
+ dynamicFlowTags.sort((a, b) => b.localeCompare(a));
202
+
203
+ for (const tag of dynamicFlowTags) {
204
+ if (!seenFlowTags.has(tag)) {
205
+ seenFlowTags.add(tag);
206
+ flowTags.push(tag);
207
+ }
208
+ }
209
+
210
+ for (const flowTag of flowTags) {
211
+ const nameSpace = `${flowTag}_${serviceTag}`;
212
+ const systemTextTag = `errors_${flowTag}_${serviceTag}`;
213
+ const openHook = `//(<${flowTag}_${serviceTag}Hook>)`;
214
+ const closeHook = `//(</${flowTag}_${serviceTag}Hook>)`;
215
+ datas.push({
216
+ nameSpace,
217
+ systemTextTag,
218
+ openHook,
219
+ closeHook
220
+ });
221
+ }
222
+
223
+ const templateContent = await readFile(
224
+ new URL('SystemTextSchemas.ejs', TEMPLATE_URL),
225
+ 'utf8'
226
+ );
227
+ const content = ejsLib.render(templateContent, { datas });
228
+
229
+ return [
230
+ {
231
+ path: SYSTEM_TEXT_SCHEMA_PATH,
232
+ content
233
+ }
234
+ ];
235
+ }
236
+
237
+ function _hasSelfRbacAddOn(object) {
238
+ if (Array.isArray(object?.addOnDataStructure)) {
239
+ return object.addOnDataStructure.some((addOn) => addOn?.type === 'rbac');
240
+ }
241
+ return false;
242
+ }
243
+
244
+ function _hasRbacAddOn(object) {
245
+ return _hasSelfRbacAddOn(object);
246
+ }
@@ -0,0 +1,10 @@
1
+ export default [
2
+ <% datas.forEach((data, index) => { -%>
3
+ {
4
+ nameSpace: "<%- data.nameSpace %>",
5
+ systemTextTag: "<%- data.systemTextTag %>",
6
+ <%- data.openHook %>
7
+ <%- data.closeHook %>
8
+ }<%= index < datas.length - 1 ? ',' : '' %>
9
+ <% }); -%>
10
+ ];
package/src/generate.js CHANGED
@@ -51,6 +51,10 @@ import {
51
51
  emitCoreFunctionArtifacts,
52
52
  coreFunctionManagedPaths
53
53
  } from './emit/coreFunction/emitCoreFunctionArtifacts.js';
54
+ import {
55
+ emitSystemTextSchemas,
56
+ systemTextSchemaManagedPaths
57
+ } from './emit/systemText/emitSystemTextSchemas.js';
54
58
 
55
59
  export async function generate(rootPath, options = {}) {
56
60
  const startedAt = Date.now();
@@ -58,10 +62,11 @@ export async function generate(rootPath, options = {}) {
58
62
  logger?.info?.(`[INFO] [Generate] STATUS=STARTED | rootPath=${rootPath}`);
59
63
  try {
60
64
  const project = compileProject({ ...options, rootPath });
61
- const [rawObjects, relationships, flows] = await Promise.all([
65
+ const [rawObjects, relationships, flows, notifications] = await Promise.all([
62
66
  loadLocalSchemaDirectory(project, 'ObjectSchemas'),
63
67
  loadLocalSchemaDirectory(project, 'RelationshipSchemas'),
64
- loadLocalSchemaDirectory(project, 'FlowSchemas')
68
+ loadLocalSchemaDirectory(project, 'FlowSchemas'),
69
+ loadLocalSchemaDirectory(project, 'NotificationSchemas')
65
70
  ]);
66
71
  const schemaBucketName =
67
72
  options.schemaBucketName || process.env.iz_serviceSchemaBucketName;
@@ -85,6 +90,7 @@ export async function generate(rootPath, options = {}) {
85
90
  objects,
86
91
  relationships,
87
92
  flows,
93
+ notifications,
88
94
  objectGraphServiceTags,
89
95
  relationshipGraphServiceTags
90
96
  });
@@ -120,8 +126,25 @@ export async function generate(rootPath, options = {}) {
120
126
  }
121
127
  }
122
128
 
123
- async function _emitGroups({ project, objects, relationships, flows, objectGraphServiceTags, relationshipGraphServiceTags }) {
124
- const [crud, relationship, rbac, system, findData, resource, coreFunction] = await Promise.all([
129
+ async function _emitGroups({
130
+ project,
131
+ objects,
132
+ relationships,
133
+ flows,
134
+ notifications,
135
+ objectGraphServiceTags,
136
+ relationshipGraphServiceTags
137
+ }) {
138
+ const [
139
+ crud,
140
+ relationship,
141
+ rbac,
142
+ system,
143
+ findData,
144
+ resource,
145
+ coreFunction,
146
+ systemText
147
+ ] = await Promise.all([
125
148
  emitCrudApplicationArtifacts({
126
149
  objects,
127
150
  flows,
@@ -133,10 +156,26 @@ async function _emitGroups({ project, objects, relationships, flows, objectGraph
133
156
  serviceTag: project.serviceTag
134
157
  }),
135
158
  emitRbacApplicationArtifacts(objects),
136
- emitSystemWebSocketArtifacts(flows, { includeRelationshipOwnTopic: true }),
159
+ emitSystemWebSocketArtifacts(flows, {
160
+ includeRelationshipOwnTopic: true
161
+ }),
137
162
  emitSystemFindDataArtifacts(objects),
138
163
  emitResourceArtifacts({ objects, relationships, flows }),
139
- emitCoreFunctionArtifacts({ project, objects, relationships, flows, objectGraphServiceTags, relationshipGraphServiceTags })
164
+ emitCoreFunctionArtifacts({
165
+ project,
166
+ objects,
167
+ relationships,
168
+ flows,
169
+ objectGraphServiceTags,
170
+ relationshipGraphServiceTags
171
+ }),
172
+ emitSystemTextSchemas({
173
+ project,
174
+ objects,
175
+ relationships,
176
+ flows,
177
+ notifications
178
+ })
140
179
  ]);
141
180
  const custom = await emitCustomFlowApplicationArtifacts(flows, {
142
181
  objects
@@ -158,11 +197,14 @@ async function _emitGroups({ project, objects, relationships, flows, objectGraph
158
197
 
159
198
  let resolvedCoreFunctionManagedPaths = coreFunctionManagedPaths;
160
199
  if (typeof coreFunctionManagedPaths === 'function') {
161
- resolvedCoreFunctionManagedPaths = coreFunctionManagedPaths(
200
+ resolvedCoreFunctionManagedPaths = coreFunctionManagedPaths({
201
+ project,
162
202
  objects,
203
+ relationships,
204
+ flows,
163
205
  objectGraphServiceTags,
164
206
  relationshipGraphServiceTags
165
- );
207
+ });
166
208
  }
167
209
 
168
210
  return [
@@ -223,6 +265,11 @@ async function _emitGroups({ project, objects, relationships, flows, objectGraph
223
265
  scope: 'core-function',
224
266
  managedPaths: resolvedCoreFunctionManagedPaths,
225
267
  artifacts: coreFunction
268
+ },
269
+ {
270
+ scope: 'system-text-schemas',
271
+ managedPaths: systemTextSchemaManagedPaths(),
272
+ artifacts: systemText
226
273
  }
227
274
  ];
228
275
  }
@@ -22,7 +22,7 @@ test('emits CRUD handlers and function YAML from schema data without reading gen
22
22
  serviceTag: 'orders'
23
23
  });
24
24
 
25
- assert.equal(result.files.length, 26);
25
+ assert.equal(result.files.length, 25);
26
26
  assert.ok(result.files.every((file) => !file.path.includes('/Process')));
27
27
  assert.match(
28
28
  await readFile(
@@ -34,16 +34,15 @@ test('emits CRUD handlers and function YAML from schema data without reading gen
34
34
  ),
35
35
  /dataFromGraphV2/
36
36
  );
37
- assert.match(
38
- await readFile(
37
+ const createMain = await readFile(
39
38
  join(
40
39
  rootPath,
41
40
  'app/src/generatedCode/FlowObjects/Create/source/Create_Main.js'
42
41
  ),
43
42
  'utf8'
44
- ),
45
- /beforeLogical_order/
46
- );
43
+ );
44
+ assert.match(createMain, /CreateLogicalLib/);
45
+ assert.doesNotMatch(createMain, /FlowHook|beforeLogical|codeHookTag/);
47
46
  const yaml = await readFile(
48
47
  join(rootPath, 'app/sls_yaml/generatedCode/source/flow-objects.yml'),
49
48
  'utf8'
@@ -53,14 +52,16 @@ test('emits CRUD handlers and function YAML from schema data without reading gen
53
52
  assert.doesNotMatch(yaml, /handler: .*\/Process/);
54
53
  assert.match(yaml, /path: \/orders\/order\/get/);
55
54
  assert.match(yaml, /GetHdrInv:/);
55
+ assert.doesNotMatch(yaml, /GetRangeHdrApi:/);
56
+ assert.match(yaml, /UpdateCompleteHdrSqs:/);
57
+ assert.match(yaml, /#<#UpdateCompleteHdrSqsFunctionSetting#>/);
56
58
  assert.match(yaml, /CreateCompleteHdrSqs:/);
57
- assert.match(yaml, /#<#CreateCompleteHdrSqsFunctionSetting#>/);
58
- assert.match(yaml, /filterPatterns:[\s\S]*CreateCompleteHdrSqs/);
59
+ assert.doesNotMatch(yaml, /FlowHook|RbacCallbackAndSendFlowHook/);
59
60
  assert.match(
60
61
  await readFile(
61
62
  join(
62
63
  rootPath,
63
- 'app/src/generatedCode/FlowObjects/Create/source/CreateComplete_Main.js'
64
+ 'app/src/generatedCode/FlowObjects/Update/source/UpdateComplete_Main.js'
64
65
  ),
65
66
  'utf8'
66
67
  ),
@@ -84,7 +85,7 @@ test('emits CRUD handlers and function YAML from schema data without reading gen
84
85
  )
85
86
  )
86
87
  );
87
- for (const action of ['Create', 'Update', 'Delete']) {
88
+ for (const action of ['Update', 'Delete']) {
88
89
  execFileSync(
89
90
  process.execPath,
90
91
  [
@@ -120,10 +121,10 @@ test('preserves function-setting hooks across CRUD regeneration', async () => {
120
121
  await generateCrudApplicationArtifacts(rootPath, { serviceTag: 'orders' });
121
122
 
122
123
  const hookCode =
123
- ' #<#CreateCompleteHdrSqsFunctionSetting#>\n memorySize: 512\n #<#/CreateCompleteHdrSqsFunctionSetting#>\n';
124
+ ' #<#UpdateCompleteHdrSqsFunctionSetting#>\n memorySize: 512\n #<#/UpdateCompleteHdrSqsFunctionSetting#>\n';
124
125
  let yaml = await readFile(yamlPath, 'utf8');
125
126
  const withHook = yaml.replace(
126
- / #<#CreateCompleteHdrSqsFunctionSetting#>[\s\S]*?#<#\/CreateCompleteHdrSqsFunctionSetting#>/,
127
+ / #<#UpdateCompleteHdrSqsFunctionSetting#>[\s\S]*?#<#\/UpdateCompleteHdrSqsFunctionSetting#>/,
127
128
  hookCode.trim()
128
129
  );
129
130
  assert.notEqual(yaml, withHook, 'hook placeholder must exist in generated YAML');
@@ -134,13 +135,13 @@ test('preserves function-setting hooks across CRUD regeneration', async () => {
134
135
  yaml = await readFile(yamlPath, 'utf8');
135
136
  assert.match(
136
137
  yaml,
137
- /#<#CreateCompleteHdrSqsFunctionSetting#>\n memorySize: 512\n #<#\/CreateCompleteHdrSqsFunctionSetting#>/
138
+ /#<#UpdateCompleteHdrSqsFunctionSetting#>\n memorySize: 512\n #<#\/UpdateCompleteHdrSqsFunctionSetting#>/
138
139
  );
139
140
 
140
141
  const customFilterHook =
141
- ' #<#CreateCompleteHdrSqsFilterPatterns#>\n filterPatterns:\n - body: custom\n #<#/CreateCompleteHdrSqsFilterPatterns#>';
142
+ ' #<#UpdateCompleteHdrSqsFilterPatterns#>\n filterPatterns:\n - body: custom\n #<#/UpdateCompleteHdrSqsFilterPatterns#>';
142
143
  const withFilterHook = yaml.replace(
143
- / #<#CreateCompleteHdrSqsFilterPatterns#>[\s\S]*?#<#\/CreateCompleteHdrSqsFilterPatterns#>/,
144
+ / #<#UpdateCompleteHdrSqsFilterPatterns#>[\s\S]*?#<#\/UpdateCompleteHdrSqsFilterPatterns#>/,
144
145
  customFilterHook
145
146
  );
146
147
  assert.notEqual(
@@ -155,7 +156,7 @@ test('preserves function-setting hooks across CRUD regeneration', async () => {
155
156
  yaml = await readFile(yamlPath, 'utf8');
156
157
  assert.match(
157
158
  yaml,
158
- /#<#CreateCompleteHdrSqsFilterPatterns#>\n filterPatterns:\n - body: custom\n #<#\/CreateCompleteHdrSqsFilterPatterns#>/
159
+ /#<#UpdateCompleteHdrSqsFilterPatterns#>\n filterPatterns:\n - body: custom\n #<#\/UpdateCompleteHdrSqsFilterPatterns#>/
159
160
  );
160
161
 
161
162
  const conflictPath = join(
@@ -172,6 +173,116 @@ test('preserves function-setting hooks across CRUD regeneration', async () => {
172
173
  assert.equal(await readFile(yamlPath, 'utf8'), yaml);
173
174
  });
174
175
 
176
+ test('does not add RBAC handlers to Create', async () => {
177
+ const rootPath = await createRbacProject();
178
+ const result = await generateCrudApplicationArtifacts(rootPath, {
179
+ serviceTag: 'orders'
180
+ });
181
+ const createPaths = result.files
182
+ .map((file) => file.path)
183
+ .filter((path) => path.includes('/FlowObjects/Create/source/'));
184
+
185
+ assert.ok(createPaths.includes(
186
+ 'app/src/generatedCode/FlowObjects/Create/source/Create_Main.js'
187
+ ));
188
+ assert.ok(!createPaths.includes(
189
+ 'app/src/generatedCode/FlowObjects/Create/source/CreateComplete_Main.js'
190
+ ));
191
+ assert.ok(
192
+ !createPaths.some((path) => path.includes('CreateRbacCallback'))
193
+ );
194
+ assert.doesNotMatch(
195
+ await readFile(
196
+ join(
197
+ rootPath,
198
+ 'app/src/generatedCode/FlowObjects/Create/source/Create_Main.js'
199
+ ),
200
+ 'utf8'
201
+ ),
202
+ /checkRbacPermission/
203
+ );
204
+ });
205
+
206
+ test('emits a single Complete RBAC handler for Update without Graph', async () => {
207
+ const rootPath = await createRbacProject();
208
+ const result = await generateCrudApplicationArtifacts(rootPath, {
209
+ serviceTag: 'orders'
210
+ });
211
+ const updatePaths = result.files
212
+ .map((file) => file.path)
213
+ .filter((path) => path.includes('/FlowObjects/Update/source/'));
214
+
215
+ assert.ok(updatePaths.includes(
216
+ 'app/src/generatedCode/FlowObjects/Update/source/UpdateComplete_Main.js'
217
+ ));
218
+ assert.ok(!updatePaths.some((path) => path.includes('UpdateRbacCallback')));
219
+ assert.match(
220
+ await readFile(
221
+ join(
222
+ rootPath,
223
+ 'app/src/generatedCode/FlowObjects/Update/source/UpdateComplete_Main.js'
224
+ ),
225
+ 'utf8'
226
+ ),
227
+ /UpdateLogicalLib/
228
+ );
229
+ });
230
+
231
+ test('emits RbacCallback and Complete handlers for Update with Graph', async () => {
232
+ const rootPath = await createRbacProject(false, true);
233
+ const result = await generateCrudApplicationArtifacts(rootPath, {
234
+ serviceTag: 'orders'
235
+ });
236
+ const updatePaths = result.files
237
+ .map((file) => file.path)
238
+ .filter((path) => path.includes('/FlowObjects/Update/source/'));
239
+
240
+ assert.ok(updatePaths.includes(
241
+ 'app/src/generatedCode/FlowObjects/Update/source/UpdateRbacCallback_Main.js'
242
+ ));
243
+ assert.ok(updatePaths.includes(
244
+ 'app/src/generatedCode/FlowObjects/Update/source/UpdateComplete_Main.js'
245
+ ));
246
+ execFileSync(
247
+ process.execPath,
248
+ [
249
+ '--check',
250
+ join(
251
+ rootPath,
252
+ 'app/src/generatedCode/FlowObjects/Update/source/UpdateRbacCallback_Main.js'
253
+ )
254
+ ],
255
+ { stdio: 'inherit' }
256
+ );
257
+ });
258
+
259
+ test('rejects deprecated FlowHook schemas', async () => {
260
+ const rootPath = await createRbacProject(true);
261
+
262
+ await assert.rejects(
263
+ generateCrudApplicationArtifacts(rootPath, { serviceTag: 'orders' }),
264
+ /generatedCodeConfig\.codeHookTag is no longer supported/
265
+ );
266
+ });
267
+
268
+ test('emits GetRangeHdrApi when an object defines a sortKey identifier', async () => {
269
+ const rootPath = await createProject(true);
270
+ await writeFile(
271
+ join(rootPath, 'app/src/schemas/ObjectSchemas/Order.js'),
272
+ "export default { objectType: 'order', generatedBy: 'userGenerated', identifiers: [{ type: 'identifier', fieldName: 'orderId' }, { type: 'sortKey', fieldName: 'createdAt' }], storageResources: { graph: { storageType: 'graph' } } };\n"
273
+ );
274
+ const result = await generateCrudApplicationArtifacts(rootPath, {
275
+ serviceTag: 'orders'
276
+ });
277
+
278
+ assert.equal(result.files.length, 27);
279
+ const yaml = await readFile(
280
+ join(rootPath, 'app/sls_yaml/generatedCode/source/flow-objects.yml'),
281
+ 'utf8'
282
+ );
283
+ assert.match(yaml, /GetRangeHdrApi:/);
284
+ assert.match(yaml, /path: \/orders\/order\/getRange/);
285
+ });
175
286
 
176
287
  test('removes managed CRUD artifacts when no applicable object remains', async () => {
177
288
  const rootPath = await createProject(true);
@@ -219,12 +330,29 @@ async function createProject(withCrudObject) {
219
330
  await writeFile(
220
331
  join(schemaRoot, 'ObjectSchemas/Order.js'),
221
332
  withCrudObject
222
- ? "export default { objectType: 'order', generatedBy: 'userGenerated' };\n"
333
+ ? "export default { objectType: 'order', generatedBy: 'userGenerated', storageResources: { graph: { storageType: 'graph' } } };\n"
223
334
  : "export default { objectType: 'order' };\n"
224
335
  );
336
+ return rootPath;
337
+ }
338
+
339
+ async function createRbacProject(withHook = false, withGraph = false) {
340
+ const rootPath = await mkdtemp(
341
+ join(tmpdir(), 'generate-service-code-v2-crud-rbac-artifacts-')
342
+ );
343
+ roots.add(rootPath);
344
+ const schemaRoot = join(rootPath, 'app/src/schemas');
345
+ await mkdir(join(schemaRoot, 'ObjectSchemas'), { recursive: true });
346
+ await mkdir(join(schemaRoot, 'FlowSchemas'), { recursive: true });
225
347
  await writeFile(
226
- join(schemaRoot, 'FlowSchemas/OrderBeforeLogical.js'),
227
- "export default { flowTag: 'orderHook', event: ['ownTopic'], generatedCodeConfig: { generatedCodeTag: 'create', codeHookTag: 'beforeLogical', invocationType: 'ownTopic', objType: { objectType: 'order' } } };\n"
348
+ join(schemaRoot, 'ObjectSchemas/Order.js'),
349
+ `export default { objectType: 'order', generatedBy: 'userGenerated', belongTo: { serviceTag: 'UserAccount', objectType: 'user' }${withGraph ? ", storageResources: { graph: { storageType: 'graph' } }" : ''} };\n`
228
350
  );
351
+ if (withHook) {
352
+ await writeFile(
353
+ join(schemaRoot, 'FlowSchemas/OrderBeforeLogical.js'),
354
+ "export default { flowTag: 'orderHook', event: ['ownTopic'], generatedCodeConfig: { generatedCodeTag: 'create', codeHookTag: 'beforeLogical', invocationType: 'ownTopic', objType: { objectType: 'order' } } };\n"
355
+ );
356
+ }
229
357
  return rootPath;
230
358
  }