@izara_project/izara-core-generate-service-code 1.0.102 → 1.0.104

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 (35) hide show
  1. package/package.json +1 -1
  2. package/src/compile/loadSchemas.js +47 -0
  3. package/src/compile/loadSchemasRemote.js +38 -4
  4. package/src/compile/schemaValidatorSetup.js +3 -0
  5. package/src/emit/application/coreFunction/templates/InitialSetupRunner_uploadSchema.ejs +7 -0
  6. package/src/emit/application/coreFunction/templates/UploadSchema_Main.ejs +7 -0
  7. package/src/emit/application/crud/emitCrudApplicationArtifacts.js +13 -0
  8. package/src/emit/application/crud/emitDynamicCrudFlowSchemas.js +13 -0
  9. package/src/emit/application/crud/templates/CreateEndpoint_Main.ejs +21 -0
  10. package/src/emit/application/rbac/templates/CancelUsage_Main.ejs +7 -6
  11. package/src/emit/application/rbac/templates/ConfirmUsage_Main.ejs +7 -6
  12. package/src/emit/application/rbac/templates/CreateTargetRole_Main.ejs +2 -2
  13. package/src/emit/application/rbac/templates/DeleteTargetRole_Main.ejs +2 -2
  14. package/src/emit/application/rbac/templates/GetMyPermissions_Main.ejs +2 -2
  15. package/src/emit/application/rbac/templates/GetTargetRole_Main.ejs +2 -2
  16. package/src/emit/application/rbac/templates/ListRbac_Main.ejs +2 -2
  17. package/src/emit/application/rbac/templates/RbacMutation_Main.ejs +2 -2
  18. package/src/emit/application/rbac/templates/ReservedDynamicUsage_Main.ejs +7 -4
  19. package/src/emit/application/rbac/templates/UpdateTargetRole_Main.ejs +2 -2
  20. package/src/emit/application/rbac/templates/UserRoleMutation_Main.ejs +2 -2
  21. package/src/emit/infrastructure/resources/sqsResourceArtifacts.js +2 -1
  22. package/src/emit/infrastructure/resources/templates/FlowOut_Yaml.ejs +2 -0
  23. package/src/emit/schemas/ledger/emitLedgerSchemas.js +10 -8
  24. package/src/emit/schemas/ledger/templates/LedgerObjectSchema.ejs +17 -1
  25. package/src/emit/schemas/ledger/templates/LedgerRelationshipSchema.ejs +11 -1
  26. package/src/generate.js +50 -3
  27. package/src/output/commitArtifacts.js +42 -3
  28. package/src/plan/buildGenerationPlan.js +3 -2
  29. package/test/integration/core.test.js +31 -0
  30. package/test/unit/application/crudApplicationArtifacts.test.js +10 -0
  31. package/test/unit/infrastructure/resourceArtifacts.test.js +20 -0
  32. package/test/unit/output/mergeHooks.test.js +49 -0
  33. package/test/unit/schemas/dynamicCrudFlowSchemas.test.js +37 -0
  34. package/test/unit/schemas/ledgerSchemas.test.js +142 -10
  35. package/test/unit/schemas/resolveExtendedObjectSchemas.test.js +136 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@izara_project/izara-core-generate-service-code",
3
- "version": "1.0.102",
3
+ "version": "1.0.104",
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": {
@@ -99,6 +99,53 @@ export async function loadLocalSchemaDirectory(
99
99
  );
100
100
  if (loadedSchemas.length === 0) continue;
101
101
 
