@izara_project/izara-core-generate-service-code 1.0.97 → 1.0.99
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/flowValidator.js +45 -8
- package/src/compile/loadSchemas.js +26 -6
- package/src/emit/application/coreFunction/emitCoreFunctionArtifacts.js +15 -1
- package/src/emit/application/coreFunction/templates/InitialSetupConfig.ejs +8 -4
- package/src/emit/application/crud/templates/CreateEndpoint_Main.ejs +4 -4
- package/src/emit/application/crud/templates/CreateLogicalLib.ejs +60 -8
- package/src/emit/application/crud/templates/DeleteEndpoint_Main.ejs +4 -4
- package/src/emit/application/crud/templates/FlowEndpoint_HdrWbs.ejs +2 -2
- package/src/emit/application/crud/templates/FlowMain_Wbs.ejs +2 -2
- package/src/emit/application/crud/templates/UpdateEndpoint_Main.ejs +4 -4
- package/src/emit/application/custom/customFlowS3.js +4 -4
- package/src/emit/application/custom/templates/FlowEndpoint_Main.ejs +2 -2
- package/src/emit/application/custom/templates/s3/CreateObject_Main.ejs +5 -2
- package/src/emit/application/custom/templates/triggerCache/CheckTriggerCache_Main.ejs +2 -2
- package/src/emit/application/rbac/emitDynamicRbacFlowSchemas.js +4 -1
- package/src/emit/application/rbac/emitRbacApplicationArtifacts.js +76 -1
- package/src/emit/application/rbac/templates/ReservedDynamicUsage_Main.ejs +2 -0
- package/src/emit/application/rbac/templates/ReservedUsageTtlExpired_HdrDss.ejs +176 -0
- package/src/emit/application/relationship/templates/ChangeRelationship_Main.ejs +3 -2
- package/src/emit/application/relationship/templates/CreateRelationship_Main.ejs +3 -2
- package/src/emit/application/relationship/templates/DeleteRelationship_Main.ejs +3 -2
- package/src/emit/application/relationship/templates/GetRelationship_Main.ejs +3 -2
- package/src/emit/application/relationship/templates/MoveRelationship_Main.ejs +3 -2
- package/src/emit/application/relationship/templates/UpdateRelationship_Main.ejs +3 -2
- package/src/emit/application/system/templates/RegisterComplete_Main.ejs +6 -2
- package/src/emit/application/system/templates/Register_Main.ejs +6 -2
- package/src/emit/application/system/templates/WebSocketComplete_Main.ejs +3 -3
- package/src/emit/infrastructure/functions/functionYamlArtifact.js +28 -0
- package/src/emit/infrastructure/functions/templates/DssFunction_Yaml.ejs +21 -0
- package/src/emit/infrastructure/resources/iamResourceArtifacts.js +46 -6
- package/src/emit/infrastructure/resources/templates/GeneratedLambdaRoleServiceConfig.ejs +5 -3
- package/src/emit/schemas/attributeTree/emitAttributeTreeSchemas.js +75 -22
- package/src/emit/schemas/ledger/emitLedgerSchemas.js +33 -11
- package/src/emit/schemas/ledger/templates/LedgerObjectSchema.ejs +3 -0
- package/src/emit/schemas/ledger/templates/LedgerRelationshipSchema.ejs +1 -0
- package/src/emit/schemas/propertyValue/emitPropertyValueSchemas.js +329 -0
- package/src/generate.js +10 -73
- package/src/output/mergeHooks.js +169 -15
- package/src/plan/domainRegistry.js +31 -5
- package/task.md +76 -0
- package/test/integration/graphDrivenFlowSchema.test.js +58 -0
- package/test/integration/publicApi.test.js +54 -1
- package/test/unit/application/createTargetRoleHandler.test.js +70 -1
- package/test/unit/application/crudApplicationArtifacts.test.js +4 -0
- package/test/unit/application/customFlowApplicationArtifacts.test.js +9 -0
- package/test/unit/application/relationshipApplicationArtifacts.test.js +30 -0
- package/test/unit/infrastructure/resourceArtifacts.test.js +53 -0
- package/test/unit/output/mergeHooks.test.js +405 -0
- package/test/unit/schemas/attributeTreeSchemas.test.js +41 -63
- package/test/unit/schemas/attributeTreeSingleService.test.js +175 -0
- package/test/unit/schemas/ledgerSchemas.test.js +33 -7
- package/test/unit/schemas/propertyValueSchemas.test.js +265 -0
- package/test/unit/templates/hookTagsValidation.test.js +83 -0
- package/src/emit/schemas/attributeTree/emitAttributeTreeTargetSchemas.js +0 -153
- package/src/emit/schemas/attributeTree/resolveTargetServicePath.js +0 -111
|
@@ -36,7 +36,7 @@ test('generates RBAC own-topic WebSocket entries alongside their SQS handlers',
|
|
|
36
36
|
const rootPath = await createProject(true);
|
|
37
37
|
const result = await generateCreateTargetRoleHandler(rootPath);
|
|
38
38
|
|
|
39
|
-
assert.equal(result.files.length,
|
|
39
|
+
assert.equal(result.files.length, 67);
|
|
40
40
|
assert.match(
|
|
41
41
|
await readFile(join(rootPath, result.files[0].path), 'utf8'),
|
|
42
42
|
/CreateTargetRole/
|
|
@@ -211,6 +211,16 @@ test('generates RBAC own-topic WebSocket entries alongside their SQS handlers',
|
|
|
211
211
|
);
|
|
212
212
|
assert.match(yaml, new RegExp(`${flowTag}HdrWbs:\\n[\\s\\S]*websocket:`));
|
|
213
213
|
}
|
|
214
|
+
|
|
215
|
+
const ttlExpiredFile = await readFile(
|
|
216
|
+
join(
|
|
217
|
+
rootPath,
|
|
218
|
+
'app/src/generatedCode/FlowLimit/ReservedUsageTtlExpired/source/ReservedUsageTtlExpired_HdrDss.js'
|
|
219
|
+
),
|
|
220
|
+
'utf8'
|
|
221
|
+
);
|
|
222
|
+
assert.match(ttlExpiredFile, /Auto-refunded expired reservation/);
|
|
223
|
+
assert.match(ttlExpiredFile, /RunningTotalUsage/);
|
|
214
224
|
});
|
|
215
225
|
|
|
216
226
|
test('removes CreateTargetRole handler when RBAC is removed', async () => {
|
|
@@ -244,6 +254,65 @@ test('removes CreateTargetRole handler when RBAC is removed', async () => {
|
|
|
244
254
|
);
|
|
245
255
|
});
|
|
246
256
|
|
|
257
|
+
test('generates synchronous HdrInv and HdrApi handlers for FlowLimit flows', async () => {
|
|
258
|
+
const rootPath = await createProject(true);
|
|
259
|
+
await generateCreateTargetRoleHandler(rootPath);
|
|
260
|
+
|
|
261
|
+
const limitYaml = await readFile(
|
|
262
|
+
join(rootPath, 'app/sls_yaml/generatedCode/source/flow-limits.yml'),
|
|
263
|
+
'utf8'
|
|
264
|
+
);
|
|
265
|
+
|
|
266
|
+
for (const flowTag of ['ReservedDynamicUsage', 'ConfirmUsage', 'CancelUsage']) {
|
|
267
|
+
assert.match(
|
|
268
|
+
limitYaml,
|
|
269
|
+
new RegExp(
|
|
270
|
+
`${flowTag}HdrInv:\\n\\s+handler: src/generatedCode/FlowLimit/${flowTag}/source/${flowTag}_HdrInv\\.main`
|
|
271
|
+
)
|
|
272
|
+
);
|
|
273
|
+
assert.match(
|
|
274
|
+
limitYaml,
|
|
275
|
+
new RegExp(
|
|
276
|
+
`${flowTag}HdrApi:\\n\\s+handler: src/generatedCode/FlowLimit/${flowTag}/source/${flowTag}_HdrApi\\.main`
|
|
277
|
+
)
|
|
278
|
+
);
|
|
279
|
+
|
|
280
|
+
const invFile = await readFile(
|
|
281
|
+
join(
|
|
282
|
+
rootPath,
|
|
283
|
+
`app/src/generatedCode/FlowLimit/${flowTag}/source/${flowTag}_HdrInv.js`
|
|
284
|
+
),
|
|
285
|
+
'utf8'
|
|
286
|
+
);
|
|
287
|
+
assert.match(invFile, /export const main = middleware\.wrap/);
|
|
288
|
+
assert.match(
|
|
289
|
+
invFile,
|
|
290
|
+
new RegExp(`import ${flowTag} from '\\./${flowTag}_Main\\.js'`)
|
|
291
|
+
);
|
|
292
|
+
|
|
293
|
+
const apiFile = await readFile(
|
|
294
|
+
join(
|
|
295
|
+
rootPath,
|
|
296
|
+
`app/src/generatedCode/FlowLimit/${flowTag}/source/${flowTag}_HdrApi.js`
|
|
297
|
+
),
|
|
298
|
+
'utf8'
|
|
299
|
+
);
|
|
300
|
+
assert.match(apiFile, /export const main = middleware\.wrap/);
|
|
301
|
+
assert.match(
|
|
302
|
+
apiFile,
|
|
303
|
+
new RegExp(`import ${flowTag} from '\\./${flowTag}_Main\\.js'`)
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
assert.match(
|
|
308
|
+
limitYaml,
|
|
309
|
+
/ReservedUsageTtlExpiredHdrDss:\n\s+handler: src\/generatedCode\/FlowLimit\/ReservedUsageTtlExpired\/source\/ReservedUsageTtlExpired_HdrDss\.main/
|
|
310
|
+
);
|
|
311
|
+
assert.match(limitYaml, /type: dynamodb/);
|
|
312
|
+
assert.match(limitYaml, /RunningTotalReservedUsageTable/);
|
|
313
|
+
assert.match(limitYaml, /eventName: \["REMOVE"\]/);
|
|
314
|
+
});
|
|
315
|
+
|
|
247
316
|
async function createProject(hasRbac) {
|
|
248
317
|
const rootPath = await mkdtemp(
|
|
249
318
|
join(tmpdir(), 'generate-service-code-v2-rbac-handler-')
|
|
@@ -1119,6 +1119,10 @@ test('emits synchronous ConfirmUsage and CancelUsage invocation in CreateLogical
|
|
|
1119
1119
|
assert.match(createLogicalLib, /CancelUsageHdrInv/);
|
|
1120
1120
|
assert.match(createLogicalLib, /lambda\.invokeSync\(/);
|
|
1121
1121
|
assert.match(createLogicalLib, /publishCompletionMessage\(/);
|
|
1122
|
+
assert.match(createLogicalLib, /OBJECT_QUOTA_SERVICE_TAGS/);
|
|
1123
|
+
assert.match(createLogicalLib, /"order":"UserAccount"/);
|
|
1124
|
+
assert.match(createLogicalLib, /targetFunction,\s*quotaServiceTag/);
|
|
1125
|
+
assert.doesNotMatch(createLogicalLib, /targetFunction,\s*['"]Rbac['"]/);
|
|
1122
1126
|
});
|
|
1123
1127
|
|
|
1124
1128
|
test('emits synchronous AfterHook invocation in Update and Delete handlers', async () => {
|
|
@@ -232,6 +232,15 @@ test('emits handlers, function YAML, and IAM roles for supported custom flow eve
|
|
|
232
232
|
join(rootPath, `app/src/generatedCode/FlowSchemas/Upload/source/${path}`)
|
|
233
233
|
);
|
|
234
234
|
}
|
|
235
|
+
const uploadCreateObject = await readFile(
|
|
236
|
+
join(
|
|
237
|
+
rootPath,
|
|
238
|
+
'app/src/generatedCode/FlowSchemas/Upload/source/UploadCreateObject_Main.js'
|
|
239
|
+
),
|
|
240
|
+
'utf8'
|
|
241
|
+
);
|
|
242
|
+
assert.match(uploadCreateObject, /getFlowSchema\.getFlowSchemaS3WithCache/);
|
|
243
|
+
assert.doesNotMatch(uploadCreateObject, /getObjectSchema\.getFlowSchemaS3WithCache/);
|
|
235
244
|
for (const path of [
|
|
236
245
|
'UploadReservedLimit_Main.js',
|
|
237
246
|
'UploadCreatePreSignUrl_Main.js',
|
|
@@ -154,6 +154,36 @@ test('emits valid hook tags without trailing periods and with correct callingFlo
|
|
|
154
154
|
assert.doesNotMatch(changeMain, /\/\/\(<requestParamCreateRel>\)/);
|
|
155
155
|
});
|
|
156
156
|
|
|
157
|
+
test('imports getRelSchema and uses getRelationshipSchemaWithCache from getRelSchema in all relationship Main files', async () => {
|
|
158
|
+
const rootPath = await createProject(true);
|
|
159
|
+
await generateRelationshipApplicationArtifacts(rootPath, {
|
|
160
|
+
serviceTag: 'orders'
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
const actions = ['Create', 'Update', 'Delete', 'Get', 'Change', 'Move'];
|
|
164
|
+
for (const action of actions) {
|
|
165
|
+
const mainContent = await readFile(
|
|
166
|
+
join(
|
|
167
|
+
rootPath,
|
|
168
|
+
`app/src/generatedCode/FlowRelationships/${action}Relationship/source/${action}Relationship_Main.js`
|
|
169
|
+
),
|
|
170
|
+
'utf8'
|
|
171
|
+
);
|
|
172
|
+
assert.match(
|
|
173
|
+
mainContent,
|
|
174
|
+
/import\s*\{[^}]*getRelSchema[^}]*\}\s*from\s*'@izara_project\/izara-core-library-service-schemas'/
|
|
175
|
+
);
|
|
176
|
+
assert.match(
|
|
177
|
+
mainContent,
|
|
178
|
+
/const\s*\{[^}]*getRelationshipSchemaWithCache[^}]*\}\s*=\s*getRelSchema/
|
|
179
|
+
);
|
|
180
|
+
assert.doesNotMatch(
|
|
181
|
+
mainContent,
|
|
182
|
+
/const\s*\{[^}]*getRelationshipSchemaWithCache[^}]*\}\s*=\s*getObjectSchema/
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
|
|
157
187
|
test('removes managed relationship artifacts after relationships are removed', async () => {
|
|
158
188
|
const rootPath = await createProject(true);
|
|
159
189
|
await generateRelationshipApplicationArtifacts(rootPath, {
|
|
@@ -167,6 +167,7 @@ test('emits queues, topics, tables, and IAM roles from schema data', async () =>
|
|
|
167
167
|
assert.match(procFindDataRole, /dynamodb:Query/);
|
|
168
168
|
assert.match(procFindDataRole, /sns:Publish/);
|
|
169
169
|
assert.match(procFindDataRole, /FindData_Out/);
|
|
170
|
+
assert.match(procFindDataRole, /ProcessLogical_Out/);
|
|
170
171
|
assert.doesNotMatch(
|
|
171
172
|
iam,
|
|
172
173
|
/s3:GetObjectVersion|sqs:DeleteMessageBatch|sqs:GetQueueUrl/
|
|
@@ -815,6 +816,58 @@ test('emits lambda:InvokeFunction in FlowObjectsRole when flow has event generat
|
|
|
815
816
|
assert.match(role, /GenSyncHook/);
|
|
816
817
|
});
|
|
817
818
|
|
|
819
|
+
test('emits DynamoDB stream permissions in FlowRbacRole and multi-tenant quota topics in FlowObjectsRole', async () => {
|
|
820
|
+
const rootPath = await createProject();
|
|
821
|
+
const schemas = join(rootPath, 'app/src/schemas');
|
|
822
|
+
await writeFile(
|
|
823
|
+
join(schemas, 'ObjectSchemas/Order.js'),
|
|
824
|
+
"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"
|
|
825
|
+
);
|
|
826
|
+
await generateResourceArtifacts(rootPath);
|
|
827
|
+
|
|
828
|
+
const iam = await readFile(
|
|
829
|
+
join(
|
|
830
|
+
rootPath,
|
|
831
|
+
'app/sls_yaml/generatedCode/source/generate-shared-resource.yml'
|
|
832
|
+
),
|
|
833
|
+
'utf8'
|
|
834
|
+
);
|
|
835
|
+
const rbacRole = roleBlock(iam, 'FlowRbacRole');
|
|
836
|
+
assert.match(rbacRole, /dynamodb:DescribeStream/);
|
|
837
|
+
assert.match(rbacRole, /RunningTotalReservedUsage\/stream\/\*/);
|
|
838
|
+
|
|
839
|
+
const flowObjectsRole = roleBlock(iam, 'FlowObjectsRole');
|
|
840
|
+
assert.match(flowObjectsRole, /SupplyNetwork_\${self:custom\.iz_stage}_ConfirmUsage_In/);
|
|
841
|
+
assert.match(flowObjectsRole, /SupplyNetwork_\${self:custom\.iz_stage}_CancelUsage_In/);
|
|
842
|
+
assert.match(flowObjectsRole, /SupplyNetwork\${self:custom\.iz_stage}ConfirmUsageHdrInv/);
|
|
843
|
+
assert.match(flowObjectsRole, /SupplyNetwork\${self:custom\.iz_stage}CancelUsageHdrInv/);
|
|
844
|
+
|
|
845
|
+
const { createResourceState } = await import(
|
|
846
|
+
'../../../src/emit/infrastructure/resources/resourceState.js'
|
|
847
|
+
);
|
|
848
|
+
const { renderLambdaRoleConfig } = await import(
|
|
849
|
+
'../../../src/emit/infrastructure/resources/iamResourceArtifacts.js'
|
|
850
|
+
);
|
|
851
|
+
const state = createResourceState({
|
|
852
|
+
objects: [
|
|
853
|
+
{
|
|
854
|
+
objectType: 'order',
|
|
855
|
+
generatedBy: 'user',
|
|
856
|
+
belongTo: { serviceTag: 'SupplyNetwork', objectType: 'tenant' },
|
|
857
|
+
addOnDataStructure: [{ type: 'rbac' }]
|
|
858
|
+
}
|
|
859
|
+
],
|
|
860
|
+
relationships: [],
|
|
861
|
+
flows: [
|
|
862
|
+
{ flowTag: 'upload', event: ['s3'], quotaServiceTag: 'SupplyNetwork' }
|
|
863
|
+
]
|
|
864
|
+
});
|
|
865
|
+
const lambdaRoleConfig = await renderLambdaRoleConfig(state);
|
|
866
|
+
assert.match(lambdaRoleConfig, /ConfirmUsage_In', 'SupplyNetwork'/);
|
|
867
|
+
assert.match(lambdaRoleConfig, /CancelUsage_In', 'SupplyNetwork'/);
|
|
868
|
+
assert.match(lambdaRoleConfig, /ReservedDynamicUsage_In', 'SupplyNetwork'/);
|
|
869
|
+
assert.doesNotMatch(lambdaRoleConfig, /'AccountLimits'/);
|
|
870
|
+
});
|
|
818
871
|
|
|
819
872
|
function roleBlock(iam, roleName) {
|
|
820
873
|
const match = iam.match(
|
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import test from 'node:test';
|
|
6
|
+
|
|
7
|
+
import { mergeHooks } from '../../../src/output/mergeHooks.js';
|
|
8
|
+
|
|
9
|
+
async function writeTestFile(tmpDir, relativePath, content) {
|
|
10
|
+
const fullPath = path.join(tmpDir, relativePath);
|
|
11
|
+
await mkdir(path.dirname(fullPath), { recursive: true });
|
|
12
|
+
await writeFile(fullPath, content, 'utf8');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
test('merges single hook in JS file when source content has custom code', async () => {
|
|
16
|
+
const tmpDir = await mkdtemp(path.join(os.tmpdir(), 'mergeHooks-test-'));
|
|
17
|
+
try {
|
|
18
|
+
const filePath = 'app/src/sample.js';
|
|
19
|
+
await writeTestFile(
|
|
20
|
+
tmpDir,
|
|
21
|
+
filePath,
|
|
22
|
+
'//(<customHook>)\nconst custom = true;\n//(</customHook>)\n'
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
const artifacts = [
|
|
26
|
+
{
|
|
27
|
+
path: filePath,
|
|
28
|
+
content: '//(<customHook>)\n//(</customHook>)\n'
|
|
29
|
+
}
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
const result = await mergeHooks(tmpDir, artifacts);
|
|
33
|
+
assert.equal(
|
|
34
|
+
result[0].content,
|
|
35
|
+
'//(<customHook>)\nconst custom = true;\n//(</customHook>)\n'
|
|
36
|
+
);
|
|
37
|
+
} finally {
|
|
38
|
+
await rm(tmpDir, { recursive: true, force: true });
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test('merges multiple occurrences of the same hook tag without losing occurrences', async () => {
|
|
43
|
+
const tmpDir = await mkdtemp(path.join(os.tmpdir(), 'mergeHooks-test-'));
|
|
44
|
+
try {
|
|
45
|
+
const filePath = 'app/src/Flow_Main.js';
|
|
46
|
+
const diskContent = [
|
|
47
|
+
'function test() {',
|
|
48
|
+
' //(<identifiers>)',
|
|
49
|
+
' firstIdentifier: true',
|
|
50
|
+
' //(</identifiers>)',
|
|
51
|
+
'',
|
|
52
|
+
' //(<identifiers>)',
|
|
53
|
+
' secondIdentifier: "customValue"',
|
|
54
|
+
' //(</identifiers>)',
|
|
55
|
+
'}'
|
|
56
|
+
].join('\n');
|
|
57
|
+
await writeTestFile(tmpDir, filePath, diskContent);
|
|
58
|
+
|
|
59
|
+
const templateContent = [
|
|
60
|
+
'function test() {',
|
|
61
|
+
' //(<identifiers>)',
|
|
62
|
+
' //(</identifiers>)',
|
|
63
|
+
'',
|
|
64
|
+
' //(<identifiers>)',
|
|
65
|
+
' //(</identifiers>)',
|
|
66
|
+
'}'
|
|
67
|
+
].join('\n');
|
|
68
|
+
|
|
69
|
+
const artifacts = [
|
|
70
|
+
{
|
|
71
|
+
path: filePath,
|
|
72
|
+
content: templateContent
|
|
73
|
+
}
|
|
74
|
+
];
|
|
75
|
+
|
|
76
|
+
const result = await mergeHooks(tmpDir, artifacts);
|
|
77
|
+
assert.equal(result[0].content, diskContent);
|
|
78
|
+
} finally {
|
|
79
|
+
await rm(tmpDir, { recursive: true, force: true });
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test('preserves second hook when the first occurrence of a duplicate hook is empty on disk', async () => {
|
|
84
|
+
const tmpDir = await mkdtemp(path.join(os.tmpdir(), 'mergeHooks-test-'));
|
|
85
|
+
try {
|
|
86
|
+
const filePath = 'app/src/Flow_Main.js';
|
|
87
|
+
const diskContent = [
|
|
88
|
+
'function test() {',
|
|
89
|
+
' //(<identifiers>)',
|
|
90
|
+
' //(</identifiers>)',
|
|
91
|
+
'',
|
|
92
|
+
' //(<identifiers>)',
|
|
93
|
+
' orderId: requestParams.orderId',
|
|
94
|
+
' //(</identifiers>)',
|
|
95
|
+
'}'
|
|
96
|
+
].join('\n');
|
|
97
|
+
await writeTestFile(tmpDir, filePath, diskContent);
|
|
98
|
+
|
|
99
|
+
const templateContent = [
|
|
100
|
+
'function test() {',
|
|
101
|
+
' //(<identifiers>)',
|
|
102
|
+
' //(</identifiers>)',
|
|
103
|
+
'',
|
|
104
|
+
' //(<identifiers>)',
|
|
105
|
+
' //(</identifiers>)',
|
|
106
|
+
'}'
|
|
107
|
+
].join('\n');
|
|
108
|
+
|
|
109
|
+
const artifacts = [
|
|
110
|
+
{
|
|
111
|
+
path: filePath,
|
|
112
|
+
content: templateContent
|
|
113
|
+
}
|
|
114
|
+
];
|
|
115
|
+
|
|
116
|
+
const result = await mergeHooks(tmpDir, artifacts);
|
|
117
|
+
assert.equal(result[0].content, diskContent);
|
|
118
|
+
} finally {
|
|
119
|
+
await rm(tmpDir, { recursive: true, force: true });
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test('merges hooks with relaxed whitespace around comment slashes and tags', async () => {
|
|
124
|
+
const tmpDir = await mkdtemp(path.join(os.tmpdir(), 'mergeHooks-test-'));
|
|
125
|
+
try {
|
|
126
|
+
const filePath = 'app/src/sample.js';
|
|
127
|
+
await writeTestFile(
|
|
128
|
+
tmpDir,
|
|
129
|
+
filePath,
|
|
130
|
+
'// (<spacedHook>)\nconst spaced = 1;\n// (</spacedHook>)\n'
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
const artifacts = [
|
|
134
|
+
{
|
|
135
|
+
path: filePath,
|
|
136
|
+
content: '//(<spacedHook>)\n//(</spacedHook>)\n'
|
|
137
|
+
}
|
|
138
|
+
];
|
|
139
|
+
|
|
140
|
+
const result = await mergeHooks(tmpDir, artifacts);
|
|
141
|
+
assert.equal(
|
|
142
|
+
result[0].content,
|
|
143
|
+
'// (<spacedHook>)\nconst spaced = 1;\n// (</spacedHook>)\n'
|
|
144
|
+
);
|
|
145
|
+
} finally {
|
|
146
|
+
await rm(tmpDir, { recursive: true, force: true });
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
test('merges hooks with hyphens and dots in tag names', async () => {
|
|
151
|
+
const tmpDir = await mkdtemp(path.join(os.tmpdir(), 'mergeHooks-test-'));
|
|
152
|
+
try {
|
|
153
|
+
const filePath = 'app/src/sample.js';
|
|
154
|
+
await writeTestFile(
|
|
155
|
+
tmpDir,
|
|
156
|
+
filePath,
|
|
157
|
+
'//(<custom-hook.name>)\nconst customName = "valid";\n//(</custom-hook.name>)\n'
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
const artifacts = [
|
|
161
|
+
{
|
|
162
|
+
path: filePath,
|
|
163
|
+
content: '//(<custom-hook.name>)\n//(</custom-hook.name>)\n'
|
|
164
|
+
}
|
|
165
|
+
];
|
|
166
|
+
|
|
167
|
+
const result = await mergeHooks(tmpDir, artifacts);
|
|
168
|
+
assert.equal(
|
|
169
|
+
result[0].content,
|
|
170
|
+
'//(<custom-hook.name>)\nconst customName = "valid";\n//(</custom-hook.name>)\n'
|
|
171
|
+
);
|
|
172
|
+
} finally {
|
|
173
|
+
await rm(tmpDir, { recursive: true, force: true });
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
test('merges hooks in YAML files with relaxed whitespace and hyphens', async () => {
|
|
178
|
+
const tmpDir = await mkdtemp(path.join(os.tmpdir(), 'mergeHooks-test-'));
|
|
179
|
+
try {
|
|
180
|
+
const filePath = 'app/sls_yaml/test.yml';
|
|
181
|
+
const diskContent = [
|
|
182
|
+
'# <#custom-filter-patterns#>',
|
|
183
|
+
'filterPatterns:',
|
|
184
|
+
' - body: custom',
|
|
185
|
+
'# <#/custom-filter-patterns#>'
|
|
186
|
+
].join('\n');
|
|
187
|
+
await writeTestFile(tmpDir, filePath, diskContent);
|
|
188
|
+
|
|
189
|
+
const templateContent = [
|
|
190
|
+
'#<#custom-filter-patterns#>',
|
|
191
|
+
'#<#/custom-filter-patterns#>'
|
|
192
|
+
].join('\n');
|
|
193
|
+
|
|
194
|
+
const artifacts = [
|
|
195
|
+
{
|
|
196
|
+
path: filePath,
|
|
197
|
+
content: templateContent
|
|
198
|
+
}
|
|
199
|
+
];
|
|
200
|
+
|
|
201
|
+
const result = await mergeHooks(tmpDir, artifacts);
|
|
202
|
+
assert.equal(result[0].content, diskContent);
|
|
203
|
+
} finally {
|
|
204
|
+
await rm(tmpDir, { recursive: true, force: true });
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
test('merges hooks in .mjs and .cjs files', async () => {
|
|
209
|
+
const tmpDir = await mkdtemp(path.join(os.tmpdir(), 'mergeHooks-test-'));
|
|
210
|
+
try {
|
|
211
|
+
const mjsPath = 'app/src/sample.mjs';
|
|
212
|
+
const cjsPath = 'app/src/sample.cjs';
|
|
213
|
+
await writeTestFile(
|
|
214
|
+
tmpDir,
|
|
215
|
+
mjsPath,
|
|
216
|
+
'//(<mjsHook>)\nexport default 1;\n//(</mjsHook>)\n'
|
|
217
|
+
);
|
|
218
|
+
await writeTestFile(
|
|
219
|
+
tmpDir,
|
|
220
|
+
cjsPath,
|
|
221
|
+
'//(<cjsHook>)\nmodule.exports = 2;\n//(</cjsHook>)\n'
|
|
222
|
+
);
|
|
223
|
+
|
|
224
|
+
const artifacts = [
|
|
225
|
+
{
|
|
226
|
+
path: mjsPath,
|
|
227
|
+
content: '//(<mjsHook>)\n//(</mjsHook>)\n'
|
|
228
|
+
},
|
|
229
|
+
{
|
|
230
|
+
path: cjsPath,
|
|
231
|
+
content: '//(<cjsHook>)\n//(</cjsHook>)\n'
|
|
232
|
+
}
|
|
233
|
+
];
|
|
234
|
+
|
|
235
|
+
const result = await mergeHooks(tmpDir, artifacts);
|
|
236
|
+
assert.equal(
|
|
237
|
+
result[0].content,
|
|
238
|
+
'//(<mjsHook>)\nexport default 1;\n//(</mjsHook>)\n'
|
|
239
|
+
);
|
|
240
|
+
assert.equal(
|
|
241
|
+
result[1].content,
|
|
242
|
+
'//(<cjsHook>)\nmodule.exports = 2;\n//(</cjsHook>)\n'
|
|
243
|
+
);
|
|
244
|
+
} finally {
|
|
245
|
+
await rm(tmpDir, { recursive: true, force: true });
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
test('respects writeWhenFileExist false flag by returning disk content unchanged', async () => {
|
|
250
|
+
const tmpDir = await mkdtemp(path.join(os.tmpdir(), 'mergeHooks-test-'));
|
|
251
|
+
try {
|
|
252
|
+
const filePath = 'app/src/immutable.js';
|
|
253
|
+
await writeTestFile(tmpDir, filePath, '// existing disk content\n');
|
|
254
|
+
|
|
255
|
+
const artifacts = [
|
|
256
|
+
{
|
|
257
|
+
path: filePath,
|
|
258
|
+
content: '// new template content\n',
|
|
259
|
+
writeWhenFileExist: false
|
|
260
|
+
}
|
|
261
|
+
];
|
|
262
|
+
|
|
263
|
+
const result = await mergeHooks(tmpDir, artifacts);
|
|
264
|
+
assert.equal(result[0].content, '// existing disk content\n');
|
|
265
|
+
} finally {
|
|
266
|
+
await rm(tmpDir, { recursive: true, force: true });
|
|
267
|
+
}
|
|
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
|
+
|