@izara_project/izara-core-generate-service-code 1.0.91 → 1.0.92

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 (30) hide show
  1. package/package.json +7 -10
  2. package/src/compile/flowValidator.js +6 -0
  3. package/src/compile/loadSchemas.js +177 -69
  4. package/src/emit/crud/generateCrudApplicationArtifacts.js +15 -17
  5. package/src/emit/crud/generateDynamicCrudFlowSchemas.js +16 -21
  6. package/src/emit/crud/loadLocalSchemaArrays.js +4 -8
  7. package/src/emit/custom/customFlowPaths.js +149 -0
  8. package/src/emit/custom/customFlowS3.js +156 -0
  9. package/src/emit/custom/customFlowSteps.js +385 -0
  10. package/src/emit/custom/customFlowTemplates.js +28 -0
  11. package/src/emit/custom/customFlows.js +48 -19
  12. package/src/emit/custom/emitCustomFlowApplicationArtifacts.js +57 -692
  13. package/src/emit/custom/generateCustomFlowApplicationArtifacts.js +27 -25
  14. package/src/emit/rbac/generateCreateTargetRoleHandler.js +11 -13
  15. package/src/emit/rbac/generateDynamicRbacFlowSchemas.js +14 -18
  16. package/src/emit/relationship/generateDynamicRelationshipFlowSchemas.js +16 -19
  17. package/src/emit/relationship/generateRelationshipApplicationArtifacts.js +19 -19
  18. package/src/emit/resource/dynamoResourceArtifacts.js +18 -0
  19. package/src/emit/resource/emitResourceArtifacts.js +13 -1247
  20. package/src/emit/resource/generateResourceArtifacts.js +20 -20
  21. package/src/emit/resource/iamResourceArtifacts.js +612 -0
  22. package/src/emit/resource/resourceState.js +564 -0
  23. package/src/emit/resource/resourceTemplate.js +30 -0
  24. package/src/emit/resource/s3ResourceArtifacts.js +14 -0
  25. package/src/emit/resource/sqsResourceArtifacts.js +113 -0
  26. package/src/emit/shared/runSubsystemGeneration.js +55 -0
  27. package/src/emit/system/emitSystemWebSocketArtifacts.js +2 -1
  28. package/test/crudApplicationArtifacts.test.js +72 -0
  29. package/test/customFlowApplicationArtifacts.test.js +98 -0
  30. package/test/resourceArtifacts.test.js +99 -0
