@izara_project/izara-core-generate-service-code 1.0.98 → 1.0.100
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 +1 -1
- package/src/compile/compileProject.js +16 -5
- package/src/compile/flowValidator.js +47 -8
- package/src/compile/loadSchemas.js +18 -7
- package/src/compile/schemaValidatorSetup.js +16 -0
- package/src/emit/application/coreFunction/emitCoreFunctionArtifacts.js +123 -109
- package/src/emit/application/coreFunction/templates/InitialSetupConfig.ejs +8 -4
- package/src/emit/application/coreFunction/templates/InitialSetupRunner.ejs +2 -2
- package/src/emit/application/crud/templates/CreateComplete_Main.ejs +2 -0
- package/src/emit/application/crud/templates/CreateEndpoint_Main.ejs +4 -4
- package/src/emit/application/crud/templates/CreateLogicalLib.ejs +74 -15
- package/src/emit/application/crud/templates/DeleteComplete_Main.ejs +2 -0
- package/src/emit/application/crud/templates/DeleteEndpoint_Main.ejs +4 -4
- package/src/emit/application/crud/templates/FlowEndpoint_HdrWbs.ejs +7 -5
- package/src/emit/application/crud/templates/UpdateComplete_Main.ejs +2 -0
- package/src/emit/application/crud/templates/UpdateEndpoint_Main.ejs +4 -4
- package/src/emit/application/custom/customFlowPaths.js +13 -16
- package/src/emit/application/custom/customFlowS3.js +277 -66
- package/src/emit/application/custom/customFlows.js +131 -0
- package/src/emit/application/custom/emitCustomFlowApplicationArtifacts.js +3 -1
- package/src/emit/application/custom/templates/FlowEndpoint_Main.ejs +2 -2
- package/src/emit/application/custom/templates/s3/ConfirmAfterUpload_HdrSqs.ejs +110 -0
- package/src/emit/application/custom/templates/s3/ConfirmAfterUpload_Main.ejs +89 -0
- package/src/emit/application/custom/templates/s3/ConfirmComplete_HdrSqs.ejs +110 -0
- package/src/emit/application/custom/templates/s3/ConfirmComplete_Main.ejs +89 -0
- package/src/emit/application/custom/templates/s3/CreateObject_HdrWbs.ejs +2 -1
- package/src/emit/application/custom/templates/s3/CreateObject_Main.ejs +4 -0
- package/src/emit/application/custom/templates/s3/CreatePreSignUrl_HdrSqs.ejs +42 -0
- package/src/emit/application/custom/templates/s3/CreatePreSignUrl_Main.ejs +23 -0
- package/src/emit/application/custom/templates/s3/ProcessS3File_Main.ejs +135 -27
- package/src/emit/application/custom/templates/s3/Start_Main.ejs +197 -0
- package/src/emit/application/rbac/emitDynamicRbacFlowSchemas.js +8 -1
- package/src/emit/application/rbac/emitRbacApplicationArtifacts.js +107 -25
- package/src/emit/application/rbac/templates/ReservedDynamicUsage_Main.ejs +29 -9
- package/src/emit/application/rbac/templates/ReservedUsageTtlExpired_HdrDss.ejs +176 -0
- package/src/emit/application/rbac/templates/UserRbacFlow_Main.ejs +4 -3
- package/src/emit/application/system/templates/Register_HdrWbs.ejs +2 -1
- package/src/emit/infrastructure/functions/functionYamlArtifact.js +31 -8
- package/src/emit/infrastructure/functions/templates/DssFunction_Yaml.ejs +21 -0
- package/src/emit/infrastructure/functions/templates/S3Function_Yaml.ejs +2 -3
- package/src/emit/infrastructure/resources/iamResourceArtifacts.js +67 -5
- package/src/emit/infrastructure/resources/resourceState.js +15 -5
- package/src/emit/infrastructure/resources/s3ResourceArtifacts.js +19 -3
- package/src/emit/infrastructure/resources/templates/GeneratedLambdaRoleServiceConfig.ejs +5 -3
- package/src/emit/infrastructure/resources/templates/S3Bucket_Yaml.ejs +1 -1
- package/src/emit/infrastructure/resources/templates/SystemDynamoDB_Yaml.ejs +2 -2
- package/src/emit/schemas/attributeTree/emitAttributeTreeSchemas.js +62 -1
- package/src/emit/schemas/error/emitErrorSchemas.js +120 -10
- package/src/emit/schemas/error/templates/errorConsts.ejs +12 -0
- package/src/generate.js +32 -34
- package/src/output/mergeHooks.js +105 -5
- package/src/plan/domainRegistry.js +0 -10
- package/task.md +40 -96
- package/test/integration/graphDrivenFlowSchema.test.js +58 -0
- package/test/integration/publicApi.test.js +33 -24
- package/test/unit/application/createTargetRoleHandler.test.js +101 -9
- package/test/unit/application/crudApplicationArtifacts.test.js +4 -0
- package/test/unit/application/customFlowApplicationArtifacts.test.js +6 -6
- package/test/unit/application/customFlowS3.test.js +730 -0
- package/test/unit/infrastructure/functionYamlArtifact.test.js +6 -4
- package/test/unit/infrastructure/resourceArtifacts.test.js +103 -0
- package/test/unit/output/mergeHooks.test.js +137 -0
- package/test/unit/schemas/attributeTreeSchemas.test.js +32 -3
- package/test/unit/schemas/attributeTreeSingleService.test.js +175 -0
- package/test/unit/schemas/dynamicRbacFlowSchemas.test.js +8 -6
- package/test/unit/schemas/dynamicRelationshipFlowSchemas.test.js +51 -0
- package/test/unit/schemas/errorSchemas.test.js +84 -5
- package/test/unit/templates/hookTagsValidation.test.js +83 -0
- package/src/emit/schemas/attributeTree/emitAttributeTreeTargetValueStandard.js +0 -132
- package/test/unit/schemas/attributeTreeDirectives.test.js +0 -177
|
@@ -83,14 +83,16 @@ test('filters all SQS functions by their calling flow', () => {
|
|
|
83
83
|
/#<#CreateCompleteHdrSqsFilterPatterns#>[\s\S]*filterPatterns:[\s\S]*\$\{self:custom\.iz_resourcePrefix\}CreateCompleteHdrSqs/
|
|
84
84
|
);
|
|
85
85
|
assert.match(completion, /#<#\/CreateCompleteHdrSqsFilterPatterns#>/);
|
|
86
|
-
assert.match(completion, /callingFlow:\s*\n\s
|
|
87
|
-
assert.
|
|
86
|
+
assert.match(completion, /callingFlow:\s*\n\s*StringValue:/);
|
|
87
|
+
assert.doesNotMatch(completion, /callingFlow:\s*\n\s*- exists: false/);
|
|
88
|
+
assert.doesNotMatch(completion, /MessageAttributes:\s*\n\s*- exists: false/);
|
|
88
89
|
assert.match(
|
|
89
90
|
ordinary,
|
|
90
91
|
/#<#CreateHdrSqsFilterPatterns#>[\s\S]*filterPatterns:[\s\S]*\$\{self:custom\.iz_resourcePrefix\}CreateHdrSqs/
|
|
91
92
|
);
|
|
92
93
|
assert.match(ordinary, /#<#\/CreateHdrSqsFilterPatterns#>/);
|
|
93
|
-
assert.match(ordinary, /callingFlow:\s*\n\s
|
|
94
|
-
assert.
|
|
94
|
+
assert.match(ordinary, /callingFlow:\s*\n\s*StringValue:/);
|
|
95
|
+
assert.doesNotMatch(ordinary, /callingFlow:\s*\n\s*- exists: false/);
|
|
96
|
+
assert.doesNotMatch(ordinary, /MessageAttributes:\s*\n\s*- exists: false/);
|
|
95
97
|
});
|
|
96
98
|
|
|
@@ -7,6 +7,10 @@ import test, { after } from 'node:test';
|
|
|
7
7
|
process.env.NODE_ENV = 'test';
|
|
8
8
|
|
|
9
9
|
import { generateResourceArtifacts } from '../../../src/emit/infrastructure/resources/generateResourceArtifacts.js';
|
|
10
|
+
import {
|
|
11
|
+
renderS3,
|
|
12
|
+
toAlphanumericPascalCase
|
|
13
|
+
} from '../../../src/emit/infrastructure/resources/s3ResourceArtifacts.js';
|
|
10
14
|
|
|
11
15
|
const roots = new Set();
|
|
12
16
|
after(() =>
|
|
@@ -816,6 +820,58 @@ test('emits lambda:InvokeFunction in FlowObjectsRole when flow has event generat
|
|
|
816
820
|
assert.match(role, /GenSyncHook/);
|
|
817
821
|
});
|
|
818
822
|
|
|
823
|
+
test('emits DynamoDB stream permissions in FlowRbacRole and multi-tenant quota topics in FlowObjectsRole', async () => {
|
|
824
|
+
const rootPath = await createProject();
|
|
825
|
+
const schemas = join(rootPath, 'app/src/schemas');
|
|
826
|
+
await writeFile(
|
|
827
|
+
join(schemas, 'ObjectSchemas/Order.js'),
|
|
828
|
+
"export default { objectType: 'order', generatedBy: 'user', belongTo: { serviceTag: 'SupplyNetwork', objectType: 'tenant' }, addOnDataStructure: [{ type: 'rbac' }], identifiers: [{ type: 'partitionKey', fieldName: 'orderId' }], storageResources: { main: { storageType: 'dynamoDB', tableName: 'Orders' } } };\n"
|
|
829
|
+
);
|
|
830
|
+
await generateResourceArtifacts(rootPath);
|
|
831
|
+
|
|
832
|
+
const iam = await readFile(
|
|
833
|
+
join(
|
|
834
|
+
rootPath,
|
|
835
|
+
'app/sls_yaml/generatedCode/source/generate-shared-resource.yml'
|
|
836
|
+
),
|
|
837
|
+
'utf8'
|
|
838
|
+
);
|
|
839
|
+
const rbacRole = roleBlock(iam, 'FlowRbacRole');
|
|
840
|
+
assert.match(rbacRole, /dynamodb:DescribeStream/);
|
|
841
|
+
assert.match(rbacRole, /RunningTotalReservedUsage\/stream\/\*/);
|
|
842
|
+
|
|
843
|
+
const flowObjectsRole = roleBlock(iam, 'FlowObjectsRole');
|
|
844
|
+
assert.match(flowObjectsRole, /SupplyNetwork_\${self:custom\.iz_stage}_ConfirmUsage_In/);
|
|
845
|
+
assert.match(flowObjectsRole, /SupplyNetwork_\${self:custom\.iz_stage}_CancelUsage_In/);
|
|
846
|
+
assert.match(flowObjectsRole, /SupplyNetwork\${self:custom\.iz_stage}ConfirmUsageHdrInv/);
|
|
847
|
+
assert.match(flowObjectsRole, /SupplyNetwork\${self:custom\.iz_stage}CancelUsageHdrInv/);
|
|
848
|
+
|
|
849
|
+
const { createResourceState } = await import(
|
|
850
|
+
'../../../src/emit/infrastructure/resources/resourceState.js'
|
|
851
|
+
);
|
|
852
|
+
const { renderLambdaRoleConfig } = await import(
|
|
853
|
+
'../../../src/emit/infrastructure/resources/iamResourceArtifacts.js'
|
|
854
|
+
);
|
|
855
|
+
const state = createResourceState({
|
|
856
|
+
objects: [
|
|
857
|
+
{
|
|
858
|
+
objectType: 'order',
|
|
859
|
+
generatedBy: 'user',
|
|
860
|
+
belongTo: { serviceTag: 'SupplyNetwork', objectType: 'tenant' },
|
|
861
|
+
addOnDataStructure: [{ type: 'rbac' }]
|
|
862
|
+
}
|
|
863
|
+
],
|
|
864
|
+
relationships: [],
|
|
865
|
+
flows: [
|
|
866
|
+
{ flowTag: 'upload', event: ['s3'], quotaServiceTag: 'SupplyNetwork' }
|
|
867
|
+
]
|
|
868
|
+
});
|
|
869
|
+
const lambdaRoleConfig = await renderLambdaRoleConfig(state);
|
|
870
|
+
assert.match(lambdaRoleConfig, /ConfirmUsage_In', 'SupplyNetwork'/);
|
|
871
|
+
assert.match(lambdaRoleConfig, /CancelUsage_In', 'SupplyNetwork'/);
|
|
872
|
+
assert.match(lambdaRoleConfig, /ReservedDynamicUsage_In', 'SupplyNetwork'/);
|
|
873
|
+
assert.doesNotMatch(lambdaRoleConfig, /'AccountLimits'/);
|
|
874
|
+
});
|
|
819
875
|
|
|
820
876
|
function roleBlock(iam, roleName) {
|
|
821
877
|
const match = iam.match(
|
|
@@ -857,3 +913,50 @@ async function createProject() {
|
|
|
857
913
|
);
|
|
858
914
|
return rootPath;
|
|
859
915
|
}
|
|
916
|
+
|
|
917
|
+
test('toAlphanumericPascalCase converts non-alphanumeric strings to PascalCase', () => {
|
|
918
|
+
assert.equal(
|
|
919
|
+
toAlphanumericPascalCase('video-record-temp-test'),
|
|
920
|
+
'VideoRecordTempTest'
|
|
921
|
+
);
|
|
922
|
+
assert.equal(
|
|
923
|
+
toAlphanumericPascalCase('video.record_temp-test'),
|
|
924
|
+
'VideoRecordTempTest'
|
|
925
|
+
);
|
|
926
|
+
assert.equal(toAlphanumericPascalCase('AlreadyPascal'), 'AlreadyPascal');
|
|
927
|
+
assert.equal(toAlphanumericPascalCase(''), '');
|
|
928
|
+
});
|
|
929
|
+
|
|
930
|
+
test('renderS3 generates alphanumeric PascalCase Logical ID for CloudFormation', async () => {
|
|
931
|
+
const yaml = await renderS3({
|
|
932
|
+
s3Buckets: ['video-record-temp-test']
|
|
933
|
+
});
|
|
934
|
+
assert.ok(
|
|
935
|
+
yaml.includes('VideoRecordTempTestS3Bucket:'),
|
|
936
|
+
'Logical ID must be alphanumeric PascalCase without hyphens'
|
|
937
|
+
);
|
|
938
|
+
assert.ok(
|
|
939
|
+
!yaml.includes('video-record-temp-testS3Bucket:'),
|
|
940
|
+
'Logical ID must not contain hyphens'
|
|
941
|
+
);
|
|
942
|
+
assert.ok(
|
|
943
|
+
yaml.includes('BucketName: video-record-temp-test'),
|
|
944
|
+
'BucketName property must preserve original bucket name'
|
|
945
|
+
);
|
|
946
|
+
});
|
|
947
|
+
|
|
948
|
+
test('roleStatements emits s3:GetObject for custom S3 flow bucket', async () => {
|
|
949
|
+
const rootPath = await createProject();
|
|
950
|
+
await generateResourceArtifacts(rootPath);
|
|
951
|
+
const iam = await readFile(
|
|
952
|
+
join(
|
|
953
|
+
rootPath,
|
|
954
|
+
'app/sls_yaml/generatedCode/source/generate-shared-resource.yml'
|
|
955
|
+
),
|
|
956
|
+
'utf8'
|
|
957
|
+
);
|
|
958
|
+
const uploadRole = roleBlock(iam, 'UploadRole');
|
|
959
|
+
assert.match(uploadRole, /s3:GetObject/);
|
|
960
|
+
assert.match(uploadRole, /arn:aws:s3:::UploadBucket\/\*/);
|
|
961
|
+
});
|
|
962
|
+
|
|
@@ -266,3 +266,140 @@ test('respects writeWhenFileExist false flag by returning disk content unchanged
|
|
|
266
266
|
await rm(tmpDir, { recursive: true, force: true });
|
|
267
267
|
}
|
|
268
268
|
});
|
|
269
|
+
|
|
270
|
+
test('merges hook when disk content has legacy tag without closing angle bracket', async () => {
|
|
271
|
+
const tmpDir = await mkdtemp(path.join(os.tmpdir(), 'mergeHooks-test-'));
|
|
272
|
+
try {
|
|
273
|
+
const filePath = 'app/src/Flow_Main.js';
|
|
274
|
+
const diskContent = [
|
|
275
|
+
'//(<rbacPublish_CreatePlugInConn)',
|
|
276
|
+
'let plugInTag = requestParams.plugInTag;',
|
|
277
|
+
'//(</rbacPublish_CreatePlugInConn)'
|
|
278
|
+
].join('\n') + '\n';
|
|
279
|
+
await writeTestFile(tmpDir, filePath, diskContent);
|
|
280
|
+
|
|
281
|
+
const artifacts = [
|
|
282
|
+
{
|
|
283
|
+
path: filePath,
|
|
284
|
+
content: [
|
|
285
|
+
'//(<rbacPublish_CreatePlugInConn>)',
|
|
286
|
+
'//(</rbacPublish_CreatePlugInConn>)'
|
|
287
|
+
].join('\n') + '\n'
|
|
288
|
+
}
|
|
289
|
+
];
|
|
290
|
+
|
|
291
|
+
const result = await mergeHooks(tmpDir, artifacts);
|
|
292
|
+
assert.equal(
|
|
293
|
+
result[0].content,
|
|
294
|
+
[
|
|
295
|
+
'//(<rbacPublish_CreatePlugInConn>)',
|
|
296
|
+
'let plugInTag = requestParams.plugInTag;',
|
|
297
|
+
'//(</rbacPublish_CreatePlugInConn>)'
|
|
298
|
+
].join('\n') + '\n'
|
|
299
|
+
);
|
|
300
|
+
} finally {
|
|
301
|
+
await rm(tmpDir, { recursive: true, force: true });
|
|
302
|
+
}
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
test('merges hook when both template and disk content omit closing angle bracket', async () => {
|
|
306
|
+
const tmpDir = await mkdtemp(path.join(os.tmpdir(), 'mergeHooks-test-'));
|
|
307
|
+
try {
|
|
308
|
+
const filePath = 'app/src/Legacy_Main.js';
|
|
309
|
+
const diskContent = [
|
|
310
|
+
'//(<legacyHook)',
|
|
311
|
+
'const legacyValue = 42;',
|
|
312
|
+
'//(</legacyHook)'
|
|
313
|
+
].join('\n') + '\n';
|
|
314
|
+
await writeTestFile(tmpDir, filePath, diskContent);
|
|
315
|
+
|
|
316
|
+
const artifacts = [
|
|
317
|
+
{
|
|
318
|
+
path: filePath,
|
|
319
|
+
content: [
|
|
320
|
+
'//(<legacyHook)',
|
|
321
|
+
'//(</legacyHook)'
|
|
322
|
+
].join('\n') + '\n'
|
|
323
|
+
}
|
|
324
|
+
];
|
|
325
|
+
|
|
326
|
+
const result = await mergeHooks(tmpDir, artifacts);
|
|
327
|
+
assert.equal(
|
|
328
|
+
result[0].content,
|
|
329
|
+
[
|
|
330
|
+
'//(<legacyHook>)',
|
|
331
|
+
'const legacyValue = 42;',
|
|
332
|
+
'//(</legacyHook>)'
|
|
333
|
+
].join('\n') + '\n'
|
|
334
|
+
);
|
|
335
|
+
} finally {
|
|
336
|
+
await rm(tmpDir, { recursive: true, force: true });
|
|
337
|
+
}
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
test('throws error when source content has malformed hook tags (unclosed open tag)', async () => {
|
|
341
|
+
const tmpDir = await mkdtemp(path.join(os.tmpdir(), 'mergeHooks-test-'));
|
|
342
|
+
try {
|
|
343
|
+
const filePath = 'app/src/Broken_Main.js';
|
|
344
|
+
const diskContent = [
|
|
345
|
+
'//(<openHook>)',
|
|
346
|
+
'const value = 1;',
|
|
347
|
+
'// without closing tag'
|
|
348
|
+
].join('\n') + '\n';
|
|
349
|
+
await writeTestFile(tmpDir, filePath, diskContent);
|
|
350
|
+
|
|
351
|
+
const artifacts = [
|
|
352
|
+
{
|
|
353
|
+
path: filePath,
|
|
354
|
+
content: '//(<openHook>)\n//(</openHook>)\n'
|
|
355
|
+
}
|
|
356
|
+
];
|
|
357
|
+
|
|
358
|
+
await assert.rejects(
|
|
359
|
+
async () => {
|
|
360
|
+
await mergeHooks(tmpDir, artifacts);
|
|
361
|
+
},
|
|
362
|
+
/\[mergeHooks\] Malformed hook tags in 'app\/src\/Broken_Main\.js'/
|
|
363
|
+
);
|
|
364
|
+
} finally {
|
|
365
|
+
await rm(tmpDir, { recursive: true, force: true });
|
|
366
|
+
}
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
test('warns when source content contains custom code in hook missing from template', async () => {
|
|
370
|
+
const tmpDir = await mkdtemp(path.join(os.tmpdir(), 'mergeHooks-test-'));
|
|
371
|
+
const originalWarn = console.warn;
|
|
372
|
+
const warnings = [];
|
|
373
|
+
console.warn = (...args) => warnings.push(args.join(' '));
|
|
374
|
+
|
|
375
|
+
try {
|
|
376
|
+
const filePath = 'app/src/Orphaned_Main.js';
|
|
377
|
+
const diskContent = [
|
|
378
|
+
'//(<removedHook>)',
|
|
379
|
+
'const importantCustomCode = true;',
|
|
380
|
+
'//(</removedHook>)'
|
|
381
|
+
].join('\n') + '\n';
|
|
382
|
+
await writeTestFile(tmpDir, filePath, diskContent);
|
|
383
|
+
|
|
384
|
+
const artifacts = [
|
|
385
|
+
{
|
|
386
|
+
path: filePath,
|
|
387
|
+
content: '//(<newHook>)\n//(</newHook>)\n'
|
|
388
|
+
}
|
|
389
|
+
];
|
|
390
|
+
|
|
391
|
+
const result = await mergeHooks(tmpDir, artifacts);
|
|
392
|
+
assert.equal(result[0].content, '//(<newHook>)\n//(</newHook>)\n');
|
|
393
|
+
assert.ok(
|
|
394
|
+
warnings.some((w) =>
|
|
395
|
+
w.includes("Hook 'removedHook' in 'app/src/Orphaned_Main.js' contains custom code")
|
|
396
|
+
),
|
|
397
|
+
'Should log warning for orphaned hook with custom code'
|
|
398
|
+
);
|
|
399
|
+
} finally {
|
|
400
|
+
console.warn = originalWarn;
|
|
401
|
+
await rm(tmpDir, { recursive: true, force: true });
|
|
402
|
+
}
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
|
|
@@ -23,7 +23,7 @@ after(async () => {
|
|
|
23
23
|
await cleanupTestProjects();
|
|
24
24
|
});
|
|
25
25
|
|
|
26
|
-
test('emitAttributeTreeObjectSchemas returns
|
|
26
|
+
test('emitAttributeTreeObjectSchemas returns 4 object schemas per attributeTree tag', () => {
|
|
27
27
|
const objects = [
|
|
28
28
|
{
|
|
29
29
|
objectType: 'sellOffer',
|
|
@@ -48,13 +48,14 @@ test('emitAttributeTreeObjectSchemas returns 3 object schemas per attributeTree
|
|
|
48
48
|
];
|
|
49
49
|
|
|
50
50
|
const artifacts = emitAttributeTreeObjectSchemas({ objects });
|
|
51
|
-
assert.equal(artifacts.length,
|
|
51
|
+
assert.equal(artifacts.length, 4);
|
|
52
52
|
|
|
53
53
|
const paths = artifacts.map((a) => a.path);
|
|
54
54
|
assert.deepEqual(paths, [
|
|
55
55
|
'app/src/schemas/ObjectSchemas/generatedCode/attributeTree/sellOfferTermLink.js',
|
|
56
56
|
'app/src/schemas/ObjectSchemas/generatedCode/attributeTree/sellOfferTermValue.js',
|
|
57
|
-
'app/src/schemas/ObjectSchemas/generatedCode/attributeTree/sellOfferTermLabel.js'
|
|
57
|
+
'app/src/schemas/ObjectSchemas/generatedCode/attributeTree/sellOfferTermLabel.js',
|
|
58
|
+
'app/src/schemas/ObjectSchemas/generatedCode/attributeTree/sellOfferTermValueStandard.js'
|
|
58
59
|
]);
|
|
59
60
|
|
|
60
61
|
const valueArtifact = artifacts.find((a) =>
|
|
@@ -63,6 +64,26 @@ test('emitAttributeTreeObjectSchemas returns 3 object schemas per attributeTree
|
|
|
63
64
|
assert.match(valueArtifact.content, /"objectType": "sellOfferTermValue"/);
|
|
64
65
|
assert.match(valueArtifact.content, /"sellOfferTermValueId"/);
|
|
65
66
|
|
|
67
|
+
const valueStandardArtifact = artifacts.find((a) =>
|
|
68
|
+
a.path.endsWith('sellOfferTermValueStandard.js')
|
|
69
|
+
);
|
|
70
|
+
assert.match(
|
|
71
|
+
valueStandardArtifact.content,
|
|
72
|
+
/objectType:\s*['"]sellOfferTermValueStandard['"]/
|
|
73
|
+
);
|
|
74
|
+
assert.match(
|
|
75
|
+
valueStandardArtifact.content,
|
|
76
|
+
/sellOfferTermValueStandardId/
|
|
77
|
+
);
|
|
78
|
+
assert.match(
|
|
79
|
+
valueStandardArtifact.content,
|
|
80
|
+
/\/\/\(<addOnDataStructuresellOfferTermValueStandard>\)/
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
const managedPaths = attributeTreeObjectSchemaManagedPaths(objects);
|
|
84
|
+
assert.equal(managedPaths.length, 4);
|
|
85
|
+
assert.deepEqual(managedPaths, paths);
|
|
86
|
+
|
|
66
87
|
for (const artifact of artifacts) {
|
|
67
88
|
assert.doesNotMatch(artifact.content, /dynamoDB/);
|
|
68
89
|
assert.match(artifact.content, /GraphHandler/);
|
|
@@ -152,6 +173,8 @@ test('generate commits and cleans up attributeTree dynamic schemas', async () =>
|
|
|
152
173
|
|
|
153
174
|
const objectRelPath =
|
|
154
175
|
'app/src/schemas/ObjectSchemas/generatedCode/attributeTree/sellOfferTermLink.js';
|
|
176
|
+
const valueStandardRelPath =
|
|
177
|
+
'app/src/schemas/ObjectSchemas/generatedCode/attributeTree/sellOfferTermValueStandard.js';
|
|
155
178
|
const relRelPath =
|
|
156
179
|
'app/src/schemas/RelationshipSchemas/generatedCode/attributeTree/hasSellOfferTermLink.js';
|
|
157
180
|
|
|
@@ -160,6 +183,9 @@ test('generate commits and cleans up attributeTree dynamic schemas', async () =>
|
|
|
160
183
|
assert.match(objectContent, /GraphHandler/);
|
|
161
184
|
assert.doesNotMatch(objectContent, /dynamoDB/);
|
|
162
185
|
|
|
186
|
+
const valueStandardContent = await readProjectFile(rootPath, valueStandardRelPath);
|
|
187
|
+
assert.match(valueStandardContent, /sellOfferTermValueStandard/);
|
|
188
|
+
|
|
163
189
|
const relContent = await readProjectFile(rootPath, relRelPath);
|
|
164
190
|
assert.match(relContent, /hasSellOfferTermLink/);
|
|
165
191
|
assert.match(relContent, /sellOfferTermLink/);
|
|
@@ -171,6 +197,9 @@ test('generate commits and cleans up attributeTree dynamic schemas', async () =>
|
|
|
171
197
|
await assert.rejects(readProjectFile(rootPath, objectRelPath), {
|
|
172
198
|
code: 'ENOENT'
|
|
173
199
|
});
|
|
200
|
+
await assert.rejects(readProjectFile(rootPath, valueStandardRelPath), {
|
|
201
|
+
code: 'ENOENT'
|
|
202
|
+
});
|
|
174
203
|
await assert.rejects(readProjectFile(rootPath, relRelPath), {
|
|
175
204
|
code: 'ENOENT'
|
|
176
205
|
});
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import test, { after } from 'node:test';
|
|
3
|
+
import { generate } from '../../../src/generate.js';
|
|
4
|
+
import {
|
|
5
|
+
createTestProject,
|
|
6
|
+
readProjectFile,
|
|
7
|
+
writeProjectFile,
|
|
8
|
+
cleanupTestProjects
|
|
9
|
+
} from '../../helpers/testProject.js';
|
|
10
|
+
|
|
11
|
+
after(async () => {
|
|
12
|
+
await cleanupTestProjects();
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test('single-service attributeTree generation and propertyValue cascading', async () => {
|
|
16
|
+
const initialSchema = `export default [
|
|
17
|
+
{
|
|
18
|
+
objectType: 'sellOffer',
|
|
19
|
+
canDelete: false,
|
|
20
|
+
generatedBy: 'user',
|
|
21
|
+
addOnDataStructure: [
|
|
22
|
+
{
|
|
23
|
+
type: 'attributeTree',
|
|
24
|
+
attributeTreeTag: 'sellOfferTerm'
|
|
25
|
+
}
|
|
26
|
+
],
|
|
27
|
+
storageResources: {
|
|
28
|
+
dynamoDB: {
|
|
29
|
+
storageType: 'dynamoDB',
|
|
30
|
+
tableName: 'SellOfferRecords',
|
|
31
|
+
serviceTag: 'SellOfferManager'
|
|
32
|
+
},
|
|
33
|
+
myGraph: {
|
|
34
|
+
storageType: 'graph',
|
|
35
|
+
graphServerTag: 'GraphHandler'
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
fieldNames: {
|
|
39
|
+
sellOfferId: {
|
|
40
|
+
type: 'string',
|
|
41
|
+
randomOnCreate: true,
|
|
42
|
+
canUpdate: false,
|
|
43
|
+
storageResourceTags: ['dynamoDB', 'myGraph']
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
identifiers: [
|
|
47
|
+
{
|
|
48
|
+
type: 'partitionKey',
|
|
49
|
+
fieldName: 'sellOfferId'
|
|
50
|
+
}
|
|
51
|
+
]
|
|
52
|
+
}
|
|
53
|
+
];
|
|
54
|
+
`;
|
|
55
|
+
|
|
56
|
+
const rootPath = await createTestProject({
|
|
57
|
+
prefix: 'izara-single-service-attr-tree-',
|
|
58
|
+
serviceTag: 'SellOfferManager',
|
|
59
|
+
schemas: {
|
|
60
|
+
'ObjectSchemas/SellOffer.js': initialSchema
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// Phase 1: generate produces all 4 ObjectSchemas and 4 RelationshipSchemas
|
|
65
|
+
await generate(rootPath);
|
|
66
|
+
|
|
67
|
+
const linkPath =
|
|
68
|
+
'app/src/schemas/ObjectSchemas/generatedCode/attributeTree/sellOfferTermLink.js';
|
|
69
|
+
const valuePath =
|
|
70
|
+
'app/src/schemas/ObjectSchemas/generatedCode/attributeTree/sellOfferTermValue.js';
|
|
71
|
+
const labelPath =
|
|
72
|
+
'app/src/schemas/ObjectSchemas/generatedCode/attributeTree/sellOfferTermLabel.js';
|
|
73
|
+
const valueStandardPath =
|
|
74
|
+
'app/src/schemas/ObjectSchemas/generatedCode/attributeTree/sellOfferTermValueStandard.js';
|
|
75
|
+
|
|
76
|
+
const linkContent = await readProjectFile(rootPath, linkPath);
|
|
77
|
+
assert.match(linkContent, /sellOfferTermLink/);
|
|
78
|
+
|
|
79
|
+
const valueContent = await readProjectFile(rootPath, valuePath);
|
|
80
|
+
assert.match(valueContent, /sellOfferTermValue/);
|
|
81
|
+
|
|
82
|
+
const labelContent = await readProjectFile(rootPath, labelPath);
|
|
83
|
+
assert.match(labelContent, /sellOfferTermLabel/);
|
|
84
|
+
|
|
85
|
+
const valueStandardContent = await readProjectFile(
|
|
86
|
+
rootPath,
|
|
87
|
+
valueStandardPath
|
|
88
|
+
);
|
|
89
|
+
assert.match(valueStandardContent, /sellOfferTermValueStandard/);
|
|
90
|
+
assert.match(
|
|
91
|
+
valueStandardContent,
|
|
92
|
+
/\/\/\(<addOnDataStructuresellOfferTermValueStandard>\)/
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
// Check relationship schemas
|
|
96
|
+
const hasLinkRelPath =
|
|
97
|
+
'app/src/schemas/RelationshipSchemas/generatedCode/attributeTree/hasSellOfferTermLink.js';
|
|
98
|
+
const hasLinkRel = await readProjectFile(rootPath, hasLinkRelPath);
|
|
99
|
+
assert.match(hasLinkRel, /hasSellOfferTermLink/);
|
|
100
|
+
|
|
101
|
+
// Phase 2: developer uncomments propertyValue addOn in ValueStandard
|
|
102
|
+
const modifiedValueStandard = valueStandardContent.replace(
|
|
103
|
+
'//(<addOnDataStructuresellOfferTermValueStandard>)\n // {\n // type: \'propertyValue\',\n // propertyValuePrefix: \'sellOfferTermValueStandard\'\n // }\n //(</addOnDataStructuresellOfferTermValueStandard>)',
|
|
104
|
+
'//(<addOnDataStructuresellOfferTermValueStandard>)\n {\n type: \'propertyValue\',\n propertyValuePrefix: \'sellOfferTermValueStandard\'\n }\n //(</addOnDataStructuresellOfferTermValueStandard>)'
|
|
105
|
+
);
|
|
106
|
+
await writeProjectFile(rootPath, valueStandardPath, modifiedValueStandard);
|
|
107
|
+
|
|
108
|
+
await generate(rootPath);
|
|
109
|
+
|
|
110
|
+
// Verify hook preservation
|
|
111
|
+
const reloadedValueStandard = await readProjectFile(
|
|
112
|
+
rootPath,
|
|
113
|
+
valueStandardPath
|
|
114
|
+
);
|
|
115
|
+
assert.match(reloadedValueStandard, /type:\s*'propertyValue'/);
|
|
116
|
+
|
|
117
|
+
// Verify cascading generation: propertyValue domain in the same repo
|
|
118
|
+
const propLabelPath =
|
|
119
|
+
'app/src/schemas/ObjectSchemas/generatedCode/propertyValue/sellOfferTermValueStandardPropertyLabel.js';
|
|
120
|
+
const propValuePath =
|
|
121
|
+
'app/src/schemas/ObjectSchemas/generatedCode/propertyValue/sellOfferTermValueStandardPropertyValue.js';
|
|
122
|
+
const hasPropRelPath =
|
|
123
|
+
'app/src/schemas/RelationshipSchemas/generatedCode/propertyValue/hasSellOfferTermValueStandardPropertyValue.js';
|
|
124
|
+
const disPropRelPath =
|
|
125
|
+
'app/src/schemas/RelationshipSchemas/generatedCode/propertyValue/disabledSellOfferTermValueStandardPropertyValue.js';
|
|
126
|
+
const isPropRelPath =
|
|
127
|
+
'app/src/schemas/RelationshipSchemas/generatedCode/propertyValue/isSellOfferTermValueStandardPropertyLabel.js';
|
|
128
|
+
|
|
129
|
+
const propLabel = await readProjectFile(rootPath, propLabelPath);
|
|
130
|
+
assert.match(propLabel, /sellOfferTermValueStandardPropertyLabel/);
|
|
131
|
+
|
|
132
|
+
const propValue = await readProjectFile(rootPath, propValuePath);
|
|
133
|
+
assert.match(propValue, /sellOfferTermValueStandardPropertyValue/);
|
|
134
|
+
|
|
135
|
+
const hasPropRel = await readProjectFile(rootPath, hasPropRelPath);
|
|
136
|
+
assert.match(hasPropRel, /hasSellOfferTermValueStandardPropertyValue/);
|
|
137
|
+
|
|
138
|
+
const disPropRel = await readProjectFile(rootPath, disPropRelPath);
|
|
139
|
+
assert.match(disPropRel, /disabledSellOfferTermValueStandardPropertyValue/);
|
|
140
|
+
|
|
141
|
+
const isPropRel = await readProjectFile(rootPath, isPropRelPath);
|
|
142
|
+
assert.match(isPropRel, /isSellOfferTermValueStandardPropertyLabel/);
|
|
143
|
+
|
|
144
|
+
// Cleanup check: remove attributeTree addOn from base schema
|
|
145
|
+
const cleanSchema = `export default [
|
|
146
|
+
{
|
|
147
|
+
objectType: 'sellOffer',
|
|
148
|
+
canDelete: false,
|
|
149
|
+
generatedBy: 'user',
|
|
150
|
+
addOnDataStructure: [],
|
|
151
|
+
storageResources: {
|
|
152
|
+
myGraph: { storageType: 'graph', graphServerTag: 'GraphHandler' }
|
|
153
|
+
},
|
|
154
|
+
fieldNames: {
|
|
155
|
+
sellOfferId: { type: 'string', storageResourceTags: ['myGraph'] }
|
|
156
|
+
},
|
|
157
|
+
identifiers: [{ type: 'partitionKey', fieldName: 'sellOfferId' }]
|
|
158
|
+
}
|
|
159
|
+
];
|
|
160
|
+
`;
|
|
161
|
+
await writeProjectFile(
|
|
162
|
+
rootPath,
|
|
163
|
+
'app/src/schemas/ObjectSchemas/SellOffer.js',
|
|
164
|
+
cleanSchema
|
|
165
|
+
);
|
|
166
|
+
await generate(rootPath);
|
|
167
|
+
|
|
168
|
+
// All attributeTree schemas should be removed
|
|
169
|
+
await assert.rejects(readProjectFile(rootPath, linkPath), { code: 'ENOENT' });
|
|
170
|
+
await assert.rejects(readProjectFile(rootPath, valuePath), { code: 'ENOENT' });
|
|
171
|
+
await assert.rejects(readProjectFile(rootPath, labelPath), { code: 'ENOENT' });
|
|
172
|
+
await assert.rejects(readProjectFile(rootPath, valueStandardPath), {
|
|
173
|
+
code: 'ENOENT'
|
|
174
|
+
});
|
|
175
|
+
});
|
|
@@ -26,7 +26,8 @@ const STATIC_RBAC_ACTIONS = [
|
|
|
26
26
|
const STATIC_LIMIT_ACTIONS = [
|
|
27
27
|
'ReservedDynamicUsage',
|
|
28
28
|
'ConfirmUsage',
|
|
29
|
-
'CancelUsage'
|
|
29
|
+
'CancelUsage',
|
|
30
|
+
'ReservedUsageTtlExpired'
|
|
30
31
|
];
|
|
31
32
|
|
|
32
33
|
after(async () => {
|
|
@@ -113,11 +114,12 @@ test('generates shared and object-specific RBAC flow schemas', async () => {
|
|
|
113
114
|
'lambdaSyncInv',
|
|
114
115
|
'lambdaSyncApi'
|
|
115
116
|
]);
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
'
|
|
119
|
-
'
|
|
120
|
-
|
|
117
|
+
const reservedUsageTtlExpired = await readGeneratedFlow(
|
|
118
|
+
rootPath,
|
|
119
|
+
'ReservedUsageTtlExpired',
|
|
120
|
+
'FlowLimit'
|
|
121
|
+
);
|
|
122
|
+
assert.deepEqual(reservedUsageTtlExpired.event, ['dss']);
|
|
121
123
|
});
|
|
122
124
|
|
|
123
125
|
test('removes RBAC schemas when the last RBAC object is removed', async () => {
|
|
@@ -6,6 +6,7 @@ import test, { after } from 'node:test';
|
|
|
6
6
|
import { pathToFileURL } from 'node:url';
|
|
7
7
|
|
|
8
8
|
import { generateDynamicRelationshipFlowSchemas } from '../../../src/emit/application/relationship/generateDynamicRelationshipFlowSchemas.js';
|
|
9
|
+
import { loadLocalSchemaDirectory } from '../../../src/compile/loadSchemas.js';
|
|
9
10
|
|
|
10
11
|
const RELATIONSHIP_ACTIONS = [
|
|
11
12
|
'CreateRelationship',
|
|
@@ -136,6 +137,56 @@ test('does not allow options to override the rootPath argument', async () => {
|
|
|
136
137
|
);
|
|
137
138
|
});
|
|
138
139
|
|
|
140
|
+
test('validates relationship schemas with systemNoUser and systemWithUser generatedBy', async () => {
|
|
141
|
+
const rootPath = await mkdtemp(
|
|
142
|
+
join(tmpdir(), 'generate-service-code-v2-rel-val-')
|
|
143
|
+
);
|
|
144
|
+
projectRoots.add(rootPath);
|
|
145
|
+
const relationshipDirectory = join(
|
|
146
|
+
rootPath,
|
|
147
|
+
'app/src/schemas/RelationshipSchemas'
|
|
148
|
+
);
|
|
149
|
+
await mkdir(relationshipDirectory, { recursive: true });
|
|
150
|
+
const relSchema = [
|
|
151
|
+
{
|
|
152
|
+
isItemLabel: {
|
|
153
|
+
fieldNames: {},
|
|
154
|
+
storageResources: {
|
|
155
|
+
myGraph: { storageType: 'graph', graphServerTag: 'GraphHandler' }
|
|
156
|
+
},
|
|
157
|
+
generatedBy: 'systemNoUser',
|
|
158
|
+
links: [
|
|
159
|
+
{
|
|
160
|
+
storageResourceTags: ['myGraph'],
|
|
161
|
+
from: {
|
|
162
|
+
objType: { serviceTag: 'ItemSvc', objectType: 'itemValue' },
|
|
163
|
+
linkType: 'many',
|
|
164
|
+
requiredOnCreate: true
|
|
165
|
+
},
|
|
166
|
+
to: {
|
|
167
|
+
objType: { serviceTag: 'ItemSvc', objectType: 'itemLabel' },
|
|
168
|
+
linkType: 'one'
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
]
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
];
|
|
175
|
+
await writeFile(
|
|
176
|
+
join(relationshipDirectory, 'isItemLabel.js'),
|
|
177
|
+
`export default ${JSON.stringify(relSchema, null, 2)};\n`
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
const schemas = await loadLocalSchemaDirectory(
|
|
181
|
+
{ schemaRoot: join(rootPath, 'app/src/schemas') },
|
|
182
|
+
'RelationshipSchemas',
|
|
183
|
+
{ validateSchemas: true }
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
assert.equal(schemas.length, 1);
|
|
187
|
+
assert.equal(schemas[0].isItemLabel.generatedBy, 'systemNoUser');
|
|
188
|
+
});
|
|
189
|
+
|
|
139
190
|
async function createProject({ singletonExport = false } = {}) {
|
|
140
191
|
const rootPath = await mkdtemp(
|
|
141
192
|
join(tmpdir(), 'generate-service-code-v2-relationship-')
|