@pdtf/schemas 3.4.0-9 → 3.4.1-1
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 +15 -0
- package/README.md +326 -6
- package/index.js +32 -0
- package/package.json +1 -1
- package/src/schemas/v3/combined.json +8 -21
- package/src/schemas/v3/overlays/README.md +299 -0
- package/src/schemas/v3/overlays/extensions/as.json +57 -0
- package/src/schemas/v3/overlays/extensions/dr.json +60 -0
- package/src/schemas/v3/overlays/extensions/er.json +71 -0
- package/src/schemas/v3/overlays/extensions/fd.json +63 -0
- package/src/schemas/v3/overlays/extensions/hi.json +44 -0
- package/src/schemas/v3/overlays/extensions/hs.json +56 -0
- package/src/schemas/v3/overlays/extensions/jk.json +55 -0
- package/src/schemas/v3/overlays/extensions/la.json +66 -0
- package/src/schemas/v3/overlays/extensions/ma.json +87 -0
- package/src/schemas/v3/overlays/extensions/mc.json +36 -0
- package/src/schemas/v3/overlays/extensions/oa.json +22 -0
- package/src/schemas/v3/overlays/extensions/oc.json +35 -0
- package/src/schemas/v3/overlays/extensions/sb.json +56 -0
- package/src/schemas/v3/overlays/extensions/sf.json +57 -0
- package/src/schemas/v3/overlays/extensions/sl.json +46 -0
- package/src/schemas/v3/overlays/extensions/tf.json +91 -0
- package/src/schemas/v3/overlays/nts2.json +6 -18
- package/src/schemas/v3/overlays/ntsl2.json +2 -8
- package/src/schemas/v3/overlays/ta6.json +1 -0
- package/src/tests/v3/extensionOverlays.test.js +253 -0
- package/src/utils/extractExtensionOverlays.js +271 -0
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
const {
|
|
2
|
+
getTransactionSchema,
|
|
3
|
+
extensionOverlays,
|
|
4
|
+
getValidator,
|
|
5
|
+
} = require("../../../index.js");
|
|
6
|
+
|
|
7
|
+
const schemaId = "https://trust.propdata.org.uk/schemas/v3/pdtf-transaction.json";
|
|
8
|
+
|
|
9
|
+
describe("Extension Overlays", () => {
|
|
10
|
+
|
|
11
|
+
describe("String key support", () => {
|
|
12
|
+
test("should load extension overlays using string keys", () => {
|
|
13
|
+
const schema = getTransactionSchema(schemaId, ["nts2023", "jk"]);
|
|
14
|
+
const jkProp = schema.properties?.propertyPack?.properties?.specialistIssues?.properties?.japaneseKnotweed;
|
|
15
|
+
|
|
16
|
+
expect(jkProp).toBeDefined();
|
|
17
|
+
expect(jkProp.ntsRef).toBe("A5.3");
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test("should work with multiple extension string keys", () => {
|
|
21
|
+
const schema = getTransactionSchema(schemaId, ["nts2023", "jk", "tf", "ma"]);
|
|
22
|
+
|
|
23
|
+
const jkProp = schema.properties?.propertyPack?.properties?.specialistIssues?.properties?.japaneseKnotweed;
|
|
24
|
+
const tfProp = schema.properties?.propertyPack?.properties?.ownership?.properties?.ownershipsToBeTransferred?.items?.oneOf?.[2]?.properties?.leaseholdInformation?.properties?.serviceCharge?.oneOf?.[1]?.properties?.transferFees;
|
|
25
|
+
const maProp = schema.properties?.propertyPack?.properties?.ownership?.properties?.ownershipsToBeTransferred?.items?.oneOf?.[2]?.properties?.leaseholdInformation?.properties?.contactDetails;
|
|
26
|
+
|
|
27
|
+
expect(jkProp?.ntsRef).toBe("A5.3");
|
|
28
|
+
expect(tfProp?.ntsRef).toBe("A3.5.1.1");
|
|
29
|
+
expect(maProp?.ntsRef).toBe("A1.5.4");
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test("should be equivalent to direct object access", () => {
|
|
33
|
+
const schemaString = getTransactionSchema(schemaId, ["nts2023", "jk"]);
|
|
34
|
+
const schemaDirect = getTransactionSchema(schemaId, ["nts2023", extensionOverlays.jk]);
|
|
35
|
+
|
|
36
|
+
expect(schemaString).toEqual(schemaDirect);
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
describe("Individual extension functionality", () => {
|
|
41
|
+
test("should add Japanese Knotweed extension correctly", () => {
|
|
42
|
+
const schema = getTransactionSchema(schemaId, ["nts2023", "jk"]);
|
|
43
|
+
const jkProp = schema.properties?.propertyPack?.properties?.specialistIssues?.properties?.japaneseKnotweed;
|
|
44
|
+
|
|
45
|
+
expect(jkProp).toBeDefined();
|
|
46
|
+
expect(jkProp.ntsRef).toBe("A5.3");
|
|
47
|
+
expect(jkProp.required).toContain("yesNo");
|
|
48
|
+
expect(jkProp.discriminator?.propertyName).toBe("yesNo");
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test("should add Transfer Fees extension correctly", () => {
|
|
52
|
+
const schema = getTransactionSchema(schemaId, ["nts2023", "tf"]);
|
|
53
|
+
const tfProp = schema.properties?.propertyPack?.properties?.ownership?.properties?.ownershipsToBeTransferred?.items?.oneOf?.[2]?.properties?.leaseholdInformation?.properties?.serviceCharge?.oneOf?.[1]?.properties?.transferFees;
|
|
54
|
+
|
|
55
|
+
expect(tfProp).toBeDefined();
|
|
56
|
+
expect(tfProp.ntsRef).toBe("A3.5.1.1");
|
|
57
|
+
expect(tfProp.title).toBe("Are there any additional fees payable on sale or letting?");
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("should add Managing Agent extension correctly", () => {
|
|
61
|
+
const schema = getTransactionSchema(schemaId, ["nts2023", "ma"]);
|
|
62
|
+
const maProp = schema.properties?.propertyPack?.properties?.ownership?.properties?.ownershipsToBeTransferred?.items?.oneOf?.[2]?.properties?.leaseholdInformation?.properties?.contactDetails;
|
|
63
|
+
|
|
64
|
+
expect(maProp).toBeDefined();
|
|
65
|
+
expect(maProp.ntsRef).toBe("A1.5.4");
|
|
66
|
+
expect(maProp.required).toContain("contacts");
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test("should add Solar Panels extension correctly", () => {
|
|
70
|
+
const schema = getTransactionSchema(schemaId, ["nts2023", "sl"]);
|
|
71
|
+
const slProp = schema.properties?.propertyPack?.properties?.electricity?.properties?.solarPanels?.oneOf?.[1]?.properties?.panelsOwnedOutright;
|
|
72
|
+
|
|
73
|
+
expect(slProp).toBeDefined();
|
|
74
|
+
expect(slProp.ntsRef).toBe("B3.7.1");
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test("should add Heating Installation extension correctly", () => {
|
|
78
|
+
const schema = getTransactionSchema(schemaId, ["nts2023", "hi"]);
|
|
79
|
+
const hiProp = schema.properties?.propertyPack?.properties?.heating?.properties?.heatingSystem?.oneOf?.[1]?.properties?.centralHeatingDetails?.properties?.centralHeatingInstalled;
|
|
80
|
+
|
|
81
|
+
expect(hiProp).toBeDefined();
|
|
82
|
+
expect(hiProp.ntsRef).toBe("B3.4.3.2");
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
describe("Extension overlay availability", () => {
|
|
87
|
+
test("should export all extension overlays", () => {
|
|
88
|
+
const expectedKeys = [
|
|
89
|
+
"as", "dr", "jk", "sb", "hs", // Specialist Issues
|
|
90
|
+
"oa", "la", "sf", "mc", // Property Features
|
|
91
|
+
"er", "ma", "tf", // Ownership & Financial
|
|
92
|
+
"sl", "hi", "fd", // Utilities & Services
|
|
93
|
+
"oc" // Transaction
|
|
94
|
+
];
|
|
95
|
+
|
|
96
|
+
expect(Object.keys(extensionOverlays).sort()).toEqual(expectedKeys.sort());
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test("should have valid JSON structure for all extensions", () => {
|
|
100
|
+
Object.entries(extensionOverlays).forEach(([key, overlay]) => {
|
|
101
|
+
expect(overlay).toBeDefined();
|
|
102
|
+
expect(overlay.$schema).toBe("http://json-schema.org/draft-07/schema#");
|
|
103
|
+
expect(overlay.$id).toContain(`extensions/${key}.json`);
|
|
104
|
+
expect(overlay.properties).toBeDefined();
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
describe("NTS2 equivalence test", () => {
|
|
110
|
+
test("should produce equivalent result to nts2 when all relevant extensions are combined", () => {
|
|
111
|
+
// Get the full NTS2 schema
|
|
112
|
+
const nts2Schema = getTransactionSchema(schemaId, ["nts2025"]);
|
|
113
|
+
|
|
114
|
+
// Get NTS base with all relevant extensions
|
|
115
|
+
const allExtensions = [
|
|
116
|
+
"nts2023",
|
|
117
|
+
// Specialist issues extensions
|
|
118
|
+
"as", "dr", "jk", "sb", "hs",
|
|
119
|
+
// Property features extensions
|
|
120
|
+
"oa", "la", "sf", "mc",
|
|
121
|
+
// Ownership & financial extensions
|
|
122
|
+
"er", "ma", "tf",
|
|
123
|
+
// Utilities & services extensions
|
|
124
|
+
"sl", "hi", "fd",
|
|
125
|
+
// Transaction extensions
|
|
126
|
+
"oc"
|
|
127
|
+
];
|
|
128
|
+
|
|
129
|
+
const ntsWithAllExtensions = getTransactionSchema(schemaId, allExtensions);
|
|
130
|
+
|
|
131
|
+
// Compare key structural elements
|
|
132
|
+
const nts2PropPack = nts2Schema.properties?.propertyPack;
|
|
133
|
+
const extPropPack = ntsWithAllExtensions.properties?.propertyPack;
|
|
134
|
+
|
|
135
|
+
// Check that specialist issues section exists in both
|
|
136
|
+
expect(nts2PropPack?.properties?.specialistIssues).toBeDefined();
|
|
137
|
+
expect(extPropPack?.properties?.specialistIssues).toBeDefined();
|
|
138
|
+
|
|
139
|
+
// Check key specialist issues properties
|
|
140
|
+
const nts2SI = nts2PropPack.properties.specialistIssues.properties;
|
|
141
|
+
const extSI = extPropPack.properties.specialistIssues.properties;
|
|
142
|
+
|
|
143
|
+
expect(extSI?.japaneseKnotweed).toBeDefined();
|
|
144
|
+
expect(extSI?.dryRotEtcTreatment).toBeDefined();
|
|
145
|
+
expect(extSI?.containsAsbestos).toBeDefined();
|
|
146
|
+
expect(extSI?.subsidenceOrStructuralFault).toBeDefined();
|
|
147
|
+
expect(extSI?.ongoingHealthOrSafetyIssue).toBeDefined();
|
|
148
|
+
|
|
149
|
+
// Check that references match (extensions use ntsRef, NTS2 uses nts2Ref)
|
|
150
|
+
if (nts2SI?.japaneseKnotweed && extSI?.japaneseKnotweed) {
|
|
151
|
+
expect(extSI.japaneseKnotweed.ntsRef).toBe("A5.3");
|
|
152
|
+
// NTS2 might have nts2Ref instead - this is expected difference
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Check outside areas
|
|
156
|
+
expect(extPropPack?.properties?.residentialPropertyFeatures?.properties?.outsideAreas).toBeDefined();
|
|
157
|
+
|
|
158
|
+
// Check leasehold extensions
|
|
159
|
+
const leaseholdInfo = extPropPack?.properties?.ownership?.properties?.ownershipsToBeTransferred?.items?.oneOf?.[2]?.properties?.leaseholdInformation;
|
|
160
|
+
expect(leaseholdInfo?.properties?.contactDetails).toBeDefined(); // Managing agent
|
|
161
|
+
expect(leaseholdInfo?.properties?.serviceCharge?.oneOf?.[1]?.properties?.transferFees).toBeDefined(); // Transfer fees
|
|
162
|
+
|
|
163
|
+
// Check utility extensions
|
|
164
|
+
expect(extPropPack?.properties?.electricity?.properties?.solarPanels?.oneOf?.[1]?.properties?.panelsOwnedOutright).toBeDefined();
|
|
165
|
+
expect(extPropPack?.properties?.heating?.properties?.heatingSystem?.oneOf?.[1]?.properties?.centralHeatingDetails?.properties?.centralHeatingInstalled).toBeDefined();
|
|
166
|
+
|
|
167
|
+
// Check construction extensions
|
|
168
|
+
expect(extPropPack?.properties?.typeOfConstruction?.properties?.loft).toBeDefined();
|
|
169
|
+
expect(extPropPack?.properties?.typeOfConstruction?.properties?.sprayFoamInsulation).toBeDefined();
|
|
170
|
+
|
|
171
|
+
// Check completion and moving
|
|
172
|
+
expect(extPropPack?.properties?.completionAndMoving?.properties?.otherPropertyInChain).toBeDefined();
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
test("should validate data successfully with all extensions", () => {
|
|
176
|
+
const allExtensions = [
|
|
177
|
+
"nts2023", "as", "dr", "jk", "sb", "hs", "oa", "la", "sf", "mc",
|
|
178
|
+
"er", "ma", "tf", "sl", "hi", "fd", "oc"
|
|
179
|
+
];
|
|
180
|
+
|
|
181
|
+
const validator = getValidator(schemaId, allExtensions);
|
|
182
|
+
expect(validator).toBeDefined();
|
|
183
|
+
expect(typeof validator).toBe("function");
|
|
184
|
+
|
|
185
|
+
// Test with minimal valid data structure
|
|
186
|
+
const testData = {
|
|
187
|
+
propertyPack: {
|
|
188
|
+
priceInformation: { price: 100000, priceQualifier: "Freehold" },
|
|
189
|
+
ownership: { ownershipsToBeTransferred: [] },
|
|
190
|
+
councilTax: { band: "A" },
|
|
191
|
+
energyEfficiency: { epcRating: "C" },
|
|
192
|
+
buildInformation: { building: { buildDate: 2000 } },
|
|
193
|
+
typeOfConstruction: { isStandardForm: { yesNo: "Yes" } },
|
|
194
|
+
electricity: { mainsElectricity: { yesNo: "Yes" } },
|
|
195
|
+
connectivity: { mobilePhoneCoverage: { signal: "Good" } },
|
|
196
|
+
waterAndDrainage: { water: { supplier: "Thames Water" } },
|
|
197
|
+
heating: { heatingSystem: { yesNo: "Yes" } },
|
|
198
|
+
parking: { parkingArrangements: { yesNo: "No" } },
|
|
199
|
+
listingAndConservation: { listedBuilding: { yesNo: "No" } },
|
|
200
|
+
rightsAndInformalArrangements: { rightsGranted: { yesNo: "No" } },
|
|
201
|
+
environmentalIssues: { flooding: { yesNo: "No" } },
|
|
202
|
+
notices: { noticesAndProposals: { yesNo: "No" } },
|
|
203
|
+
specialistIssues: {
|
|
204
|
+
japaneseKnotweed: { yesNo: "No" },
|
|
205
|
+
dryRotEtcTreatment: { yesNo: "No" },
|
|
206
|
+
containsAsbestos: { yesNo: "No" },
|
|
207
|
+
subsidenceOrStructuralFault: { yesNo: "No" },
|
|
208
|
+
ongoingHealthOrSafetyIssue: { yesNo: "No" }
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
// This might not validate due to missing required fields, but validator should not throw
|
|
214
|
+
expect(() => validator(testData)).not.toThrow();
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
describe("Extension merging behavior", () => {
|
|
219
|
+
test("should preserve required arrays when merging extensions", () => {
|
|
220
|
+
const schema = getTransactionSchema(schemaId, ["nts2023", "jk", "as"]);
|
|
221
|
+
const siSection = schema.properties?.propertyPack?.properties?.specialistIssues;
|
|
222
|
+
|
|
223
|
+
expect(siSection?.required).toBeDefined();
|
|
224
|
+
expect(siSection.required).toContain("japaneseKnotweed");
|
|
225
|
+
expect(siSection.required).toContain("containsAsbestos");
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
test("should handle discriminator patterns correctly", () => {
|
|
229
|
+
const schema = getTransactionSchema(schemaId, ["nts2023", "jk"]);
|
|
230
|
+
const jkProp = schema.properties?.propertyPack?.properties?.specialistIssues?.properties?.japaneseKnotweed;
|
|
231
|
+
|
|
232
|
+
expect(jkProp?.discriminator).toBeDefined();
|
|
233
|
+
expect(jkProp.discriminator.propertyName).toBe("yesNo");
|
|
234
|
+
expect(jkProp.oneOf).toBeDefined();
|
|
235
|
+
expect(Array.isArray(jkProp.oneOf)).toBe(true);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
test("should merge oneOf schemas correctly", () => {
|
|
239
|
+
const schema = getTransactionSchema(schemaId, ["nts2023", "tf"]);
|
|
240
|
+
const tfProp = schema.properties?.propertyPack?.properties?.ownership?.properties?.ownershipsToBeTransferred?.items?.oneOf?.[2]?.properties?.leaseholdInformation?.properties?.serviceCharge?.oneOf?.[1]?.properties?.transferFees;
|
|
241
|
+
|
|
242
|
+
expect(tfProp?.oneOf).toBeDefined();
|
|
243
|
+
expect(tfProp.oneOf.length).toBeGreaterThan(0);
|
|
244
|
+
|
|
245
|
+
// Check the structure - details should be in the conditional oneOf branch
|
|
246
|
+
const conditionalBranch = tfProp.oneOf.find(branch =>
|
|
247
|
+
branch.properties?.details || branch.required?.includes("details")
|
|
248
|
+
);
|
|
249
|
+
expect(conditionalBranch).toBeDefined();
|
|
250
|
+
expect(conditionalBranch.properties?.details || conditionalBranch.required?.includes("details")).toBeTruthy();
|
|
251
|
+
});
|
|
252
|
+
});
|
|
253
|
+
});
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
const traverse = require("traverse");
|
|
2
|
+
const jp = require("jsonpointer");
|
|
3
|
+
const fs = require("fs");
|
|
4
|
+
const path = require("path");
|
|
5
|
+
|
|
6
|
+
const combinedSchema = require("../schemas/v3/combined.json");
|
|
7
|
+
|
|
8
|
+
// Define the extension overlay mappings
|
|
9
|
+
const extensionMappings = {
|
|
10
|
+
// Outside areas
|
|
11
|
+
oa: {
|
|
12
|
+
name: "outsideAreas",
|
|
13
|
+
description: "Outside areas extension for NTS",
|
|
14
|
+
paths: ["/properties/propertyPack/properties/residentialPropertyFeatures/properties/outsideAreas"]
|
|
15
|
+
},
|
|
16
|
+
|
|
17
|
+
// Estate rentcharges for freehold
|
|
18
|
+
er: {
|
|
19
|
+
name: "estateRentcharges",
|
|
20
|
+
description: "Estate rentcharges for freehold properties",
|
|
21
|
+
paths: ["/properties/propertyPack/properties/ownership/properties/ownershipsToBeTransferred/items/oneOf/0/properties/estateRentcharges"]
|
|
22
|
+
},
|
|
23
|
+
|
|
24
|
+
// Managing agent contact details for leasehold
|
|
25
|
+
ma: {
|
|
26
|
+
name: "managingAgent",
|
|
27
|
+
description: "Managing agent contact details for leasehold",
|
|
28
|
+
paths: ["/properties/propertyPack/properties/ownership/properties/ownershipsToBeTransferred/items/oneOf/2/properties/leaseholdInformation/properties/contactDetails"]
|
|
29
|
+
},
|
|
30
|
+
|
|
31
|
+
// Transfer fees for leasehold
|
|
32
|
+
tf: {
|
|
33
|
+
name: "transferFees",
|
|
34
|
+
description: "Transfer fees for leasehold",
|
|
35
|
+
paths: ["/properties/propertyPack/properties/ownership/properties/ownershipsToBeTransferred/items/oneOf/2/properties/leaseholdInformation/properties/serviceCharge/oneOf/1/properties/transferFees"]
|
|
36
|
+
},
|
|
37
|
+
|
|
38
|
+
// Main construction type if standard
|
|
39
|
+
mc: {
|
|
40
|
+
name: "mainConstruction",
|
|
41
|
+
description: "Main construction type if standard form",
|
|
42
|
+
paths: ["/properties/propertyPack/properties/typeOfConstruction/properties/isStandardForm/oneOf/0/properties/constructionType"]
|
|
43
|
+
},
|
|
44
|
+
|
|
45
|
+
// Loft access and details
|
|
46
|
+
la: {
|
|
47
|
+
name: "loftAccess",
|
|
48
|
+
description: "Loft access and details",
|
|
49
|
+
paths: ["/properties/propertyPack/properties/typeOfConstruction/properties/loft"]
|
|
50
|
+
},
|
|
51
|
+
|
|
52
|
+
// Spray foam insulation
|
|
53
|
+
sf: {
|
|
54
|
+
name: "sprayFoam",
|
|
55
|
+
description: "Spray foam insulation",
|
|
56
|
+
paths: ["/properties/propertyPack/properties/typeOfConstruction/properties/sprayFoamInsulation"]
|
|
57
|
+
},
|
|
58
|
+
|
|
59
|
+
// Specialist issues - dry rot
|
|
60
|
+
dr: {
|
|
61
|
+
name: "dryRot",
|
|
62
|
+
description: "Dry rot treatment specialist issue",
|
|
63
|
+
paths: ["/properties/propertyPack/properties/specialistIssues/properties/dryRotEtcTreatment"]
|
|
64
|
+
},
|
|
65
|
+
|
|
66
|
+
// Specialist issues - asbestos
|
|
67
|
+
as: {
|
|
68
|
+
name: "asbestos",
|
|
69
|
+
description: "Asbestos specialist issue",
|
|
70
|
+
paths: ["/properties/propertyPack/properties/specialistIssues/properties/containsAsbestos"]
|
|
71
|
+
},
|
|
72
|
+
|
|
73
|
+
// Specialist issues - Japanese knotweed
|
|
74
|
+
jk: {
|
|
75
|
+
name: "japaneseKnotweed",
|
|
76
|
+
description: "Japanese knotweed specialist issue",
|
|
77
|
+
paths: ["/properties/propertyPack/properties/specialistIssues/properties/japaneseKnotweed"]
|
|
78
|
+
},
|
|
79
|
+
|
|
80
|
+
// Specialist issues - subsidence
|
|
81
|
+
sb: {
|
|
82
|
+
name: "subsidence",
|
|
83
|
+
description: "Subsidence or structural fault specialist issue",
|
|
84
|
+
paths: ["/properties/propertyPack/properties/specialistIssues/properties/subsidenceOrStructuralFault"]
|
|
85
|
+
},
|
|
86
|
+
|
|
87
|
+
// Specialist issues - health and safety
|
|
88
|
+
hs: {
|
|
89
|
+
name: "healthSafety",
|
|
90
|
+
description: "Health and safety specialist issue",
|
|
91
|
+
paths: ["/properties/propertyPack/properties/specialistIssues/properties/ongoingHealthOrSafetyIssue"]
|
|
92
|
+
},
|
|
93
|
+
|
|
94
|
+
// Solar panels leased
|
|
95
|
+
sl: {
|
|
96
|
+
name: "solarPanelsLeased",
|
|
97
|
+
description: "Solar panels ownership details",
|
|
98
|
+
paths: ["/properties/propertyPack/properties/electricity/properties/solarPanels/oneOf/1/properties/panelsOwnedOutright"]
|
|
99
|
+
},
|
|
100
|
+
|
|
101
|
+
// Heating installation date
|
|
102
|
+
hi: {
|
|
103
|
+
name: "heatingInstalled",
|
|
104
|
+
description: "Central heating installation date",
|
|
105
|
+
paths: ["/properties/propertyPack/properties/heating/properties/heatingSystem/oneOf/1/properties/centralHeatingDetails/properties/centralHeatingInstalled"]
|
|
106
|
+
},
|
|
107
|
+
|
|
108
|
+
// Flood defences
|
|
109
|
+
fd: {
|
|
110
|
+
name: "floodDefences",
|
|
111
|
+
description: "Flood defence information",
|
|
112
|
+
paths: ["/properties/propertyPack/properties/environmentalIssues/properties/flooding/properties/floodDefences"]
|
|
113
|
+
},
|
|
114
|
+
|
|
115
|
+
// Other property in chain
|
|
116
|
+
oc: {
|
|
117
|
+
name: "otherPropertyChain",
|
|
118
|
+
description: "Other property in chain dependency",
|
|
119
|
+
paths: ["/properties/propertyPack/properties/completionAndMoving/properties/otherPropertyInChain"]
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
// Function to extract properties at specific paths with nts2Ref
|
|
124
|
+
function extractPathsWithNts2Ref(schema, paths) {
|
|
125
|
+
const result = {};
|
|
126
|
+
|
|
127
|
+
paths.forEach(path => {
|
|
128
|
+
try {
|
|
129
|
+
const value = jp.get(schema, path);
|
|
130
|
+
if (value && hasNts2Ref(value)) {
|
|
131
|
+
// Set the value at the same path in result
|
|
132
|
+
jp.set(result, path, extractNts2Properties(value));
|
|
133
|
+
}
|
|
134
|
+
} catch (err) {
|
|
135
|
+
console.warn(`Path not found: ${path}`);
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
return result;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Check if an object or its descendants have nts2Ref
|
|
143
|
+
function hasNts2Ref(obj) {
|
|
144
|
+
let found = false;
|
|
145
|
+
traverse(obj).forEach(function(element) {
|
|
146
|
+
if (element && element.nts2Ref) {
|
|
147
|
+
found = true;
|
|
148
|
+
this.stop();
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
return found;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Extract only NTS2-specific properties (with nts2Ref)
|
|
155
|
+
function extractNts2Properties(obj) {
|
|
156
|
+
const result = {};
|
|
157
|
+
|
|
158
|
+
// Copy nts2-specific metadata at current level
|
|
159
|
+
if (obj.nts2Ref) result.ntsRef = obj.nts2Ref;
|
|
160
|
+
if (obj.nts2Required) result.required = obj.nts2Required;
|
|
161
|
+
if (obj.nts2Title) result.title = obj.nts2Title;
|
|
162
|
+
if (obj.nts2Description) result.description = obj.nts2Description;
|
|
163
|
+
if (obj.nts2Enum) result.enum = obj.nts2Enum;
|
|
164
|
+
|
|
165
|
+
// Handle discriminator
|
|
166
|
+
if (obj.discriminator) {
|
|
167
|
+
result.discriminator = obj.discriminator;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Recursively process properties
|
|
171
|
+
if (obj.properties) {
|
|
172
|
+
result.properties = {};
|
|
173
|
+
Object.keys(obj.properties).forEach(key => {
|
|
174
|
+
const prop = obj.properties[key];
|
|
175
|
+
if (hasNts2Ref(prop)) {
|
|
176
|
+
result.properties[key] = extractNts2Properties(prop);
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
// Only keep properties if we found some with nts2Ref
|
|
180
|
+
if (Object.keys(result.properties).length === 0) {
|
|
181
|
+
delete result.properties;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Handle arrays
|
|
186
|
+
if (obj.items) {
|
|
187
|
+
if (hasNts2Ref(obj.items)) {
|
|
188
|
+
result.items = extractNts2Properties(obj.items);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Handle oneOf
|
|
193
|
+
if (obj.oneOf) {
|
|
194
|
+
const processedOneOf = [];
|
|
195
|
+
obj.oneOf.forEach((schema, index) => {
|
|
196
|
+
if (hasNts2Ref(schema)) {
|
|
197
|
+
const extracted = extractNts2Properties(schema);
|
|
198
|
+
// For oneOf schemas, we need to preserve discriminator enum values
|
|
199
|
+
if (obj.discriminator && schema.properties) {
|
|
200
|
+
const propName = obj.discriminator.propertyName;
|
|
201
|
+
if (schema.properties[propName]) {
|
|
202
|
+
if (!extracted.properties) extracted.properties = {};
|
|
203
|
+
extracted.properties[propName] = {
|
|
204
|
+
enum: schema.properties[propName].nts2Enum || schema.properties[propName].enum
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
processedOneOf.push(extracted);
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
if (processedOneOf.length > 0) {
|
|
212
|
+
result.oneOf = processedOneOf;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Copy other JSON Schema keywords that might be present
|
|
217
|
+
['type', 'format', 'minimum', 'maximum', 'minLength', 'maxLength', 'minItems', 'maxItems'].forEach(keyword => {
|
|
218
|
+
if (obj[keyword] !== undefined) {
|
|
219
|
+
result[keyword] = obj[keyword];
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
return result;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Add required array at parent levels if needed
|
|
227
|
+
function addRequiredArrays(overlay, schema) {
|
|
228
|
+
traverse(overlay).forEach(function(node) {
|
|
229
|
+
if (node && node.properties) {
|
|
230
|
+
const schemaPath = "/" + this.path.join("/");
|
|
231
|
+
try {
|
|
232
|
+
const schemaNode = jp.get(schema, schemaPath);
|
|
233
|
+
if (schemaNode && schemaNode.nts2Required) {
|
|
234
|
+
// Filter to only include properties that exist in this overlay
|
|
235
|
+
const overlayProps = Object.keys(node.properties);
|
|
236
|
+
const requiredProps = schemaNode.nts2Required.filter(prop => overlayProps.includes(prop));
|
|
237
|
+
if (requiredProps.length > 0) {
|
|
238
|
+
node.required = requiredProps;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
} catch (err) {
|
|
242
|
+
// Path doesn't exist in schema, skip
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
return overlay;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Generate extension overlays
|
|
250
|
+
Object.entries(extensionMappings).forEach(([code, config]) => {
|
|
251
|
+
console.log(`\nGenerating extension overlay: ${code} (${config.name})`);
|
|
252
|
+
|
|
253
|
+
// Extract the specific paths
|
|
254
|
+
let overlay = extractPathsWithNts2Ref(combinedSchema, config.paths);
|
|
255
|
+
|
|
256
|
+
// Add required arrays where needed
|
|
257
|
+
overlay = addRequiredArrays(overlay, combinedSchema);
|
|
258
|
+
|
|
259
|
+
// Add schema metadata
|
|
260
|
+
overlay.$schema = "http://json-schema.org/draft-07/schema#";
|
|
261
|
+
overlay.$id = `https://trust.propdata.org.uk/schemas/v3/overlays/extensions/${code}.json`;
|
|
262
|
+
overlay.$comment = config.description;
|
|
263
|
+
|
|
264
|
+
// Write the overlay file
|
|
265
|
+
const fileName = path.join(__dirname, `../schemas/v3/overlays/extensions/${code}.json`);
|
|
266
|
+
fs.writeFileSync(fileName, JSON.stringify(overlay, null, 2));
|
|
267
|
+
console.log(`Extension overlay ${code} written to ${fileName}`);
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
console.log("\nExtension overlay extraction complete!");
|