@izara_project/izara-core-generate-service-code 1.0.95 → 1.0.96

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@izara_project/izara-core-generate-service-code",
3
- "version": "1.0.95",
3
+ "version": "1.0.96",
4
4
  "description": "Code for locally generating per service files",
5
5
  "homepage": "https://bitbucket.org/izara-core-support-services/izara-core-support-services-generate-service-code#readme",
6
6
  "repository": {
@@ -1,3 +1,6 @@
1
+ import { resolveTargetServicePath } from './resolveTargetServicePath.js';
2
+ import { emitAttributeTreeTargetSchemas } from './emitAttributeTreeTargetSchemas.js';
3
+
1
4
  /**
2
5
  * Emits dynamic ObjectSchemas and RelationshipSchemas for attributeTree add-ons
3
6
  */
@@ -68,12 +71,10 @@ export function attributeTreeObjectSchemaManagedPaths(objects = []) {
68
71
 
69
72
  for (const { attributeTreeTag } of trees) {
70
73
  const tag = attributeTreeTag;
71
- const tagCap = _capitalize(tag);
72
74
  paths.push(
73
75
  `app/src/schemas/ObjectSchemas/generatedAttributeTree/${tag}Link.js`,
74
- `app/src/schemas/ObjectSchemas/generatedAttributeTree/attribute${tagCap}Value.js`,
75
- `app/src/schemas/ObjectSchemas/generatedAttributeTree/${tag}Label.js`,
76
- `app/src/schemas/ObjectSchemas/generatedAttributeTree/${tag}ValueStandard.js`
76
+ `app/src/schemas/ObjectSchemas/generatedAttributeTree/${tag}Value.js`,
77
+ `app/src/schemas/ObjectSchemas/generatedAttributeTree/${tag}Label.js`
77
78
  );
78
79
  }
79
80
 
@@ -144,14 +145,14 @@ export function emitAttributeTreeObjectSchemas({ objects = [] } = {}) {
144
145
  content: _renderObjectSchema(linkSchema)
145
146
  });
146
147
 
147
- // 2. attribute{Tag}Value.js
148
+ // 2. {tag}Value.js
148
149
  const valueSchema = {
149
- objectType: `attribute${tagCap}Value`,
150
+ objectType: `${tag}Value`,
150
151
  canDelete: true,
151
152
  generatedBy: 'user',
152
153
  storageResources: graphStorageResources,
153
154
  fieldNames: {
154
- [`attribute${tagCap}ValueId`]: {
155
+ [`${tag}ValueId`]: {
155
156
  type: 'string',
156
157
  randomOnCreate: true,
157
158
  canUpdate: false,
@@ -167,12 +168,12 @@ export function emitAttributeTreeObjectSchemas({ objects = [] } = {}) {
167
168
  identifiers: [
168
169
  {
169
170
  type: 'identifier',
170
- fieldName: `attribute${tagCap}ValueId`
171
+ fieldName: `${tag}ValueId`
171
172
  }
172
173
  ]
173
174
  };
174
175
  artifacts.push({
175
- path: `app/src/schemas/ObjectSchemas/generatedAttributeTree/attribute${tagCap}Value.js`,
176
+ path: `app/src/schemas/ObjectSchemas/generatedAttributeTree/${tag}Value.js`,
176
177
  content: _renderObjectSchema(valueSchema)
177
178
  });
178
179
 
@@ -207,38 +208,6 @@ export function emitAttributeTreeObjectSchemas({ objects = [] } = {}) {
207
208
  path: `app/src/schemas/ObjectSchemas/generatedAttributeTree/${tag}Label.js`,
208
209
  content: _renderObjectSchema(labelSchema)
209
210
  });
210
-
211
- // 4. {tag}ValueStandard.js
212
- const standardValueSchema = {
213
- objectType: `${tag}ValueStandard`,
214
- canDelete: true,
215
- generatedBy: 'user',
216
- storageResources: graphStorageResources,
217
- fieldNames: {
218
- [`${tag}ValueStandardId`]: {
219
- type: 'string',
220
- randomOnCreate: true,
221
- canUpdate: false,
222
- storageResourceTags
223
- },
224
- value: {
225
- type: 'string',
226
- requiredOnCreate: true,
227
- canUpdate: true,
228
- storageResourceTags
229
- }
230
- },
231
- identifiers: [
232
- {
233
- type: 'identifier',
234
- fieldName: `${tag}ValueStandardId`
235
- }
236
- ]
237
- };
238
- artifacts.push({
239
- path: `app/src/schemas/ObjectSchemas/generatedAttributeTree/${tag}ValueStandard.js`,
240
- content: _renderObjectSchema(standardValueSchema)
241
- });
242
211
  }
243
212
 
244
213
  return artifacts;
@@ -268,6 +237,7 @@ export function emitAttributeTreeRelationshipSchemas({
268
237
  const hasLinkRel = {
269
238
  fieldNames: {},
270
239
  storageResources: graphStorageResources,
240
+ generatedBy: 'user',
271
241
  links: [
272
242
  {
273
243
  storageResourceTags,
@@ -283,7 +253,8 @@ export function emitAttributeTreeRelationshipSchemas({
283
253
  serviceTag: effectiveServiceTag,
284
254
  objectType: `${tag}Link`
285
255
  },
286
- linkType: 'many'
256
+ linkType: 'many',
257
+ requiredOnCreate: true
287
258
  }
288
259
  }
289
260
  ]
@@ -297,6 +268,7 @@ export function emitAttributeTreeRelationshipSchemas({
297
268
  const disabledLinkRel = {
298
269
  fieldNames: {},
299
270
  storageResources: graphStorageResources,
271
+ generatedBy: 'user',
300
272
  links: [
301
273
  {
302
274
  storageResourceTags,
@@ -329,6 +301,7 @@ export function emitAttributeTreeRelationshipSchemas({
329
301
  const isRel = {
330
302
  fieldNames: {},
331
303
  storageResources: graphStorageResources,
304
+ generatedBy: 'systemNoUser',
332
305
  links: [
333
306
  {
334
307
  storageResourceTags,
@@ -337,14 +310,15 @@ export function emitAttributeTreeRelationshipSchemas({
337
310
  serviceTag: effectiveServiceTag,
338
311
  objectType: `${tag}Link`
339
312
  },
340
- linkType: 'one'
313
+ linkType: 'many',
314
+ requiredOnCreate: true
341
315
  },
342
316
  to: {
343
317
  objType: {
344
318
  serviceTag: effectiveServiceTag,
345
- objectType: `attribute${tagCap}Value`
319
+ objectType: `${tag}Value`
346
320
  },
347
- linkType: 'many'
321
+ linkType: 'one'
348
322
  }
349
323
  }
350
324
  ]
@@ -358,22 +332,24 @@ export function emitAttributeTreeRelationshipSchemas({
358
332
  const isLabelRel = {
359
333
  fieldNames: {},
360
334
  storageResources: graphStorageResources,
335
+ generatedBy: 'systemNoUser',
361
336
  links: [
362
337
  {
363
338
  storageResourceTags,
364
339
  from: {
365
340
  objType: {
366
341
  serviceTag: effectiveServiceTag,
367
- objectType: `${tag}Link`
342
+ objectType: `${tag}Value`
368
343
  },
369
- linkType: 'one'
344
+ linkType: 'many',
345
+ requiredOnCreate: true
370
346
  },
371
347
  to: {
372
348
  objType: {
373
349
  serviceTag: effectiveServiceTag,
374
350
  objectType: `${tag}Label`
375
351
  },
376
- linkType: 'many'
352
+ linkType: 'one'
377
353
  }
378
354
  }
379
355
  ]
@@ -386,3 +362,8 @@ export function emitAttributeTreeRelationshipSchemas({
386
362
 
387
363
  return artifacts;
388
364
  }
365
+
366
+ export {
367
+ resolveTargetServicePath,
368
+ emitAttributeTreeTargetSchemas
369
+ };
@@ -0,0 +1,153 @@
1
+ import path from 'node:path';
2
+
3
+ function _renderObjectSchema(schema) {
4
+ return `export default [\n ${JSON.stringify(schema, null, 2).replace(/\n/g, '\n ')}\n];\n`;
5
+ }
6
+
7
+ /**
8
+ * Emits standard schemas into target service folder for attributeTree.
9
+ *
10
+ * @param {Object} params
11
+ * @param {string} params.attributeTag - e.g. 'sellOfferTerm'
12
+ * @param {string} params.targetServiceRoot - Target service root directory
13
+ * @param {Object} [params.graphStorageResources] - Graph storage resources
14
+ * @returns {Array<{ path: string, content: string }>} Emitted target artifacts
15
+ */
16
+ export function emitAttributeTreeTargetSchemas({
17
+ attributeTag,
18
+ targetServiceRoot,
19
+ graphStorageResources = {
20
+ myGraph: {
21
+ storageType: 'graph',
22
+ graphServerTag: 'GraphHandler'
23
+ }
24
+ }
25
+ } = {}) {
26
+ if (!attributeTag || !targetServiceRoot) {
27
+ return [];
28
+ }
29
+
30
+ const tag = attributeTag;
31
+ const storageResourceTags = Object.keys(graphStorageResources);
32
+
33
+ // 1. {tag}ValueStandardPropertyLabel.js
34
+ const labelSchema = {
35
+ objectType: `${tag}ValueStandardPropertyLabel`,
36
+ canDelete: true,
37
+ generatedBy: 'user',
38
+ storageResources: graphStorageResources,
39
+ fieldNames: {
40
+ [`${tag}ValueStandardPropertyLabelId`]: {
41
+ type: 'string',
42
+ randomOnCreate: true,
43
+ canUpdate: false,
44
+ storageResourceTags
45
+ },
46
+ label: {
47
+ type: 'string',
48
+ requiredOnCreate: true,
49
+ canUpdate: true,
50
+ storageResourceTags
51
+ }
52
+ },
53
+ identifiers: [
54
+ {
55
+ type: 'identifier',
56
+ fieldName: `${tag}ValueStandardPropertyLabelId`
57
+ }
58
+ ]
59
+ };
60
+
61
+ // 2. {tag}ValueStandardPropertyValue.js
62
+ const valueSchema = {
63
+ objectType: `${tag}ValueStandardPropertyValue`,
64
+ canDelete: true,
65
+ generatedBy: 'user',
66
+ storageResources: graphStorageResources,
67
+ fieldNames: {
68
+ [`${tag}ValueStandardPropertyValueId`]: {
69
+ type: 'string',
70
+ randomOnCreate: true,
71
+ canUpdate: false,
72
+ storageResourceTags
73
+ },
74
+ value: {
75
+ type: 'string',
76
+ requiredOnCreate: true,
77
+ canUpdate: true,
78
+ storageResourceTags
79
+ }
80
+ },
81
+ identifiers: [
82
+ {
83
+ type: 'identifier',
84
+ fieldName: `${tag}ValueStandardPropertyValueId`
85
+ }
86
+ ]
87
+ };
88
+
89
+ // 3. {tag}ValueStandard.js
90
+ const standardValueSchema = {
91
+ objectType: `${tag}ValueStandard`,
92
+ canDelete: true,
93
+ generatedBy: 'user',
94
+ storageResources: graphStorageResources,
95
+ fieldNames: {
96
+ [`${tag}ValueStandardId`]: {
97
+ type: 'string',
98
+ randomOnCreate: true,
99
+ canUpdate: false,
100
+ storageResourceTags
101
+ },
102
+ value: {
103
+ type: 'string',
104
+ requiredOnCreate: true,
105
+ canUpdate: true,
106
+ storageResourceTags
107
+ }
108
+ },
109
+ identifiers: [
110
+ {
111
+ type: 'identifier',
112
+ fieldName: `${tag}ValueStandardId`
113
+ }
114
+ ]
115
+ };
116
+
117
+ const relDir = path.join(
118
+ 'app',
119
+ 'src',
120
+ 'schemas',
121
+ 'ObjectSchemas',
122
+ 'generatedCode',
123
+ 'attributeTree',
124
+ 'propertyValue'
125
+ );
126
+
127
+ return [
128
+ {
129
+ path: path.join(
130
+ targetServiceRoot,
131
+ relDir,
132
+ `${tag}ValueStandardPropertyLabel.js`
133
+ ),
134
+ content: _renderObjectSchema(labelSchema)
135
+ },
136
+ {
137
+ path: path.join(
138
+ targetServiceRoot,
139
+ relDir,
140
+ `${tag}ValueStandardPropertyValue.js`
141
+ ),
142
+ content: _renderObjectSchema(valueSchema)
143
+ },
144
+ {
145
+ path: path.join(
146
+ targetServiceRoot,
147
+ relDir,
148
+ `${tag}ValueStandard.js`
149
+ ),
150
+ content: _renderObjectSchema(standardValueSchema)
151
+ }
152
+ ];
153
+ }
@@ -0,0 +1,111 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+
4
+ /**
5
+ * Resolves target service root path based on attributeTag and developer suffix.
6
+ *
7
+ * @param {Object} params
8
+ * @param {string} params.currentServiceRoot - Root path of the current service
9
+ * @param {string} params.attributeTag - The attribute tag, e.g. 'sellOfferTerm'
10
+ * @param {string} [params.customOverridePath] - Optional custom path override
11
+ * @returns {string|null} Resolved absolute path or null if not found
12
+ */
13
+ export function resolveTargetServicePath({
14
+ currentServiceRoot,
15
+ attributeTag,
16
+ customOverridePath
17
+ } = {}) {
18
+ if (!currentServiceRoot || !attributeTag) {
19
+ return null;
20
+ }
21
+
22
+ if (customOverridePath) {
23
+ const resolvedCustom = path.resolve(
24
+ currentServiceRoot,
25
+ customOverridePath
26
+ );
27
+ if (fs.existsSync(resolvedCustom)) {
28
+ return resolvedCustom;
29
+ }
30
+ }
31
+
32
+ const currentFolderName = path.basename(currentServiceRoot);
33
+ const parentDir = path.resolve(currentServiceRoot, '..');
34
+
35
+ if (!fs.existsSync(parentDir)) {
36
+ return null;
37
+ }
38
+
39
+ let entries = [];
40
+ try {
41
+ entries = fs.readdirSync(parentDir, { withFileTypes: true });
42
+ } catch (err) {
43
+ return null;
44
+ }
45
+
46
+ const suffixMatch = currentFolderName.match(/-([a-zA-Z0-9]+)$/);
47
+ let devSuffix = '';
48
+ if (suffixMatch && suffixMatch[1]) {
49
+ devSuffix = suffixMatch[1].toLowerCase();
50
+ }
51
+
52
+ const normalizedTag = attributeTag.toLowerCase();
53
+ const kebabTag = attributeTag
54
+ .replace(/([a-z0-9])([A-Z])/g, '$1-$2')
55
+ .toLowerCase();
56
+
57
+ const priority1Matches = [];
58
+ const priority2Matches = [];
59
+ const priority3Matches = [];
60
+
61
+ for (const entry of entries) {
62
+ if (!entry.isDirectory()) {
63
+ continue;
64
+ }
65
+
66
+ if (entry.name === currentFolderName) {
67
+ continue;
68
+ }
69
+
70
+ const folderNameLower = entry.name.toLowerCase();
71
+ const hasTagMatch =
72
+ folderNameLower.includes(normalizedTag) ||
73
+ folderNameLower.includes(kebabTag);
74
+
75
+ if (!hasTagMatch) {
76
+ continue;
77
+ }
78
+
79
+ const fullPath = path.join(parentDir, entry.name);
80
+ const hasStructure =
81
+ fs.existsSync(path.join(fullPath, 'app', 'src')) ||
82
+ fs.existsSync(path.join(fullPath, 'package.json'));
83
+
84
+ if (!hasStructure) {
85
+ continue;
86
+ }
87
+
88
+ if (devSuffix !== '' && folderNameLower.endsWith('-' + devSuffix)) {
89
+ priority1Matches.push(fullPath);
90
+ } else if (
91
+ folderNameLower.endsWith(normalizedTag) ||
92
+ folderNameLower.endsWith(kebabTag)
93
+ ) {
94
+ priority2Matches.push(fullPath);
95
+ } else {
96
+ priority3Matches.push(fullPath);
97
+ }
98
+ }
99
+
100
+ if (priority1Matches.length > 0) {
101
+ return priority1Matches[0];
102
+ }
103
+ if (priority2Matches.length > 0) {
104
+ return priority2Matches[0];
105
+ }
106
+ if (priority3Matches.length > 0) {
107
+ return priority3Matches[0];
108
+ }
109
+
110
+ return null;
111
+ }
@@ -53,10 +53,19 @@ export async function emitExtTopicStep({ flow, step, flowTag, stepName }) {
53
53
  };
54
54
  }
55
55
 
56
+ const flowSteps = flow?.flowSteps ?? [];
57
+ const isLastStep =
58
+ flowSteps.length > 1 &&
59
+ flowSteps[flowSteps.length - 1]?.stepName === step?.stepName;
60
+
56
61
  const artifacts = [
57
62
  {
58
63
  path: `${source}/${mainFileName}.js`,
59
- content: await renderStepMain(functionName, _startConfig(flow))
64
+ content: await renderStepMain(functionName, _startConfig(flow), {
65
+ flow,
66
+ flowTag: flow.flowTag,
67
+ isLastStep
68
+ })
60
69
  }
61
70
  ];
62
71
  const functions = [];
@@ -107,10 +116,19 @@ export async function emitDsqStep({ flow, step, flowTag, stepName }) {
107
116
  stepEvents = step.event;
108
117
  }
109
118
  const artifacts = [];
119
+ const flowSteps = flow?.flowSteps ?? [];
120
+ const isLastStep =
121
+ flowSteps.length > 1 &&
122
+ flowSteps[flowSteps.length - 1]?.stepName === step?.stepName;
123
+
110
124
  if (!stepEvents.includes('extTopic')) {
111
125
  artifacts.push({
112
126
  path: `${source}/${mainFileName}.js`,
113
- content: await renderStepMain(functionName, startConfig)
127
+ content: await renderStepMain(functionName, startConfig, {
128
+ flow,
129
+ flowTag: flow.flowTag,
130
+ isLastStep
131
+ })
114
132
  });
115
133
  }
116
134
  const functions = [];
@@ -343,8 +361,19 @@ export function storedCacheTableName(flow, objects) {
343
361
  );
344
362
  }
345
363
 
346
- export async function renderStepMain(functionName, startConfig = null) {
347
- return renderCustom('FlowStep_Main.ejs', { functionName, startConfig });
364
+ export async function renderStepMain(
365
+ functionName,
366
+ startConfig = null,
367
+ options = {}
368
+ ) {
369
+ const { flow, flowTag, isLastStep } = options;
370
+ return renderCustom('FlowStep_Main.ejs', {
371
+ functionName,
372
+ startConfig,
373
+ flow,
374
+ flowTag: flowTag || flow?.flowTag,
375
+ isLastStep: Boolean(isLastStep)
376
+ });
348
377
  }
349
378
 
350
379
  export function renderMain(functionName) {
@@ -26,6 +26,9 @@ import lambdaSharedLib from '@izara_project/izara-core-library-lambda';
26
26
  <% if (typeof flow !== 'undefined' && flow.statusType === 'storedCache') { %>
27
27
  import storedCacheSharedLib from '@izara_project/izara-core-library-stored-cache';
28
28
  <% } %>
29
+ <% if (typeof flow !== 'undefined' && flow.statusType === 'statusField') { %>
30
+ import statusFieldSharedLib from '@izara_project/izara-core-library-status-field';
31
+ <% } %>
29
32
  <% if (typeof flow !== 'undefined' && flow.statusType === 'triggerCache') { %>
30
33
  import { triggeredCacheSharedLibV2 } from '@izara_project/izara-core-library-trigger-cache';
31
34
  <% } %>
@@ -69,13 +72,48 @@ export default async function <%- typeof functionName !== 'undefined' ? function
69
72
  //(</additionalParams>)
70
73
  ) {
71
74
  try {
72
- _izContext.logger.debug(`[${process.env.iz_serviceTag}:<%- typeof functionName !== 'undefined' ? functionName : upperCaseFlowTag %>] _izContext:`, _izContext);
73
- _izContext.logger.debug(`[${process.env.iz_serviceTag}:<%- typeof functionName !== 'undefined' ? functionName : upperCaseFlowTag %>] requestParams:`, requestParams);
74
- _izContext.logger.debug(
75
- `[${process.env.iz_serviceTag}:<%- typeof functionName !== 'undefined' ? functionName : upperCaseFlowTag %>] callingFlowConfig:`,
75
+ _izContext.logger.debug('[<%- typeof functionName !== 'undefined' ? functionName : upperCaseFlowTag %>] started', {
76
+ requestParams,
76
77
  callingFlowConfig
78
+ });
79
+
80
+ <% if (typeof flow !== 'undefined' && flow.statusType === 'statusField') { -%>
81
+
82
+ //(<beforeCheckedStatusField>)
83
+ //(</beforeCheckedStatusField>)
84
+
85
+ let [status, flowStatusFieldRecord] = await statusFieldSharedLib.checkStatusField(
86
+ _izContext,
87
+ {
88
+ flowTag: '<%= flowTag %>',
89
+ serviceTag: process.env.iz_serviceTag
90
+ },
91
+ {
92
+ //(<identifiers>)
93
+ //(</identifiers>)
94
+ },
95
+ //(<overwriteUniqueRequestId>)
96
+ //(</overwriteUniqueRequestId>)
97
+ );
98
+
99
+ _izContext.logger.debug(
100
+ `[${process.env.iz_serviceTag}:<%- typeof functionName !== 'undefined' ? functionName : upperCaseFlowTag %>] checkStatusField result:`,
101
+ { status, flowStatusFieldRecord }
77
102
  );
78
103
 
104
+ //(<afterCheckStatusField>)
105
+ //(</afterCheckStatusField>)
106
+
107
+ if (status !== 'process') {
108
+ if (status === 'error' || status === 'complete') {
109
+ //(<afterValidateStatusField>)
110
+ //(</afterValidateStatusField>)
111
+ }
112
+
113
+ //(<checkStatusFieldHook>)
114
+ //(</checkStatusFieldHook>)
115
+ }
116
+ <% } -%>
79
117
  <% if (typeof flow !== 'undefined' && flow.statusType === 'triggerCache') { -%>
80
118
 
81
119
  //(<beforeCheckTriggeredCache>)
@@ -217,6 +255,30 @@ export default async function <%- typeof functionName !== 'undefined' ? function
217
255
 
218
256
  //(<endpointHook>)
219
257
  //(</endpointHook>)
258
+ <% if (typeof flow !== 'undefined' && flow.statusType === 'statusField' && (!flow.flowSteps || flow.flowSteps.length <= 1)) { -%>
259
+
260
+ //(<beforeCompleteStatusField>)
261
+ //(</beforeCompleteStatusField>)
262
+
263
+ await statusFieldSharedLib.completeStatusField(
264
+ _izContext,
265
+ {
266
+ flowTag: '<%= flowTag %>',
267
+ serviceTag: process.env.iz_serviceTag
268
+ },
269
+ {
270
+ //(<identifiers>)
271
+ //(</identifiers>)
272
+ },
273
+ //(<statusField>)
274
+ //(</statusField>)
275
+ //(<errorFounds>)
276
+ //(</errorFounds>)
277
+ );
278
+
279
+ //(<afterCompleteStatusField>)
280
+ //(</afterCompleteStatusField>)
281
+ <% } -%>
220
282
  <% } -%>
221
283
 
222
284
  } catch (err) {
@@ -16,6 +16,9 @@ along with this program. If not, see
16
16
  <http: //www.gnu.org/licenses />.
17
17
  */
18
18
  import { identifierUuid } from '@izara_project/izara-shared-core';
19
+ <% if (typeof flow !== 'undefined' && flow.statusType === 'statusField' && typeof isLastStep !== 'undefined' && isLastStep) { %>
20
+ import statusFieldSharedLib from '@izara_project/izara-core-library-status-field';
21
+ <% } %>
19
22
 
20
23
  //(<optionalRequire>)
21
24
  //(</optionalRequire>)
@@ -51,6 +54,30 @@ export default async function <%= functionName %>(
51
54
 
52
55
  //(<hookCode>)
53
56
  //(</hookCode>)
57
+ <% if (typeof flow !== 'undefined' && flow.statusType === 'statusField' && typeof isLastStep !== 'undefined' && isLastStep) { -%>
58
+
59
+ //(<beforeCompleteStatusField>)
60
+ //(</beforeCompleteStatusField>)
61
+
62
+ await statusFieldSharedLib.completeStatusField(
63
+ _izContext,
64
+ {
65
+ flowTag: '<%= flowTag %>',
66
+ serviceTag: process.env.iz_serviceTag
67
+ },
68
+ {
69
+ //(<identifiers>)
70
+ //(</identifiers>)
71
+ },
72
+ //(<statusField>)
73
+ //(</statusField>)
74
+ //(<errorFounds>)
75
+ //(</errorFounds>)
76
+ );
77
+
78
+ //(<afterCompleteStatusField>)
79
+ //(</afterCompleteStatusField>)
80
+ <% } -%>
54
81
  } catch (err) {
55
82
  _izContext.logger.error(`[${process.env.iz_serviceTag}:<%= functionName %>] error:`, err);
56
83
  throw err;
package/src/generate.js CHANGED
@@ -1,3 +1,5 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
1
3
  import { buildGenerationPlan } from './plan/buildGenerationPlan.js';
2
4
  import { commitArtifacts } from './output/commitArtifacts.js';
3
5
  import { compileProject } from './compile/compileProject.js';
@@ -59,7 +61,9 @@ import {
59
61
  emitAttributeTreeObjectSchemas,
60
62
  emitAttributeTreeRelationshipSchemas,
61
63
  attributeTreeObjectSchemaManagedPaths,
62
- attributeTreeRelationshipSchemaManagedPaths
64
+ attributeTreeRelationshipSchemaManagedPaths,
65
+ resolveTargetServicePath,
66
+ emitAttributeTreeTargetSchemas
63
67
  } from './emit/attributeTree/emitAttributeTreeSchemas.js';
64
68
  import {
65
69
  emitErrorSchemas,
@@ -147,6 +151,11 @@ export async function generate(rootPath, options = {}) {
147
151
  )
148
152
  );
149
153
  }
154
+ await _emitTargetServiceAttributeTreeSchemas({
155
+ project,
156
+ objects,
157
+ logger: options.logger
158
+ });
150
159
  const durationMs = Date.now() - startedAt;
151
160
  console.log(
152
161
  `[INFO] [Generate] STATUS=FINISHED | ` +
@@ -343,3 +352,50 @@ async function _emitGroups({
343
352
  }
344
353
  ];
345
354
  }
355
+
356
+ async function _emitTargetServiceAttributeTreeSchemas({
357
+ project,
358
+ objects,
359
+ logger
360
+ }) {
361
+ if (!Array.isArray(objects)) {
362
+ return;
363
+ }
364
+ for (const object of objects) {
365
+ if (!Array.isArray(object?.addOnDataStructure)) {
366
+ continue;
367
+ }
368
+ for (const addOn of object.addOnDataStructure) {
369
+ if (addOn?.type === 'attributeTree' && addOn?.attributeTreeTag) {
370
+ const attributeTag = addOn.attributeTreeTag;
371
+ const targetServiceRoot = resolveTargetServicePath({
372
+ currentServiceRoot: project.rootPath,
373
+ attributeTag
374
+ });
375
+ if (targetServiceRoot) {
376
+ const artifacts = emitAttributeTreeTargetSchemas({
377
+ attributeTag,
378
+ targetServiceRoot
379
+ });
380
+ for (const artifact of artifacts) {
381
+ await fs.promises.mkdir(path.dirname(artifact.path), {
382
+ recursive: true
383
+ });
384
+ await fs.promises.writeFile(
385
+ artifact.path,
386
+ artifact.content,
387
+ 'utf8'
388
+ );
389
+ }
390
+ if (logger && typeof logger.info === 'function') {
391
+ logger.info(
392
+ `[INFO] [AttributeTree] Target schemas emitted for ` +
393
+ `tag=${attributeTag} to ${targetServiceRoot}`
394
+ );
395
+ }
396
+ }
397
+ }
398
+ }
399
+ }
400
+ }
401
+
@@ -8,7 +8,9 @@ import {
8
8
  emitAttributeTreeObjectSchemas,
9
9
  emitAttributeTreeRelationshipSchemas,
10
10
  attributeTreeObjectSchemaManagedPaths,
11
- attributeTreeRelationshipSchemaManagedPaths
11
+ attributeTreeRelationshipSchemaManagedPaths,
12
+ resolveTargetServicePath,
13
+ emitAttributeTreeTargetSchemas
12
14
  } from '../src/emit/attributeTree/emitAttributeTreeSchemas.js';
13
15
  import { generate } from '../src/generate.js';
14
16
 
@@ -22,7 +24,7 @@ after(async () => {
22
24
  );
23
25
  });
24
26
 
25
- test('emitAttributeTreeObjectSchemas returns 4 object schemas per attributeTree tag', () => {
27
+ test('emitAttributeTreeObjectSchemas returns 3 object schemas per attributeTree tag', () => {
26
28
  const objects = [
27
29
  {
28
30
  objectType: 'sellOffer',
@@ -47,16 +49,21 @@ test('emitAttributeTreeObjectSchemas returns 4 object schemas per attributeTree
47
49
  ];
48
50
 
49
51
  const artifacts = emitAttributeTreeObjectSchemas({ objects });
50
- assert.equal(artifacts.length, 4);
52
+ assert.equal(artifacts.length, 3);
51
53
 
52
54
  const paths = artifacts.map((a) => a.path);
53
55
  assert.deepEqual(paths, [
54
56
  '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'
57
+ 'app/src/schemas/ObjectSchemas/generatedAttributeTree/sellOfferTermValue.js',
58
+ 'app/src/schemas/ObjectSchemas/generatedAttributeTree/sellOfferTermLabel.js'
58
59
  ]);
59
60
 
61
+ const valueArtifact = artifacts.find((a) =>
62
+ a.path.endsWith('sellOfferTermValue.js')
63
+ );
64
+ assert.match(valueArtifact.content, /"objectType": "sellOfferTermValue"/);
65
+ assert.match(valueArtifact.content, /"sellOfferTermValueId"/);
66
+
60
67
  for (const artifact of artifacts) {
61
68
  assert.doesNotMatch(artifact.content, /dynamoDB/);
62
69
  assert.match(artifact.content, /GraphHandler/);
@@ -101,6 +108,31 @@ test('emitAttributeTreeRelationshipSchemas returns 4 relationship schemas per at
101
108
  'app/src/schemas/RelationshipSchemas/generatedAttributeTree/isSellOfferTermLabel.js'
102
109
  ]);
103
110
 
111
+ const hasRel = artifacts.find((a) => a.path.endsWith('hasSellOfferTermLink.js'));
112
+ assert.match(hasRel.content, /"generatedBy": "user"/);
113
+ assert.match(hasRel.content, /"requiredOnCreate": true/);
114
+
115
+ const disabledRel = artifacts.find((a) =>
116
+ a.path.endsWith('disabledSellOfferTermLink.js')
117
+ );
118
+ assert.match(disabledRel.content, /"generatedBy": "user"/);
119
+
120
+ const isRel = artifacts.find((a) => a.path.endsWith('isSellOfferTerm.js'));
121
+ assert.match(isRel.content, /"generatedBy": "systemNoUser"/);
122
+ assert.match(isRel.content, /"objectType": "sellOfferTermValue"/);
123
+ assert.match(isRel.content, /"requiredOnCreate": true/);
124
+
125
+ const isLabelRel = artifacts.find((a) =>
126
+ a.path.endsWith('isSellOfferTermLabel.js')
127
+ );
128
+ assert.match(isLabelRel.content, /"generatedBy": "systemNoUser"/);
129
+ assert.match(isLabelRel.content, /"requiredOnCreate": true/);
130
+ // from objType should be sellOfferTermValue
131
+ assert.match(
132
+ isLabelRel.content,
133
+ /"from":\s*\{\s*"objType":\s*\{[^}]*"objectType": "sellOfferTermValue"/
134
+ );
135
+
104
136
  for (const artifact of artifacts) {
105
137
  assert.doesNotMatch(artifact.content, /dynamoDB/);
106
138
  assert.match(artifact.content, /GraphHandler/);
@@ -217,3 +249,53 @@ async function createProjectWithAttributeTree({
217
249
 
218
250
  return rootPath;
219
251
  }
252
+
253
+ test('resolveTargetServicePath prioritizes dev suffix and matches attributeTag', async () => {
254
+ const tmpRoot = await mkdtemp(join(tmpdir(), 'izara-services-test-'));
255
+ projectRoots.add(tmpRoot);
256
+
257
+ const currentServiceDir = join(
258
+ tmpRoot,
259
+ 'izara-market-products-sell-offer-manager-pong'
260
+ );
261
+ const targetWithSuffix = join(
262
+ tmpRoot,
263
+ 'izara-market-products-sell-offer-term-pong'
264
+ );
265
+ const targetOtherDev = join(
266
+ tmpRoot,
267
+ 'izara-market-products-sell-offer-term-alice'
268
+ );
269
+ const unrelatedService = join(
270
+ tmpRoot,
271
+ 'izara-market-products-cart-pong'
272
+ );
273
+
274
+ await mkdir(join(currentServiceDir, 'app/src'), { recursive: true });
275
+ await mkdir(join(targetWithSuffix, 'app/src'), { recursive: true });
276
+ await mkdir(join(targetOtherDev, 'app/src'), { recursive: true });
277
+ await mkdir(join(unrelatedService, 'app/src'), { recursive: true });
278
+
279
+ const resolved = resolveTargetServicePath({
280
+ currentServiceRoot: currentServiceDir,
281
+ attributeTag: 'sellOfferTerm'
282
+ });
283
+
284
+ assert.equal(resolved, targetWithSuffix);
285
+ });
286
+
287
+ test('emitAttributeTreeTargetSchemas produces expected standard schemas', () => {
288
+ const targetDir = '/dummy/path/izara-market-products-sellOfferTerm';
289
+ const artifacts = emitAttributeTreeTargetSchemas({
290
+ attributeTag: 'sellOfferTerm',
291
+ targetServiceRoot: targetDir
292
+ });
293
+
294
+ assert.equal(artifacts.length, 3);
295
+ const paths = artifacts.map((a) => a.path);
296
+
297
+ assert.ok(paths.some((p) => p.endsWith('sellOfferTermValueStandardPropertyLabel.js')));
298
+ assert.ok(paths.some((p) => p.endsWith('sellOfferTermValueStandardPropertyValue.js')));
299
+ assert.ok(paths.some((p) => p.endsWith('sellOfferTermValueStandard.js')));
300
+ });
301
+
@@ -1421,6 +1421,50 @@ test('emits early return cacheMain on cache hit in FlowEndpoint_Main for storedC
1421
1421
  await parseAsEsm(mainPath);
1422
1422
  });
1423
1423
 
1424
+ test('emits canonical checkStatusField and hooks in FlowEndpoint_Main for statusField', async () => {
1425
+ const rootPath = await createProject();
1426
+ await writeFile(
1427
+ join(rootPath, 'app/src/schemas/FlowSchemas/StatusFieldFlow.js'),
1428
+ `export default {
1429
+ flowTag: 'statusFieldOrder',
1430
+ event: ['ownTopic'],
1431
+ statusType: 'statusField',
1432
+ objType: { objectType: 'order', serviceTag: 'Current' },
1433
+ flowSteps: [{ stepName: 'Start' }]
1434
+ };\n`
1435
+ );
1436
+
1437
+ await generateCustomFlowApplicationArtifacts(rootPath);
1438
+
1439
+ const mainPath = join(
1440
+ rootPath,
1441
+ 'app/src/generatedCode/FlowSchemas/StatusFieldOrder/source/StatusFieldOrder_Main.js'
1442
+ );
1443
+ const mainContent = await readFile(mainPath, 'utf8');
1444
+ assert.match(
1445
+ mainContent,
1446
+ /import statusFieldSharedLib from '@izara_project\/izara-core-library-status-field';/
1447
+ );
1448
+ assert.match(mainContent, /statusFieldSharedLib\.checkStatusField/);
1449
+ assert.match(mainContent, /\/\/\(<beforeCheckedStatusField>\)/);
1450
+ assert.match(mainContent, /\/\/\(<identifiers>\)/);
1451
+ assert.match(mainContent, /\/\/\(<overwriteUniqueRequestId>\)/);
1452
+ assert.match(mainContent, /\/\/\(<afterCheckStatusField>\)/);
1453
+ assert.match(mainContent, /\/\/\(<afterValidateStatusField>\)/);
1454
+ assert.match(mainContent, /\/\/\(<checkStatusFieldHook>\)/);
1455
+ assert.match(mainContent, /\/\/\(<endpointHook>\)/);
1456
+ assert.match(mainContent, /\/\/\(<beforeCompleteStatusField>\)/);
1457
+ assert.match(mainContent, /statusFieldSharedLib\.completeStatusField/);
1458
+ assert.match(mainContent, /\/\/\(<statusField>\)/);
1459
+ assert.match(mainContent, /\/\/\(<errorFounds>\)/);
1460
+ assert.match(mainContent, /\/\/\(<afterCompleteStatusField>\)/);
1461
+ assert.match(
1462
+ mainContent,
1463
+ /_izContext\.logger\.debug\('\[StatusFieldOrder_Main\] started', \{\s*requestParams,\s*callingFlowConfig\s*\}\);/
1464
+ );
1465
+ await parseAsEsm(mainPath);
1466
+ });
1467
+
1424
1468
  test('does not emit RbacCallback artifacts or duplicate artifact paths for custom flow with RBAC', async () => {
1425
1469
  const { generate } = await import('../src/generate.js');
1426
1470
  const rootPath = await createProject();
@@ -1538,4 +1582,90 @@ test('skips checkRbacPermission in FlowEndpoint_Main when initiateBy is systemNo
1538
1582
  await parseAsEsm(mainPath);
1539
1583
  });
1540
1584
 
1585
+ test('emits completeStatusField only in the final flowStep for statusType statusField', async () => {
1586
+ const rootPath = await createProject();
1587
+ await writeFile(
1588
+ join(rootPath, 'app/src/schemas/FlowSchemas/MultiStepStatus.js'),
1589
+ `export default {
1590
+ flowTag: 'multiStepStatus',
1591
+ event: ['ownTopic'],
1592
+ statusType: 'statusField',
1593
+ objType: { objectType: 'order', serviceTag: 'Current' },
1594
+ flowSteps: [
1595
+ {
1596
+ stepName: 'Start',
1597
+ nextSteps: [
1598
+ {
1599
+ targetType: 'extTopic',
1600
+ serviceTag: 'PaymentService',
1601
+ topicName: 'Process_In',
1602
+ toStepNames: ['Process']
1603
+ }
1604
+ ]
1605
+ },
1606
+ {
1607
+ stepName: 'Process',
1608
+ event: ['extTopic'],
1609
+ previousSteps: [{ sourceType: 'ownTopic', stepName: 'Start' }],
1610
+ nextSteps: [
1611
+ {
1612
+ targetType: 'extTopic',
1613
+ serviceTag: 'PaymentService',
1614
+ topicName: 'Finalize_In',
1615
+ toStepNames: ['Finalize']
1616
+ }
1617
+ ]
1618
+ },
1619
+ {
1620
+ stepName: 'Finalize',
1621
+ event: ['extTopic'],
1622
+ previousSteps: [{ sourceType: 'extTopic', serviceTag: 'PaymentService', topicName: 'Process_Out', fromStepNames: ['Process'] }]
1623
+ }
1624
+ ]
1625
+ };\n`
1626
+ );
1627
+
1628
+ await generateCustomFlowApplicationArtifacts(rootPath);
1629
+
1630
+ // 1. Start step (FlowEndpoint_Main) has checkStatusField, NOT completeStatusField
1631
+ const startPath = join(
1632
+ rootPath,
1633
+ 'app/src/generatedCode/FlowSchemas/MultiStepStatus/source/MultiStepStatus_Main.js'
1634
+ );
1635
+ const startContent = await readFile(startPath, 'utf8');
1636
+ assert.match(startContent, /statusFieldSharedLib\.checkStatusField/);
1637
+ assert.doesNotMatch(startContent, /completeStatusField/);
1638
+ await parseAsEsm(startPath);
1639
+
1640
+ // 2. Middle step (Process) is not the last step, so NO completeStatusField
1641
+ const processPath = join(
1642
+ rootPath,
1643
+ 'app/src/generatedCode/FlowSchemas/MultiStepStatus/source/MultiStepStatusProcess_Main.js'
1644
+ );
1645
+ const processContent = await readFile(processPath, 'utf8');
1646
+ assert.doesNotMatch(processContent, /statusFieldSharedLib/);
1647
+ assert.doesNotMatch(processContent, /completeStatusField/);
1648
+ await parseAsEsm(processPath);
1649
+
1650
+ // 3. Final step (Finalize) IS the last step, so it has completeStatusField and canonical hooks
1651
+ const finalizePath = join(
1652
+ rootPath,
1653
+ 'app/src/generatedCode/FlowSchemas/MultiStepStatus/source/MultiStepStatusFinalize_Main.js'
1654
+ );
1655
+ const finalizeContent = await readFile(finalizePath, 'utf8');
1656
+ assert.match(
1657
+ finalizeContent,
1658
+ /import statusFieldSharedLib from '@izara_project\/izara-core-library-status-field';/
1659
+ );
1660
+ assert.match(finalizeContent, /\/\/\(<beforeCompleteStatusField>\)/);
1661
+ assert.match(finalizeContent, /statusFieldSharedLib\.completeStatusField/);
1662
+ assert.match(finalizeContent, /flowTag:\s*'multiStepStatus'/);
1663
+ assert.match(finalizeContent, /\/\/\(<identifiers>\)/);
1664
+ assert.match(finalizeContent, /\/\/\(<statusField>\)/);
1665
+ assert.match(finalizeContent, /\/\/\(<errorFounds>\)/);
1666
+ assert.match(finalizeContent, /\/\/\(<afterCompleteStatusField>\)/);
1667
+ await parseAsEsm(finalizePath);
1668
+ });
1669
+
1670
+
1541
1671