@@ -0,0 +1,113 @@
1
+ import { indentYaml, renderResourceTemplate } from './resourceTemplate.js';
2
+
3
+ export async function renderResourceGroup({
4
+ ownTopics = [],
5
+ sqsQueues = [],
6
+ dsqQueues = [],
7
+ webSocketSubscriptions = [],
8
+ s3Subscriptions = []
9
+ }) {
10
+ const hasResources =
11
+ ownTopics.length > 0 ||
12
+ sqsQueues.length > 0 ||
13
+ dsqQueues.length > 0 ||
14
+ webSocketSubscriptions.length > 0 ||
15
+ s3Subscriptions.length > 0;
16
+ if (!hasResources) {
17
+ return '';
18
+ }
19
+
20
+ const chunks = await Promise.all([
21
+ ...ownTopics.map((queueName) =>
22
+ renderResourceTemplate('ResourceYaml.ejs', { queueName })
23
+ ),
24
+ ...sqsQueues.map((queueName) =>
25
+ renderResourceTemplate('ResourceSqsYaml.ejs', { queueName })
26
+ ),
27
+ ...dsqQueues.map((queueName) =>
28
+ renderResourceTemplate('ResourceSqsYaml.ejs', { queueName })
29
+ )
30
+ ]);
31
+ const wsSubs = await Promise.all(
32
+ webSocketSubscriptions.map((topicName) =>
33
+ renderWebSocketSubscription(topicName)
34
+ )
35
+ );
36
+ const s3Subs = await Promise.all(
37
+ s3Subscriptions.map(({ subscriptionName, topicName, queueName }) =>
38
+ renderResourceTemplate('S3Subscription_Yaml.ejs', {
39
+ subscriptionName,
40
+ topicName,
41
+ queueName
42
+ })
43
+ )
44
+ );
45
+ const parts = [chunks.join('\n')];
46
+ if (wsSubs.length > 0) {
47
+ parts.push(wsSubs.join('\n'));
48
+ }
49
+ if (s3Subs.length > 0) {
50
+ parts.push(s3Subs.join('\n'));
51
+ }
52
+ const body = parts.filter(Boolean).join('\n');
53
+ if (!body.trim()) {
54
+ return '';
55
+ }
56
+ return `Resources:\n${indentYaml(body, 2)}`;
57
+ }
58
+
59
+ export async function renderCrudInSqs(state) {
60
+ return renderResourceGroup({
61
+ ownTopics: state.crudOwnTopics,
62
+ sqsQueues: state.crudSqsQueues
63
+ });
64
+ }
65
+
66
+ export async function renderRelationshipInSqs(state) {
67
+ return renderResourceGroup({
68
+ ownTopics: state.relationshipOwnTopics,
69
+ sqsQueues: state.relationshipSqsQueues,
70
+ dsqQueues: state.relationshipDsqQueues
71
+ });
72
+ }
73
+
74
+ export async function renderRbacInSqs(state) {
75
+ return renderResourceGroup({
76
+ ownTopics: state.rbacOwnTopics,
77
+ sqsQueues: state.rbacSqsQueues
78
+ });
79
+ }
80
+
81
+ export async function renderCustomFlowInSqs(state) {
82
+ return renderResourceGroup({
83
+ ownTopics: state.customOwnTopics,
84
+ sqsQueues: state.customSqsQueues,
85
+ dsqQueues: state.customDsqQueues,
86
+ s3Subscriptions: state.s3Subscriptions
87
+ });
88
+ }
89
+
90
+ export async function renderSystemInSqs(state) {
91
+ return renderResourceGroup({
92
+ ownTopics: state.systemOwnTopics,
93
+ sqsQueues: state.systemSqsQueues,
94
+ dsqQueues: state.systemDsqQueues,
95
+ webSocketSubscriptions: state.webSocketSubscriptions
96
+ });
97
+ }
98
+
99
+ export async function renderOut(state) {
100
+ const renderedTopics = await Promise.all(
101
+ state.outTopics.map((topicName) =>
102
+ renderResourceTemplate('FlowOut_Yaml.ejs', { topicName })
103
+ )
104
+ );
105
+ const body = renderedTopics.join('\n');
106
+ return `Resources:\n${indentYaml(body, 2)}`;
107
+ }
108
+
109
+ export function renderWebSocketSubscription(topicName) {
110
+ return renderResourceTemplate('WebSocketSubscription_Yaml.ejs', {
111
+ topicName
112
+ });
113
+ }
@@ -0,0 +1,55 @@
1
+ import { compileProject } from '../../compile/compileProject.js';
2
+ import { buildGenerationPlan } from '../../plan/buildGenerationPlan.js';
3
+ import { commitArtifacts } from '../../output/commitArtifacts.js';
4
+
5
+ /**
6
+ * Common pipeline runner for emitting and committing subsystem artifacts.
7
+ *
8
+ * @param {Object} params
9
+ * @param {string} params.rootPath Root path of the service project.
10
+ * @param {Object} [params.options={}] Options passed to compile and load.
11
+ * @param {Function} [params.loadInputs] Function to load schemas or inputs.
12
+ * @param {Function} params.emitArtifacts Function to emit artifacts.
13
+ * @param {string} params.managedScope Scope identifier for generation plan.
14
+ * @param {Array<string>|Function} params.resolveManagedPaths Managed paths.
15
+ * @returns {Promise<{ project: Object, files: Array<string> }>}
16
+ */
17
+ export async function runSubsystemGeneration({
18
+ rootPath,
19
+ options = {},
20
+ loadInputs,
21
+ emitArtifacts,
22
+ managedScope,
23
+ resolveManagedPaths
24
+ }) {
25
+ const project = compileProject({ ...options, rootPath });
26
+
27
+ let inputs = {};
28
+ if (typeof loadInputs === 'function') {
29
+ inputs = await loadInputs(project, options);
30
+ }
31
+
32
+ const artifacts = await emitArtifacts({
33
+ project,
34
+ options,
35
+ ...inputs
36
+ });
37
+
38
+ let managedPaths = resolveManagedPaths;
39
+ if (typeof resolveManagedPaths === 'function') {
40
+ managedPaths = await resolveManagedPaths({
41
+ project,
42
+ options,
43
+ artifacts,
44
+ ...inputs
45
+ });
46
+ }
47
+
48
+ const plan = buildGenerationPlan(project, artifacts, {
49
+ managedPaths,
50
+ managedScope
51
+ });
52
+ const files = await commitArtifacts(plan);
53
+
54
+ return { project, files };
55
+ }
@@ -1,5 +1,6 @@
1
1
  import { readFile } from 'node:fs/promises';
