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

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 (63) 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 +34 -1
  6. package/src/emit/coreFunction/templates/InitialSetupConfig.ejs +29 -7
  7. package/src/emit/crud/crudActionPlan.js +41 -6
  8. package/src/emit/crud/emitCrudApplicationArtifacts.js +56 -8
  9. package/src/emit/crud/emitDynamicCrudFlowSchemas.js +153 -27
  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/CreateConfirmComplete_Main.ejs +54 -0
  15. package/src/emit/crud/templates/CreateEndpoint_Main.ejs +252 -154
  16. package/src/emit/crud/templates/CreateLogicalLib.ejs +1163 -236
  17. package/src/emit/crud/templates/CreateNodeCallback_Main.ejs +221 -0
  18. package/src/emit/crud/templates/DeleteComplete_Main.ejs +1 -1
  19. package/src/emit/crud/templates/DeleteEndpoint_Main.ejs +33 -8
  20. package/src/emit/crud/templates/DeleteLogicalLib.ejs +6 -3
  21. package/src/emit/crud/templates/FlowEndpointRbacCallback_Main.ejs +20 -120
  22. package/src/emit/crud/templates/FlowEndpoint_HdrWbs.ejs +79 -10
  23. package/src/emit/crud/templates/FlowMain_Wbs.ejs +31 -35
  24. package/src/emit/crud/templates/GetEndpoint_Main.ejs +13 -0
  25. package/src/emit/crud/templates/UpdateComplete_Main.ejs +1 -1
  26. package/src/emit/crud/templates/UpdateEndpoint_Main.ejs +52 -3
  27. package/src/emit/crud/templates/UpdateLogicalLib.ejs +4 -3
  28. package/src/emit/custom/customFlows.js +47 -0
  29. package/src/emit/custom/emitCustomFlowApplicationArtifacts.js +168 -49
  30. package/src/emit/custom/generateCustomFlowApplicationArtifacts.js +1 -1
  31. package/src/emit/custom/templates/FlowEndpointRbacCallback_Main.ejs +194 -0
  32. package/src/emit/custom/templates/FlowEndpoint_Main.ejs +14 -22
  33. package/src/emit/custom/templates/FlowStep_Main.ejs +1 -1
  34. package/src/emit/function/templates/WbsFunction_Yaml.ejs +0 -2
  35. package/src/emit/rbac/emitDynamicRbacFlowSchemas.js +33 -8
  36. package/src/emit/rbac/emitRbacApplicationArtifacts.js +204 -7
  37. package/src/emit/rbac/templates/CancelUsage_Main.ejs +163 -0
  38. package/src/emit/rbac/templates/ConfirmUsage_Main.ejs +187 -0
  39. package/src/emit/rbac/templates/CreateTargetRole_Main.ejs +68 -6
  40. package/src/emit/rbac/templates/ListRbac_Main.ejs +9 -3
  41. package/src/emit/rbac/templates/RbacMutation_Main.ejs +1 -1
  42. package/src/emit/rbac/templates/ReservedDynamicUsage_Main.ejs +166 -0
  43. package/src/emit/rbac/templates/UserRbacFlow_Main.ejs +84 -23
  44. package/src/emit/rbac/templates/UserRoleMutation_Main.ejs +11 -8
  45. package/src/emit/relationship/emitDynamicRelationshipFlowSchemas.js +1 -1
  46. package/src/emit/relationship/templates/ChangeRelationship_Main.ejs +4 -4
  47. package/src/emit/resource/emitResourceArtifacts.js +351 -108
  48. package/src/emit/resource/templates/GeneratedLambdaRoleServiceConfig.ejs +3 -1
  49. package/src/emit/resource/templates/SystemDynamoDB_Yaml.ejs +55 -2
  50. package/src/emit/shared/templates/FlowHandler_HdrSqs.ejs +1 -1
  51. package/src/emit/system/emitSystemWebSocketArtifacts.js +6 -5
  52. package/src/emit/system/templates/WebSocketConnect.ejs +71 -12
  53. package/src/emit/system/templates/WebSocketConnect_Yaml.ejs +6 -0
  54. package/src/emit/systemText/emitSystemTextSchemas.js +1 -1
  55. package/src/generate.js +41 -10
  56. package/test/attributeTreeSchemas.test.js +219 -0
  57. package/test/createTargetRoleHandler.test.js +59 -4
  58. package/test/crudApplicationArtifacts.test.js +20 -3
  59. package/test/customFlowApplicationArtifacts.test.js +5 -5
  60. package/test/dynamicCrudFlowSchemas.test.js +107 -6
  61. package/test/dynamicRbacFlowSchemas.test.js +15 -2
  62. package/test/publicApi.test.js +6 -6
  63. package/test/resourceArtifacts.test.js +89 -27
