@pdtf/schemas 3.4.1-6 → 3.4.1-7
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/.claude/settings.local.json +2 -1
- package/index.js +33 -85
- package/package.json +1 -1
- package/src/schemas/v3/combined.json +12 -5
- package/src/schemas/v3/overlays/nts.json +1 -4
- package/src/schemas/v3/overlays/nts2.json +1 -4
- package/src/schemas/v3/overlays/ntsl.json +1 -4
- package/src/schemas/v3/overlays/ntsl2.json +12 -4
- package/src/tests/v3/transactionSchema.test.js +33 -65
- package/src/tests/v3/caching.test.js +0 -317
package/index.js
CHANGED
|
@@ -10,9 +10,6 @@ const ajv = new Ajv({
|
|
|
10
10
|
// Adds date formats among other types to the validator.
|
|
11
11
|
addFormats(ajv);
|
|
12
12
|
|
|
13
|
-
// Enhanced caching structure for subschemas and validators
|
|
14
|
-
const schemaCache = new Map();
|
|
15
|
-
|
|
16
13
|
const verifiedClaimsSchema = require("./src/schemas/verifiedClaims/pdtf-verified-claims.json");
|
|
17
14
|
const v2CoreSchema = require("./src/schemas/v2/pdtf-transaction.json");
|
|
18
15
|
const v3CoreSchema = require("./src/schemas/v3/pdtf-transaction.json");
|
|
@@ -183,74 +180,32 @@ const generateOverlayKey = (overlays) => {
|
|
|
183
180
|
return overlays.join(".");
|
|
184
181
|
};
|
|
185
182
|
|
|
186
|
-
// Enhanced caching function that stores both subschemas and validators
|
|
187
|
-
const getCachedSchemaData = (path, schemaId, overlays) => {
|
|
188
|
-
const overlayKey = generateOverlayKey(overlays);
|
|
189
|
-
const cacheKey = `${path}-${schemaId}-${overlayKey}`;
|
|
190
|
-
|
|
191
|
-
let cached = schemaCache.get(cacheKey);
|
|
192
|
-
if (!cached) {
|
|
193
|
-
// Compute subschema using the original logic
|
|
194
|
-
const sourceSchema = getTransactionSchema(schemaId, overlays);
|
|
195
|
-
const pathArray = path.split("/").slice(1);
|
|
196
|
-
let subSchema = sourceSchema;
|
|
197
|
-
|
|
198
|
-
if (pathArray.length >= 1) {
|
|
199
|
-
subSchema = pathArray.reduce((schema, pathElement) => {
|
|
200
|
-
if (!schema) return undefined;
|
|
201
|
-
const { type, items, properties, oneOf } = schema;
|
|
202
|
-
if (type === "array") return items;
|
|
203
|
-
if (properties?.[pathElement]) return properties[pathElement];
|
|
204
|
-
if (oneOf) {
|
|
205
|
-
let matchingProperty;
|
|
206
|
-
oneOf.forEach((aOneOf) => {
|
|
207
|
-
if (aOneOf.type === "array" && !Number.isNaN(pathElement)) {
|
|
208
|
-
matchingProperty = aOneOf.items;
|
|
209
|
-
} else if (aOneOf.properties?.[pathElement]) {
|
|
210
|
-
matchingProperty = aOneOf.properties?.[pathElement];
|
|
211
|
-
}
|
|
212
|
-
});
|
|
213
|
-
if (matchingProperty) return matchingProperty;
|
|
214
|
-
}
|
|
215
|
-
return undefined;
|
|
216
|
-
}, sourceSchema);
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
// Add schema to AJV and get validator
|
|
220
|
-
// Only add valid schemas to AJV
|
|
221
|
-
let validator;
|
|
222
|
-
if (subSchema && typeof subSchema === 'object') {
|
|
223
|
-
// Check if schema already exists in AJV to avoid duplicates
|
|
224
|
-
validator = ajv.getSchema(cacheKey);
|
|
225
|
-
if (!validator) {
|
|
226
|
-
ajv.addSchema(subSchema, cacheKey);
|
|
227
|
-
validator = ajv.getSchema(cacheKey);
|
|
228
|
-
}
|
|
229
|
-
} else {
|
|
230
|
-
// For invalid paths, create a validator that always fails
|
|
231
|
-
validator = () => false;
|
|
232
|
-
validator.errors = [`Invalid path: schema is ${subSchema}`];
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
cached = {
|
|
236
|
-
subSchema,
|
|
237
|
-
validator,
|
|
238
|
-
cacheKey
|
|
239
|
-
};
|
|
240
|
-
schemaCache.set(cacheKey, cached);
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
return cached;
|
|
244
|
-
};
|
|
245
|
-
|
|
246
183
|
const getValidator = (schemaId, overlays) => {
|
|
247
184
|
return getSubschemaValidator("", schemaId, overlays);
|
|
248
185
|
};
|
|
249
186
|
|
|
250
|
-
// common functions for v1 and v2
|
|
187
|
+
// common functions for v1 and v2
|
|
251
188
|
const getSubschema = (path, schemaId, overlays) => {
|
|
252
|
-
const
|
|
253
|
-
|
|
189
|
+
const sourceSchema = getTransactionSchema(schemaId, overlays);
|
|
190
|
+
const pathArray = path.split("/").slice(1);
|
|
191
|
+
if (pathArray.length < 1) return sourceSchema;
|
|
192
|
+
return pathArray.reduce((schema, pathElement) => {
|
|
193
|
+
const { type, items, properties, oneOf } = schema;
|
|
194
|
+
if (type === "array") return items;
|
|
195
|
+
if (properties?.[pathElement]) return properties[pathElement];
|
|
196
|
+
if (oneOf) {
|
|
197
|
+
let matchingProperty;
|
|
198
|
+
oneOf.forEach((aOneOf) => {
|
|
199
|
+
if (aOneOf.type === "array" && !Number.isNaN(pathElement)) {
|
|
200
|
+
matchingProperty = aOneOf.items;
|
|
201
|
+
} else if (aOneOf.properties?.[pathElement]) {
|
|
202
|
+
matchingProperty = aOneOf.properties?.[pathElement];
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
if (matchingProperty) return matchingProperty;
|
|
206
|
+
}
|
|
207
|
+
return undefined;
|
|
208
|
+
}, sourceSchema);
|
|
254
209
|
};
|
|
255
210
|
|
|
256
211
|
const isPathValid = (path, schemaId, overlays) => {
|
|
@@ -262,8 +217,18 @@ const isPathValid = (path, schemaId, overlays) => {
|
|
|
262
217
|
};
|
|
263
218
|
|
|
264
219
|
const getSubschemaValidator = (path, schemaId, overlays) => {
|
|
265
|
-
const
|
|
266
|
-
|
|
220
|
+
const subSchema = getSubschema(path, schemaId, overlays);
|
|
221
|
+
const overlayKey = generateOverlayKey(overlays);
|
|
222
|
+
// see if we can retrieve the schema by path, schemaId and overlays
|
|
223
|
+
const cacheKey = `${path}-${schemaId}-${overlayKey}`;
|
|
224
|
+
let validator = ajv.getSchema(cacheKey);
|
|
225
|
+
// retrieve whole schema by $id if available
|
|
226
|
+
if (!validator && subSchema.$id) validator = ajv.getSchema(cacheKey);
|
|
227
|
+
if (!validator) {
|
|
228
|
+
ajv.addSchema(subSchema, cacheKey);
|
|
229
|
+
validator = ajv.getSchema(cacheKey);
|
|
230
|
+
}
|
|
231
|
+
return validator;
|
|
267
232
|
};
|
|
268
233
|
|
|
269
234
|
// v1, deprecated
|
|
@@ -347,20 +312,6 @@ const validateVerifiedClaims = (verifiedClaims, schemaId, overlays) => {
|
|
|
347
312
|
return validationErrorsArr;
|
|
348
313
|
};
|
|
349
314
|
|
|
350
|
-
// Cache management functions
|
|
351
|
-
const getCacheStats = () => ({
|
|
352
|
-
totalEntries: schemaCache.size,
|
|
353
|
-
cacheKeys: Array.from(schemaCache.keys())
|
|
354
|
-
});
|
|
355
|
-
|
|
356
|
-
const clearSchemaCache = () => {
|
|
357
|
-
// Clear our cache
|
|
358
|
-
schemaCache.clear();
|
|
359
|
-
// Also clear AJV's internal cache to prevent duplicates
|
|
360
|
-
// Note: This removes all schemas from AJV, which is what we want for testing
|
|
361
|
-
ajv.removeSchema();
|
|
362
|
-
};
|
|
363
|
-
|
|
364
315
|
module.exports = {
|
|
365
316
|
ajv,
|
|
366
317
|
getTransactionSchema,
|
|
@@ -373,7 +324,4 @@ module.exports = {
|
|
|
373
324
|
validateVerifiedClaims,
|
|
374
325
|
overlaysMap,
|
|
375
326
|
extensionOverlays,
|
|
376
|
-
// New cache management functions
|
|
377
|
-
getCacheStats,
|
|
378
|
-
clearSchemaCache,
|
|
379
327
|
};
|
package/package.json
CHANGED
|
@@ -9228,6 +9228,12 @@
|
|
|
9228
9228
|
"controlledParking",
|
|
9229
9229
|
"electricVehicleChargingPoint"
|
|
9230
9230
|
],
|
|
9231
|
+
"ntsl2Required": [
|
|
9232
|
+
"parkingArrangements",
|
|
9233
|
+
"disabledParking",
|
|
9234
|
+
"controlledParking",
|
|
9235
|
+
"electricVehicleChargingPoint"
|
|
9236
|
+
],
|
|
9231
9237
|
"ta6Required": ["parkingArrangements", "controlledParking"],
|
|
9232
9238
|
"properties": {
|
|
9233
9239
|
"parkingArrangements": {
|
|
@@ -9456,6 +9462,11 @@
|
|
|
9456
9462
|
"isConservationArea",
|
|
9457
9463
|
"hasTreePreservationOrder"
|
|
9458
9464
|
],
|
|
9465
|
+
"ntsl2Required": [
|
|
9466
|
+
"isListed",
|
|
9467
|
+
"isConservationArea",
|
|
9468
|
+
"hasTreePreservationOrder"
|
|
9469
|
+
],
|
|
9459
9470
|
"baspi4Required": [
|
|
9460
9471
|
"isListed",
|
|
9461
9472
|
"isConservationArea",
|
|
@@ -20071,11 +20082,7 @@
|
|
|
20071
20082
|
}
|
|
20072
20083
|
]
|
|
20073
20084
|
}
|
|
20074
|
-
}
|
|
20075
|
-
"ntsRequired": ["offMainsDrainageSystem"],
|
|
20076
|
-
"nts2Required": ["offMainsDrainageSystem"],
|
|
20077
|
-
"ntslRequired": ["offMainsDrainageSystem"],
|
|
20078
|
-
"ntsl2Required": ["offMainsDrainageSystem"]
|
|
20085
|
+
}
|
|
20079
20086
|
}
|
|
20080
20087
|
]
|
|
20081
20088
|
},
|
|
@@ -252,6 +252,12 @@
|
|
|
252
252
|
},
|
|
253
253
|
"parking": {
|
|
254
254
|
"ntsl2Ref": "B4",
|
|
255
|
+
"required": [
|
|
256
|
+
"parkingArrangements",
|
|
257
|
+
"disabledParking",
|
|
258
|
+
"controlledParking",
|
|
259
|
+
"electricVehicleChargingPoint"
|
|
260
|
+
],
|
|
255
261
|
"properties": {
|
|
256
262
|
"parkingArrangements": {
|
|
257
263
|
"ntsl2Ref": "B4.1",
|
|
@@ -320,6 +326,11 @@
|
|
|
320
326
|
},
|
|
321
327
|
"listingAndConservation": {
|
|
322
328
|
"ntsl2Ref": "C2.1",
|
|
329
|
+
"required": [
|
|
330
|
+
"isListed",
|
|
331
|
+
"isConservationArea",
|
|
332
|
+
"hasTreePreservationOrder"
|
|
333
|
+
],
|
|
323
334
|
"properties": {
|
|
324
335
|
"isListed": {
|
|
325
336
|
"ntsl2Ref": "C2.1.1",
|
|
@@ -1580,10 +1591,7 @@
|
|
|
1580
1591
|
}
|
|
1581
1592
|
}
|
|
1582
1593
|
}
|
|
1583
|
-
}
|
|
1584
|
-
"required": [
|
|
1585
|
-
"offMainsDrainageSystem"
|
|
1586
|
-
]
|
|
1594
|
+
}
|
|
1587
1595
|
}
|
|
1588
1596
|
],
|
|
1589
1597
|
"required": [
|
|
@@ -449,80 +449,48 @@ test("validates a valid contract", () => {
|
|
|
449
449
|
expect(isValid).toBe(true);
|
|
450
450
|
});
|
|
451
451
|
|
|
452
|
-
test("
|
|
453
|
-
|
|
452
|
+
test("ntsl2025 requires parking and listingAndConservation fields", () => {
|
|
453
|
+
// This test validates that the ntsl2025 overlay correctly enforces required fields
|
|
454
|
+
// for the parking and listingAndConservation sections.
|
|
455
|
+
// It should fail validation when these fields are missing.
|
|
456
|
+
// NOTE: This test will fail until overlays are regenerated from combined.json
|
|
457
|
+
const validator = getValidator(schemaId, ["ntsl2025"]);
|
|
454
458
|
const clonedExampleTransaction = JSON.parse(
|
|
455
459
|
JSON.stringify(exampleTransaction)
|
|
456
460
|
);
|
|
457
461
|
|
|
458
|
-
// Set
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
"
|
|
462
|
-
|
|
463
|
-
"yesNo": "Yes",
|
|
464
|
-
"waterMeter": {
|
|
465
|
-
"isSupplyMetered": "No"
|
|
466
|
-
}
|
|
467
|
-
}
|
|
468
|
-
},
|
|
469
|
-
"drainage": {
|
|
470
|
-
"mainsSurfaceWaterDrainage": {
|
|
471
|
-
"yesNo": "Yes"
|
|
472
|
-
},
|
|
473
|
-
"mainsFoulDrainage": {
|
|
474
|
-
"yesNo": "No"
|
|
475
|
-
// Missing offMainsDrainageSystem - this should make validation fail
|
|
476
|
-
}
|
|
477
|
-
}
|
|
462
|
+
// Set up lettings-specific fields
|
|
463
|
+
clonedExampleTransaction.propertyPack.lettingInformation = {
|
|
464
|
+
rent: 3500,
|
|
465
|
+
rentFrequency: "Monthly",
|
|
466
|
+
securityDeposit: 5000,
|
|
478
467
|
};
|
|
468
|
+
delete clonedExampleTransaction.propertyPack.priceInformation;
|
|
469
|
+
delete clonedExampleTransaction.propertyPack.ownership;
|
|
479
470
|
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
error.instancePath.includes('mainsFoulDrainage') &&
|
|
486
|
-
error.message.includes('offMainsDrainageSystem')
|
|
487
|
-
);
|
|
488
|
-
expect(relevantError).toBeDefined();
|
|
489
|
-
});
|
|
490
|
-
|
|
491
|
-
test("waterAndDrainage state is invalid under nts2023 overlay when mainsFoulDrainage yesNo is Not known", () => {
|
|
492
|
-
const validator = getValidator(schemaId, ["nts2023"]);
|
|
493
|
-
const clonedExampleTransaction = JSON.parse(
|
|
494
|
-
JSON.stringify(exampleTransaction)
|
|
495
|
-
);
|
|
471
|
+
// Remove ALL parking fields - this should make validation fail
|
|
472
|
+
delete clonedExampleTransaction.propertyPack.parking.parkingArrangements;
|
|
473
|
+
delete clonedExampleTransaction.propertyPack.parking.disabledParking;
|
|
474
|
+
delete clonedExampleTransaction.propertyPack.parking.controlledParking;
|
|
475
|
+
delete clonedExampleTransaction.propertyPack.parking.electricVehicleChargingPoint;
|
|
496
476
|
|
|
497
|
-
//
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
clonedExampleTransaction.propertyPack.
|
|
501
|
-
"water": {
|
|
502
|
-
"mainsWater": {
|
|
503
|
-
"yesNo": "Yes",
|
|
504
|
-
"waterMeter": {
|
|
505
|
-
"isSupplyMetered": "No"
|
|
506
|
-
}
|
|
507
|
-
}
|
|
508
|
-
},
|
|
509
|
-
"drainage": {
|
|
510
|
-
"mainsSurfaceWaterDrainage": {
|
|
511
|
-
"yesNo": "Yes"
|
|
512
|
-
},
|
|
513
|
-
"mainsFoulDrainage": {
|
|
514
|
-
"yesNo": "Not known" // This is invalid under nts2023 overlay
|
|
515
|
-
}
|
|
516
|
-
}
|
|
517
|
-
};
|
|
477
|
+
// Remove ALL listingAndConservation fields - this should make validation fail
|
|
478
|
+
delete clonedExampleTransaction.propertyPack.listingAndConservation.isListed;
|
|
479
|
+
delete clonedExampleTransaction.propertyPack.listingAndConservation.isConservationArea;
|
|
480
|
+
delete clonedExampleTransaction.propertyPack.listingAndConservation.hasTreePreservationOrder;
|
|
518
481
|
|
|
519
482
|
const isValid = validator(clonedExampleTransaction);
|
|
483
|
+
|
|
484
|
+
// The data should be INVALID because required fields are missing
|
|
520
485
|
expect(isValid).toBe(false);
|
|
521
486
|
|
|
522
|
-
//
|
|
523
|
-
const
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
);
|
|
527
|
-
expect(
|
|
487
|
+
// Specifically check that parking and listingAndConservation fields are reported as missing
|
|
488
|
+
const errorMessages = validator.errors.map(e => e.message);
|
|
489
|
+
expect(errorMessages).toContain("must have required property 'parkingArrangements'");
|
|
490
|
+
expect(errorMessages).toContain("must have required property 'disabledParking'");
|
|
491
|
+
expect(errorMessages).toContain("must have required property 'controlledParking'");
|
|
492
|
+
expect(errorMessages).toContain("must have required property 'electricVehicleChargingPoint'");
|
|
493
|
+
expect(errorMessages).toContain("must have required property 'isListed'");
|
|
494
|
+
expect(errorMessages).toContain("must have required property 'isConservationArea'");
|
|
495
|
+
expect(errorMessages).toContain("must have required property 'hasTreePreservationOrder'");
|
|
528
496
|
});
|
|
@@ -1,317 +0,0 @@
|
|
|
1
|
-
const {
|
|
2
|
-
getSubschema,
|
|
3
|
-
getSubschemaValidator,
|
|
4
|
-
getCacheStats,
|
|
5
|
-
clearSchemaCache,
|
|
6
|
-
isPathValid,
|
|
7
|
-
} = require("../../../index");
|
|
8
|
-
|
|
9
|
-
describe("Schema Caching System", () => {
|
|
10
|
-
const schemaId = "https://trust.propdata.org.uk/schemas/v3/pdtf-transaction.json";
|
|
11
|
-
const testPath = "/propertyPack/surveys";
|
|
12
|
-
const overlays = ["baspiV5", "ta6ed4"];
|
|
13
|
-
|
|
14
|
-
beforeEach(() => {
|
|
15
|
-
clearSchemaCache();
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
describe("Cache Hit/Miss Behavior", () => {
|
|
19
|
-
test("should start with empty cache", () => {
|
|
20
|
-
const stats = getCacheStats();
|
|
21
|
-
expect(stats.totalEntries).toBe(0);
|
|
22
|
-
expect(stats.cacheKeys).toEqual([]);
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
test("should cache subschema on first call", () => {
|
|
26
|
-
// First call should populate cache
|
|
27
|
-
const schema1 = getSubschema(testPath, schemaId, overlays);
|
|
28
|
-
|
|
29
|
-
const stats = getCacheStats();
|
|
30
|
-
expect(stats.totalEntries).toBe(1);
|
|
31
|
-
expect(stats.cacheKeys).toHaveLength(1);
|
|
32
|
-
expect(stats.cacheKeys[0]).toContain(testPath);
|
|
33
|
-
expect(stats.cacheKeys[0]).toContain("baspiV5.ta6ed4");
|
|
34
|
-
|
|
35
|
-
// Second call should return cached version
|
|
36
|
-
const schema2 = getSubschema(testPath, schemaId, overlays);
|
|
37
|
-
|
|
38
|
-
// Same object reference indicates cache hit
|
|
39
|
-
expect(schema1).toBe(schema2);
|
|
40
|
-
|
|
41
|
-
// Cache size shouldn't change
|
|
42
|
-
const stats2 = getCacheStats();
|
|
43
|
-
expect(stats2.totalEntries).toBe(1);
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
test("should cache validator on first call", () => {
|
|
47
|
-
// First call should populate cache
|
|
48
|
-
const validator1 = getSubschemaValidator(testPath, schemaId, overlays);
|
|
49
|
-
|
|
50
|
-
const stats = getCacheStats();
|
|
51
|
-
expect(stats.totalEntries).toBe(1);
|
|
52
|
-
|
|
53
|
-
// Second call should return cached version
|
|
54
|
-
const validator2 = getSubschemaValidator(testPath, schemaId, overlays);
|
|
55
|
-
|
|
56
|
-
// Same object reference indicates cache hit
|
|
57
|
-
expect(validator1).toBe(validator2);
|
|
58
|
-
|
|
59
|
-
// Cache size shouldn't change
|
|
60
|
-
const stats2 = getCacheStats();
|
|
61
|
-
expect(stats2.totalEntries).toBe(1);
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
test("should create separate cache entries for different paths", () => {
|
|
65
|
-
const path1 = "/participants";
|
|
66
|
-
const path2 = "/propertyPack/surveys";
|
|
67
|
-
const path3 = "/status";
|
|
68
|
-
|
|
69
|
-
getSubschema(path1, schemaId, overlays);
|
|
70
|
-
getSubschema(path2, schemaId, overlays);
|
|
71
|
-
getSubschema(path3, schemaId, overlays);
|
|
72
|
-
|
|
73
|
-
const stats = getCacheStats();
|
|
74
|
-
expect(stats.totalEntries).toBe(3);
|
|
75
|
-
expect(stats.cacheKeys.some(key => key.includes(path1))).toBe(true);
|
|
76
|
-
expect(stats.cacheKeys.some(key => key.includes(path2))).toBe(true);
|
|
77
|
-
expect(stats.cacheKeys.some(key => key.includes(path3))).toBe(true);
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
test("should create separate cache entries for different overlays", () => {
|
|
81
|
-
const overlays1 = ["baspiV5"];
|
|
82
|
-
const overlays2 = ["ta6ed4"];
|
|
83
|
-
const overlays3 = ["baspiV5", "ta6ed4"];
|
|
84
|
-
|
|
85
|
-
getSubschema(testPath, schemaId, overlays1);
|
|
86
|
-
getSubschema(testPath, schemaId, overlays2);
|
|
87
|
-
getSubschema(testPath, schemaId, overlays3);
|
|
88
|
-
|
|
89
|
-
const stats = getCacheStats();
|
|
90
|
-
expect(stats.totalEntries).toBe(3);
|
|
91
|
-
expect(stats.cacheKeys.some(key => key.includes("baspiV5"))).toBe(true);
|
|
92
|
-
expect(stats.cacheKeys.some(key => key.includes("ta6ed4"))).toBe(true);
|
|
93
|
-
expect(stats.cacheKeys.some(key => key.includes("baspiV5.ta6ed4"))).toBe(true);
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
test("should handle null/empty overlays consistently", () => {
|
|
97
|
-
getSubschema(testPath, schemaId, null);
|
|
98
|
-
getSubschema(testPath, schemaId, []);
|
|
99
|
-
getSubschema(testPath, schemaId, undefined);
|
|
100
|
-
|
|
101
|
-
const stats = getCacheStats();
|
|
102
|
-
// Should create separate entries for each null-equivalent
|
|
103
|
-
expect(stats.totalEntries).toBeGreaterThan(0);
|
|
104
|
-
});
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
describe("Cache Management Functions", () => {
|
|
108
|
-
test("getCacheStats should return accurate information", () => {
|
|
109
|
-
// Start with empty cache
|
|
110
|
-
let stats = getCacheStats();
|
|
111
|
-
expect(stats).toHaveProperty("totalEntries");
|
|
112
|
-
expect(stats).toHaveProperty("cacheKeys");
|
|
113
|
-
expect(stats.totalEntries).toBe(0);
|
|
114
|
-
expect(Array.isArray(stats.cacheKeys)).toBe(true);
|
|
115
|
-
|
|
116
|
-
// Add some entries
|
|
117
|
-
getSubschema("/property", schemaId, ["baspiV5"]);
|
|
118
|
-
getSubschema("/property/propertyPack", schemaId, ["ta6ed4"]);
|
|
119
|
-
|
|
120
|
-
stats = getCacheStats();
|
|
121
|
-
expect(stats.totalEntries).toBe(2);
|
|
122
|
-
expect(stats.cacheKeys).toHaveLength(2);
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
test("clearSchemaCache should empty the cache", () => {
|
|
126
|
-
// Populate cache
|
|
127
|
-
getSubschema(testPath, schemaId, overlays);
|
|
128
|
-
getSubschemaValidator(testPath, schemaId, overlays);
|
|
129
|
-
|
|
130
|
-
let stats = getCacheStats();
|
|
131
|
-
expect(stats.totalEntries).toBe(1);
|
|
132
|
-
|
|
133
|
-
// Clear cache
|
|
134
|
-
clearSchemaCache();
|
|
135
|
-
|
|
136
|
-
stats = getCacheStats();
|
|
137
|
-
expect(stats.totalEntries).toBe(0);
|
|
138
|
-
expect(stats.cacheKeys).toEqual([]);
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
test("should rebuild cache after clearing", () => {
|
|
142
|
-
// Populate cache
|
|
143
|
-
const schema1 = getSubschema(testPath, schemaId, overlays);
|
|
144
|
-
|
|
145
|
-
// Clear cache
|
|
146
|
-
clearSchemaCache();
|
|
147
|
-
|
|
148
|
-
// Get schema again - should recompute
|
|
149
|
-
const schema2 = getSubschema(testPath, schemaId, overlays);
|
|
150
|
-
|
|
151
|
-
// Should be equivalent content
|
|
152
|
-
expect(schema1).toEqual(schema2);
|
|
153
|
-
// Note: The actual schema content may be the same object due to how schemas are stored,
|
|
154
|
-
// but cache entry should be recreated
|
|
155
|
-
|
|
156
|
-
const stats = getCacheStats();
|
|
157
|
-
expect(stats.totalEntries).toBe(1);
|
|
158
|
-
});
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
describe("Schema Identity and Validator Consistency", () => {
|
|
162
|
-
test("cached schemas should be identical objects", () => {
|
|
163
|
-
const schema1 = getSubschema(testPath, schemaId, overlays);
|
|
164
|
-
const schema2 = getSubschema(testPath, schemaId, overlays);
|
|
165
|
-
const schema3 = getSubschema(testPath, schemaId, overlays);
|
|
166
|
-
|
|
167
|
-
expect(schema1).toBe(schema2);
|
|
168
|
-
expect(schema2).toBe(schema3);
|
|
169
|
-
expect(schema1).toBe(schema3);
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
test("cached validators should be identical objects", () => {
|
|
173
|
-
const validator1 = getSubschemaValidator(testPath, schemaId, overlays);
|
|
174
|
-
const validator2 = getSubschemaValidator(testPath, schemaId, overlays);
|
|
175
|
-
const validator3 = getSubschemaValidator(testPath, schemaId, overlays);
|
|
176
|
-
|
|
177
|
-
expect(validator1).toBe(validator2);
|
|
178
|
-
expect(validator2).toBe(validator3);
|
|
179
|
-
expect(validator1).toBe(validator3);
|
|
180
|
-
});
|
|
181
|
-
|
|
182
|
-
test("mixed calls should use same cached data", () => {
|
|
183
|
-
// First get schema
|
|
184
|
-
const schema = getSubschema(testPath, schemaId, overlays);
|
|
185
|
-
|
|
186
|
-
// Then get validator - should use same cache entry
|
|
187
|
-
const validator = getSubschemaValidator(testPath, schemaId, overlays);
|
|
188
|
-
|
|
189
|
-
// Then get schema again
|
|
190
|
-
const schema2 = getSubschema(testPath, schemaId, overlays);
|
|
191
|
-
|
|
192
|
-
expect(schema).toBe(schema2);
|
|
193
|
-
expect(typeof validator).toBe("function");
|
|
194
|
-
|
|
195
|
-
// Should still be only one cache entry
|
|
196
|
-
const stats = getCacheStats();
|
|
197
|
-
expect(stats.totalEntries).toBe(1);
|
|
198
|
-
});
|
|
199
|
-
|
|
200
|
-
test("validators should work correctly with cached schemas", () => {
|
|
201
|
-
// Use a path that exists - status
|
|
202
|
-
const validator = getSubschemaValidator("/status", schemaId, overlays);
|
|
203
|
-
|
|
204
|
-
// Test that the validator is a function
|
|
205
|
-
expect(typeof validator).toBe("function");
|
|
206
|
-
|
|
207
|
-
// Test with a valid status value
|
|
208
|
-
const result = validator("active");
|
|
209
|
-
|
|
210
|
-
// The validator should exist and be callable
|
|
211
|
-
expect(typeof result).toBe("boolean");
|
|
212
|
-
});
|
|
213
|
-
});
|
|
214
|
-
|
|
215
|
-
describe("Integration with Existing Functions", () => {
|
|
216
|
-
test("isPathValid should work with cached schemas", () => {
|
|
217
|
-
// Use a path that definitely exists
|
|
218
|
-
const validPath = "/participants";
|
|
219
|
-
|
|
220
|
-
// First call will populate cache
|
|
221
|
-
const isValid1 = isPathValid(validPath, schemaId, overlays);
|
|
222
|
-
|
|
223
|
-
// Second call should use cache
|
|
224
|
-
const isValid2 = isPathValid(validPath, schemaId, overlays);
|
|
225
|
-
|
|
226
|
-
expect(isValid1).toBe(isValid2);
|
|
227
|
-
expect(isValid1).toBe(true);
|
|
228
|
-
|
|
229
|
-
// Should have cached entry
|
|
230
|
-
const stats = getCacheStats();
|
|
231
|
-
expect(stats.totalEntries).toBeGreaterThan(0);
|
|
232
|
-
});
|
|
233
|
-
|
|
234
|
-
test("should handle invalid paths gracefully", () => {
|
|
235
|
-
const invalidPath = "/nonexistent/path/here";
|
|
236
|
-
|
|
237
|
-
const schema = getSubschema(invalidPath, schemaId, overlays);
|
|
238
|
-
const validator = getSubschemaValidator(invalidPath, schemaId, overlays);
|
|
239
|
-
const isValid = isPathValid(invalidPath, schemaId, overlays);
|
|
240
|
-
|
|
241
|
-
expect(schema).toBeUndefined();
|
|
242
|
-
expect(typeof validator).toBe("function");
|
|
243
|
-
expect(isValid).toBe(false);
|
|
244
|
-
|
|
245
|
-
// Should still cache the result
|
|
246
|
-
const stats = getCacheStats();
|
|
247
|
-
expect(stats.totalEntries).toBe(1);
|
|
248
|
-
});
|
|
249
|
-
});
|
|
250
|
-
|
|
251
|
-
describe("Performance Characteristics", () => {
|
|
252
|
-
test("should demonstrate significant speedup on cached calls", () => {
|
|
253
|
-
const iterations = 10;
|
|
254
|
-
|
|
255
|
-
// First call (cold cache)
|
|
256
|
-
const start1 = process.hrtime.bigint();
|
|
257
|
-
for (let i = 0; i < iterations; i++) {
|
|
258
|
-
clearSchemaCache();
|
|
259
|
-
getSubschema(testPath, schemaId, overlays);
|
|
260
|
-
}
|
|
261
|
-
const end1 = process.hrtime.bigint();
|
|
262
|
-
const coldTime = Number(end1 - start1) / 1_000_000; // Convert to milliseconds
|
|
263
|
-
|
|
264
|
-
// Warm up cache
|
|
265
|
-
getSubschema(testPath, schemaId, overlays);
|
|
266
|
-
|
|
267
|
-
// Subsequent calls (warm cache)
|
|
268
|
-
const start2 = process.hrtime.bigint();
|
|
269
|
-
for (let i = 0; i < iterations; i++) {
|
|
270
|
-
getSubschema(testPath, schemaId, overlays);
|
|
271
|
-
}
|
|
272
|
-
const end2 = process.hrtime.bigint();
|
|
273
|
-
const warmTime = Number(end2 - start2) / 1_000_000; // Convert to milliseconds
|
|
274
|
-
|
|
275
|
-
// Warm cache should be significantly faster
|
|
276
|
-
expect(warmTime).toBeLessThan(coldTime / 10); // At least 10x faster
|
|
277
|
-
});
|
|
278
|
-
|
|
279
|
-
test("cache should scale well with multiple entries", () => {
|
|
280
|
-
const paths = [
|
|
281
|
-
"/participants",
|
|
282
|
-
"/status",
|
|
283
|
-
"/propertyPack/surveys",
|
|
284
|
-
"/propertyPack/valuations",
|
|
285
|
-
];
|
|
286
|
-
|
|
287
|
-
const overlaySet = [
|
|
288
|
-
["baspiV5"],
|
|
289
|
-
["ta6ed4"],
|
|
290
|
-
["baspiV5", "ta6ed4"],
|
|
291
|
-
];
|
|
292
|
-
|
|
293
|
-
// Populate cache with multiple combinations
|
|
294
|
-
paths.forEach(path => {
|
|
295
|
-
overlaySet.forEach(overlays => {
|
|
296
|
-
getSubschema(path, schemaId, overlays);
|
|
297
|
-
});
|
|
298
|
-
});
|
|
299
|
-
|
|
300
|
-
const stats = getCacheStats();
|
|
301
|
-
expect(stats.totalEntries).toBe(paths.length * overlaySet.length);
|
|
302
|
-
|
|
303
|
-
// All subsequent calls should be fast
|
|
304
|
-
const start = process.hrtime.bigint();
|
|
305
|
-
paths.forEach(path => {
|
|
306
|
-
overlaySet.forEach(overlays => {
|
|
307
|
-
getSubschema(path, schemaId, overlays);
|
|
308
|
-
});
|
|
309
|
-
});
|
|
310
|
-
const end = process.hrtime.bigint();
|
|
311
|
-
const timeMs = Number(end - start) / 1_000_000;
|
|
312
|
-
|
|
313
|
-
// Should be very fast (less than 1ms total for all cached calls)
|
|
314
|
-
expect(timeMs).toBeLessThan(1);
|
|
315
|
-
});
|
|
316
|
-
});
|
|
317
|
-
});
|