@izara_project/izara-core-generate-service-code 1.0.83 → 1.0.84

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 (59) hide show
  1. package/package.json +4 -4
  2. package/src/compile/flowValidator.js +13 -2
  3. package/src/compile/loadSchemas.js +116 -7
  4. package/src/emit/attributeTree/emitAttributeTreeSchemas.js +388 -0
  5. package/src/emit/coreFunction/emitCoreFunctionArtifacts.js +29 -1
  6. package/src/emit/coreFunction/templates/InitialSetupConfig.ejs +16 -3
  7. package/src/emit/crud/crudActionPlan.js +38 -5
  8. package/src/emit/crud/emitCrudApplicationArtifacts.js +19 -4
  9. package/src/emit/crud/emitDynamicCrudFlowSchemas.js +27 -6
  10. package/src/emit/crud/generateCrudApplicationArtifacts.js +3 -2
  11. package/src/emit/crud/generateDynamicCrudFlowSchemas.js +3 -2
  12. package/src/emit/crud/loadLocalSchemaArrays.js +3 -3
  13. package/src/emit/crud/templates/CreateComplete_Main.ejs +1 -1
  14. package/src/emit/crud/templates/CreateEndpoint_Main.ejs +50 -19
  15. package/src/emit/crud/templates/CreateLogicalLib.ejs +73 -8
  16. package/src/emit/crud/templates/DeleteComplete_Main.ejs +1 -1
  17. package/src/emit/crud/templates/DeleteEndpoint_Main.ejs +33 -8
  18. package/src/emit/crud/templates/DeleteLogicalLib.ejs +6 -3
  19. package/src/emit/crud/templates/FlowEndpointRbacCallback_Main.ejs +2 -2
  20. package/src/emit/crud/templates/FlowEndpoint_HdrWbs.ejs +7 -0
  21. package/src/emit/crud/templates/FlowMain_Wbs.ejs +1 -1
  22. package/src/emit/crud/templates/GetEndpoint_Main.ejs +13 -0
  23. package/src/emit/crud/templates/UpdateComplete_Main.ejs +1 -1
  24. package/src/emit/crud/templates/UpdateEndpoint_Main.ejs +52 -3
  25. package/src/emit/crud/templates/UpdateLogicalLib.ejs +4 -3
  26. package/src/emit/custom/customFlows.js +47 -0
  27. package/src/emit/custom/emitCustomFlowApplicationArtifacts.js +168 -49
  28. package/src/emit/custom/generateCustomFlowApplicationArtifacts.js +1 -1
  29. package/src/emit/custom/templates/FlowEndpointRbacCallback_Main.ejs +194 -0
  30. package/src/emit/custom/templates/FlowEndpoint_Main.ejs +14 -22
  31. package/src/emit/custom/templates/FlowStep_Main.ejs +1 -1
  32. package/src/emit/function/templates/WbsFunction_Yaml.ejs +0 -2
  33. package/src/emit/rbac/emitDynamicRbacFlowSchemas.js +33 -8
  34. package/src/emit/rbac/emitRbacApplicationArtifacts.js +204 -7
  35. package/src/emit/rbac/templates/CancelUsage_Main.ejs +103 -0
  36. package/src/emit/rbac/templates/ConfirmUsage_Main.ejs +71 -0
  37. package/src/emit/rbac/templates/CreateTargetRole_Main.ejs +49 -6
  38. package/src/emit/rbac/templates/ListRbac_Main.ejs +9 -3
  39. package/src/emit/rbac/templates/RbacMutation_Main.ejs +1 -1
  40. package/src/emit/rbac/templates/ReservedDynamicUsage_Main.ejs +147 -0
  41. package/src/emit/rbac/templates/UserRbacFlow_Main.ejs +59 -23
  42. package/src/emit/rbac/templates/UserRoleMutation_Main.ejs +1 -1
  43. package/src/emit/relationship/emitDynamicRelationshipFlowSchemas.js +1 -1
  44. package/src/emit/relationship/templates/ChangeRelationship_Main.ejs +4 -4
  45. package/src/emit/resource/emitResourceArtifacts.js +342 -108
  46. package/src/emit/resource/templates/SystemDynamoDB_Yaml.ejs +53 -2
  47. package/src/emit/system/emitSystemWebSocketArtifacts.js +6 -5
  48. package/src/emit/system/templates/WebSocketConnect.ejs +71 -12
  49. package/src/emit/system/templates/WebSocketConnect_Yaml.ejs +7 -0
  50. package/src/emit/systemText/emitSystemTextSchemas.js +1 -1
  51. package/src/generate.js +41 -10
  52. package/test/attributeTreeSchemas.test.js +219 -0
  53. package/test/createTargetRoleHandler.test.js +59 -4
  54. package/test/crudApplicationArtifacts.test.js +20 -3
  55. package/test/customFlowApplicationArtifacts.test.js +5 -5
  56. package/test/dynamicCrudFlowSchemas.test.js +91 -4
  57. package/test/dynamicRbacFlowSchemas.test.js +15 -2
  58. package/test/publicApi.test.js +6 -6
  59. package/test/resourceArtifacts.test.js +68 -27
