@pdtf/schemas 3.4.1-4 → 3.4.1-6

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/index.js CHANGED
@@ -10,6 +10,9 @@ 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
+
13
16
  const verifiedClaimsSchema = require("./src/schemas/verifiedClaims/pdtf-verified-claims.json");
14
17
  const v2CoreSchema = require("./src/schemas/v2/pdtf-transaction.json");
15
18
  const v3CoreSchema = require("./src/schemas/v3/pdtf-transaction.json");
@@ -180,32 +183,74 @@ const generateOverlayKey = (overlays) => {
180
183
  return overlays.join(".");
181
184
  };
182
185
 
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
+
183
246
  const getValidator = (schemaId, overlays) => {
184
247
  return getSubschemaValidator("", schemaId, overlays);
185
248
  };
186
249
 
187
- // common functions for v1 and v2
250
+ // common functions for v1 and v2 - now with caching
188
251
  const getSubschema = (path, schemaId, overlays) => {
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);
252
+ const cached = getCachedSchemaData(path, schemaId, overlays);
253
+ return cached.subSchema;
209
254
  };
210
255
 
211
256
  const isPathValid = (path, schemaId, overlays) => {
@@ -217,18 +262,8 @@ const isPathValid = (path, schemaId, overlays) => {
217
262
  };
218
263
 
219
264
  const getSubschemaValidator = (path, schemaId, overlays) => {
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;
265
+ const cached = getCachedSchemaData(path, schemaId, overlays);
266
+ return cached.validator;
232
267
  };
233
268
 
234
269
  // v1, deprecated
@@ -312,6 +347,20 @@ const validateVerifiedClaims = (verifiedClaims, schemaId, overlays) => {
312
347
  return validationErrorsArr;
313
348
  };
314
349
 
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
+
315
364
  module.exports = {
316
365
  ajv,
317
366
  getTransactionSchema,
@@ -324,4 +373,7 @@ module.exports = {
324
373
  validateVerifiedClaims,
325
374
  overlaysMap,
326
375
  extensionOverlays,
376
+ // New cache management functions
377
+ getCacheStats,
378
+ clearSchemaCache,
327
379
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pdtf/schemas",
3
- "version": "3.4.1-4",
3
+ "version": "3.4.1-6",
4
4
  "description": "Property Data Trust Framework Schemas and Utilities",
5
5
  "main": "index.js",
6
6
  "directories": {
@@ -190,6 +190,65 @@
190
190
  "participantStatus": {
191
191
  "type": "string",
192
192
  "enum": ["Proposed", "Invited", "Active", "Removed"]
193
+ },
194
+ "verification": {
195
+ "type": "object",
196
+ "properties": {
197
+ "identity": {
198
+ "type": "object",
199
+ "properties": {
200
+ "result": {
201
+ "type": "string",
202
+ "enum": ["pass", "fail", "consider"]
203
+ },
204
+ "reports": {
205
+ "type": "array",
206
+ "items": {
207
+ "type": "object",
208
+ "properties": {
209
+ "reportName": {
210
+ "type": "string"
211
+ },
212
+ "result": {
213
+ "type": "string",
214
+ "enum": ["pass", "fail", "consider"]
215
+ },
216
+ "details": {
217
+ "type": "string"
218
+ }
219
+ }
220
+ }
221
+ }
222
+ }
223
+ },
224
+ "antiMoneyLaundering": {
225
+ "type": "object",
226
+ "properties": {
227
+ "result": {
228
+ "type": "string",
229
+ "enum": ["pass", "fail", "consider"]
230
+ },
231
+ "reports": {
232
+ "type": "array",
233
+ "items": {
234
+ "type": "object",
235
+ "properties": {
236
+ "reportName": {
237
+ "type": "string"
238
+ },
239
+ "result": {
240
+ "type": "string",
241
+ "enum": ["pass", "fail", "consider"]
242
+ },
243
+ "details": {
244
+ "type": "string"
245
+ }
246
+ }
247
+ }
248
+ }
249
+ }
250
+ }
251
+ }
193
252
  }
194
253
  },
195
254
  "discriminator": {
@@ -20012,7 +20071,11 @@
20012
20071
  }
20013
20072
  ]
20014
20073
  }
20015
- }
20074
+ },
20075
+ "ntsRequired": ["offMainsDrainageSystem"],
20076
+ "nts2Required": ["offMainsDrainageSystem"],
20077
+ "ntslRequired": ["offMainsDrainageSystem"],
20078
+ "ntsl2Required": ["offMainsDrainageSystem"]
20016
20079
  }
20017
20080
  ]
20018
20081
  },
@@ -1568,7 +1568,10 @@
1568
1568
  }
1569
1569
  }
1570
1570
  }
1571
- }
1571
+ },
1572
+ "required": [
1573
+ "offMainsDrainageSystem"
1574
+ ]
1572
1575
  }
1573
1576
  ],
1574
1577
  "required": [
@@ -318,7 +318,10 @@
318
318
  }
319
319
  }
320
320
  }
321
- }
321
+ },
322
+ "required": [
323
+ "estateRentcharges"
324
+ ]
322
325
  },
323
326
  {
324
327
  "properties": {
@@ -2069,7 +2072,10 @@
2069
2072
  }
2070
2073
  }
2071
2074
  }