@@ -144,12 +144,12 @@ Resources:
144
144
  TableName: ${self:custom.iz_resourcePrefix}TargetRoles
145
145
  BillingMode: PAY_PER_REQUEST
146
146
  AttributeDefinitions:
147
- - AttributeName: targetId
147
+ - AttributeName: identifiersConcat
148
148
  AttributeType: S
149
149
  - AttributeName: roleIdKey
150
150
  AttributeType: S
151
151
  KeySchema:
152
- - AttributeName: targetId
152
+ - AttributeName: identifiersConcat
153
153
  KeyType: HASH
154
154
  - AttributeName: roleIdKey
155
155
  KeyType: RANGE
@@ -220,6 +220,59 @@ Resources:
220
220
  TimeToLiveSpecification:
221
221
  AttributeName: ttlTimestamp
222
222
  Enabled: true
223
+
224
+ RoleLimitTable:
225
+ Type: 'AWS::DynamoDB::Table'
226
+ Properties:
227
+ TableName: ${self:custom.iz_resourcePrefix}RoleLimit
228
+ BillingMode: PAY_PER_REQUEST
229
+ AttributeDefinitions:
230
+ - AttributeName: roleLimitId
231
+ AttributeType: S
232
+ - AttributeName: limitType
233
+ AttributeType: S
234
+ KeySchema:
235
+ - AttributeName: roleLimitId
236
+ KeyType: HASH
237
+ - AttributeName: limitType
238
+ KeyType: RANGE
239
+
240
+ RunningTotalUsageTable:
241
+ Type: 'AWS::DynamoDB::Table'
242
+ Properties:
243
+ TableName: ${self:custom.iz_resourcePrefix}RunningTotalUsage
244
+ BillingMode: PAY_PER_REQUEST
245
+ AttributeDefinitions:
246
+ - AttributeName: identifiersConcat
247
+ AttributeType: S
248
+ - AttributeName: limitTag
249
+ AttributeType: S
250
+ KeySchema:
251
+ - AttributeName: identifiersConcat
252
+ KeyType: HASH
253
+ - AttributeName: limitTag
254
+ KeyType: RANGE
255
+
256
+ RunningTotalReservedUsageTable:
257
+ Type: 'AWS::DynamoDB::Table'
258
+ Properties:
259
+ TableName: ${self:custom.iz_resourcePrefix}RunningTotalReservedUsage
260
+ BillingMode: PAY_PER_REQUEST
261
+ AttributeDefinitions:
262
+ - AttributeName: runningTotalReservedId
263
+ AttributeType: S
264
+ - AttributeName: usedTimestampId
265
+ AttributeType: S
266
+ KeySchema:
267
+ - AttributeName: runningTotalReservedId
268
+ KeyType: HASH
269
+ - AttributeName: usedTimestampId
270
+ KeyType: RANGE
271
+ TimeToLiveSpecification:
272
+ AttributeName: ttlTimestamp
273
+ Enabled: true
274
+ StreamSpecification:
275
+ StreamViewType: NEW_AND_OLD_IMAGES
223
276
  <% } %>
224
277
  <% if (hasS3) { %>
225
278
  ReservedDataMainTable:
@@ -88,7 +88,7 @@ export const main = middleware.wrap(async (event, context) => {
88
88
 
89
89
  // add argument (to invoke lambda) to passOnProperties[]
90
90
  passOnProperties.push(record.body.Message);
91
- <% if (typeof usePassBack === 'undefined' ? functionName.endsWith('Complete') : usePassBack) { %>
91
+ <% if (typeof usePassBack === 'undefined' ? (functionName.endsWith('Complete') || functionName.endsWith('CompleteMain') || functionName.includes('Callback')) : usePassBack) { %>
92
92
  passOnProperties.push(
93
93
  callingFlowSharedLib.addPassBackPropertiesToPassOnProperties(
94
94
  record.body.Message
@@ -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,12 @@ 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.querystring.access_token'
13
+ #<#/WebSocketConnectAuthorizer#>
8
14
  - websocket:
9
15
  route: $disconnect
10
16
  - 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
  ),