102
+ if (directoryName === 'ObjectSchemas') {
103
+ for (let i = 0; i < loadedSchemas.length; i++) {
104
+ let schema = loadedSchemas[i];
105
+ if (
106
+ schema &&
107
+ typeof schema === 'object' &&
108
+ 'result' in schema &&
109
+ schema.result &&
110
+ typeof schema.result === 'object'
111
+ ) {
112
+ schema = schema.result;
113
+ loadedSchemas[i] = schema;
114
+ }
115
+ if (!Object.prototype.hasOwnProperty.call(schema, '_sourceFile')) {
116
+ Object.defineProperty(schema, '_sourceFile', {
117
+ value: entry.name,
118
+ enumerable: false,
119
+ writable: true,
120
+ configurable: true
121
+ });
122
+ }
123
+ if (
124
+ !schema.objectType ||
125
+ typeof schema.objectType !== 'string' ||
126
+ schema.objectType.trim() === ''
127
+ ) {
128
+ const keys = Object.keys(schema || {});
129
+ let hint = '';
130
+ if (
131
+ keys.length === 1 &&
132
+ typeof schema[keys[0]] === 'object' &&
133
+ schema[keys[0]] !== null
134
+ ) {
135
+ const outerKey = keys[0];
136
+ hint =
137
+ ` Schema appears to be wrapped in key '${outerKey}'. ` +
138
+ `ObjectSchemas must not wrap with the object name; ` +
139
+ `export [{ objectType: '${outerKey}', ... }] directly.`;
140
+ }
141
+ throw new Error(
142
+ `Invalid ObjectSchema in ${entry.name}: 'objectType' is required ` +
143
+ `and must be a non-empty string.${hint}`
144
+ );
145
+ }
146
+ }
147
+ }
148
+
102
149
  if (directoryName === 'ErrorSchemas') {
103
150
  for (const schema of loadedSchemas) {
104
151
  if (!schema._sourceFile) {
@@ -20,11 +20,13 @@ const consoleLogger = {
20
20
  */
21
21
  export async function resolveExtendedObjectSchemas(
22
22
  objectSchemas,
23
- bucketName = process.env.iz_serviceSchemaBucketName
23
+ bucketName = process.env.iz_serviceSchemaBucketName,
24
+ dependencies = {}
24
25
  ) {
25
26
  if (!Array.isArray(objectSchemas)) {
26
27
  throw new TypeError('objectSchemas must be an array');
27
28
  }
29
+ const getObjectSchemaLib = dependencies.getObjectSchema || getObjectSchema;
28
30
  const resolvedSchemas = [];
29
31
  for (const schema of objectSchemas) {
30
32
  let currentSchema = schema;
@@ -36,15 +38,31 @@ export async function resolveExtendedObjectSchemas(
36
38
  { extendObjType: currentSchema.extendObjType }
37
39
  );
38
40
  const initialResources = currentSchema.storageResources || {};
39
- const childStorageResourceKeys = new Set(Object.keys(initialResources));
41
+ const initialResourceKeys = Object.keys(initialResources);
42
+ const childStorageResourceKeys = new Set(initialResourceKeys);
40
43
  const parentServiceTag = currentSchema.extendObjType.serviceTag;
41
44
 
42
- const mergedSchema = await getObjectSchema.mergeExtendObjSchema(
45
+ const mergeResult = await getObjectSchemaLib.mergeExtendObjSchema(
43
46
  { logger: consoleLogger },
44
47
  currentSchema,
45
48
  bucketName
46
49
  );
47
50
 
51
+ if (
52
+ mergeResult &&
53
+ typeof mergeResult === 'object' &&
54
+ Array.isArray(mergeResult.errorsFound) &&
55
+ mergeResult.errorsFound.length > 0
56
+ ) {
57
+ const errMsg = mergeResult.errorsFound.join('; ');
58
+ throw new Error(
59
+ `[resolveExtendedObjectSchemas] Failed to merge extendObjType ` +
60
+ `for objectType=${currentSchema.objectType}: ${errMsg}`
61
+ );
62
+ }
63
+
64
+ const mergedSchema = _unwrapSchemaResult(mergeResult);
65
+
48
66
  const externalDynamoResources = [];
49
67
  const ownStorageResources = {};
50
68
  const mergedStorageResources = mergedSchema.storageResources || {};
@@ -102,12 +120,13 @@ export async function resolveExtendedObjectSchemas(
102
120
  serviceTag: belongToTarget.serviceTag,
103
121
  objectType: belongToTarget.objectType
104
122
  };
105
- const belongToSchema = await getObjectSchema.getObjSchemaS3(
123
+ const belongToResult = await getObjectSchemaLib.getObjSchemaS3(
106
124
  { logger: consoleLogger },
107
125
  belongToObjType,
108
126
  false,
109
127
  bucketName
110
128
  );
129
+ const belongToSchema = _unwrapSchemaResult(belongToResult);
111
130
  currentSchema.belongToSchema = belongToSchema;
112
131
  console.log(
113
132
  `[DEBUG] [resolveExtendedObjectSchemas] Successfully fetched ` +
@@ -124,11 +143,26 @@ export async function resolveExtendedObjectSchemas(
124
143
  }
125
144
  }
126
145
 
146
+ currentSchema = _unwrapSchemaResult(currentSchema);
127
147
  resolvedSchemas.push(currentSchema);
128
148
  }
129
149
  return resolvedSchemas;
130
150
  }
131
151
 
152
+ function _unwrapSchemaResult(schemaOrResult) {
153
+ if (!schemaOrResult || typeof schemaOrResult !== 'object') {
154
+ return schemaOrResult;
155
+ }
156
+ if (
157
+ 'result' in schemaOrResult &&
158
+ schemaOrResult.result &&
159
+ typeof schemaOrResult.result === 'object'
160
+ ) {
161
+ return schemaOrResult.result;
162
+ }
163
+ return schemaOrResult;
164
+ }
165
+
132
166
  export async function resolveGraphServiceTags(
133
167
  schemas,
134
168
  bucketName = process.env.iz_serviceSchemaBucketName
@@ -144,6 +144,9 @@ export function setupBasicValidatorSchema(targetSchema = basicValidatorSchema) {
144
144
  item.required = item.required.filter((r) => r !== 'billalble');
145
145
  }
146
146
  }
147
+ if (item?.properties?.type?.enum?.includes('ledger')) {
148
+ item.properties.entryObject = { type: 'boolean' };
149
+ }
147
150
  }
148
151
  }
149
152
  }
@@ -1,4 +1,11 @@
1
1
  import initialSetupLib from '@izara_project/izara-core-library-initial-setup';
2
+ import { basicValidatorSchema } from '@izara_project/izara-shared-service-schemas';
3
+
4
+ if (basicValidatorSchema?.basicFlowSchema?.properties?.event?.items?.enum) {
5
+ if (!basicValidatorSchema.basicFlowSchema.properties.event.items.enum.includes('dss')) {
6
+ basicValidatorSchema.basicFlowSchema.properties.event.items.enum.push('dss');
7
+ }
8
+ }
2
9
 
3
10
  const SERVICE_TAG = {
4
11
  TRANSLATIONS: 'Translations',
@@ -16,6 +16,13 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
16
16
  */
17
17
 
18
18
  import initialSetupSharedLib from '@izara_project/izara-core-library-initial-setup';
19
+ import { basicValidatorSchema } from '@izara_project/izara-shared-service-schemas';
20
+
21
+ if (basicValidatorSchema?.basicFlowSchema?.properties?.event?.items?.enum) {
22
+ if (!basicValidatorSchema.basicFlowSchema.properties.event.items.enum.includes('dss')) {
23
+ basicValidatorSchema.basicFlowSchema.properties.event.items.enum.push('dss');
24
+ }
25
+ }
19
26
 
20
27
  const SERVICE_TAG = {
21
28
  TRANSLATIONS: 'Translations',
@@ -29,6 +29,19 @@ export async function emitCrudApplicationArtifacts({
29
29
  if (!Array.isArray(objects) || !Array.isArray(flows)) {
30
30
  throw new TypeError('objects and flows must be arrays');
31
31
  }
32
+ const malformedObj = objects.find(
33
+ (obj) =>
34
+ !obj ||
35
+ typeof obj.objectType !== 'string' ||
36
+ obj.objectType.trim() === ''
37
+ );
38
+ if (malformedObj) {
39
+ throw new Error(
40
+ `[CRUD Application] Malformed ObjectSchema: 'objectType' is required ` +
41
+ `and must be a non-empty string.`
42
+ );
43
+ }
44
+
32
45
  if (!objects.some(isCrudObject)) {
33
46
  if (objects.length > 0) {
34
47
  const summary = objects.map((obj) => ({
@@ -23,6 +23,19 @@ export function emitDynamicCrudFlowSchemas({
23
23
  if (!Array.isArray(objects) || !Array.isArray(flows)) {
24
24
  throw new TypeError('objects and flows must be arrays');
25
25
  }
26
+ const malformedObj = objects.find(
27
+ (obj) =>
28
+ !obj ||
29
+ typeof obj.objectType !== 'string' ||
30
+ obj.objectType.trim() === ''
31
+ );
32
+ if (malformedObj) {
33
+ throw new Error(
34
+ `[CRUD FlowSchemas] Malformed ObjectSchema: 'objectType' is required ` +
35
+ `and must be a non-empty string.`
36
+ );
37
+ }
38
+
26
39
  if (!objects.some(isCrudObject)) {
27
40
  if (objects.length > 0) {
28
41
  const summary = objects.map((obj) => ({
@@ -153,6 +153,10 @@ export default async function <%- flowTag %>(
153
153
  requestParams.billable = true;
154
154
  }
155
155
 
156
+ //(<rbacUsageValue_<%- flowTag %>>)
157
+ let usageValue = requestParams.value || requestParams.usageValue || 1;
158
+ //(</rbacUsageValue_<%- flowTag %>>)
159
+
156
160
  const resultPermissionCheck = await checkRbacPermission(
157
161
  _izContext,
158
162
  {
@@ -168,6 +172,7 @@ export default async function <%- flowTag %>(
168
172
  serviceTag: <%- rbacServiceTags.length === 1 ? rbacServiceTags[0] : `{ ${Object.entries(rbacServiceTagMap).map(([k, v]) => `'${k}': ${v}`).join(', ')} }[requestParams.objectType] || process.env.iz_serviceTag` %>
169
173
  },
170
174
  requestParams: requestParams,
175
+ value: usageValue,
171
176
  sync: true
172
177
  }
173
178
  );
@@ -187,6 +192,14 @@ export default async function <%- flowTag %>(
187
192
  );
188
193
  }
189
194
 
195
+ if (resultPermissionCheck?.uniqueIdentify) {
196
+ requestParams.reservedUsageId = resultPermissionCheck.uniqueIdentify;
197
+ }
198
+ if (resultPermissionCheck?.runningTotalReservedId) {
199
+ requestParams.runningTotalReservedId =
200
+ resultPermissionCheck.runningTotalReservedId;
201
+ }
202
+
190
203
  //(<rbacPublish_<%- flowTag %>>)
191
204
  //(</rbacPublish_<%- flowTag %>>)
192
205
  }
@@ -275,6 +288,7 @@ export default async function <%- flowTag %>(
275
288
  //(</afterBeforeLogical_<%= objectType %>>)
276
289
 
277
290
  <%_ if (hasOwnTopic) { _%>
291
+ /*
278
292
  // ── ownTopic (async — dispatch to before-hook and await callback) ──────────
279
293
  <%_ configs.filter(c => c?.generatedCodeConfig?.invocationType === 'ownTopic' || c?.event?.includes('ownTopic')).forEach(config => { _%>
280
294
  let hookPayload_<%= config.flowTag %> = {
@@ -341,6 +355,13 @@ export default async function <%- flowTag %>(
341
355
  status: 'pending',
342
356
  message: 'Dispatched to BeforeHook'
343
357
  };
358
+ */
359
+ return await <%- flowTag %>LogicalLib(
360
+ _izContext,
361
+ requestParams,
362
+ callingFlowConfig,
363
+ foundErrors
364
+ );
344
365
  <%_ } else { _%>
345
366
  return await <%- flowTag %>LogicalLib(
346
367
  _izContext,
@@ -154,7 +154,7 @@ export default async function CancelUsage(_izContext, requestParams) {
154
154
  };
155
155
 
156
156
  const eventSourceTag =
157
- _izContext.izEventSource?.eventSourceTag ||
157
+ _izContext?.izEventSource?.eventSourceTag ||
158
158
  requestParams?.izEventSource?.eventSourceTag;
159
159
 
160
160
  let isSync = false;
@@ -162,16 +162,17 @@ export default async function CancelUsage(_izContext, requestParams) {
162
162
  eventSourceTag === 'DirectInvoke' ||
163
163
  eventSourceTag === 'ApiGateway' ||
164
164
  eventSourceTag === 'ApiGatewayV2' ||
165
- requestParams?.hasOwnProperty('__context__')
165
+ requestParams?.hasOwnProperty('__context__') ||
166
+ _izContext?.functionName?.endsWith('HdrInv') ||
167
+ _izContext?.functionName?.endsWith('HdrApi') ||
168
+ eventSourceTag !== 'Sqs'
166
169
  ) {
167
170
  isSync = true;
168
171
  }
169
172
 
170
173
  if (!isSync) {
171
- await sns.publishMessage(_izContext, {
172
- message: {
173
- returnResult
174
- },
174
+ await sns.publishAsync(_izContext, {
175
+ Message: JSON.stringify({ returnResult }),
175
176
  TopicArn: snsSharedLib.snsTopicArnByFlowSchema(
176
177
  _izContext,
177
178
  'CancelUsage_Out'
@@ -178,7 +178,7 @@ export default async function ConfirmUsage(_izContext, requestParams) {
178
178
  };
179
179
 
180
180
  const eventSourceTag =
181
- _izContext.izEventSource?.eventSourceTag ||
181
+ _izContext?.izEventSource?.eventSourceTag ||
182
182
  requestParams?.izEventSource?.eventSourceTag;
183
183
 
184
184
  let isSync = false;
@@ -186,16 +186,17 @@ export default async function ConfirmUsage(_izContext, requestParams) {
186
186
  eventSourceTag === 'DirectInvoke' ||
187
187
  eventSourceTag === 'ApiGateway' ||
188
188
  eventSourceTag === 'ApiGatewayV2' ||
189
- requestParams?.hasOwnProperty('__context__')
189
+ requestParams?.hasOwnProperty('__context__') ||
190
+ _izContext?.functionName?.endsWith('HdrInv') ||
191
+ _izContext?.functionName?.endsWith('HdrApi') ||
192
+ eventSourceTag !== 'Sqs'
190
193
  ) {
191
194
  isSync = true;
192
195
  }
193
196
 
194
197
  if (!isSync) {
195
- await sns.publishMessage(_izContext, {
196
- message: {
197
- returnResult
198
- },
198
+ await sns.publishAsync(_izContext, {
199
+ Message: JSON.stringify({ returnResult }),
199
200
  TopicArn: snsSharedLib.snsTopicArnByFlowSchema(
200
201
  _izContext,
201
202
  'ConfirmUsage_Out'
@@ -133,8 +133,8 @@ export default async function CreateTargetRole(_izContext, requestParams) {
133
133
  }
134
134
  }
135
135
 
136
- await sns.publishMessage(_izContext, {
137
- message: { returnResult: { roleId, roleIdKey } },
136
+ await sns.publishAsync(_izContext, {
137
+ Message: JSON.stringify({ returnResult: { roleId, roleIdKey } }),
138
138
  TopicArn: snsSharedLib.snsTopicArnByFlowSchema(
139
139
  _izContext,
140
140
  'CreateTargetRole_Out'
@@ -82,8 +82,8 @@ export default async function DeleteTargetRole(_izContext, requestParams) {
82
82
  }
83
83
  }
84
84
 
85
- await sns.publishMessage(_izContext, {
86
- message: { returnResult: { identifiersConcat, roleIdKey, status: 'deleted' } },
85
+ await sns.publishAsync(_izContext, {
86
+ Message: JSON.stringify({ returnResult: { identifiersConcat, roleIdKey, status: 'deleted' } }),
87
87
  TopicArn: snsSharedLib.snsTopicArnByFlowSchema(
88
88
  _izContext,
89
89
  'DeleteTargetRole_Out'
@@ -95,8 +95,8 @@ export default async function GetMyPermissions(_izContext, requestParams) {
95
95
  permissions: permissionsMap
96
96
  };
97
97
 
98
- await sns.publishMessage(_izContext, {
99
- message: { returnResult: result },
98
+ await sns.publishAsync(_izContext, {
99
+ Message: JSON.stringify({ returnResult: result }),
100
100
  TopicArn: snsSharedLib.snsTopicArnByFlowSchema(
101
101
  _izContext,
102
102
  'GetMyPermissions_Out'
@@ -47,8 +47,8 @@ export default async function GetTargetRole(_izContext, requestParams) {
47
47
  users: usersResult?.Items || []
48
48
  };
49
49
 
50
- await sns.publishMessage(_izContext, {
51
- message: { returnResult: result },
50
+ await sns.publishAsync(_izContext, {
51
+ Message: JSON.stringify({ returnResult: result }),
52
52
  TopicArn: snsSharedLib.snsTopicArnByFlowSchema(
53
53
  _izContext,
54
54
  'GetTargetRole_Out'
@@ -41,8 +41,8 @@ export default async function <%- flowTag %>(_izContext, requestParams) {
41
41
  dynamodbSharedLib.tableName(_izContext, '<%- tableName %>'),
42
42
  queryParams
43
43
  );
44
- await sns.publishMessage(_izContext, {
45
- message: { returnResult: { records: resultQuery.Items || [] } },
44
+ await sns.publishAsync(_izContext, {
45
+ Message: JSON.stringify({ returnResult: { records: resultQuery.Items || [] } }),
46
46
  TopicArn: snsSharedLib.snsTopicArnByFlowSchema(
47
47
  _izContext,
48
48
  '<%- flowTag %>_Out'
@@ -28,8 +28,8 @@ export default async function <%- flowTag %>(_izContext, requestParams) {
28
28
  dynamodbSharedLib.tableName(_izContext, '<%- tableName %>'),
29
29
  { <%- fieldList %> }
30
30
  );
31
- await sns.publishMessage(_izContext, {
32
- message: { returnResult: { <%- fieldList %> } },
31
+ await sns.publishAsync(_izContext, {
32
+ Message: JSON.stringify({ returnResult: { <%- fieldList %> } }),
33
33
  TopicArn: snsSharedLib.snsTopicArnByFlowSchema(
34
34
  _izContext,
35
35
  '<%- flowTag %>_Out'
@@ -197,7 +197,7 @@ export default async function ReservedDynamicUsage(_izContext, requestParams) {
197
197
  };
198
198
 
199
199
  const eventSourceTag =
200
- _izContext.izEventSource?.eventSourceTag ||
200
+ _izContext?.izEventSource?.eventSourceTag ||
201
201
  requestParams?.izEventSource?.eventSourceTag;
202
202
 
203
203
  let isSync = false;
@@ -205,14 +205,17 @@ export default async function ReservedDynamicUsage(_izContext, requestParams) {
205
205
  eventSourceTag === 'DirectInvoke' ||
206
206
  eventSourceTag === 'ApiGateway' ||
207
207
  eventSourceTag === 'ApiGatewayV2' ||
208
- requestParams?.hasOwnProperty('__context__')
208
+ requestParams?.hasOwnProperty('__context__') ||
209
+ _izContext?.functionName?.endsWith('HdrInv') ||
210
+ _izContext?.functionName?.endsWith('HdrApi') ||
211
+ eventSourceTag !== 'Sqs'
209
212
  ) {
210
213
  isSync = true;
211
214
  }
212
215
 
213
216
  if (!isSync) {
214
- await sns.publishMessage(_izContext, {
215
- message: { returnResult },
217
+ await sns.publishAsync(_izContext, {
218
+ Message: JSON.stringify({ returnResult }),
216
219
  TopicArn: snsSharedLib.snsTopicArnByFlowSchema(
217
220
  _izContext,
218
221
  'ReservedDynamicUsage_Out'
@@ -93,8 +93,8 @@ export default async function UpdateTargetRole(_izContext, requestParams) {
93
93
  }
94
94
  }
95
95
 
96
- await sns.publishMessage(_izContext, {
97
- message: { returnResult: { roleIdKey, status: 'updated' } },
96
+ await sns.publishAsync(_izContext, {
97
+ Message: JSON.stringify({ returnResult: { roleIdKey, status: 'updated' } }),
98
98
  TopicArn: snsSharedLib.snsTopicArnByFlowSchema(
99
99
  _izContext,
100
100
  'UpdateTargetRole_Out'
@@ -54,8 +54,8 @@ export default async function <%- flowTag %>(_izContext, requestParams) {
54
54
  }
55
55
  }
56
56
  ]);
57
- await sns.publishMessage(_izContext, {
58
- message: { returnResult: { userId, roleIdKey } },
57
+ await sns.publishAsync(_izContext, {
58
+ Message: JSON.stringify({ returnResult: { userId, roleIdKey } }),
59
59
  TopicArn: snsSharedLib.snsTopicArnByFlowSchema(
60
60
  _izContext,
61
61
  '<%- flowTag %>_Out'
@@ -102,7 +102,8 @@ export async function renderOut(state) {
102
102
  renderResourceTemplate('FlowOut_Yaml.ejs', { topicName })
103
103
  )
104
104
  );
105
- const body = renderedTopics.join('\n');
105
+ const fileHook = '#<#customTopicOut#>\n#<#/customTopicOut#>';
106
+ const body = [...renderedTopics, fileHook].filter(Boolean).join('\n');
106
107
  return `Resources:\n${indentYaml(body, 2)}`;
107
108
  }
108
109
 
@@ -4,3 +4,5 @@ Out<%- topicName %>:
4
4
  Properties:
5
5
  DisplayName: 'SNS Message out'
6
6
  TopicName: ${self:custom.iz_serviceTag}_${self:custom.iz_stage}_<%- topicName %>_Out
7
+ #<#Out<%- topicName %>TopicSetting#>
8
+ #<#/Out<%- topicName %>TopicSetting#>
@@ -126,7 +126,6 @@ function _buildParentToEntryLinks({
126
126
  objectType: parentObjectType
127
127
  },
128
128
  linkType: 'many',
129
- requiredOnCreate: false,
130
129
  belongTo: true
131
130
  },
132
131
  to: {
@@ -134,8 +133,7 @@ function _buildParentToEntryLinks({
134
133
  serviceTag: effectiveServiceTag,
135
134
  objectType: entryObjectType
136
135
  },
137
- linkType: 'one',
138
- requiredOnCreate: false
136
+ linkType: 'one'
139
137
  }
140
138
  }
141
139
  ];
@@ -145,7 +143,10 @@ export function ledgerObjectSchemaManagedPaths(objects = []) {
145
143
  const ledgers = _extractLedgers(objects);
146
144
  const paths = [];
147
145
 
148
- for (const { component } of ledgers) {
146
+ for (const { component, addOn } of ledgers) {
147
+ if (addOn?.entryObject === false) {
148
+ continue;
149
+ }
149
150
  paths.push(
150
151
  `app/src/schemas/ObjectSchemas/generatedLedger/${component.objectType}.js`
151
152
  );
@@ -198,6 +199,9 @@ export async function emitLedgerObjectSchemas({ objects = [] } = {}) {
198
199
  const artifacts = [];
199
200
 
200
201
  for (const { parentObject, addOn, component } of ledgers) {
202
+ if (addOn?.entryObject === false) {
203
+ continue;
204
+ }
201
205
  const objectType = component.objectType;
202
206
  const graphStorageResources = _extractGraphStorageResources(
203
207
  parentObject,
@@ -362,16 +366,14 @@ export async function emitLedgerRelationshipSchemas({
362
366
  serviceTag: effectiveServiceTag,
363
367
  objectType: component.objectType
364
368
  },
365
- linkType: 'many',
366
- requiredOnCreate: false
369
+ linkType: 'many'
367
370
  },
368
371
  to: {
369
372
  objType: {
370
373
  serviceTag: targetServiceTag,
371
374
  objectType: target.objectType
372
375
  },
373
- linkType: 'one',
374
- requiredOnCreate: false
376
+ linkType: 'one'
375
377
  }
376
378
  });
377
379
  }
@@ -1,3 +1,19 @@
1
1
  export default [
2
- <%- JSON.stringify(schema, null, 2).replace(/\n/g, '\n ') %>
2
+ {
3
+ "objectType": "<%= schema.objectType %>",
4
+ "generatedBy": "systemNoUser",
5
+ "canDelete": false,
6
+ "storageResources": <%- JSON.stringify(schema.storageResources, null, 4).replace(/\n/g, '\n ') %>,
7
+ "fieldNames": {
8
+ "<%= schema.objectType %>Id": <%- JSON.stringify(schema.fieldNames[schema.objectType + 'Id'], null, 6).replace(/\n/g, '\n ') %>,
9
+ //(<<%= schema.objectType %>HookCode>)
10
+ //(</<%= schema.objectType %>HookCode>)
11
+ },
12
+ "identifiers": [
13
+ {
14
+ "type": "identifier",
15
+ "fieldName": "<%= schema.objectType %>Id"
16
+ }
17
+ ]
18
+ }
3
19
  ];
@@ -1 +1,11 @@
1
- export default <%- JSON.stringify(wrapper, null, 2) %>;
1
+ export default [
2
+ {
3
+ "<%= relationshipTag %>": {
4
+ <% Object.entries(content).forEach(([key, val]) => { -%>
5
+ "<%- key %>": <%- JSON.stringify(val, null, 6).replace(/\n/g, '\n ') %>,
6
+ <% }); -%>
7
+ //(<<%= relationshipTag %>HookCode>)
8
+ //(</<%= relationshipTag %>HookCode>)
9
+ }
10
+ }
11
+ ];
package/src/generate.js CHANGED
@@ -42,10 +42,22 @@ export async function generate(rootPath, options = {}) {
42
42
  options.schemaBucketName ||
43
43
  project.schemaBucketName ||
44
44
  process.env.iz_serviceSchemaBucketName;
45
- const objects = await resolveExtendedObjectSchemas(
45
+ const resolvedObjects = await resolveExtendedObjectSchemas(
46
46
  rawObjects,
47
47
  schemaBucketName
48
48
  );
49
+ const objects = resolvedObjects.map((obj) => {
50
+ if (
51
+ obj &&
52
+ typeof obj === 'object' &&
53
+ 'result' in obj &&
54
+ obj.result &&
55
+ typeof obj.result === 'object'
56
+ ) {
57
+ return obj.result;
58
+ }
59
+ return obj;
60
+ });
49
61
  const objectGraphServiceTags = await resolveGraphServiceTags(
50
62
  objects,
51
63
  schemaBucketName
@@ -93,16 +105,35 @@ export async function generate(rootPath, options = {}) {
93
105
  console.log(
94
106
  `[INFO] [Generate] STATUS=ARTIFACTS_EMITTED | ${emittedScopeSummary}`
95
107
  );
108
+ const allArtifacts = groups.flatMap((group) => group.artifacts);
109
+ const dirSummary = _summarizeArtifactDirectories(allArtifacts);
110
+ const summaryEntries = Object.entries(dirSummary);
111
+ if (summaryEntries.length > 0) {
112
+ const summaryItems = summaryEntries
113
+ .map(([dir, count]) => `${dir}=${count}`)
114
+ .join(' ');
115
+ console.log(`[INFO] [Generate] OUTPUT_SUMMARY | ${summaryItems}`);
116
+ }
96
117
  let files = [];
97
118
  if (groups.length > 0) {
98
119
  files = await commitArtifacts(
99
120
  buildGenerationPlan(
100
121
  project,
101
- groups.flatMap((group) => group.artifacts),
122
+ allArtifacts,
102
123
  {
103
124
  managedScopes: Object.fromEntries(
104
125
  groups.map((group) => [group.scope, group.managedPaths])
105
- )
126
+ ),
127
+ scopeGuards: {
128
+ hasMalformedSchemas:
129
+ objects.some(
130
+ (obj) =>
131
+ !obj ||
132
+ typeof obj.objectType !== 'string' ||
133
+ obj.objectType.trim() === ''
134
+ ) ||
135
+ relationships.some((rel) => !rel || typeof rel !== 'object')
136
+ }
106
137
  }
107
138
  )
108
139
  );
@@ -123,3 +154,19 @@ export async function generate(rootPath, options = {}) {
123
154
  throw error;
124
155
  }
125
156
  }
157
+
158
+ function _summarizeArtifactDirectories(artifacts) {
159
+ const dirCounts = {};
160
+ for (const artifact of artifacts) {
161
+ const parts = artifact.path.split('/');
162
+ let category = parts.slice(0, 3).join('/');
163
+ if (parts.length < 3) {
164
+ category = parts.slice(0, -1).join('/');
165
+ if (!category) {
166
+ category = '.';
167
+ }
168
+ }
169
+ dirCounts[category] = (dirCounts[category] || 0) + 1;
170
+ }
171
+ return dirCounts;
172
+ }
@@ -42,8 +42,17 @@ export async function commitArtifacts(plan) {
42
42
  const stalePaths = _staleManagedPaths(
43
43
  managedScopes,
44
44
  managedArtifacts,
45
- artifacts
45
+ artifacts,
46
+ plan.scopeGuards
46
47
  );
48
+ if (stalePaths.length > 0) {
49
+ console.log(
50
+ `[WARN] [Commit] REMOVED_STALE | count=${stalePaths.length} files:`
51
+ );
52
+ for (const path of stalePaths) {
53
+ console.log(` - ${path}`);
54
+ }
55
+ }
47
56
  const mergedArtifacts = await mergeHooks(physicalOutputRoot, artifacts);
48
57
  const stagedRoot = await mkdtemp(join(physicalOutputRoot, STAGING_PREFIX));
49
58
 
@@ -67,6 +76,10 @@ export async function commitArtifacts(plan) {
67
76
  const snapshots = await _snapshotChanges(changes, stagedRoot);
68
77
  try {
69
78
  await _applyChanges(changes);
79
+ console.log(
80
+ `[INFO] [Commit] STATUS=APPLIED | written=${artifacts.length} ` +
81
+ `removed=${stalePaths.length}`
82
+ );
70
83
  } catch (error) {
71
84
  await _rollbackChanges(changes, snapshots);
72
85
  throw error;
@@ -90,7 +103,12 @@ function _normalizeManagedScopes(plan) {
90
103
  };
91
104
  }
92
105
 
93
- function _staleManagedPaths(managedScopes, managedArtifacts, artifacts) {
106
+ function _staleManagedPaths(
107
+ managedScopes,
108
+ managedArtifacts,
109
+ artifacts,
110
+ scopeGuards
111
+ ) {
94
112
  if (!managedScopes) {
95
113
  return [];
96
114
  }
@@ -100,7 +118,28 @@ function _staleManagedPaths(managedScopes, managedArtifacts, artifacts) {
100
118
  if (!Object.hasOwn(managedArtifacts, scope)) {
101
119
  continue;
102
120
  }
103
- for (const path of [...declaredPaths, ...managedArtifacts[scope]]) {
121
+ const previousPaths = managedArtifacts[scope] || [];
122
+ if (previousPaths.length > 0) {
123
+ const scopeEmittedCount = declaredPaths.filter((path) =>
124
+ emittedPaths.has(path)
125
+ ).length;
126
+ if (scopeEmittedCount === 0) {
127
+ if (scopeGuards?.hasMalformedSchemas) {
128
+ throw new Error(
129
+ `[SAFETY_GUARD] Refusing to delete all artifacts in managed ` +
130
+ `scope '${scope}'. Malformed schemas were detected in the ` +
131
+ `project.`
132
+ );
133
+ }
134
+ if (scopeGuards?.protectedScopes?.includes(scope)) {
135
+ throw new Error(
136
+ `[SAFETY_GUARD] Refusing to delete all artifacts in protected ` +
137
+ `scope '${scope}'.`
138
+ );
139
+ }
140
+ }
141
+ }
142
+ for (const path of [...declaredPaths, ...previousPaths]) {
104
143
  if (!emittedPaths.has(path)) {
105
144
  stalePaths.add(path);
106
145
  }
@@ -6,7 +6,7 @@ import {
6
6
  export function buildGenerationPlan(
7
7
  project,
8
8
  artifacts = [],
9
- { managedPaths = [], managedScope, managedScopes } = {}
9
+ { managedPaths = [], managedScope, managedScopes, scopeGuards } = {}
10
10
  ) {
11
11
  if (!project?.rootPath || !project?.outputPath) {
12
12
  throw new TypeError('ProjectModel must include rootPath and outputPath');
@@ -21,7 +21,8 @@ export function buildGenerationPlan(
21
21
  artifacts: validateArtifacts(artifacts),
22
22
  managedPaths: normalizeArtifactPaths(managedPaths),
23
23
  managedScope: _normalizeManagedScope(managedScope),
24
- managedScopes: _normalizeManagedScopes(managedScopes)
24
+ managedScopes: _normalizeManagedScopes(managedScopes),
25
+ scopeGuards: scopeGuards ? Object.freeze({ ...scopeGuards }) : null
25
26
  });
26
27
  }
27
28
 
@@ -229,3 +229,34 @@ test('rejects a failed output transaction without partial files, hooks, or stagi
229
229
  );
230
230
  assert.equal((await readdir(outputPath)).includes('.bak'), false);
231
231
  });
232
+
233
+ test('commitArtifacts throws SAFETY_GUARD error when hasMalformedSchemas is true and scope would be wiped', async () => {
234
+ const outputPath = await mkdtemp(
235
+ join(tmpdir(), 'generate-service-code-v2-safety-guard-')
236
+ );
237
+ const project = compileProject({ rootPath: outputPath });
238
+ const managedScopes = {
239
+ 'crud-application': ['app/sls_yaml/generatedCode/source/flow-objects.yml']
240
+ };
241
+ await commitArtifacts(
242
+ buildGenerationPlan(
243
+ project,
244
+ [{ path: 'app/sls_yaml/generatedCode/source/flow-objects.yml', content: 'content\n' }],
245
+ { managedScopes }
246
+ )
247
+ );
248
+
249
+ await assert.rejects(
250
+ commitArtifacts(
251
+ buildGenerationPlan(project, [], {
252
+ managedScopes,
253
+ scopeGuards: { hasMalformedSchemas: true }
254
+ })
255
+ ),
256
+ /\[SAFETY_GUARD\] Refusing to delete all artifacts in managed scope 'crud-application'/
257
+ );
258
+ assert.equal(
259
+ await readFile(join(outputPath, 'app/sls_yaml/generatedCode/source/flow-objects.yml'), 'utf8'),
260
+ 'content\n'
261
+ );
262
+ });
@@ -8,6 +8,7 @@ import test, { after } from 'node:test';
8
8
  process.env.NODE_ENV = 'test';
9
9
 
10
10
  import { generateCrudApplicationArtifacts } from '../../../src/emit/application/crud/generateCrudApplicationArtifacts.js';
11
+ import { emitCrudApplicationArtifacts } from '../../../src/emit/application/crud/emitCrudApplicationArtifacts.js';
11
12
 
12
13
  const roots = new Set();
13
14
  after(() =>
@@ -1464,6 +1465,15 @@ test('emits async before-hook callback and SNS publish in Create_Main when event
1464
1465
  assert.match(callbackHandler, /CreateBeforeHookCallback_Main/);
1465
1466
  });
1466
1467
 
1468
+ test('emitCrudApplicationArtifacts throws error when an object is malformed', async () => {
1469
+ await assert.rejects(
1470
+ emitCrudApplicationArtifacts({
1471
+ objects: [{ generatedBy: 'user' }]
1472
+ }),
1473
+ /Malformed ObjectSchema: 'objectType' is required/
1474
+ );
1475
+ });
1476
+
1467
1477
  async function createProject(withCrudObject) {
1468
1478
  const rootPath = await mkdtemp(
1469
1479
  join(tmpdir(), 'generate-service-code-v2-crud-application-')
@@ -964,3 +964,23 @@ test('roleStatements emits s3:GetObject for custom S3 flow bucket', async () =>
964
964
  assert.match(uploadRole, /arn:aws:s3:::UploadBucket\/\*/);
965
965
  });
966
966
 
967
+ test('emits per-topic hook tags in generated-sns-out.yml', async () => {
968
+ const rootPath = await createProject();
969
+ await generateResourceArtifacts(rootPath);
970
+ const snsOut = await readFile(
971
+ join(rootPath, 'resource/sls_yaml/generatedCode/source/generated-sns-out.yml'),
972
+ 'utf8'
973
+ );
974
+ assert.match(snsOut, /#<#Out\w+TopicSetting#>\s*#<#\/Out\w+TopicSetting#>/);
975
+ });
976
+
977
+ test('emits customTopicOut file-level hook in generated-sns-out.yml', async () => {
978
+ const rootPath = await createProject();
979
+ await generateResourceArtifacts(rootPath);
980
+ const snsOut = await readFile(
981
+ join(rootPath, 'resource/sls_yaml/generatedCode/source/generated-sns-out.yml'),
982
+ 'utf8'
983
+ );
984
+ assert.match(snsOut, /#<#customTopicOut#>\s*#<#\/customTopicOut#>/);
985
+ });
986
+
@@ -402,4 +402,53 @@ test('warns when source content contains custom code in hook missing from templa
402
402
  }
403
403
  });
404
404
 
405
+ test('preserves custom settings inside Out<topicName>TopicSetting and customTopicOut in generated-sns-out.yml', async () => {
406
+ const tmpDir = await mkdtemp(path.join(os.tmpdir(), 'mergeHooks-test-'));
407
+ try {
408
+ const filePath = 'resource/sls_yaml/generatedCode/source/generated-sns-out.yml';
409
+ const diskContent = [
410
+ 'Resources:',
411
+ ' ##===== [Topic Out]',
412
+ ' OutCreate:',
413
+ ' Type: AWS::SNS::Topic',
414
+ ' Properties:',
415
+ " DisplayName: 'SNS Message out'",
416
+ ' TopicName: ${self:custom.iz_serviceTag}_${self:custom.iz_stage}_Create_Out',
417
+ ' #<#OutCreateTopicSetting#>',
418
+ ' FifoTopic: true',
419
+ ' #<#/OutCreateTopicSetting#>',
420
+ '',
421
+ ' #<#customTopicOut#>',
422
+ ' OutCustomNotification:',
423
+ ' Type: AWS::SNS::Topic',
424
+ ' #<#/customTopicOut#>'
425
+ ].join('\n') + '\n';
426
+ await writeTestFile(tmpDir, filePath, diskContent);
427
+
428
+ const generatedArtifact = {
429
+ path: filePath,
430
+ content: [
431
+ 'Resources:',
432
+ ' ##===== [Topic Out]',
433
+ ' OutCreate:',
434
+ ' Type: AWS::SNS::Topic',
435
+ ' Properties:',
436
+ " DisplayName: 'SNS Message out'",
437
+ ' TopicName: ${self:custom.iz_serviceTag}_${self:custom.iz_stage}_Create_Out',
438
+ ' #<#OutCreateTopicSetting#>',
439
+ ' #<#/OutCreateTopicSetting#>',
440
+ '',
441
+ ' #<#customTopicOut#>',
442
+ ' #<#/customTopicOut#>'
443
+ ].join('\n') + '\n'
444
+ };
445
+
446
+ const [result] = await mergeHooks(tmpDir, [generatedArtifact]);
447
+ assert.match(result.content, /FifoTopic: true/);
448
+ assert.match(result.content, /OutCustomNotification:/);
449
+ } finally {
450
+ await rm(tmpDir, { recursive: true, force: true });
451
+ }
452
+ });
453
+
405
454
 
@@ -6,6 +6,7 @@ import test from 'node:test';
6
6
  import { pathToFileURL } from 'node:url';
7
7
 
8
8
  import { generateDynamicCrudFlowSchemas } from '../../../src/emit/application/crud/generateDynamicCrudFlowSchemas.js';
9
+ import { emitDynamicCrudFlowSchemas } from '../../../src/emit/application/crud/emitDynamicCrudFlowSchemas.js';
9
10
  import { createCrudActionPlans } from '../../../src/emit/application/crud/crudActionPlan.js';
10
11
 
11
12
  test('generates dynamic CRUD schemas from local schema modules', async () => {
@@ -422,3 +423,39 @@ test('throws error when ObjectSchema validation fails with validateSchemas: true
422
423
  );
423
424
  });
424
425
 
426
+ test('throws error when ObjectSchema is missing objectType', async () => {
427
+ const rootPath = await createProject({ generatedBy: 'user' });
428
+ await writeFile(
429
+ join(rootPath, 'app/src/schemas/ObjectSchemas/Order.js'),
430
+ "export default [{ generatedBy: 'user' }];\n"
431
+ );
432
+
433
+ await assert.rejects(
434
+ generateDynamicCrudFlowSchemas(rootPath),
435
+ /Invalid ObjectSchema in Order\.js: 'objectType' is required/
436
+ );
437
+ });
438
+
439
+ test('throws helpful error when ObjectSchema is wrapped in outer key', async () => {
440
+ const rootPath = await createProject({ generatedBy: 'user' });
441
+ await writeFile(
442
+ join(rootPath, 'app/src/schemas/ObjectSchemas/Order.js'),
443
+ "export default [{ order: { objectType: 'order', generatedBy: 'user' } }];\n"
444
+ );
445
+
446
+ await assert.rejects(
447
+ generateDynamicCrudFlowSchemas(rootPath),
448
+ /Schema appears to be wrapped in key 'order'/
449
+ );
450
+ });
451
+
452
+ test('emitDynamicCrudFlowSchemas throws error when an object is malformed', () => {
453
+ assert.throws(
454
+ () =>
455
+ emitDynamicCrudFlowSchemas({
456
+ objects: [{ generatedBy: 'user' }]
457
+ }),
458
+ /Malformed ObjectSchema: 'objectType' is required/
459
+ );
460
+ });
461
+
@@ -79,6 +79,10 @@ test('emitLedgerObjectSchemas returns 1 object schema per ledger addOn', async (
79
79
  assert.match(entryArtifact.content, /"generatedBy": "systemNoUser"/);
80
80
  assert.match(entryArtifact.content, /"canDelete": false/);
81
81
  assert.match(entryArtifact.content, /GraphHandler/);
82
+ assert.match(
83
+ entryArtifact.content,
84
+ /\/\/\(<unitStandardSupplyLocationEntryHookCode>\)[\s\S]*?\/\/\(<\/unitStandardSupplyLocationEntryHookCode>\)/
85
+ );
82
86
  });
83
87
 
84
88
  test('emitLedgerRelationshipSchemas returns current, old, and is relationship schemas per ledger addOn', async () => {
@@ -157,30 +161,46 @@ test('emitLedgerRelationshipSchemas returns current, old, and is relationship sc
157
161
  assert.match(isRel.content, /"supplyLocation"/);
158
162
  assert.match(isRel.content, /"unitPacking"/);
159
163
 
160
- // 4. Verify requiredOnCreate is false in links
164
+ // 4. Verify hookCode in relationship schemas
165
+ assert.match(
166
+ currentRel.content,
167
+ /\/\/\(<currentUnitStandardSupplyLocationHookCode>\)[\s\S]*?\/\/\(<\/currentUnitStandardSupplyLocationHookCode>\)/
168
+ );
169
+ assert.match(
170
+ oldRel.content,
171
+ /\/\/\(<oldUnitStandardSupplyLocationHookCode>\)[\s\S]*?\/\/\(<\/oldUnitStandardSupplyLocationHookCode>\)/
172
+ );
173
+ assert.match(
174
+ isRel.content,
175
+ /\/\/\(<isUnitStandardSupplyLocationEntryHookCode>\)[\s\S]*?\/\/\(<\/isUnitStandardSupplyLocationEntryHookCode>\)/
176
+ );
177
+
178
+ // 5. Verify requiredOnCreate is removed from links
161
179
  const _parseRel = (content) => {
162
180
  const raw = content
163
181
  .replace(/^export default\s+/, '')
164
- .replace(/;\s*$/, '');
182
+ .replace(/;\s*$/, '')
183
+ .replace(/\/\/.*/g, '')
184
+ .replace(/,(\s*})/g, '$1');
165
185
  return JSON.parse(raw)[0];
166
186
  };
167
187
 
168
188
  const currentRelData = Object.values(_parseRel(currentRel.content))[0];
169
189
  for (const link of currentRelData.links) {
170
- assert.equal(link.from.requiredOnCreate, false);
171
- assert.equal(link.to.requiredOnCreate, false);
190
+ assert.equal(link.from.requiredOnCreate, undefined);
191
+ assert.equal(link.to.requiredOnCreate, undefined);
172
192
  }
173
193
 
174
194
  const oldRelData = Object.values(_parseRel(oldRel.content))[0];
175
195
  for (const link of oldRelData.links) {
176
- assert.equal(link.from.requiredOnCreate, false);
177
- assert.equal(link.to.requiredOnCreate, false);
196
+ assert.equal(link.from.requiredOnCreate, undefined);
197
+ assert.equal(link.to.requiredOnCreate, undefined);
178
198
  }
179
199
 
180
200
  const isRelData = Object.values(_parseRel(isRel.content))[0];
181
201
  for (const link of isRelData.links) {
182
- assert.equal(link.from.requiredOnCreate, false);
183
- assert.equal(link.to.requiredOnCreate, false);
202
+ assert.equal(link.from.requiredOnCreate, undefined);
203
+ assert.equal(link.to.requiredOnCreate, undefined);
184
204
  }
185
205
  });
186
206
 
@@ -221,10 +241,122 @@ test('generate commits and cleans up ledger dynamic schemas', async () => {
221
241
  });
222
242
  });
223
243
 
244
+ test('generate preserves user hooks in ledger ObjectSchemas and RelationshipSchemas across regeneration', async () => {
245
+ const rootPath = await createProjectWithLedger({ hasAddOn: true });
246
+ await generate(rootPath);
247
+
248
+ const objectRelPath =
249
+ 'app/src/schemas/ObjectSchemas/generatedLedger/unitStandardSupplyLocationEntry.js';
250
+ const relRelPath =
251
+ 'app/src/schemas/RelationshipSchemas/generatedLedger/currentUnitStandardSupplyLocation.js';
252
+
253
+ // Inject custom code into dynamic hook tags
254
+ let objectContent = await readProjectFile(rootPath, objectRelPath);
255
+ objectContent = objectContent.replace(
256
+ '//(<unitStandardSupplyLocationEntryHookCode>)\n //(</unitStandardSupplyLocationEntryHookCode>)',
257
+ '//(<unitStandardSupplyLocationEntryHookCode>)\n "customField": { "type": "string" },\n //(</unitStandardSupplyLocationEntryHookCode>)'
258
+ );
259
+ await writeProjectFile(rootPath, objectRelPath, objectContent);
260
+
261
+ let relContent = await readProjectFile(rootPath, relRelPath);
262
+ relContent = relContent.replace(
263
+ '//(<currentUnitStandardSupplyLocationHookCode>)\n //(</currentUnitStandardSupplyLocationHookCode>)',
264
+ '//(<currentUnitStandardSupplyLocationHookCode>)\n "customRelProp": true,\n //(</currentUnitStandardSupplyLocationHookCode>)'
265
+ );
266
+ await writeProjectFile(rootPath, relRelPath, relContent);
267
+
268
+ // Re-run generate
269
+ await generate(rootPath);
270
+
271
+ const preservedObjectContent = await readProjectFile(rootPath, objectRelPath);
272
+ assert.match(preservedObjectContent, /"customField": \{ "type": "string" \}/);
273
+
274
+ const preservedRelContent = await readProjectFile(rootPath, relRelPath);
275
+ assert.match(preservedRelContent, /"customRelProp": true/);
276
+ });
277
+
278
+ test('emitLedgerObjectSchemas and managedPaths skip when entryObject is false', async () => {
279
+ const objects = [
280
+ {
281
+ objectType: 'unitStandard',
282
+ serviceTag: 'UnitStandard',
283
+ storageResources: {
284
+ graph: {
285
+ storageType: 'graph',
286
+ graphServerTag: 'GraphHandler'
287
+ }
288
+ },
289
+ addOnDataStructure: [
290
+ {
291
+ type: 'ledger',
292
+ prefix: 'unitStandardSupplyLocation',
293
+ storageResourceTag: 'graph',
294
+ ledgerType: 'movable',
295
+ entryObject: false,
296
+ targetObjType: {
297
+ serviceTag: 'SupplyLocationStandard',
298
+ objectType: 'supplyLocation'
299
+ }
300
+ }
301
+ ]
302
+ }
303
+ ];
304
+
305
+ const artifacts = await emitLedgerObjectSchemas({ objects });
306
+ assert.equal(artifacts.length, 0);
307
+
308
+ const managedPaths = ledgerObjectSchemaManagedPaths(objects);
309
+ assert.deepEqual(managedPaths, []);
310
+
311
+ const relArtifacts = await emitLedgerRelationshipSchemas({
312
+ objects,
313
+ serviceTag: 'UnitStandard'
314
+ });
315
+ assert.equal(relArtifacts.length, 3);
316
+ const relPaths = ledgerRelationshipSchemaManagedPaths(objects);
317
+ assert.equal(relPaths.length, 3);
318
+ });
319
+
320
+ test('generate cleans up entry ObjectSchema when entryObject is set to false', async () => {
321
+ const rootPath = await createProjectWithLedger({
322
+ hasAddOn: true,
323
+ entryObject: true
324
+ });
325
+ await generate(rootPath);
326
+
327
+ const objectRelPath =
328
+ 'app/src/schemas/ObjectSchemas/generatedLedger/unitStandardSupplyLocationEntry.js';
329
+ const relRelPath =
330
+ 'app/src/schemas/RelationshipSchemas/generatedLedger/currentUnitStandardSupplyLocation.js';
331
+
332
+ const initialObj = await readProjectFile(rootPath, objectRelPath);
333
+ assert.ok(initialObj);
334
+ const initialRel = await readProjectFile(rootPath, relRelPath);
335
+ assert.ok(initialRel);
336
+
337
+ await createProjectWithLedger({
338
+ rootPath,
339
+ hasAddOn: true,
340
+ entryObject: false
341
+ });
342
+ await generate(rootPath);
343
+
344
+ await assert.rejects(readProjectFile(rootPath, objectRelPath), {
345
+ code: 'ENOENT'
346
+ });
347
+ const preservedRel = await readProjectFile(rootPath, relRelPath);
348
+ assert.ok(preservedRel);
349
+ });
350
+
224
351
  async function createProjectWithLedger({
225
352
  rootPath: existingRoot,
226
- hasAddOn = true
353
+ hasAddOn = true,
354
+ entryObject = true
227
355
  } = {}) {
356
+ let entryObjectLine = '';
357
+ if (entryObject === false) {
358
+ entryObjectLine = '\n entryObject: false,';
359
+ }
228
360
  const addOnContent = hasAddOn
229
361
  ? `
230
362
  addOnDataStructure: [
@@ -232,7 +364,7 @@ async function createProjectWithLedger({
232
364
  type: 'ledger',
233
365
  prefix: 'unitStandardSupplyLocation',
234
366
  storageResourceTag: 'graph',
235
- ledgerType: 'movable',
367
+ ledgerType: 'movable',${entryObjectLine}
236
368
  targetObjType: {
237
369
  serviceTag: 'SupplyLocationStandard',
238
370
  objectType: 'supplyLocation'
@@ -0,0 +1,136 @@
1
+ import assert from 'node:assert/strict';
2
+ import test from 'node:test';
3
+
4
+ import {
5
+ resolveExtendedObjectSchemas
6
+ } from '../../../src/compile/loadSchemasRemote.js';
7
+
8
+ test(
9
+ 'resolveExtendedObjectSchemas unwraps result from mergeExtendObjSchema',
10
+ async () => {
11
+ const mockServiceSchemas = {
12
+ mergeExtendObjSchema: async () => {
13
+ return {
14
+ errorsFound: [],
15
+ result: {
16
+ objectType: 'unitStandard',
17
+ generatedBy: 'user',
18
+ storageResources: {
19
+ dynamo1: {
20
+ tableName: 'UnitStandards',
21
+ storageType: 'dynamo'
22
+ },
23
+ childDynamo: {
24
+ tableName: 'ChildTable',
25
+ storageType: 'dynamo'
26
+ }
27
+ }
28
+ }
29
+ };
30
+ }
31
+ };
32
+
33
+ const inputSchemas = [
34
+ {
35
+ objectType: 'unitStandard',
36
+ generatedBy: 'user',
37
+ extendObjType: {
38
+ serviceTag: 'CoreService',
39
+ objectType: 'standard'
40
+ },
41
+ storageResources: {
42
+ childDynamo: {
43
+ tableName: 'ChildTable',
44
+ storageType: 'dynamo'
45
+ }
46
+ }
47
+ }
48
+ ];
49
+
50
+ const resolved = await resolveExtendedObjectSchemas(
51
+ inputSchemas,
52
+ 'bucket',
53
+ { getObjectSchema: mockServiceSchemas }
54
+ );
55
+ assert.equal(resolved.length, 1);
56
+ const resolvedObject = resolved[0];
57
+ assert.equal(resolvedObject.objectType, 'unitStandard');
58
+ assert.equal(resolvedObject.result, undefined);
59
+ assert.equal(resolvedObject.errorsFound, undefined);
60
+ assert.ok(resolvedObject.storageResources);
61
+ assert.ok(resolvedObject.storageResources.childDynamo);
62
+ assert.ok(resolvedObject.externalDynamoResources);
63
+ assert.equal(resolvedObject.externalDynamoResources.length, 1);
64
+ assert.equal(
65
+ resolvedObject.externalDynamoResources[0].tableName,
66
+ 'UnitStandards'
67
+ );
68
+ }
69
+ );
70
+
71
+ test(
72
+ 'resolveExtendedObjectSchemas throws if merge returns errorsFound',
73
+ async () => {
74
+ const mockServiceSchemas = {
75
+ mergeExtendObjSchema: async () => {
76
+ return {
77
+ errorsFound: ['Parent schema not found in S3'],
78
+ result: null
79
+ };
80
+ }
81
+ };
82
+
83
+ const inputSchemas = [
84
+ {
85
+ objectType: 'unitStandard',
86
+ extendObjType: {
87
+ serviceTag: 'CoreService',
88
+ objectType: 'missing'
89
+ }
90
+ }
91
+ ];
92
+
93
+ await assert.rejects(
94
+ resolveExtendedObjectSchemas(inputSchemas, 'bucket', {
95
+ getObjectSchema: mockServiceSchemas
96
+ }),
97
+ /Parent schema not found in S3/
98
+ );
99
+ }
100
+ );
101
+
102
+ test(
103
+ 'resolveExtendedObjectSchemas unwraps belongToSchema from getObjSchemaS3',
104
+ async () => {
105
+ const mockServiceSchemas = {
106
+ getObjSchemaS3: async () => {
107
+ return {
108
+ errorsFound: [],
109
+ result: {
110
+ objectType: 'organization',
111
+ generatedBy: 'user'
112
+ }
113
+ };
114
+ }
115
+ };
116
+
117
+ const inputSchemas = [
118
+ {
119
+ objectType: 'member',
120
+ belongTo: {
121
+ serviceTag: 'OrgService',
122
+ objectType: 'organization'
123
+ }
124
+ }
125
+ ];
126
+
127
+ const resolved = await resolveExtendedObjectSchemas(
128
+ inputSchemas,
129
+ 'bucket',
130
+ { getObjectSchema: mockServiceSchemas }
131
+ );
132
+ assert.equal(resolved.length, 1);
133
+ assert.equal(resolved[0].belongToSchema?.objectType, 'organization');
134
+ assert.equal(resolved[0].belongToSchema?.result, undefined);
135
+ }
136
+ );