2072
- }
2075
+ },
2076
+ "required": [
2077
+ "offMainsDrainageSystem"
2078
+ ]
2073
2079
  }
2074
2080
  ],
2075
2081
  "required": [
@@ -1229,7 +1229,10 @@
1229
1229
  }
1230
1230
  }
1231
1231
  }
1232
- }
1232
+ },
1233
+ "required": [
1234
+ "offMainsDrainageSystem"
1235
+ ]
1233
1236
  }
1234
1237
  ],
1235
1238
  "required": [
@@ -1580,7 +1580,10 @@
1580
1580
  }
1581
1581
  }
1582
1582
  }
1583
- }
1583
+ },
1584
+ "required": [
1585
+ "offMainsDrainageSystem"
1586
+ ]
1584
1587
  }
1585
1588
  ],
1586
1589
  "required": [
@@ -191,6 +191,81 @@
191
191
  "Active",
192
192
  "Removed"
193
193
  ]
194
+ },
195
+ "verification": {
196
+ "type": "object",
197
+ "properties": {
198
+ "identity": {
199
+ "type": "object",
200
+ "properties": {
201
+ "result": {
202
+ "type": "string",
203
+ "enum": [
204
+ "pass",
205
+ "fail",
206
+ "consider"
207
+ ]
208
+ },
209
+ "reports": {
210
+ "type": "array",
211
+ "items": {
212
+ "type": "object",
213
+ "properties": {
214
+ "reportName": {
215
+ "type": "string"
216
+ },
217
+ "result": {
218
+ "type": "string",
219
+ "enum": [
220
+ "pass",
221
+ "fail",
222
+ "consider"
223
+ ]
224
+ },
225
+ "details": {
226
+ "type": "string"
227
+ }
228
+ }
229
+ }
230
+ }
231
+ }
232
+ },
233
+ "antiMoneyLaundering": {
234
+ "type": "object",
235
+ "properties": {
236
+ "result": {
237
+ "type": "string",
238
+ "enum": [
239
+ "pass",
240
+ "fail",
241
+ "consider"
242
+ ]
243
+ },
244
+ "reports": {
245
+ "type": "array",
246
+ "items": {
247
+ "type": "object",
248
+ "properties": {
249
+ "reportName": {
250
+ "type": "string"
251
+ },
252
+ "result": {
253
+ "type": "string",
254
+ "enum": [
255
+ "pass",
256
+ "fail",
257
+ "consider"
258
+ ]
259
+ },
260
+ "details": {
261
+ "type": "string"
262
+ }
263
+ }
264
+ }
265
+ }
266
+ }
267
+ }
268
+ }
194
269
  }
195
270
  },
196
271
  "oneOf": [
@@ -31,6 +31,24 @@
31
31
  "role": {},
32
32
  "externalIds": {},
33
33
  "participantStatus": {},
34
+ "verification": {
35
+ "identity": {
36
+ "result": {},
37
+ "reports": {
38
+ "reportName": {},
39
+ "result": {},
40
+ "details": {}
41
+ }
42
+ },
43
+ "antiMoneyLaundering": {
44
+ "result": {},
45
+ "reports": {
46
+ "reportName": {},
47
+ "result": {},
48
+ "details": {}
49
+ }
50
+ }
51
+ },
34
52
  "sellersCapacity": {
35
53
  "capacity": {},
36
54
  "sellersCapacityDetails": {},
@@ -0,0 +1,317 @@
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
+ });
@@ -448,3 +448,81 @@ test("validates a valid contract", () => {
448
448
  const isValid = validator(data);
449
449
  expect(isValid).toBe(true);
450
450
  });
451
+
452
+ test("waterAndDrainage state with mainsFoulDrainage No missing offMainsDrainageSystem is now invalid under nts2023 overlay", () => {
453
+ const validator = getValidator(schemaId, ["nts2023"]);
454
+ const clonedExampleTransaction = JSON.parse(
455
+ JSON.stringify(exampleTransaction)
456
+ );
457
+
458
+ // Set the waterAndDrainage to the state from the user request
459
+ // This SHOULD be invalid because offMainsDrainageSystem is required when mainsFoulDrainage.yesNo is "No"
460
+ clonedExampleTransaction.propertyPack.waterAndDrainage = {
461
+ "water": {
462
+ "mainsWater": {
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
+ }
478
+ };
479
+
480
+ const isValid = validator(clonedExampleTransaction);
481
+ expect(isValid).toBe(false);
482
+
483
+ // Check that the validation error is about missing offMainsDrainageSystem
484
+ const relevantError = validator.errors.find(error =>
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
+ );
496
+
497
+ // Set the waterAndDrainage to an actually invalid state for nts2023
498
+ // The nts overlay restricts mainsFoulDrainage.yesNo to only "Yes" or "No"
499
+ // (not "Not known" like the base schema allows)
500
+ clonedExampleTransaction.propertyPack.waterAndDrainage = {
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
+ };
518
+
519
+ const isValid = validator(clonedExampleTransaction);
520
+ expect(isValid).toBe(false);
521
+
522
+ // Check that the validation error is about the invalid enum value
523
+ const relevantError = validator.errors.find(error =>
524
+ error.instancePath.includes('mainsFoulDrainage') &&
525
+ error.message.includes('must be equal to one of the allowed values')
526
+ );
527
+ expect(relevantError).toBeDefined();
528
+ });