@pdtf/schemas 3.2.1-14 → 3.2.1-15

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
@@ -61,14 +61,49 @@ const transactionSchemas = {
61
61
  };
62
62
 
63
63
  const combineMerge = (target, source, options) => {
64
+ // Special handling for 'required' arrays - combine them uniquely
65
+ if (target.some((item) => typeof item === "string")) {
66
+ return [...new Set([...target, ...source])];
67
+ }
68
+
69
+ // For other arrays, concatenate while preserving uniqueness for primitive values
70
+ if (!options.isMergeableObject(target[0])) {
71
+ return [...new Set([...target, ...source])];
72
+ }
73
+
74
+ // Special handling for oneOf arrays - merge matching schemas based on discriminator
75
+ if (target[0]?.properties && source[0]?.properties) {
76
+ // Find a common discriminator property (a property with an enum)
77
+ const findDiscriminator = (schema) => {
78
+ const props = schema.properties;
79
+ return Object.keys(props).find((key) => Array.isArray(props[key]?.enum));
80
+ };
81
+
82
+ const discriminator = findDiscriminator(target[0]);
83
+ if (discriminator) {
84
+ return target.map((targetSchema) => {
85
+ const targetEnum = targetSchema.properties?.[discriminator]?.enum || [];
86
+ const matchingSourceSchema = source.find((sourceSchema) => {
87
+ const sourceEnum =
88
+ sourceSchema.properties?.[discriminator]?.enum || [];
89
+ // Check if there's any overlap between the enum values
90
+ return sourceEnum.some((value) => targetEnum.includes(value));
91
+ });
92
+ if (matchingSourceSchema) {
93
+ return merge(targetSchema, matchingSourceSchema, options);
94
+ }
95
+ return targetSchema;
96
+ });
97
+ }
98
+ }
99
+
100
+ // For arrays of objects, merge by index and append remaining items
64
101
  const destination = target.slice();
65
102
  source.forEach((item, index) => {
66
103
  if (typeof destination[index] === "undefined") {
67
104
  destination[index] = options.cloneUnlessOtherwiseSpecified(item, options);
68
105
  } else if (options.isMergeableObject(item)) {
69
106
  destination[index] = merge(target[index], item, options);
70
- } else if (target.indexOf(item) === -1) {
71
- destination.push(item);
72
107
  }
73
108
  });
74
109
  return destination;
@@ -84,7 +119,12 @@ const getTransactionSchema = (
84
119
  if (!overlays || overlays.length < 1) return sourceSchema;
85
120
  let mergedSchema = sourceSchema;
86
121
  overlays.forEach((overlay) => {
87
- const overlaySchema = overlaysMap[schemaId][overlay] || {};
122
+ // Handle both string keys and direct overlay objects
123
+ const overlaySchema =
124
+ typeof overlay === "string"
125
+ ? overlaysMap[schemaId][overlay] || {}
126
+ : overlay;
127
+
88
128
  mergedSchema = merge(mergedSchema, overlaySchema, {
89
129
  customMerge: (key) => {
90
130
  if (key === "enum") {
@@ -97,6 +137,23 @@ const getTransactionSchema = (
97
137
  return mergedSchema;
98
138
  };
99
139
 
140
+ // Add helper function to generate cache key
141
+ const generateOverlayKey = (overlays) => {
142
+ if (!overlays) return "";
143
+ return overlays
144
+ .map((overlay) => {
145
+ if (typeof overlay === "string") return overlay;
146
+ // Generate a simple hash of the object for caching
147
+ return JSON.stringify(overlay)
148
+ .split("")
149
+ .reduce((hash, char) => {
150
+ return ((hash << 5) - hash + char.charCodeAt(0)) | 0;
151
+ }, 0)
152
+ .toString(36);
153
+ })
154
+ .join(".");
155
+ };
156
+
100
157
  const getValidator = (schemaId, overlays) => {
101
158
  return getSubschemaValidator("", schemaId, overlays);
102
159
  };
@@ -135,7 +192,7 @@ const isPathValid = (path, schemaId, overlays) => {
135
192
 
136
193
  const getSubschemaValidator = (path, schemaId, overlays) => {
137
194
  const subSchema = getSubschema(path, schemaId, overlays);
138
- const overlayKey = (overlays || []).join(".");
195
+ const overlayKey = generateOverlayKey(overlays);
139
196
  // see if we can retrieve the schema by path, schemaId and overlays
140
197
  const cacheKey = `${path}-${schemaId}-${overlayKey}`;
141
198
  let validator = ajv.getSchema(cacheKey);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pdtf/schemas",
3
- "version": "3.2.1-14",
3
+ "version": "3.2.1-15",
4
4
  "description": "Property Data Trust Framework Schemas and Utilities",
5
5
  "main": "index.js",
6
6
  "directories": {
@@ -0,0 +1,55 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "$id": "https://trust.propdata.org.uk/schemas/v3/overlays/custom.json",
4
+ "properties": {
5
+ "propertyPack": {
6
+ "customRef": "JKW",
7
+ "type": "object",
8
+ "required": ["specialistIssues"],
9
+ "properties": {
10
+ "specialistIssues": {
11
+ "customRef": "JKW.7",
12
+ "required": ["japaneseKnotweed"],
13
+ "properties": {
14
+ "japaneseKnotweed": {
15
+ "customRef": "JKW.7.8",
16
+ "discriminator": {
17
+ "propertyName": "yesNo"
18
+ },
19
+ "oneOf": [
20
+ {
21
+ "properties": {
22
+ "yesNo": {
23
+ "enum": ["No", "Not known"]
24
+ }
25
+ }
26
+ },
27
+ {
28
+ "properties": {
29
+ "yesNo": {
30
+ "enum": ["Yes"]
31
+ },
32
+ "managementPlanInPlace": {
33
+ "customRef": "JKW.7.8.2"
34
+ },
35
+ "attachments": {
36
+ "customRef": "JKW.7.8.3"
37
+ }
38
+ },
39
+ "required": ["managementPlanInPlace", "attachments"]
40
+ }
41
+ ],
42
+ "required": ["yesNo"],
43
+ "title": "Is the property affected by Japanese knotweed?",
44
+ "properties": {
45
+ "yesNo": {
46
+ "customRef": "JKW.7.8.1"
47
+ }
48
+ }
49
+ }
50
+ }
51
+ }
52
+ }
53
+ }
54
+ }
55
+ }
@@ -0,0 +1,93 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "$id": "https://trust.propdata.org.uk/schemas/v3/overlays/ta7.json",
4
+ "properties": {
5
+ "propertyPack": {
6
+ "macRef": "MAC",
7
+ "required": ["ownership"],
8
+ "properties": {
9
+ "ownership": {
10
+ "macRef": "MAC.1",
11
+ "required": ["ownershipsToBeTransferred"],
12
+ "properties": {
13
+ "ownershipsToBeTransferred": {
14
+ "macRef": "MAC.1",
15
+ "items": {
16
+ "discriminator": {
17
+ "propertyName": "ownershipType"
18
+ },
19
+ "oneOf": [
20
+ {
21
+ "properties": {
22
+ "ownershipType": {
23
+ "enum": ["Leasehold"]
24
+ },
25
+ "leaseholdInformation": {
26
+ "required": ["contactDetails"],
27
+ "properties": {
28
+ "contactDetails": {
29
+ "macRef": "MAC.4.1",
30
+ "required": ["contacts"],
31
+ "properties": {
32
+ "contacts": {
33
+ "macRef": "MAC.4.1",
34
+ "required": ["managingAgent"],
35
+ "properties": {
36
+ "managingAgent": {
37
+ "macRef": "MAC.4.1c",
38
+ "required": ["contact"],
39
+ "properties": {
40
+ "contact": {
41
+ "macRef": "MAC.4.1c",
42
+ "required": ["nameOrOrganisation"],
43
+ "properties": {
44
+ "nameOrOrganisation": {
45
+ "macRef": "MAC.4.1c.1"
46
+ },
47
+ "address": {
48
+ "macRef": "MAC.4.1c.0",
49
+ "properties": {
50
+ "line1": {
51
+ "macRef": "MAC.4.1c.2"
52
+ },
53
+ "line2": {
54
+ "macRef": "MAC.4.1c.3"
55
+ },
56
+ "line3": {
57
+ "macRef": "MAC.4.1c.4"
58
+ },
59
+ "town": {
60
+ "macRef": "MAC.4.1c.5"
61
+ },
62
+ "postcode": {
63
+ "macRef": "MAC.4.1c.6"
64
+ }
65
+ }
66
+ },
67
+ "telephone": {
68
+ "macRef": "MAC.4.1c.7"
69
+ },
70
+ "emailAddress": {
71
+ "macRef": "MAC.4.1c.8"
72
+ }
73
+ }
74
+ }
75
+ }
76
+ }
77
+ }
78
+ }
79
+ }
80
+ }
81
+ }
82
+ }
83
+ }
84
+ }
85
+ ]
86
+ }
87
+ }
88
+ }
89
+ }
90
+ }
91
+ }
92
+ }
93
+ }
@@ -8,6 +8,7 @@ const {
8
8
  getValidator,
9
9
  getSubschemaValidator,
10
10
  getTitleAtPath,
11
+ overlaysMap,
11
12
  } = require("../../../index.js");
12
13
 
13
14
  const exampleTransaction = require("../../examples/v3/exampleTransaction.json");
@@ -403,3 +404,227 @@ test("validates a valid contract", () => {
403
404
  const isValid = validator(data);
404
405
  expect(isValid).toBe(true);
405
406
  });
407
+
408
+ test("overlay as key and as object produce identical results", () => {
409
+ // Get the NTS overlay object directly from the overlays map
410
+ const ntsOverlayObject = { ...overlaysMap[schemaId]["nts2023"] };
411
+ // Remove the $id to prevent schema registration conflicts
412
+ ntsOverlayObject.$id =
413
+ "https://trust.propdata.org.uk/schemas/v3/overlays/nts2024.json";
414
+
415
+ // Get schema using the key
416
+ const schemaWithKey = getTransactionSchema(schemaId, ["nts2023"]);
417
+
418
+ // Get schema using the object
419
+ const schemaWithObject = getTransactionSchema(schemaId, [ntsOverlayObject]);
420
+
421
+ // Create copies without $id for comparison
422
+ const schemaWithKeyNoId = { ...schemaWithKey };
423
+ const schemaWithObjectNoId = { ...schemaWithObject };
424
+ delete schemaWithKeyNoId.$id;
425
+ delete schemaWithObjectNoId.$id;
426
+
427
+ // Deep compare the results (excluding $id)
428
+ expect(schemaWithObjectNoId).toEqual(schemaWithKeyNoId);
429
+
430
+ // Test that validation produces the same errors
431
+ const validatorWithKey = getValidator(schemaId, ["nts2023"]);
432
+ const validatorWithObject = getValidator(schemaId, [ntsOverlayObject]);
433
+
434
+ validatorWithKey(exampleTransaction);
435
+ validatorWithObject(exampleTransaction);
436
+
437
+ // Compare validation errors
438
+ expect(validatorWithObject.errors).toEqual(validatorWithKey.errors);
439
+ });
440
+
441
+ test("correctly merges custom Japanese Knotweed overlay with base schema", () => {
442
+ // Load the custom overlay
443
+ const customOverlay = require("../../examples/v3/exampleCustomOverlay.json");
444
+
445
+ // Get base schema and schema with overlay
446
+ const baseSchema = getTransactionSchema(schemaId);
447
+ const schemaWithOverlay = getTransactionSchema(schemaId, [customOverlay]);
448
+
449
+ // Verify that other properties in propertyPack are preserved
450
+ expect(
451
+ Object.keys(schemaWithOverlay.properties.propertyPack.properties)
452
+ ).toEqual(Object.keys(baseSchema.properties.propertyPack.properties));
453
+
454
+ // Verify that other properties in specialistIssues are preserved
455
+ const baseSpecialistIssues =
456
+ baseSchema.properties.propertyPack.properties.specialistIssues;
457
+ const overlaidSpecialistIssues =
458
+ schemaWithOverlay.properties.propertyPack.properties.specialistIssues;
459
+ expect(Object.keys(overlaidSpecialistIssues.properties)).toEqual(
460
+ Object.keys(baseSpecialistIssues.properties)
461
+ );
462
+
463
+ // Verify that Japanese Knotweed section is updated
464
+ const japaneseKnotweed = overlaidSpecialistIssues.properties.japaneseKnotweed;
465
+ expect(japaneseKnotweed.title).toBe(
466
+ "Is the property affected by Japanese knotweed?"
467
+ );
468
+ expect(japaneseKnotweed.required).toEqual(["yesNo"]);
469
+
470
+ // Verify the oneOf conditions are updated
471
+ expect(japaneseKnotweed.oneOf).toHaveLength(2);
472
+ expect(japaneseKnotweed.oneOf[0].properties.yesNo.enum).toEqual([
473
+ "No",
474
+ "Not known",
475
+ ]);
476
+ expect(japaneseKnotweed.oneOf[1].properties.yesNo.enum).toEqual(["Yes"]);
477
+ expect(japaneseKnotweed.oneOf[1].required).toEqual([
478
+ "managementPlanInPlace",
479
+ "attachments",
480
+ ]);
481
+ });
482
+
483
+ test("correctly merges multiple overlays using both key and object references", () => {
484
+ // Load the custom overlay
485
+ const customOverlay = require("../../examples/v3/exampleCustomOverlay.json");
486
+
487
+ // Get schema with both overlays
488
+ const schemaWithOverlays = getTransactionSchema(schemaId, [
489
+ "nts2023",
490
+ customOverlay,
491
+ ]);
492
+
493
+ // Get individual overlay schemas for comparison
494
+ const ntsSchema = getTransactionSchema(schemaId, ["nts2023"]);
495
+ const customSchema = getTransactionSchema(schemaId, [customOverlay]);
496
+
497
+ // Verify propertyPack structure is preserved
498
+ const propertyPack = schemaWithOverlays.properties.propertyPack;
499
+ expect(Object.keys(propertyPack.properties)).toEqual(
500
+ expect.arrayContaining(
501
+ Object.keys(ntsSchema.properties.propertyPack.properties)
502
+ )
503
+ );
504
+
505
+ // Verify Japanese Knotweed changes from custom overlay are present
506
+ const japaneseKnotweed =
507
+ propertyPack.properties.specialistIssues.properties.japaneseKnotweed;
508
+ expect(japaneseKnotweed.title).toBe(
509
+ "Is the property affected by Japanese knotweed?"
510
+ );
511
+ expect(japaneseKnotweed.oneOf[1].required).toEqual([
512
+ "managementPlanInPlace",
513
+ "attachments",
514
+ ]);
515
+
516
+ // Verify customRef properties are correctly merged
517
+ expect(propertyPack.customRef).toBe("JKW");
518
+ expect(propertyPack.properties.specialistIssues.customRef).toBe("JKW.7");
519
+ expect(japaneseKnotweed.customRef).toBe("JKW.7.8");
520
+ expect(
521
+ japaneseKnotweed.oneOf[1].properties.managementPlanInPlace.customRef
522
+ ).toBe("JKW.7.8.2");
523
+ expect(japaneseKnotweed.oneOf[1].properties.attachments.customRef).toBe(
524
+ "JKW.7.8.3"
525
+ );
526
+ expect(japaneseKnotweed.properties.yesNo.customRef).toBe("JKW.7.8.1");
527
+
528
+ // Verify NTS specific changes are present (e.g., simplified enums)
529
+ const mainsElectricity =
530
+ propertyPack.properties.electricity.properties.mainsElectricity;
531
+ expect(mainsElectricity.properties.yesNo.enum).toEqual(["Yes", "No"]);
532
+
533
+ // Verify required fields from both overlays are merged
534
+ expect(propertyPack.required).toEqual(
535
+ expect.arrayContaining([
536
+ ...ntsSchema.properties.propertyPack.required,
537
+ ...customSchema.properties.propertyPack.required,
538
+ ])
539
+ );
540
+ });
541
+
542
+ test("correctly merges NTS and both custom overlays with proper required fields", () => {
543
+ // Load both custom overlays
544
+ const japaneseKnoweedOverlay = require("../../examples/v3/exampleCustomOverlay.json");
545
+ const managingAgentOverlay = require("../../examples/v3/exampleCustomOverlay2.json");
546
+
547
+ // Get schema with all overlays
548
+ const schemaWithOverlays = getTransactionSchema(schemaId, [
549
+ "nts2023",
550
+ japaneseKnoweedOverlay,
551
+ managingAgentOverlay,
552
+ ]);
553
+
554
+ // Navigate to the leasehold section in the oneOf array
555
+ const leaseholdSchema =
556
+ schemaWithOverlays.properties.propertyPack.properties.ownership.properties.ownershipsToBeTransferred.items.oneOf.find(
557
+ (schema) => schema.properties.ownershipType.enum[0] === "Leasehold"
558
+ );
559
+
560
+ // Verify the leasehold information structure
561
+ const leaseholdInfo = leaseholdSchema.properties.leaseholdInformation;
562
+
563
+ // Check required fields - should include both NTS and custom overlay requirements
564
+ expect(leaseholdInfo.required).toContain("contactDetails");
565
+ expect(leaseholdInfo.required).toContain("leaseTerm"); // from NTS
566
+ expect(leaseholdInfo.required).toContain("groundRent"); // from NTS
567
+
568
+ // Verify managing agent contact structure and refs
569
+ const managingAgent =
570
+ leaseholdInfo.properties.contactDetails.properties.contacts.properties
571
+ .managingAgent;
572
+ expect(managingAgent.macRef).toBe("MAC.4.1c");
573
+
574
+ // Verify contact details structure
575
+ const contact = managingAgent.properties.contact;
576
+ expect(contact.properties.nameOrOrganisation.macRef).toBe("MAC.4.1c.1");
577
+ expect(contact.properties.address.macRef).toBe("MAC.4.1c.0");
578
+ expect(contact.properties.telephone.macRef).toBe("MAC.4.1c.7");
579
+ expect(contact.properties.emailAddress.macRef).toBe("MAC.4.1c.8");
580
+
581
+ // Verify Japanese Knotweed section is still present and correct
582
+ const japaneseKnotweed =
583
+ schemaWithOverlays.properties.propertyPack.properties.specialistIssues
584
+ .properties.japaneseKnotweed;
585
+ expect(japaneseKnotweed.customRef).toBe("JKW.7.8");
586
+
587
+ // Verify NTS changes are still present
588
+ const mainsElectricity =
589
+ schemaWithOverlays.properties.propertyPack.properties.electricity.properties
590
+ .mainsElectricity;
591
+ expect(mainsElectricity.properties.yesNo.enum).toEqual(["Yes", "No"]);
592
+ });
593
+
594
+ test("ta7 overlay as key and as object produce identical results at leasehold info level", () => {
595
+ // Get the TA7 overlay object directly from the overlays map
596
+ const ta7OverlayObject = { ...overlaysMap[schemaId]["ta7ed3"] };
597
+ // Remove the $id to prevent schema registration conflicts
598
+ ta7OverlayObject.$id =
599
+ "https://trust.propdata.org.uk/schemas/v3/overlays/ta7ed3-copy.json";
600
+
601
+ // Get schemas using both methods
602
+ const schemaWithKey = getTransactionSchema(schemaId, ["ta7ed3"]);
603
+ const schemaWithObject = getTransactionSchema(schemaId, [ta7OverlayObject]);
604
+
605
+ // Navigate to the leasehold information section in both schemas
606
+ const getLeaseholdInfo = (schema) => {
607
+ const ownershipsToBeTransferred =
608
+ schema.properties.propertyPack.properties.ownership.properties
609
+ .ownershipsToBeTransferred;
610
+ const leaseholdSchema = ownershipsToBeTransferred.items.oneOf.find(
611
+ (schema) => schema.properties.ownershipType.enum[0] === "Leasehold"
612
+ );
613
+ return leaseholdSchema.properties.leaseholdInformation;
614
+ };
615
+
616
+ const leaseholdInfoWithKey = getLeaseholdInfo(schemaWithKey);
617
+ const leaseholdInfoWithObject = getLeaseholdInfo(schemaWithObject);
618
+
619
+ // Deep compare the leasehold information sections
620
+ expect(leaseholdInfoWithObject).toEqual(leaseholdInfoWithKey);
621
+
622
+ // Verify specific TA7 requirements are present in both
623
+ expect(leaseholdInfoWithKey.required).toContain("contactDetails");
624
+ expect(leaseholdInfoWithObject.required).toContain("contactDetails");
625
+
626
+ // Verify TA7 refs are present and identical
627
+ const contactDetails = leaseholdInfoWithKey.properties.contactDetails;
628
+ expect(contactDetails.ta7Ref).toBe("4.1");
629
+ expect(leaseholdInfoWithObject.properties.contactDetails.ta7Ref).toBe("4.1");
630
+ });