@pdtf/schemas 3.6.0-dev.15 → 3.6.0-dev.17
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 +191 -0
- package/package.json +1 -1
- package/src/schemas/v3/combined.json +130 -323
- package/src/schemas/v3/compactSkeleton.txt +10 -51
- package/src/schemas/v3/overlays/baspi4.json +9 -0
- package/src/schemas/v3/overlays/baspi5.json +9 -0
- package/src/schemas/v3/overlays/piq.json +9 -0
- package/src/schemas/v3/overlays/ta6ed6.json +55 -129
- package/src/schemas/v3/overlays/ta6ed6v2.json +55 -129
- package/src/schemas/v3/pdtf-transaction.json +124 -289
- package/src/schemas/v3/skeleton.json +17 -53
package/index.js
CHANGED
|
@@ -225,6 +225,183 @@ const generateOverlayKey = (overlays) => {
|
|
|
225
225
|
return overlays.join(".");
|
|
226
226
|
};
|
|
227
227
|
|
|
228
|
+
|
|
229
|
+
// ---------------------------------------------------------------------------
|
|
230
|
+
// Overlay scoping
|
|
231
|
+
//
|
|
232
|
+
// A composed subschema is the CORE schema merged with the requested overlays,
|
|
233
|
+
// so it carries every field the core schema knows about — including questions
|
|
234
|
+
// the requested overlays never ask. Validating that full shape makes a form's
|
|
235
|
+
// completeness depend on questions it does not render: e.g. the NTS Material
|
|
236
|
+
// Information "Ownership" section validated the TA7 leasehold
|
|
237
|
+
// `ownershipAndManagement` node, so a seller who part-answered TA7 could never
|
|
238
|
+
// complete Material Information, with no field named in the error (the core
|
|
239
|
+
// schema keeps the `oneOf` but `extractOverlay` strips the `discriminator` that
|
|
240
|
+
// disambiguated it, so an absent tag matches EVERY branch: "must match exactly
|
|
241
|
+
// one schema in oneOf", passingSchemas [0,1]).
|
|
242
|
+
//
|
|
243
|
+
// Scoping prunes the composed subschema to the fields the requested overlays
|
|
244
|
+
// actually reference, so a form is validated against exactly what it asks.
|
|
245
|
+
//
|
|
246
|
+
// Discriminator tags are preserved even when unreferenced: they are structural,
|
|
247
|
+
// and removing one leaves AJV compiling a `discriminator` whose tag is gone
|
|
248
|
+
// ("oneOf subschemas must have properties/<tag>"). Tags are inherited into
|
|
249
|
+
// `oneOf` branches and `items`, because that is where the tag property lives.
|
|
250
|
+
// ---------------------------------------------------------------------------
|
|
251
|
+
|
|
252
|
+
// Every form overlay id -> the marker `extractOverlay` stamps for it. This must
|
|
253
|
+
// cover EVERY id in `overlaysMap`: an id missing here contributes no ref key, so
|
|
254
|
+
// its fields would look unreferenced and be pruned away. `getOverlayRefKeys`
|
|
255
|
+
// returns null if it sees an unknown id, which disables scoping altogether —
|
|
256
|
+
// under-scoping merely preserves today's behaviour, over-scoping would mark
|
|
257
|
+
// sections complete that are not.
|
|
258
|
+
const overlayToRefMapping = {
|
|
259
|
+
baspiV4: "baspi4Ref",
|
|
260
|
+
baspiV5: "baspi5Ref",
|
|
261
|
+
nts2023: "ntsRef",
|
|
262
|
+
nts2025: "nts2Ref",
|
|
263
|
+
ntsl2023: "ntslRef",
|
|
264
|
+
ntsl2025: "ntsl2Ref",
|
|
265
|
+
ta6ed4: "ta6Ref",
|
|
266
|
+
ta6ed6: "ta6ed6Ref",
|
|
267
|
+
ta6ed6v2: "ta6ed6v2Ref",
|
|
268
|
+
ta7ed3: "ta7Ref",
|
|
269
|
+
ta7ed5: "ta7ed5Ref",
|
|
270
|
+
ta10ed3: "ta10Ref",
|
|
271
|
+
lpe1ed4: "lpe1Ref",
|
|
272
|
+
fme1ed2: "fme1Ref",
|
|
273
|
+
llc1v2: "llc1Ref",
|
|
274
|
+
con29R2019: "con29RRef",
|
|
275
|
+
con29DW: "con29DWRef",
|
|
276
|
+
rdsV333: "rdsRef",
|
|
277
|
+
oc1v21: "oc1Ref",
|
|
278
|
+
piqV3: "piqRef",
|
|
279
|
+
sr24: "sr24Ref",
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
// Extension overlays contribute their host form's ref key(s). `ta` is the TA6
|
|
283
|
+
// compatibility overlay: it exists so TA6 answers can never invalidate the NTS
|
|
284
|
+
// sections, and its marker must therefore count as visible here too — otherwise
|
|
285
|
+
// scoping would prune away the very fields that overlay was added to protect.
|
|
286
|
+
const extensionOverlayToRefs = {
|
|
287
|
+
as: ["ntsRef"],
|
|
288
|
+
dr: ["ntsRef"],
|
|
289
|
+
er: ["ntsRef"],
|
|
290
|
+
fd: ["ntsRef"],
|
|
291
|
+
hi: ["ntsRef"],
|
|
292
|
+
hs: ["ntsRef"],
|
|
293
|
+
jk: ["ntsRef"],
|
|
294
|
+
la: ["ntsRef"],
|
|
295
|
+
ma: ["ntsRef"],
|
|
296
|
+
mc: ["ntsRef"],
|
|
297
|
+
oa: ["ntsRef"],
|
|
298
|
+
oc: ["ntsRef"],
|
|
299
|
+
sb: ["ntsRef"],
|
|
300
|
+
sf: ["ntsRef"],
|
|
301
|
+
sl: ["ntsRef"],
|
|
302
|
+
tf: ["ntsRef"],
|
|
303
|
+
dk: ["ntsRef", "sef25Ref"],
|
|
304
|
+
ic: ["ntsRef", "sef25Ref"],
|
|
305
|
+
lc: ["ntsRef", "sef25Ref"],
|
|
306
|
+
mi: ["ntsRef", "sef25Ref"],
|
|
307
|
+
nd: ["ntsRef", "sef25Ref"],
|
|
308
|
+
pc: ["ntsRef", "sef25Ref"],
|
|
309
|
+
ph: ["ntsRef", "sef25Ref"],
|
|
310
|
+
rw: ["ntsRef", "sef25Ref"],
|
|
311
|
+
sc: ["ntsRef", "sef25Ref"],
|
|
312
|
+
sd: ["ntsRef", "sef25Ref"],
|
|
313
|
+
tr: ["ntsRef", "sef25Ref"],
|
|
314
|
+
wg: ["ntsRef", "sef25Ref"],
|
|
315
|
+
ac: ["ntsRef", "sef25Ref"],
|
|
316
|
+
ta: ["ta6ed6CompatRef"],
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
// Returns the ref keys these overlays reference, or null when scoping must be
|
|
320
|
+
// skipped: an unrecognised overlay id (whose markers we therefore cannot
|
|
321
|
+
// identify) would otherwise have all of its fields pruned as unreferenced.
|
|
322
|
+
const getOverlayRefKeys = (overlays = []) => {
|
|
323
|
+
const overlayIds = (Array.isArray(overlays) ? overlays : [overlays]).filter(
|
|
324
|
+
(id) => id !== null && id !== undefined,
|
|
325
|
+
);
|
|
326
|
+
const refKeys = new Set();
|
|
327
|
+
for (const overlay of overlayIds) {
|
|
328
|
+
const refKey = overlayToRefMapping[overlay];
|
|
329
|
+
const extensionRefs = extensionOverlayToRefs[overlay];
|
|
330
|
+
if (!refKey && !extensionRefs) return null; // unknown id -> do not scope
|
|
331
|
+
if (refKey) refKeys.add(refKey);
|
|
332
|
+
(extensionRefs || []).forEach((k) => refKeys.add(k));
|
|
333
|
+
}
|
|
334
|
+
return [...refKeys];
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
// True when this node, or anything beneath it, is referenced by the overlays.
|
|
338
|
+
const schemaHasVisibleOverlayRef = (schema, refKeys = []) => {
|
|
339
|
+
if (!schema || typeof schema !== "object") return false;
|
|
340
|
+
if (refKeys.some((refKey) => !!schema[refKey])) return true;
|
|
341
|
+
if (schema.items && schemaHasVisibleOverlayRef(schema.items, refKeys))
|
|
342
|
+
return true;
|
|
343
|
+
if (schema.properties) {
|
|
344
|
+
if (
|
|
345
|
+
Object.values(schema.properties).some((p) =>
|
|
346
|
+
schemaHasVisibleOverlayRef(p, refKeys),
|
|
347
|
+
)
|
|
348
|
+
)
|
|
349
|
+
return true;
|
|
350
|
+
}
|
|
351
|
+
if (Array.isArray(schema.oneOf)) {
|
|
352
|
+
return schema.oneOf.some((b) => schemaHasVisibleOverlayRef(b, refKeys));
|
|
353
|
+
}
|
|
354
|
+
return false;
|
|
355
|
+
};
|
|
356
|
+
|
|
357
|
+
// The property names that discriminate this node's `oneOf`: the declared
|
|
358
|
+
// `discriminator`, plus — for a `oneOf` whose discriminator was stripped when
|
|
359
|
+
// the core schema was built — the enum key every branch constrains.
|
|
360
|
+
const discriminatingTags = (node) => {
|
|
361
|
+
const tags = new Set();
|
|
362
|
+
if (node.discriminator?.propertyName) tags.add(node.discriminator.propertyName);
|
|
363
|
+
if (Array.isArray(node.oneOf)) {
|
|
364
|
+
const branchKeys = node.oneOf.map(
|
|
365
|
+
(b) => new Set(Object.keys(b?.properties || {})),
|
|
366
|
+
);
|
|
367
|
+
const common = [...(branchKeys[0] || [])].filter((k) =>
|
|
368
|
+
branchKeys.every((s) => s.has(k)),
|
|
369
|
+
);
|
|
370
|
+
const inferred = common.find((k) =>
|
|
371
|
+
node.oneOf.every((b) => Array.isArray(b.properties[k]?.enum)),
|
|
372
|
+
);
|
|
373
|
+
if (inferred) tags.add(inferred);
|
|
374
|
+
}
|
|
375
|
+
return tags;
|
|
376
|
+
};
|
|
377
|
+
|
|
378
|
+
const pruneToOverlayScope = (node, refKeys, inheritedTags = new Set()) => {
|
|
379
|
+
if (!node || typeof node !== "object") return node;
|
|
380
|
+
const ownTags = discriminatingTags(node);
|
|
381
|
+
const keep = new Set([...ownTags, ...inheritedTags]);
|
|
382
|
+
|
|
383
|
+
if (node.properties) {
|
|
384
|
+
for (const [key, propertySchema] of Object.entries(node.properties)) {
|
|
385
|
+
if (!keep.has(key) && !schemaHasVisibleOverlayRef(propertySchema, refKeys)) {
|
|
386
|
+
delete node.properties[key];
|
|
387
|
+
if (Array.isArray(node.required)) {
|
|
388
|
+
node.required = node.required.filter((r) => r !== key);
|
|
389
|
+
}
|
|
390
|
+
} else {
|
|
391
|
+
pruneToOverlayScope(propertySchema, refKeys, new Set());
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
// Tags live inside branches and array items, so they must be inherited there.
|
|
396
|
+
if (node.items) pruneToOverlayScope(node.items, refKeys, ownTags);
|
|
397
|
+
if (Array.isArray(node.oneOf)) {
|
|
398
|
+
for (const branch of node.oneOf) {
|
|
399
|
+
pruneToOverlayScope(branch, refKeys, ownTags);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
return node;
|
|
403
|
+
};
|
|
404
|
+
|
|
228
405
|
// Enhanced caching function that stores both subschemas and validators
|
|
229
406
|
const getCachedSchemaData = (path, schemaId, overlays) => {
|
|
230
407
|
const overlayKey = generateOverlayKey(overlays);
|
|
@@ -313,6 +490,17 @@ const getCachedSchemaData = (path, schemaId, overlays) => {
|
|
|
313
490
|
}, sourceSchema);
|
|
314
491
|
}
|
|
315
492
|
|
|
493
|
+
// Scope the composed subschema to what these overlays actually ask, so a
|
|
494
|
+
// form's completeness never depends on questions it does not render.
|
|
495
|
+
// No overlays (or none carrying ref keys) => no scoping, shape unchanged.
|
|
496
|
+
const refKeys = getOverlayRefKeys(overlays);
|
|
497
|
+
if (refKeys && refKeys.length > 0 && subSchema && typeof subSchema === "object") {
|
|
498
|
+
subSchema = pruneToOverlayScope(
|
|
499
|
+
JSON.parse(JSON.stringify(subSchema)),
|
|
500
|
+
refKeys,
|
|
501
|
+
);
|
|
502
|
+
}
|
|
503
|
+
|
|
316
504
|
// Add schema to AJV and get validator
|
|
317
505
|
// Only add valid schemas to AJV
|
|
318
506
|
let validator;
|
|
@@ -641,6 +829,9 @@ module.exports = {
|
|
|
641
829
|
isPathValid,
|
|
642
830
|
getSubschemaValidator,
|
|
643
831
|
getTitleAtPath,
|
|
832
|
+
getOverlayRefKeys,
|
|
833
|
+
schemaHasVisibleOverlayRef,
|
|
834
|
+
pruneToOverlayScope,
|
|
644
835
|
verifiedClaimsSchema,
|
|
645
836
|
validateVerifiedClaims,
|
|
646
837
|
overlaysMap,
|