2
2
  import ejsLib from 'ejs';
3
+ import { flowHasOwnTopic } from '../custom/customFlows.js';
3
4
 
4
5
  const TEMPLATE_URL = new URL('./templates/', import.meta.url);
5
6
  const SOURCE = 'app/src/generatedCode/SystemFlowSchemas/WebSocketMain/source';
@@ -126,7 +127,7 @@ export function hasSystemWebSocket(
126
127
  Array.isArray(flow?.event) &&
127
128
  (flow.event.includes('s3') ||
128
129
  (includeRelationshipOwnTopic &&
129
- flow.event.includes('ownTopic') &&
130
+ flowHasOwnTopic(flow) &&
130
131
  (flow.initiateBy === 'user' || flow.initiateBy === 'systemWithUser')))
131
132
  )
132
133
  );
@@ -1388,6 +1388,78 @@ test('emits autoProvisionOwnerTargetRole in CreateNodeCallback when service has
1388
1388
  );
1389
1389
  });
1390
1390
 
1391
+ test('emits synchronous before-hook invocation in Create_Main when event is generateCode and invocationType is lambdaSyncInv', async () => {
1392
+ const rootPath = await createProject(true);
1393
+ const schemas = join(rootPath, 'app/src/schemas');
1394
+ await writeFile(
1395
+ join(schemas, 'FlowSchemas/ValidateBeforeCreateSync.js'),
1396
+ "export default { flowTag: 'validateBeforeCreateSync', event: ['generateCode'], generatedCodeConfig: { generateCodeTag: 'create', codeHookTag: 'before', invocationType: 'lambdaSyncInv' }, objType: { objectType: 'order' } };\n"
1397
+ );
1398
+ await generateCrudApplicationArtifacts(rootPath, {
1399
+ serviceTag: 'orders'
1400
+ });
1401
+
1402
+ const createMain = await readFile(
1403
+ join(
1404
+ rootPath,
1405
+ 'app/src/generatedCode/FlowObjects/Create/source/Create_Main.js'
1406
+ ),
1407
+ 'utf8'
1408
+ );
1409
+ assert.match(
1410
+ createMain,
1411
+ /lambda\.invokeSync\(\s*_izContext,\s*await lambdaSharedLib\.lambdaFunctionName\(\s*_izContext,\s*'validateBeforeCreateSync'\s*\)/
1412
+ );
1413
+ assert.match(createMain, /Hook validateBeforeCreateSync failed/);
1414
+
1415
+ execFileSync(
1416
+ process.execPath,
1417
+ [
1418
+ '--check',
1419
+ join(
1420
+ rootPath,
1421
+ 'app/src/generatedCode/FlowObjects/Create/source/Create_Main.js'
1422
+ )
1423
+ ],
1424
+ { stdio: 'inherit' }
1425
+ );
1426
+ });
1427
+
1428
+ test('emits async before-hook callback and SNS publish in Create_Main when event is generateCode and invocationType is ownTopic', async () => {
1429
+ const rootPath = await createProject(true);
1430
+ const schemas = join(rootPath, 'app/src/schemas');
1431
+ await writeFile(
1432
+ join(schemas, 'FlowSchemas/ValidateBeforeCreateAsync.js'),
1433
+ "export default { flowTag: 'validateBeforeCreateAsync', event: ['generateCode'], generatedCodeConfig: { generateCodeTag: 'create', codeHookTag: 'before', invocationType: 'ownTopic' }, objType: { objectType: 'order' } };\n"
1434
+ );
1435
+ await generateCrudApplicationArtifacts(rootPath, {
1436
+ serviceTag: 'orders'
1437
+ });
1438
+
1439
+ const createMain = await readFile(
1440
+ join(
1441
+ rootPath,
1442
+ 'app/src/generatedCode/FlowObjects/Create/source/Create_Main.js'
1443
+ ),
1444
+ 'utf8'
1445
+ );
1446
+ assert.match(createMain, /sns\.publishAsync\(_izContext,/);
1447
+ assert.match(
1448
+ createMain,
1449
+ /snsTopicArnByFlowSchema\(\s*_izContext,\s*'validateBeforeCreateAsync_In'\s*\)/
1450
+ );
1451
+ assert.match(createMain, /CreateBeforeHookCallbackHdrSqs/);
1452
+
1453
+ const callbackHandler = await readFile(
1454
+ join(
1455
+ rootPath,
1456
+ 'app/src/generatedCode/FlowObjects/Create/source/CreateBeforeHookCallback_HdrSqs.js'
1457
+ ),
1458
+ 'utf8'
1459
+ );
1460
+ assert.match(callbackHandler, /CreateBeforeHookCallback_Main/);
1461
+ });
1462
+
1391
1463
  async function createProject(withCrudObject) {
1392
1464
  const rootPath = await mkdtemp(
1393
1465
  join(tmpdir(), 'generate-service-code-v2-crud-application-')
@@ -1173,3 +1173,101 @@ test('renders previousSteps extTopic subscriptions in InitialSetupConfig', async
1173
1173
  /sqsEndpoint: 'OrderBuildOrderSellOfferLinkAndReserveOrderDataHdrSqs'/
1174
1174
  );
1175
1175
  });
1176
+
1177
+ test('emits handlers, YAML and templates for flow with event generateCode and invocationType ownTopic', async () => {
1178
+ const rootPath = await mkdtemp(
1179
+ join(tmpdir(), 'generate-service-code-v2-custom-gen-own-')
1180
+ );
1181
+ roots.add(rootPath);
1182
+ const schemaRoot = join(rootPath, 'app/src/schemas/FlowSchemas');
1183
+ await mkdir(schemaRoot, { recursive: true });
1184
+ await writeFile(
1185
+ join(schemaRoot, 'GenOwn.js'),
1186
+ `export default {
1187
+ flowTag: 'genOwn',
1188
+ event: ['generateCode'],
1189
+ generatedCodeConfig: {
1190
+ generateCodeTag: 'create',
1191
+ codeHookTag: 'before',
1192
+ invocationType: 'ownTopic'
1193
+ },
1194
+ flowSteps: [{ stepName: 'Start', properties: ['dummyId'] }],
1195
+ stepProperties: { dummyId: { type: 'string' } }
1196
+ };\n`
1197
+ );
1198
+
1199
+ await generateCustomFlowApplicationArtifacts(rootPath);
1200
+
1201
+ const mainPath = join(
1202
+ rootPath,
1203
+ 'app/src/generatedCode/FlowSchemas/GenOwn/source/GenOwn_Main.js'
1204
+ );
1205
+ const hdrSqsPath = join(
1206
+ rootPath,
1207
+ 'app/src/generatedCode/FlowSchemas/GenOwn/source/GenOwn_HdrSqs.js'
1208
+ );
1209
+ const wbsMainPath = join(
1210
+ rootPath,
1211
+ 'app/src/generatedCode/FlowSchemas/GenOwn/source/GenOwn_WbsMain.js'
1212
+ );
1213
+ const hdrWbsPath = join(
1214
+ rootPath,
1215
+ 'app/src/generatedCode/FlowSchemas/GenOwn/source/GenOwn_HdrWbs.js'
1216
+ );
1217
+
1218
+ assert.match(await readFile(mainPath, 'utf8'), /asyncFlowSharedLib/);
1219
+ assert.match(await readFile(hdrSqsPath, 'utf8'), /GenOwn_Main/);
1220
+ assert.match(await readFile(wbsMainPath, 'utf8'), /GenOwn_WbsMain/);
1221
+ assert.match(await readFile(hdrWbsPath, 'utf8'), /GenOwn_WbsMain/);
1222
+
1223
+ const yaml = await readFile(
1224
+ join(rootPath, 'app/sls_yaml/generatedCode/source/flow-schema.yml'),
1225
+ 'utf8'
1226
+ );
1227
+ assert.match(yaml, /GenOwnHdrWbs:/);
1228
+ assert.match(yaml, /GenOwnHdrSqs:/);
1229
+ });
1230
+
1231
+ test('emits handler and YAML for flow with event generateCode and invocationType lambdaSyncInv', async () => {
1232
+ const rootPath = await mkdtemp(
1233
+ join(tmpdir(), 'generate-service-code-v2-custom-gen-sync-')
1234
+ );
1235
+ roots.add(rootPath);
1236
+ const schemaRoot = join(rootPath, 'app/src/schemas/FlowSchemas');
1237
+ await mkdir(schemaRoot, { recursive: true });
1238
+ await writeFile(
1239
+ join(schemaRoot, 'GenSync.js'),
1240
+ `export default {
1241
+ flowTag: 'genSync',
1242
+ event: ['generateCode'],
1243
+ generatedCodeConfig: {
1244
+ generateCodeTag: 'create',
1245
+ codeHookTag: 'before',
1246
+ invocationType: 'lambdaSyncInv'
1247
+ },
1248
+ flowSteps: [{ stepName: 'Start', properties: ['dummyId'] }],
1249
+ stepProperties: { dummyId: { type: 'string' } }
1250
+ };\n`
1251
+ );
1252
+
1253
+ await generateCustomFlowApplicationArtifacts(rootPath);
1254
+
1255
+ const mainPath = join(
1256
+ rootPath,
1257
+ 'app/src/generatedCode/FlowSchemas/GenSync/source/GenSync_Main.js'
1258
+ );
1259
+ const hdrInvPath = join(
1260
+ rootPath,
1261
+ 'app/src/generatedCode/FlowSchemas/GenSync/source/GenSync_HdrInv.js'
1262
+ );
1263
+
1264
+ assert.match(await readFile(mainPath, 'utf8'), /export default async function GenSync/);
1265
+ assert.match(await readFile(hdrInvPath, 'utf8'), /GenSync_Main/);
1266
+
1267
+ const yaml = await readFile(
1268
+ join(rootPath, 'app/sls_yaml/generatedCode/source/flow-schema.yml'),
1269
+ 'utf8'
1270
+ );
1271
+ assert.match(yaml, /GenSyncHdrInv:/);
1272
+ });
1273
+
@@ -672,6 +672,105 @@ test('emits SQS queues and deduplicated IAM policies when generateCodeTag is an
672
672
  assert.strictEqual(notifyTopicMatches?.length, 1);
673
673
  });
674
674
 
675
+ test('rejects flow schema with event generateCode missing generatedCodeConfig', async () => {
676
+ const rootPath = await createProject();
677
+ const flowDirectory = join(rootPath, 'app/src/schemas/FlowSchemas');
678
+ await writeFile(
679
+ join(flowDirectory, 'MissingConfig.js'),
680
+ "export default { flowTag: 'missingConfig', event: ['generateCode'] };\n"
681
+ );
682
+
683
+ await assert.rejects(
684
+ generateResourceArtifacts(rootPath),
685
+ /flow with event 'generateCode' requires generatedCodeConfig/
686
+ );
687
+ });
688
+
689
+ test('rejects flow schema with event generateCode and invalid invocationType', async () => {
690
+ const rootPath = await createProject();
691
+ const flowDirectory = join(rootPath, 'app/src/schemas/FlowSchemas');
692
+ await writeFile(
693
+ join(flowDirectory, 'InvalidInvocation.js'),
694
+ "export default { flowTag: 'invalidInvocation', event: ['generateCode'], " +
695
+ "generatedCodeConfig: { invocationType: 'invalidType' } };\n"
696
+ );
697
+
698
+ await assert.rejects(
699
+ generateResourceArtifacts(rootPath),
700
+ /invocationType 'invalidType' is invalid/
701
+ );
702
+ });
703
+
704
+ test('emits SQS resources, SNS topics, and IAM role when flow has event generateCode with invocationType ownTopic', async () => {
705
+ const rootPath = await createProject();
706
+ const schemas = join(rootPath, 'app/src/schemas');
707
+ await writeFile(
708
+ join(schemas, 'FlowSchemas/GenOwnHook.js'),
709
+ "export default { flowTag: 'genOwnHook', event: ['generateCode'], " +
710
+ "generatedCodeConfig: { generateCodeTag: 'create', codeHookTag: 'before', " +
711
+ "invocationType: 'ownTopic' }, objType: { objectType: 'order' } };\n"
712
+ );
713
+ await generateResourceArtifacts(rootPath);
714
+
715
+ const crudResources = await readFile(
716
+ join(
717
+ rootPath,
718
+ 'resource/sls_yaml/generatedCode/source/generated-sns-sqs-crud.yml'
719
+ ),
720
+ 'utf8'
721
+ );
722
+ assert.match(crudResources, /^ CreateBeforeHookCallbackHdrSqs:/m);
723
+
724
+ const customResources = await readFile(
725
+ join(
726
+ rootPath,
727
+ 'resource/sls_yaml/generatedCode/source/generated-sns-sqs-custom-flows.yml'
728
+ ),
729
+ 'utf8'
730
+ );
731
+ assert.match(customResources, /^ GenOwnHookHdrSqs:/m);
732
+ assert.match(customResources, /^ InGenOwnHook:/m);
733
+
734
+ const iam = await readFile(
735
+ join(
736
+ rootPath,
737
+ 'app/sls_yaml/generatedCode/source/generate-shared-resource.yml'
738
+ ),
739
+ 'utf8'
740
+ );
741
+ const flowRole = roleBlock(iam, 'FlowObjectsRole');
742
+ assert.match(flowRole, /CreateBeforeHookCallbackHdrSqs/);
743
+ assert.match(flowRole, /GenOwnHook_In/);
744
+
745
+ const customRole = roleBlock(iam, 'GenOwnHookRole');
746
+ assert.match(customRole, /GenOwnHookHdrSqs/);
747
+ assert.match(customRole, /GenOwnHook_In/);
748
+ });
749
+
750
+ test('emits lambda:InvokeFunction in FlowObjectsRole when flow has event generateCode with invocationType lambdaSyncInv', async () => {
751
+ const rootPath = await createProject();
752
+ const schemas = join(rootPath, 'app/src/schemas');
753
+ await writeFile(
754
+ join(schemas, 'FlowSchemas/GenSyncHook.js'),
755
+ "export default { flowTag: 'genSyncHook', event: ['generateCode'], " +
756
+ "generatedCodeConfig: { generateCodeTag: 'create', codeHookTag: 'before', " +
757
+ "invocationType: 'lambdaSyncInv' }, objType: { objectType: 'order' } };\n"
758
+ );
759
+ await generateResourceArtifacts(rootPath);
760
+
761
+ const iam = await readFile(
762
+ join(
763
+ rootPath,
764
+ 'app/sls_yaml/generatedCode/source/generate-shared-resource.yml'
765
+ ),
766
+ 'utf8'
767
+ );
768
+ const role = roleBlock(iam, 'FlowObjectsRole');
769
+ assert.match(role, /lambda:InvokeFunction/);
770
+ assert.match(role, /GenSyncHook/);
771
+ });
772
+
773
+
675
774
  function roleBlock(iam, roleName) {
676
775
  const match = iam.match(
677
776
  new RegExp(