@@ -14,9 +14,9 @@ async function _renderSystemTemplate(templateName, data = {}) {
14
14
 
15
15
  export async function emitSystemWebSocketArtifacts(
16
16
  flows,
17
- { includeRelationshipOwnTopic = false } = {}
17
+ { includeRelationshipOwnTopic = false, objects = [], relationships = [] } = {}
18
18
  ) {
19
- if (!hasSystemWebSocket(flows, { includeRelationshipOwnTopic })) {
19
+ if (!hasSystemWebSocket(flows, { includeRelationshipOwnTopic, objects, relationships })) {
20
20
  return { artifacts: [], functions: [] };
21
21
  }
22
22
 
@@ -115,8 +115,10 @@ export function systemWebSocketManagedPaths() {
115
115
 
116
116
  export function hasSystemWebSocket(
117
117
  flows,
118
- { includeRelationshipOwnTopic = false } = {}
118
+ { includeRelationshipOwnTopic = false, objects = [], relationships = [] } = {}
119
119
  ) {
120
+ if (Array.isArray(objects) && objects.length > 0) return true;
121
+ if (Array.isArray(relationships) && relationships.length > 0) return true;
120
122
  return (
121
123
  Array.isArray(flows) &&
122
124
  flows.some(
@@ -125,8 +127,7 @@ export function hasSystemWebSocket(
125
127
  (flow.event.includes('s3') ||
126
128
  (includeRelationshipOwnTopic &&
127
129
  flow.event.includes('ownTopic') &&
128
- (flow.initiateBy === 'user' ||
129
- flow.initiateBy === 'systemWithUser')))
130
+ (flow.initiateBy === 'user' || flow.initiateBy === 'systemWithUser')))
130
131
  )
131
132
  );
132
133
  }
@@ -1,45 +1,104 @@
1
1
  import dynamodbSharedLib from '@izara_project/izara-core-library-dynamodb';
2
2
  import {
3
3
  middlewareHandler as middleware,
4
+ consts,
4
5
  response
5
6
  } from '@izara_project/izara-middleware';
6
7
 
7
8
  export const main = middleware.wrap(async (event) => {
8
9
  const { eventType, connectionId } = event.requestContext ?? {};
9
10
  try {
10
- event._izContext.logger.debug('Event:', event);
11
+ event._izContext.logger.debug('[Lib:WebSocketMain:webSocketConnect] Event:', event);
11
12
  if (eventType === 'CONNECT') {
12
- const { userId, submittedByUserId } = event.queryStringParameters ?? {};
13
+ const fromCorrelationIds =
14
+ event._izContext?.correlationIds?.get(consts.SUBMITTED_BY_USER_ID);
15
+ const fromAuthorizerSubmittedBy =
16
+ event.requestContext?.authorizer?.submittedByUserId;
17
+ const fromAuthorizerPrincipal =
18
+ event.requestContext?.authorizer?.principalId;
19
+ const fromQueryString =
20
+ event.queryStringParameters?.submittedByUserId;
21
+
22
+ const submittedByUserId =
23
+ fromCorrelationIds ||
24
+ fromAuthorizerSubmittedBy ||
25
+ fromAuthorizerPrincipal ||
26
+ fromQueryString;
27
+
28
+ event._izContext.logger.debug(
29
+ '[Lib:WebSocketMain:webSocketConnect] submittedByUserId sources:',
30
+ {
31
+ connectionId,
32
+ fromCorrelationIds,
33
+ fromAuthorizerSubmittedBy,
34
+ fromAuthorizerPrincipal,
35
+ fromQueryString,
36
+ resolvedSubmittedByUserId: submittedByUserId,
37
+ authorizer: event.requestContext?.authorizer
38
+ }
39
+ );
40
+
41
+ const tableName = await dynamodbSharedLib.tableName(
42
+ event._izContext,
43
+ 'UserUploadRecords'
44
+ );
45
+ event._izContext.logger.debug(
46
+ '[Lib:WebSocketMain:webSocketConnect] Saving connection to table:',
47
+ {
48
+ tableName,
49
+ connectionId,
50
+ submittedByUserId
51
+ }
52
+ );
53
+
13
54
  await dynamodbSharedLib.putItem(
14
55
  event._izContext,
15
- await dynamodbSharedLib.tableName(
16
- event._izContext,
17
- 'UserUploadRecords'
18
- ),
56
+ tableName,
19
57
  {
20
58
  connectionId,
21
- userId,
22
59
  submittedByUserId,
23
60
  expirationTime: Math.floor(Date.now() / 1000) + 7200
24
61
  }
25
62
  );
63
+ event._izContext.logger.debug(
64
+ '[Lib:WebSocketMain:webSocketConnect] Successfully stored connection record'
65
+ );
26
66
  //(<webSocketConnect>)
27
67
  //(</webSocketConnect>)
28
68
  } else if (eventType === 'DISCONNECT') {
69
+ const tableName = await dynamodbSharedLib.tableName(
70
+ event._izContext,
71
+ 'UserUploadRecords'
72
+ );
73
+ event._izContext.logger.debug(
74
+ '[Lib:WebSocketMain:webSocketConnect] WebSocket DISCONNECT:',
75
+ {
76
+ tableName,
77
+ connectionId
78
+ }
79
+ );
80
+
29
81
  await dynamodbSharedLib.deleteItem(
30
82
  event._izContext,
31
- await dynamodbSharedLib.tableName(
32
- event._izContext,
33
- 'UserUploadRecords'
34
- ),
83
+ tableName,
35
84
  { connectionId }
36
85
  );
86
+ event._izContext.logger.debug(
87
+ '[Lib:WebSocketMain:webSocketConnect] Successfully deleted connection record'
88
+ );
37
89
  //(<webSocketDisconnect>)
38
90
  //(</webSocketDisconnect>)
39
91
  }
92
+
93
+ event._izContext.logger.debug(
94
+ '[Lib:WebSocketMain:webSocketConnect] WebSocket event processed successfully'
95
+ );
40
96
  return response.webSocketSuccess();
41
97
  } catch (error) {
42
- event._izContext.logger.error('Error, WebSocketConnect: ', error);
98
+ event._izContext.logger.error(
99
+ '[Lib:WebSocketMain:webSocketConnect] Error: ',
100
+ error
101
+ );
43
102
  return response.failure(error);
44
103
  }
45
104
  });
@@ -5,6 +5,13 @@ WebSocketConnect:
5
5
  events:
6
6
  - websocket:
7
7
  route: $connect
8
+ #<#WebSocketConnectAuthorizer#>
9
+ authorizer:
10
+ arn: arn:aws:lambda:${self:custom.iz_region}:${self:custom.iz_accountId}:function:UserAccount${self:custom.iz_stage}UserIdentityAuthorizer
11
+ identitySource:
12
+ - 'route.request.header.Authorization'
13
+ - 'route.request.querystring.access_token'
14
+ #<#/WebSocketConnectAuthorizer#>
8
15
  - websocket:
9
16
  route: $disconnect
10
17
  - websocket:
@@ -179,7 +179,7 @@ export async function emitSystemTextSchemas({
179
179
  (object) =>
180
180
  object?.generatedBy === 'user' ||
181
181
  object?.generatedBy === 'systemWithUser' ||
182
- object?.generatedBy === 'systemWithNoUser'
182
+ object?.generatedBy === 'systemNoUser'
183
183
  );
184
184
  if (hasCrudObjects) {
185
185
  for (const action of CRUD_ACTIONS) {
package/src/generate.js CHANGED
@@ -55,6 +55,12 @@ import {
55
55
  emitSystemTextSchemas,
56
56
  systemTextSchemaManagedPaths
57
57
  } from './emit/systemText/emitSystemTextSchemas.js';
58
+ import {
59
+ emitAttributeTreeObjectSchemas,
60
+ emitAttributeTreeRelationshipSchemas,
61
+ attributeTreeObjectSchemaManagedPaths,
62
+ attributeTreeRelationshipSchemaManagedPaths
63
+ } from './emit/attributeTree/emitAttributeTreeSchemas.js';
58
64
 
59
65
  export async function generate(rootPath, options = {}) {
60
66
  const startedAt = Date.now();
@@ -62,11 +68,15 @@ export async function generate(rootPath, options = {}) {
62
68
  logger?.info?.(`[INFO] [Generate] STATUS=STARTED | rootPath=${rootPath}`);
63
69
  try {
64
70
  const project = compileProject({ ...options, rootPath });
71
+ const schemaLoadOptions = {
72
+ logger,
73
+ skipValidation: options.skipValidation
74
+ };
65
75
  const [rawObjects, relationships, flows, notifications] = await Promise.all([
66
- loadLocalSchemaDirectory(project, 'ObjectSchemas'),
67
- loadLocalSchemaDirectory(project, 'RelationshipSchemas'),
68
- loadLocalSchemaDirectory(project, 'FlowSchemas'),
69
- loadLocalSchemaDirectory(project, 'NotificationSchemas')
76
+ loadLocalSchemaDirectory(project, 'ObjectSchemas', schemaLoadOptions),
77
+ loadLocalSchemaDirectory(project, 'RelationshipSchemas', schemaLoadOptions),
78
+ loadLocalSchemaDirectory(project, 'FlowSchemas', schemaLoadOptions),
79
+ loadLocalSchemaDirectory(project, 'NotificationSchemas', schemaLoadOptions)
70
80
  ]);
71
81
  const schemaBucketName =
72
82
  options.schemaBucketName || process.env.iz_serviceSchemaBucketName;
@@ -92,7 +102,8 @@ export async function generate(rootPath, options = {}) {
92
102
  flows,
93
103
  notifications,
94
104
  objectGraphServiceTags,
95
- relationshipGraphServiceTags
105
+ relationshipGraphServiceTags,
106
+ logger
96
107
  });
97
108
  logger?.debug?.(
98
109
  `[DEBUG] [Generate] STATUS=ARTIFACTS_EMITTED | ${groups.map((group) => `${group.scope}=${group.artifacts.length}`).join(' ')}`
@@ -133,7 +144,8 @@ async function _emitGroups({
133
144
  flows,
134
145
  notifications,
135
146
  objectGraphServiceTags,
136
- relationshipGraphServiceTags
147
+ relationshipGraphServiceTags,
148
+ logger
137
149
  }) {
138
150
  const [
139
151
  crud,
@@ -148,7 +160,8 @@ async function _emitGroups({
148
160
  emitCrudApplicationArtifacts({
149
161
  objects,
150
162
  flows,
151
- serviceTag: project.serviceTag
163
+ serviceTag: project.serviceTag,
164
+ logger
152
165
  }),
153
166
  emitRelationshipApplicationArtifacts({
154
167
  relationships,
@@ -157,7 +170,9 @@ async function _emitGroups({
157
170
  }),
158
171
  emitRbacApplicationArtifacts(objects),
159
172
  emitSystemWebSocketArtifacts(flows, {
160
- includeRelationshipOwnTopic: true
173
+ includeRelationshipOwnTopic: true,
174
+ objects,
175
+ relationships
161
176
  }),
162
177
  emitSystemFindDataArtifacts(objects),
163
178
  emitResourceArtifacts({ objects, relationships, flows }),
@@ -211,7 +226,7 @@ async function _emitGroups({
211
226
  {
212
227
  scope: 'crud-flow-schemas',
213
228
  managedPaths: dynamicCrudFlowSchemaPaths,
214
- artifacts: emitDynamicCrudFlowSchemas({ objects, flows })
229
+ artifacts: emitDynamicCrudFlowSchemas({ objects, flows, logger })
215
230
  },
216
231
  {
217
232
  scope: 'relationship-flow-schemas',
@@ -226,6 +241,22 @@ async function _emitGroups({
226
241
  serviceTag: project.serviceTag
227
242
  })
228
243
  },
244
+ {
245
+ scope: 'attribute-tree-object-schemas',
246
+ managedPaths: attributeTreeObjectSchemaManagedPaths(objects),
247
+ artifacts: emitAttributeTreeObjectSchemas({
248
+ objects,
249
+ serviceTag: project.serviceTag
250
+ })
251
+ },
252
+ {
253
+ scope: 'attribute-tree-relationship-schemas',
254
+ managedPaths: attributeTreeRelationshipSchemaManagedPaths(objects),
255
+ artifacts: emitAttributeTreeRelationshipSchemas({
256
+ objects,
257
+ serviceTag: project.serviceTag
258
+ })
259
+ },
229
260
  {
230
261
  scope: 'crud-application',
231
262
  managedPaths: crudApplicationManagedPaths(),
@@ -253,7 +284,7 @@ async function _emitGroups({
253
284
  },
254
285
  {
255
286
  scope: 'custom-flow-application',
256
- managedPaths: customFlowApplicationManagedPaths(flows),
287
+ managedPaths: customFlowApplicationManagedPaths(flows, { objects }),
257
288
  artifacts: custom
258
289
  },
259
290
  {
@@ -0,0 +1,219 @@
1
+ import assert from 'node:assert/strict';
2
+ import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
3
+ import { tmpdir } from 'node:os';
4
+ import { join } from 'node:path';
5
+ import test, { after } from 'node:test';
6
+
7
+ import {
8
+ emitAttributeTreeObjectSchemas,
9
+ emitAttributeTreeRelationshipSchemas,
10
+ attributeTreeObjectSchemaManagedPaths,
11
+ attributeTreeRelationshipSchemaManagedPaths
12
+ } from '../src/emit/attributeTree/emitAttributeTreeSchemas.js';
13
+ import { generate } from '../src/generate.js';
14
+
15
+ const projectRoots = new Set();
16
+
17
+ after(async () => {
18
+ await Promise.all(
19
+ [...projectRoots].map((rootPath) =>
20
+ rm(rootPath, { recursive: true, force: true })
21
+ )
22
+ );
23
+ });
24
+
25
+ test('emitAttributeTreeObjectSchemas returns 4 object schemas per attributeTree tag', () => {
26
+ const objects = [
27
+ {
28
+ objectType: 'sellOffer',
29
+ serviceTag: 'SellOfferManager',
30
+ storageResources: {
31
+ dynamoDB: {
32
+ storageType: 'dynamoDB',
33
+ tableName: 'SellOfferRecords'
34
+ },
35
+ myGraph: {
36
+ storageType: 'graph',
37
+ graphServerTag: 'GraphHandler'
38
+ }
39
+ },
40
+ addOnDataStructure: [
41
+ {
42
+ type: 'attributeTree',
43
+ attributeTreeTag: 'sellOfferTerm'
44
+ }
45
+ ]
46
+ }
47
+ ];
48
+
49
+ const artifacts = emitAttributeTreeObjectSchemas({ objects });
50
+ assert.equal(artifacts.length, 4);
51
+
52
+ const paths = artifacts.map((a) => a.path);
53
+ assert.deepEqual(paths, [
54
+ 'app/src/schemas/ObjectSchemas/generatedAttributeTree/sellOfferTermLink.js',
55
+ 'app/src/schemas/ObjectSchemas/generatedAttributeTree/attributeSellOfferTermValue.js',
56
+ 'app/src/schemas/ObjectSchemas/generatedAttributeTree/sellOfferTermLabel.js',
57
+ 'app/src/schemas/ObjectSchemas/generatedAttributeTree/sellOfferTermValueStandard.js'
58
+ ]);
59
+
60
+ for (const artifact of artifacts) {
61
+ assert.doesNotMatch(artifact.content, /dynamoDB/);
62
+ assert.match(artifact.content, /GraphHandler/);
63
+ }
64
+ });
65
+
66
+ test('emitAttributeTreeRelationshipSchemas returns 4 relationship schemas per attributeTree tag', () => {
67
+ const objects = [
68
+ {
69
+ objectType: 'sellOffer',
70
+ serviceTag: 'SellOfferManager',
71
+ storageResources: {
72
+ dynamoDB: {
73
+ storageType: 'dynamoDB',
74
+ tableName: 'SellOfferRecords'
75
+ },
76
+ myGraph: {
77
+ storageType: 'graph',
78
+ graphServerTag: 'GraphHandler'
79
+ }
80
+ },
81
+ addOnDataStructure: [
82
+ {
83
+ type: 'attributeTree',
84
+ attributeTreeTag: 'sellOfferTerm'
85
+ }
86
+ ]
87
+ }
88
+ ];
89
+
90
+ const artifacts = emitAttributeTreeRelationshipSchemas({
91
+ objects,
92
+ serviceTag: 'SellOfferManager'
93
+ });
94
+ assert.equal(artifacts.length, 4);
95
+
96
+ const paths = artifacts.map((a) => a.path);
97
+ assert.deepEqual(paths, [
98
+ 'app/src/schemas/RelationshipSchemas/generatedAttributeTree/hasSellOfferTermLink.js',
99
+ 'app/src/schemas/RelationshipSchemas/generatedAttributeTree/disabledSellOfferTermLink.js',
100
+ 'app/src/schemas/RelationshipSchemas/generatedAttributeTree/isSellOfferTerm.js',
101
+ 'app/src/schemas/RelationshipSchemas/generatedAttributeTree/isSellOfferTermLabel.js'
102
+ ]);
103
+
104
+ for (const artifact of artifacts) {
105
+ assert.doesNotMatch(artifact.content, /dynamoDB/);
106
+ assert.match(artifact.content, /GraphHandler/);
107
+ }
108
+ });
109
+
110
+ test('managed paths return empty array when no attributeTree is present', () => {
111
+ const objects = [{ objectType: 'user', addOnDataStructure: [] }];
112
+ assert.deepEqual(attributeTreeObjectSchemaManagedPaths(objects), []);
113
+ assert.deepEqual(attributeTreeRelationshipSchemaManagedPaths(objects), []);
114
+ assert.deepEqual(emitAttributeTreeObjectSchemas({ objects }), []);
115
+ assert.deepEqual(emitAttributeTreeRelationshipSchemas({ objects }), []);
116
+ });
117
+
118
+ test('generate commits and cleans up attributeTree dynamic schemas', async () => {
119
+ const rootPath = await createProjectWithAttributeTree({ hasAddOn: true });
120
+ const result = await generate(rootPath);
121
+
122
+ const expectedObjectPath = join(
123
+ rootPath,
124
+ 'app/src/schemas/ObjectSchemas/generatedAttributeTree/sellOfferTermLink.js'
125
+ );
126
+ const expectedRelPath = join(
127
+ rootPath,
128
+ 'app/src/schemas/RelationshipSchemas/generatedAttributeTree/hasSellOfferTermLink.js'
129
+ );
130
+
131
+ const objectContent = await readFile(expectedObjectPath, 'utf8');
132
+ assert.match(objectContent, /sellOfferTermLink/);
133
+ assert.match(objectContent, /GraphHandler/);
134
+ assert.doesNotMatch(objectContent, /dynamoDB/);
135
+
136
+ const relContent = await readFile(expectedRelPath, 'utf8');
137
+ assert.match(relContent, /hasSellOfferTermLink/);
138
+ assert.match(relContent, /sellOfferTermLink/);
139
+
140
+ // Re-run without addOn to verify stale file cleanup
141
+ await createProjectWithAttributeTree({ rootPath, hasAddOn: false });
142
+ await generate(rootPath);
143
+
144
+ await assert.rejects(readFile(expectedObjectPath, 'utf8'), {
145
+ code: 'ENOENT'
146
+ });
147
+ await assert.rejects(readFile(expectedRelPath, 'utf8'), {
148
+ code: 'ENOENT'
149
+ });
150
+ });
151
+
152
+ async function createProjectWithAttributeTree({
153
+ rootPath: existingRoot,
154
+ hasAddOn = true
155
+ } = {}) {
156
+ const rootPath =
157
+ existingRoot || (await mkdtemp(join(tmpdir(), 'izara-attr-tree-test-')));
158
+ projectRoots.add(rootPath);
159
+
160
+ const schemaRoot = join(rootPath, 'app/src/schemas');
161
+ await mkdir(join(schemaRoot, 'ObjectSchemas'), { recursive: true });
162
+ await mkdir(join(schemaRoot, 'RelationshipSchemas'), { recursive: true });
163
+ await mkdir(join(schemaRoot, 'FlowSchemas'), { recursive: true });
164
+ await mkdir(join(rootPath, 'config'), { recursive: true });
165
+
166
+ await writeFile(
167
+ join(rootPath, 'config/serverless.config.yml'),
168
+ 'service: SellOfferManager\ncustom:\n serviceTag: SellOfferManager\n'
169
+ );
170
+
171
+ const addOnContent = hasAddOn
172
+ ? `
173
+ addOnDataStructure: [
174
+ {
175
+ type: 'attributeTree',
176
+ attributeTreeTag: 'sellOfferTerm'
177
+ }
178
+ ],`
179
+ : ' addOnDataStructure: [],';
180
+
181
+ await writeFile(
182
+ join(schemaRoot, 'ObjectSchemas/SellOffer.js'),
183
+ `export default [
184
+ {
185
+ objectType: 'sellOffer',
186
+ canDelete: false,
187
+ generatedBy: 'user',${addOnContent}
188
+ storageResources: {
189
+ dynamoDB: {
190
+ storageType: 'dynamoDB',
191
+ tableName: 'SellOfferRecords',
192
+ serviceTag: 'SellOfferManager'
193
+ },
194
+ myGraph: {
195
+ storageType: 'graph',
196
+ graphServerTag: 'GraphHandler'
197
+ }
198
+ },
199
+ fieldNames: {
200
+ sellOfferId: {
201
+ type: 'string',
202
+ randomOnCreate: true,
203
+ canUpdate: false,
204
+ storageResourceTags: ['dynamoDB', 'myGraph']
205
+ }
206
+ },
207
+ identifiers: [
208
+ {
209
+ type: 'partitionKey',
210
+ fieldName: 'sellOfferId'
211
+ }
212
+ ]
213
+ }
214
+ ];
215
+ `
216
+ );
217
+
218
+ return rootPath;
219
+ }
@@ -13,8 +13,22 @@ const OWN_TOPIC_FLOWS = [
13
13
  'DeleteRolePermissions',
14
14
  'DeleteTargetRole',
15
15
  'CreateUserRole',
16
- 'DeleteUserFromRole'
16
+ 'DeleteUserFromRole',
17
+ 'CreateRoleLimit',
18
+ 'DeleteRoleLimit',
19
+ 'ReservedDynamicUsage',
20
+ 'ConfirmUsage',
21
+ 'CancelUsage'
17
22
  ];
23
+
24
+ const LIMIT_FLOWS = [
25
+ 'CreateRoleLimit',
26
+ 'DeleteRoleLimit',
27
+ 'ReservedDynamicUsage',
28
+ 'ConfirmUsage',
29
+ 'CancelUsage'
30
+ ];
31
+
18
32
  after(() =>
19
33
  Promise.all(
20
34
  [...roots].map((root) => rm(root, { recursive: true, force: true }))
@@ -25,11 +39,15 @@ test('generates RBAC own-topic WebSocket entries alongside their SQS handlers',
25
39
  const rootPath = await createProject(true);
26
40
  const result = await generateCreateTargetRoleHandler(rootPath);
27
41
 
28
- assert.equal(result.files.length, 36);
42
+ assert.equal(result.files.length, 60);
29
43
  assert.match(
30
44
  await readFile(join(rootPath, result.files[0].path), 'utf8'),
31
45
  /CreateTargetRole/
32
46
  );
47
+ assert.match(
48
+ await readFile(join(rootPath, result.files[0].path), 'utf8'),
49
+ /roleName = 'owner'/
50
+ );
33
51
  assert.match(
34
52
  await readFile(join(rootPath, result.files[1].path), 'utf8'),
35
53
  /CreateTargetRoleHdrSqs/
@@ -51,16 +69,53 @@ test('generates RBAC own-topic WebSocket entries alongside their SQS handlers',
51
69
  ),
52
70
  /ListTargetRole/
53
71
  );
54
- const yaml = await readFile(
72
+ assert.match(
73
+ await readFile(
74
+ join(
75
+ rootPath,
76
+ 'app/src/generatedCode/FlowRbac/ValidatorRbac/source/ValidatorRbac_Main.js'
77
+ ),
78
+ 'utf8'
79
+ ),
80
+ /Fast-path triggered: user self-action/
81
+ );
82
+ assert.match(
83
+ await readFile(
84
+ join(
85
+ rootPath,
86
+ 'app/src/generatedCode/FlowLimit/ReservedDynamicUsage/source/ReservedDynamicUsage_Main.js'
87
+ ),
88
+ 'utf8'
89
+ ),
90
+ /Math\.min\(maxIncrease, minDecrease\)/
91
+ );
92
+ assert.match(
93
+ await readFile(
94
+ join(
95
+ rootPath,
96
+ 'app/src/generatedCode/FlowLimit/CreateRoleLimit/source/CreateRoleLimit_Main.js'
97
+ ),
98
+ 'utf8'
99
+ ),
100
+ /actionType/
101
+ );
102
+ const rbacYaml = await readFile(
55
103
  join(rootPath, 'app/sls_yaml/generatedCode/source/flow-rbac.yml'),
56
104
  'utf8'
57
105
  );
106
+ const limitYaml = await readFile(
107
+ join(rootPath, 'app/sls_yaml/generatedCode/source/flow-limits.yml'),
108
+ 'utf8'
109
+ );
58
110
  for (const flowTag of OWN_TOPIC_FLOWS) {
111
+ const isLimit = LIMIT_FLOWS.includes(flowTag);
112
+ const folder = isLimit ? 'FlowLimit' : 'FlowRbac';
113
+ const yaml = isLimit ? limitYaml : rbacYaml;
59
114
  assert.match(
60
115
  await readFile(
61
116
  join(
62
117
  rootPath,
63
- `app/src/generatedCode/FlowRbac/${flowTag}/source/${flowTag}_HdrWbs.js`
118
+ `app/src/generatedCode/${folder}/${flowTag}/source/${flowTag}_HdrWbs.js`
64
119
  ),
65
120
  'utf8'
66
121
  ),
@@ -184,7 +184,7 @@ test('preserves function-setting hooks across CRUD regeneration', async () => {
184
184
  assert.equal(await readFile(yamlPath, 'utf8'), yaml);
185
185
  });
186
186
 
187
- test('does not add RBAC handlers to Create', async () => {
187
+ test('emits a single Complete RBAC handler for Create without Graph', async () => {
188
188
  const rootPath = await createRbacProject();
189
189
  const result = await generateCrudApplicationArtifacts(rootPath, {
190
190
  serviceTag: 'orders'
@@ -196,13 +196,13 @@ test('does not add RBAC handlers to Create', async () => {
196
196
  assert.ok(createPaths.includes(
197
197
  'app/src/generatedCode/FlowObjects/Create/source/Create_Main.js'
198
198
  ));
199
- assert.ok(!createPaths.includes(
199
+ assert.ok(createPaths.includes(
200
200
  'app/src/generatedCode/FlowObjects/Create/source/CreateComplete_Main.js'
201
201
  ));
202
202
  assert.ok(
203
203
  !createPaths.some((path) => path.includes('CreateRbacCallback'))
204
204
  );
205
- assert.doesNotMatch(
205
+ assert.match(
206
206
  await readFile(
207
207
  join(
208
208
  rootPath,
@@ -341,6 +341,23 @@ test('removes managed CRUD artifacts when no applicable object remains', async (
341
341
  );
342
342
  });
343
343
 
344
+ test('logs warning when ObjectSchema has invalid or non-CRUD generatedBy in crudApplicationArtifacts', async () => {
345
+ const rootPath = await createProject(false);
346
+ const warnings = [];
347
+ const logger = {
348
+ warn: (msg) => warnings.push(msg)
349
+ };
350
+
351
+ const result = await generateCrudApplicationArtifacts(rootPath, { logger });
352
+ assert.deepEqual(result.files, []);
353
+ assert.equal(warnings.length, 1);
354
+ assert.match(
355
+ warnings[0],
356
+ /\[WARN\] \[CRUD Application\] Skipped FlowObjects application artifacts generation/
357
+ );
358
+ });
359
+
360
+
344
361
  async function createProject(withCrudObject) {
345
362
  const rootPath = await mkdtemp(
346
363
  join(tmpdir(), 'generate-service-code-v2-crud-application-')