@izara_project/izara-core-generate-service-code 1.0.94 → 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/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
+
@@ -88,6 +88,15 @@ test('emits handlers, function YAML, and IAM roles for supported custom flow eve
88
88
  assert.match(iam, /ApiRole:/);
89
89
  assert.match(iam, /OwnRole:/);
90
90
  assert.match(iam, /UploadRole:/);
91
+ const customSqsYaml = await readFile(
92
+ join(
93
+ rootPath,
94
+ 'resource/sls_yaml/generatedCode/source/generated-sns-sqs-custom-flows.yml'
95
+ ),
96
+ 'utf8'
97
+ );
98
+ assert.match(customSqsYaml, /ExtHdrSqs:/);
99
+ assert.doesNotMatch(customSqsYaml, /ProcessExtHdrSqs/);
91
100
  assert.match(
92
101
  await readFile(
93
102
  join(
@@ -1412,4 +1421,251 @@ test('emits early return cacheMain on cache hit in FlowEndpoint_Main for storedC
1412
1421
  await parseAsEsm(mainPath);
1413
1422
  });
1414
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
+
1468
+ test('does not emit RbacCallback artifacts or duplicate artifact paths for custom flow with RBAC', async () => {
1469
+ const { generate } = await import('../src/generate.js');
1470
+ const rootPath = await createProject();
1471
+ await writeFile(
1472
+ join(rootPath, 'app/src/schemas/ObjectSchemas/ComAcc.js'),
1473
+ `export default {
1474
+ objectType: 'comAcc',
1475
+ belongTo: {
1476
+ objectType: 'user',
1477
+ serviceTag: 'UserAccount'
1478
+ }
1479
+ };\n`
1480
+ );
1481
+ await writeFile(
1482
+ join(
1483
+ rootPath,
1484
+ 'app/src/schemas/FlowSchemas/CreateBusinessToComAcc.js'
1485
+ ),
1486
+ `export default {
1487
+ flowTag: 'createBusinessToComAcc',
1488
+ event: ['lambdaSyncInv', 'ownTopic'],
1489
+ objType: {
1490
+ objectType: 'comAcc',
1491
+ serviceTag: 'ComAcc'
1492
+ },
1493
+ flowSteps: [{ stepName: 'Start' }]
1494
+ };\n`
1495
+ );
1496
+
1497
+ const result = await generate(rootPath, {
1498
+ serviceTag: 'BusinessService'
1499
+ });
1500
+ assert.ok(result);
1501
+
1502
+ const sourceDir = join(
1503
+ rootPath,
1504
+ 'app/src/generatedCode/FlowSchemas/CreateBusinessToComAcc/source'
1505
+ );
1506
+ const mainPath = join(sourceDir, 'CreateBusinessToComAcc_Main.js');
1507
+ const mainContent = await readFile(mainPath, 'utf8');
1508
+ assert.match(mainContent, /checkRbacPermission/);
1509
+ assert.doesNotMatch(mainContent, /CreateBusinessToComAccRbacCallback/);
1510
+
1511
+ await assert.rejects(
1512
+ () =>
1513
+ readFile(
1514
+ join(sourceDir, 'CreateBusinessToComAccRbacCallback_Main.js'),
1515
+ 'utf8'
1516
+ ),
1517
+ { code: 'ENOENT' }
1518
+ );
1519
+ await assert.rejects(
1520
+ () =>
1521
+ readFile(
1522
+ join(sourceDir, 'CreateBusinessToComAccRbacCallback_HdrSqs.js'),
1523
+ 'utf8'
1524
+ ),
1525
+ { code: 'ENOENT' }
1526
+ );
1527
+
1528
+ const initialSetupConfigContent = await readFile(
1529
+ join(
1530
+ rootPath,
1531
+ 'app/src/generatedCode/CoreFunctions/InitialSetupConfig.js'
1532
+ ),
1533
+ 'utf8'
1534
+ );
1535
+ assert.doesNotMatch(
1536
+ initialSetupConfigContent,
1537
+ /CreateBusinessToComAccRbacCallbackHdrSqs/
1538
+ );
1539
+ });
1540
+
1541
+ test('skips checkRbacPermission in FlowEndpoint_Main when initiateBy is systemNoUser', async () => {
1542
+ const { generate } = await import('../src/generate.js');
1543
+ const rootPath = await createProject();
1544
+ await writeFile(
1545
+ join(rootPath, 'app/src/schemas/ObjectSchemas/ComAcc.js'),
1546
+ `export default {
1547
+ objectType: 'comAcc',
1548
+ belongTo: {
1549
+ objectType: 'user',
1550
+ serviceTag: 'UserAccount'
1551
+ }
1552
+ };\n`
1553
+ );
1554
+ await writeFile(
1555
+ join(
1556
+ rootPath,
1557
+ 'app/src/schemas/FlowSchemas/SystemCronFlow.js'
1558
+ ),
1559
+ `export default {
1560
+ flowTag: 'systemCronFlow',
1561
+ event: ['ownTopic'],
1562
+ initiateBy: 'systemNoUser',
1563
+ objType: {
1564
+ objectType: 'comAcc',
1565
+ serviceTag: 'ComAcc'
1566
+ },
1567
+ flowSteps: [{ stepName: 'Start' }]
1568
+ };\n`
1569
+ );
1570
+
1571
+ const result = await generate(rootPath, {
1572
+ serviceTag: 'BusinessService'
1573
+ });
1574
+ assert.ok(result);
1575
+
1576
+ const mainPath = join(
1577
+ rootPath,
1578
+ 'app/src/generatedCode/FlowSchemas/SystemCronFlow/source/SystemCronFlow_Main.js'
1579
+ );
1580
+ const mainContent = await readFile(mainPath, 'utf8');
1581
+ assert.doesNotMatch(mainContent, /checkRbacPermission/);
1582
+ await parseAsEsm(mainPath);
1583
+ });
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
+
1415
1671
 
@@ -125,6 +125,35 @@ test('emits relationship action and complete handlers with data-first function Y
125
125
  assert.match(yaml, /CreateRelationshipCompleteHdrSqs:/);
126
126
  });
127
127
 
128
+ test('emits valid hook tags without trailing periods and with correct callingFlowProperties placement in relationship Main files', async () => {
129
+ const rootPath = await createProject(true);
130
+ await generateRelationshipApplicationArtifacts(rootPath, {
131
+ serviceTag: 'orders'
132
+ });
133
+
134
+ const actions = ['Create', 'Update', 'Delete', 'Move', 'Change'];
135
+ for (const action of actions) {
136
+ const mainContent = await readFile(
137
+ join(
138
+ rootPath,
139
+ `app/src/generatedCode/FlowRelationships/${action}Relationship/source/${action}Relationship_Main.js`
140
+ ),
141
+ 'utf8'
142
+ );
143
+ assert.doesNotMatch(mainContent, /\/\/\(<\/[^>]+>\)\./);
144
+ }
145
+
146
+ const changeMain = await readFile(
147
+ join(
148
+ rootPath,
149
+ 'app/src/generatedCode/FlowRelationships/ChangeRelationship/source/ChangeRelationship_Main.js'
150
+ ),
151
+ 'utf8'
152
+ );
153
+ assert.match(changeMain, /\/\/\(<requestParamChangeRel>\)/);
154
+ assert.doesNotMatch(changeMain, /\/\/\(<requestParamCreateRel>\)/);
155
+ });
156
+
128
157
  test('removes managed relationship artifacts after relationships are removed', async () => {
129
158
  const rootPath = await createProject(true);
130
159
  await generateRelationshipApplicationArtifacts(rootPath, {