Merged PR 2399: Adds characterization coverage and documentation for CRM query field profile
Related work items: #23754
This commit is contained in:
@@ -0,0 +1,308 @@
|
|||||||
|
# Query Profile Compatibility Contract
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
This document records the current compatibility contract around:
|
||||||
|
|
||||||
|
- `actions/selectQueryTypes.js`
|
||||||
|
|
||||||
|
That file is primarily:
|
||||||
|
|
||||||
|
- **CRM/OData query-shape configuration**
|
||||||
|
- **CRM compatibility handling**
|
||||||
|
|
||||||
|
It is **not primarily business-rule logic**.
|
||||||
|
|
||||||
|
Its main job today is to ensure the portal requests the fields that each CRM entity actually exposes so downstream case-summary, representation, status, lifecycle, and infrastructure views continue to function.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Source of Truth
|
||||||
|
|
||||||
|
The **CRM field names are the source of truth**.
|
||||||
|
|
||||||
|
The website must query the fields that actually exist on each CRM entity.
|
||||||
|
|
||||||
|
This includes preserving:
|
||||||
|
|
||||||
|
- historic naming differences
|
||||||
|
- spelling mistakes
|
||||||
|
- singular/plural variants
|
||||||
|
- entity-specific field names
|
||||||
|
- legislation-specific field models
|
||||||
|
|
||||||
|
Future work must treat field selection as a **compatibility-sensitive integration concern first**.
|
||||||
|
|
||||||
|
The goal is not to make the query profiles look tidy.
|
||||||
|
|
||||||
|
The goal is to preserve the fields that the live system currently depends on.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Appeal-Type Differences
|
||||||
|
|
||||||
|
Different appeal types legitimately require different fields because they support different:
|
||||||
|
|
||||||
|
- legislation
|
||||||
|
- workflows
|
||||||
|
- representation windows
|
||||||
|
- event models
|
||||||
|
- authority models
|
||||||
|
- infrastructure/project processes
|
||||||
|
|
||||||
|
This means:
|
||||||
|
|
||||||
|
- similar-looking appeal types must **not automatically be treated as identical**
|
||||||
|
- near-duplicate field bundles must **not automatically be collapsed**
|
||||||
|
- infrastructure-style entities must **not automatically be forced into planning-style shapes**
|
||||||
|
- minimal profiles must **not automatically be expanded to match richer bundles**
|
||||||
|
|
||||||
|
Bundle-by-bundle evaluation is required.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## CRM Compatibility Variants
|
||||||
|
|
||||||
|
The current compatibility contract includes representative field variants that must be preserved.
|
||||||
|
|
||||||
|
### Specialist-process variants
|
||||||
|
|
||||||
|
- `pinswg_specialistcaseprocess`
|
||||||
|
- `pinswg_speacialistcaseprocess`
|
||||||
|
|
||||||
|
These are not interchangeable in the current query profiles.
|
||||||
|
|
||||||
|
Representative characterization showed:
|
||||||
|
|
||||||
|
- ROW uses `pinswg_specialistcaseprocess`
|
||||||
|
- Adverts uses `pinswg_speacialistcaseprocess`
|
||||||
|
|
||||||
|
### Start-date variants
|
||||||
|
|
||||||
|
- `pinswg_startdate`
|
||||||
|
- `pinswg_startdates`
|
||||||
|
- `pinswg_applicationacceptedasvalid`
|
||||||
|
|
||||||
|
These reflect different representation-window and process models across entities.
|
||||||
|
|
||||||
|
### Statement-date variants
|
||||||
|
|
||||||
|
- `pinswg_statementduedate`
|
||||||
|
- `pinswg_statementsduedate`
|
||||||
|
|
||||||
|
These must not be normalized away.
|
||||||
|
|
||||||
|
### Project description variants
|
||||||
|
|
||||||
|
- `pinswg_projectdescription`
|
||||||
|
- `pinswg_projectdiscription`
|
||||||
|
|
||||||
|
Harbour Revision Orders currently preserve the misspelled variant.
|
||||||
|
|
||||||
|
### Decision variant
|
||||||
|
|
||||||
|
- `pinswg_dicision`
|
||||||
|
|
||||||
|
Planning S78 currently preserves this field name in its query profile.
|
||||||
|
|
||||||
|
### Other representative compatibility-sensitive variants
|
||||||
|
|
||||||
|
- `pinswg_otherpartiesstatement`
|
||||||
|
- `pinswg_otherpartiesstatements`
|
||||||
|
- `pinswg_otherpartiesstmt`
|
||||||
|
|
||||||
|
- `pinswg_suspensionstartdates`
|
||||||
|
- `pinswg_suspensionstartdate`
|
||||||
|
|
||||||
|
- `pinswg_startdateofevent`
|
||||||
|
- `pinswg_startdateoftheevent`
|
||||||
|
- `pinswg_starttimeofevent`
|
||||||
|
- `pinswg_starttimeoftheevent`
|
||||||
|
- `pinswg_startdatetimeiftheevent`
|
||||||
|
|
||||||
|
These variants must be preserved until characterization-based extraction or change work explicitly proves otherwise.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Characterized Field Families
|
||||||
|
|
||||||
|
The current characterization work identifies the following conceptual families already present inside `selectQueryTypes.js`.
|
||||||
|
|
||||||
|
### 1. Planning-style fields
|
||||||
|
|
||||||
|
Representative concerns include:
|
||||||
|
|
||||||
|
- base case identity fields
|
||||||
|
- planning-style address fields
|
||||||
|
- decision/date/status fields
|
||||||
|
- event support fields
|
||||||
|
|
||||||
|
These appear across many planning and enforcement-style entities, but not always with identical field names.
|
||||||
|
|
||||||
|
### 2. Representation-window fields
|
||||||
|
|
||||||
|
Representative fields include:
|
||||||
|
|
||||||
|
- `pinswg_startdate`
|
||||||
|
- `pinswg_startdates`
|
||||||
|
- `pinswg_applicationacceptedasvalid`
|
||||||
|
- `pinswg_statementduedate`
|
||||||
|
- `pinswg_statementsduedate`
|
||||||
|
- `pinswg_finalcommentsduedate`
|
||||||
|
- `pinswg_endofrepresentationperiod`
|
||||||
|
|
||||||
|
These coexist today and reflect different entity models.
|
||||||
|
|
||||||
|
### 3. Specialist-process fields
|
||||||
|
|
||||||
|
Representative fields include:
|
||||||
|
|
||||||
|
- `pinswg_specialistcaseprocess`
|
||||||
|
- `pinswg_speacialistcaseprocess`
|
||||||
|
|
||||||
|
These must not be silently unified.
|
||||||
|
|
||||||
|
### 4. Infrastructure / project fields
|
||||||
|
|
||||||
|
Representative concerns include:
|
||||||
|
|
||||||
|
- project description
|
||||||
|
- project location
|
||||||
|
- submission / publication milestones
|
||||||
|
- recommendation / report fields
|
||||||
|
- suspension dates
|
||||||
|
- grid-reference fields
|
||||||
|
|
||||||
|
These are particularly important in DNS / Harbour / Transport / Electricity style bundles.
|
||||||
|
|
||||||
|
### 5. Authority / LPA fields
|
||||||
|
|
||||||
|
Representative fields include:
|
||||||
|
|
||||||
|
- `_pinswg_localplanningauthority_value`
|
||||||
|
- `_pinswg_associatedlpa_value`
|
||||||
|
- `pinswg_relevantauthorityname`
|
||||||
|
|
||||||
|
These represent different authority models across bundles.
|
||||||
|
|
||||||
|
### 6. Consultation fields
|
||||||
|
|
||||||
|
Representative fields include:
|
||||||
|
|
||||||
|
- `pinswg_consultationopen`
|
||||||
|
- `pinswg_consultationclose`
|
||||||
|
|
||||||
|
These are currently characteristic of SIPS-style profiles.
|
||||||
|
|
||||||
|
### 7. Minimal-profile bundles
|
||||||
|
|
||||||
|
Representative examples include:
|
||||||
|
|
||||||
|
- `pinswg_nonvalidations`
|
||||||
|
- `pinswg_ldps`
|
||||||
|
|
||||||
|
These are intentionally smaller than richer planning and infrastructure bundles.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Characterized Divergences
|
||||||
|
|
||||||
|
Characterization work also confirmed that some similar bundles intentionally differ.
|
||||||
|
|
||||||
|
Representative examples:
|
||||||
|
|
||||||
|
### Advert vs ROW specialist-process fields
|
||||||
|
|
||||||
|
- Advert preserves `pinswg_speacialistcaseprocess`
|
||||||
|
- ROW preserves `pinswg_specialistcaseprocess`
|
||||||
|
|
||||||
|
These must not be accidentally collapsed into one shared field choice.
|
||||||
|
|
||||||
|
### Harbour vs DNS project-description fields
|
||||||
|
|
||||||
|
- Harbour preserves `pinswg_projectdiscription`
|
||||||
|
- DNS preserves `pinswg_projectdescription`
|
||||||
|
|
||||||
|
### Suspension-start variants
|
||||||
|
|
||||||
|
- DNS preserves `pinswg_suspensionstartdates`
|
||||||
|
- Harbour and Transport preserve `pinswg_suspensionstartdate`
|
||||||
|
|
||||||
|
### Authority / LPA model differences
|
||||||
|
|
||||||
|
- S78 preserves `_pinswg_localplanningauthority_value`
|
||||||
|
- ROW preserves `_pinswg_associatedlpa_value` plus `pinswg_relevantauthorityname`
|
||||||
|
- DNS preserves `_pinswg_associatedlpa_value` without the planning-style relevant-authority field
|
||||||
|
|
||||||
|
### Event-date and event-time variants
|
||||||
|
|
||||||
|
Representative coexistence includes:
|
||||||
|
|
||||||
|
- `pinswg_startdateofevent`
|
||||||
|
- `pinswg_startdateoftheevent`
|
||||||
|
- `pinswg_starttimeofevent`
|
||||||
|
- `pinswg_starttimeoftheevent`
|
||||||
|
- `pinswg_startdatetimeiftheevent`
|
||||||
|
|
||||||
|
### Representation-support differences in similar bundles
|
||||||
|
|
||||||
|
Representative differences include:
|
||||||
|
|
||||||
|
- singular vs plural other-parties fields
|
||||||
|
- singular vs plural statement-due-date fields
|
||||||
|
- bundle-specific combinations of proof/questionnaire/final-comments fields
|
||||||
|
|
||||||
|
These differences must be treated as compatibility-sensitive unless explicitly re-characterized.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Future Guidance
|
||||||
|
|
||||||
|
Future work in this area should follow these rules:
|
||||||
|
|
||||||
|
1. **Characterize first**
|
||||||
|
- prove current behaviour before changing structure
|
||||||
|
|
||||||
|
2. **Extract later**
|
||||||
|
- do not extract shared bundles or constants until the current compatibility contract is protected
|
||||||
|
|
||||||
|
3. **Preserve compatibility**
|
||||||
|
- CRM field variants are not “cleanup noise”; they are part of the current live contract
|
||||||
|
|
||||||
|
4. **Evaluate bundle-by-bundle**
|
||||||
|
- each bundle should be assessed on its own actual field requirements
|
||||||
|
|
||||||
|
5. **Avoid broad normalization**
|
||||||
|
- do not assume similar names mean safe consolidation
|
||||||
|
|
||||||
|
6. **Protect intentional differences**
|
||||||
|
- especially for:
|
||||||
|
- specialist-process fields
|
||||||
|
- representation-window fields
|
||||||
|
- project-description fields
|
||||||
|
- suspension-start fields
|
||||||
|
- authority/LPA fields
|
||||||
|
- event-date/time fields
|
||||||
|
|
||||||
|
The safest sequence remains:
|
||||||
|
|
||||||
|
- characterize current bundles
|
||||||
|
- characterize current families
|
||||||
|
- characterize duplicated clusters and divergences
|
||||||
|
- only then consider bounded extraction work
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Explicit Non-Goals
|
||||||
|
|
||||||
|
This characterization and documentation work does **not**:
|
||||||
|
|
||||||
|
- rename CRM fields
|
||||||
|
- remove typo fields
|
||||||
|
- redesign CRM entities
|
||||||
|
- standardize legislation-specific data
|
||||||
|
- force appeal types into identical query profiles
|
||||||
|
- introduce a query DSL
|
||||||
|
- move this concern into a broader business-rule domain boundary
|
||||||
|
|
||||||
|
Any future cleanup or extraction must preserve the documented compatibility contract unless an explicit behaviour-change decision is made.
|
||||||
@@ -0,0 +1,456 @@
|
|||||||
|
const fs = require("fs");
|
||||||
|
const path = require("path");
|
||||||
|
const vm = require("vm");
|
||||||
|
const assert = require("assert");
|
||||||
|
|
||||||
|
const rootDir = path.resolve(__dirname, "..", "..");
|
||||||
|
|
||||||
|
const loadSelectQueryTypesModule = (relayPath) => {
|
||||||
|
const filePath = path.join(rootDir, "actions", "selectQueryTypes.js");
|
||||||
|
|
||||||
|
let source = fs.readFileSync(filePath, "utf8");
|
||||||
|
source = source.replace(
|
||||||
|
/export const\s+getSelectQuery\s*=\s*/,
|
||||||
|
"const getSelectQuery = "
|
||||||
|
);
|
||||||
|
source += `
|
||||||
|
module.exports = {
|
||||||
|
getSelectQuery
|
||||||
|
};
|
||||||
|
`;
|
||||||
|
|
||||||
|
const context = {
|
||||||
|
module: { exports: {} },
|
||||||
|
exports: {},
|
||||||
|
require,
|
||||||
|
process: {
|
||||||
|
env: {
|
||||||
|
RELAYPATH: relayPath
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
vm.runInNewContext(source, context, { filename: filePath });
|
||||||
|
return context.module.exports;
|
||||||
|
};
|
||||||
|
|
||||||
|
const tests = [];
|
||||||
|
const test = (name, fn) => tests.push({ name, fn });
|
||||||
|
|
||||||
|
const splitFields = (query) => {
|
||||||
|
assert.ok(
|
||||||
|
query.startsWith("&$select="),
|
||||||
|
`Expected query to start with &$select= but received: ${query}`
|
||||||
|
);
|
||||||
|
|
||||||
|
return query
|
||||||
|
.replace(/^&\$select=/, "")
|
||||||
|
.split(",")
|
||||||
|
.map((field) => field.trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getFieldSet = (appealTypeName, relayPath) =>
|
||||||
|
new Set(
|
||||||
|
splitFields(
|
||||||
|
loadSelectQueryTypesModule(relayPath).getSelectQuery(appealTypeName)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
const assertHasFields = (fieldSet, expectedFields, label) => {
|
||||||
|
for (const field of expectedFields) {
|
||||||
|
assert.ok(fieldSet.has(field), `${label}: expected field \`${field}\``);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
test("planning-style near-clones preserve a shared base cluster while retaining intentional representation-field differences", () => {
|
||||||
|
const planningConditions = getFieldSet("pinswg_planningconditionss73s79s");
|
||||||
|
const planningObligation = getFieldSet(
|
||||||
|
"pinswg_planningobligationappeals106s"
|
||||||
|
);
|
||||||
|
const enforcement = getFieldSet("pinswg_enforcementnoticeappeals174s");
|
||||||
|
|
||||||
|
const sharedPlanningCluster = [
|
||||||
|
"modifiedon",
|
||||||
|
"_pinswg_appellant_value",
|
||||||
|
"pinswg_name",
|
||||||
|
"pinswg_siteaddressline1",
|
||||||
|
"pinswg_siteaddressline2",
|
||||||
|
"pinswg_siteaddresspostcode",
|
||||||
|
"pinswg_siteaddresstown",
|
||||||
|
"_pinswg_associatedlpa_value",
|
||||||
|
"pinswg_casedecisiondate",
|
||||||
|
"pinswg_dateeventrequested",
|
||||||
|
"pinswg_finalcommentsduedate",
|
||||||
|
"pinswg_procedure",
|
||||||
|
"pinswg_questionnaireduedate",
|
||||||
|
"pinswg_startdate",
|
||||||
|
"pinswg_statementduedate",
|
||||||
|
"statuscode"
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const [label, fields] of [
|
||||||
|
["Planning conditions", planningConditions],
|
||||||
|
["Planning obligation", planningObligation],
|
||||||
|
["Enforcement", enforcement]
|
||||||
|
]) {
|
||||||
|
assertHasFields(fields, sharedPlanningCluster, label);
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.ok(
|
||||||
|
planningConditions.has("pinswg_otherpartiesstatement") &&
|
||||||
|
!planningConditions.has("pinswg_otherpartiesstatements"),
|
||||||
|
"Planning conditions should preserve singular other-parties field variant"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.ok(
|
||||||
|
planningObligation.has("pinswg_otherpartiesstatements") &&
|
||||||
|
!planningObligation.has("pinswg_otherpartiesstatement"),
|
||||||
|
"Planning obligation should preserve plural other-parties field variant"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.ok(
|
||||||
|
enforcement.has("pinswg_startdateoftheevent") &&
|
||||||
|
!enforcement.has("pinswg_startdateofevent"),
|
||||||
|
"Enforcement should preserve its current event-date field variant"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("ROW and Hedges preserve a shared specialist/date/representation near-clone cluster", () => {
|
||||||
|
const rows = getFieldSet("pinswg_rows");
|
||||||
|
const hedges = getFieldSet(
|
||||||
|
"pinswg_hedgeshedgerowstreepreservationreplacems"
|
||||||
|
);
|
||||||
|
|
||||||
|
const sharedRowHedgesCluster = [
|
||||||
|
"pinswg_specialistcaseprocess",
|
||||||
|
"modifiedon",
|
||||||
|
"_pinswg_appellant_value",
|
||||||
|
"pinswg_name",
|
||||||
|
"_pinswg_associatedlpa_value",
|
||||||
|
"pinswg_casedecisiondate",
|
||||||
|
"pinswg_finalcommentsduedate",
|
||||||
|
"pinswg_otherpartiesstatement",
|
||||||
|
"pinswg_procedure",
|
||||||
|
"pinswg_proofsofevidencewrittenstatementsofeviden",
|
||||||
|
"pinswg_questionnaireduedate",
|
||||||
|
"pinswg_relevantauthorityname",
|
||||||
|
"pinswg_startdate",
|
||||||
|
"pinswg_startdateoftheevent",
|
||||||
|
"pinswg_typeofevent",
|
||||||
|
"pinswg_statementduedate",
|
||||||
|
"statuscode"
|
||||||
|
];
|
||||||
|
|
||||||
|
assertHasFields(rows, sharedRowHedgesCluster, "ROW near-clone cluster");
|
||||||
|
assertHasFields(
|
||||||
|
hedges,
|
||||||
|
sharedRowHedgesCluster,
|
||||||
|
"Hedges near-clone cluster"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.ok(
|
||||||
|
rows.has("pinswg_detailedeiascreeningrequired") &&
|
||||||
|
!rows.has("pinswg_detailedeiascreening"),
|
||||||
|
"ROW should preserve detailed EIA screening required variant"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.ok(
|
||||||
|
hedges.has("pinswg_detailedeiascreening") &&
|
||||||
|
!hedges.has("pinswg_detailedeiascreeningrequired"),
|
||||||
|
"Hedges should preserve detailed EIA screening variant without accidental normalization"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Advert specialist divergence is preserved against ROW canonical specialist-process selection", () => {
|
||||||
|
const adverts = getFieldSet("pinswg_advertses");
|
||||||
|
const rows = getFieldSet("pinswg_rows");
|
||||||
|
|
||||||
|
assert.ok(
|
||||||
|
adverts.has("pinswg_speacialistcaseprocess") &&
|
||||||
|
!adverts.has("pinswg_specialistcaseprocess"),
|
||||||
|
"Adverts should preserve misspelled specialist-process field only"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.ok(
|
||||||
|
rows.has("pinswg_specialistcaseprocess") &&
|
||||||
|
!rows.has("pinswg_speacialistcaseprocess"),
|
||||||
|
"ROW should preserve canonical specialist-process field only"
|
||||||
|
);
|
||||||
|
|
||||||
|
assertHasFields(
|
||||||
|
adverts,
|
||||||
|
[
|
||||||
|
"pinswg_finalcommentsduedate",
|
||||||
|
"pinswg_questionnaireduedate",
|
||||||
|
"pinswg_statementduedate",
|
||||||
|
"pinswg_startdate",
|
||||||
|
"pinswg_startdateoftheevent"
|
||||||
|
],
|
||||||
|
"Adverts specialist divergence support cluster"
|
||||||
|
);
|
||||||
|
|
||||||
|
assertHasFields(
|
||||||
|
rows,
|
||||||
|
[
|
||||||
|
"pinswg_finalcommentsduedate",
|
||||||
|
"pinswg_questionnaireduedate",
|
||||||
|
"pinswg_statementduedate",
|
||||||
|
"pinswg_startdate",
|
||||||
|
"pinswg_startdateoftheevent"
|
||||||
|
],
|
||||||
|
"ROW specialist divergence support cluster"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("infrastructure near-clones preserve shared milestone concepts and schema-specific divergences", () => {
|
||||||
|
const dns = getFieldSet("pinswg_dnses");
|
||||||
|
const harbour = getFieldSet("pinswg_harbourrevisionorders");
|
||||||
|
const transport = getFieldSet("pinswg_transportandworkacts");
|
||||||
|
const electricity = getFieldSet("pinswg_electricityacts");
|
||||||
|
|
||||||
|
const sharedInfrastructureCluster = [
|
||||||
|
"pinswg_eiarequired",
|
||||||
|
"pinswg_name",
|
||||||
|
"pinswg_addressline1",
|
||||||
|
"pinswg_addressline2",
|
||||||
|
"pinswg_postcode",
|
||||||
|
"pinswg_addresstown",
|
||||||
|
"pinswg_applicationacceptedasvalid",
|
||||||
|
"pinswg_confirmrequesttovaryapplication",
|
||||||
|
"pinswg_dateeventrequested",
|
||||||
|
"pinswg_dateofsubmissionofapplication",
|
||||||
|
"pinswg_dateprojectpublishedonwebsite",
|
||||||
|
"pinswg_deadlineforsubmissionofapplication",
|
||||||
|
"pinswg_endofrepresentationperiod",
|
||||||
|
"pinswg_inspectorreportsubmittedtowelshgovernment",
|
||||||
|
"pinswg_projectlocation",
|
||||||
|
"pinswg_recommendation",
|
||||||
|
"pinswg_rejectrequesttovaryapplication",
|
||||||
|
"pinswg_reportdueby",
|
||||||
|
"pinswg_suspensiondates",
|
||||||
|
"pinswg_validnoticeofintentiontosubmitanapplicati",
|
||||||
|
"pinswg_webaddress",
|
||||||
|
"pinswg_withdrawndate",
|
||||||
|
"pinswg_writtenacceptanceofnotification",
|
||||||
|
"pinswg_startdateofevent",
|
||||||
|
"pinswg_typeofevent",
|
||||||
|
"statuscode"
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const [label, fields] of [
|
||||||
|
["DNS", dns],
|
||||||
|
["Harbour", harbour],
|
||||||
|
["Transport", transport],
|
||||||
|
["Electricity", electricity]
|
||||||
|
]) {
|
||||||
|
assertHasFields(fields, sharedInfrastructureCluster, label);
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.ok(
|
||||||
|
dns.has("pinswg_projectdescription") &&
|
||||||
|
!dns.has("pinswg_projectdiscription"),
|
||||||
|
"DNS should preserve canonical project description"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.ok(
|
||||||
|
harbour.has("pinswg_projectdiscription") &&
|
||||||
|
!harbour.has("pinswg_projectdescription"),
|
||||||
|
"Harbour should preserve misspelled project description variant"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.ok(
|
||||||
|
transport.has("pinswg_projectdescription") &&
|
||||||
|
!transport.has("pinswg_projectdiscription"),
|
||||||
|
"Transport should preserve canonical project description field"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.ok(
|
||||||
|
dns.has("pinswg_suspensionstartdates") &&
|
||||||
|
!dns.has("pinswg_suspensionstartdate"),
|
||||||
|
"DNS should preserve plural suspension start field"
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const [label, fields] of [
|
||||||
|
["Harbour", harbour],
|
||||||
|
["Transport", transport]
|
||||||
|
]) {
|
||||||
|
assert.ok(
|
||||||
|
fields.has("pinswg_suspensionstartdate") &&
|
||||||
|
!fields.has("pinswg_suspensionstartdates"),
|
||||||
|
`${label} should preserve singular suspension start field variant`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.ok(
|
||||||
|
dns.has("pinswg_anticipatedgridreferenceeastingtext") &&
|
||||||
|
dns.has("pinswg_anticipatedgridreferencenorthingtext"),
|
||||||
|
"DNS should preserve grid reference fields"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.ok(
|
||||||
|
harbour.has("pinswg_anticipatedgridreferenceeastingtext") &&
|
||||||
|
harbour.has("pinswg_anticipatedgridreferencenorthingtext"),
|
||||||
|
"Harbour should preserve grid reference fields"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.ok(
|
||||||
|
!electricity.has("pinswg_anticipatedgridreferenceeastingtext") &&
|
||||||
|
!electricity.has("pinswg_anticipatedgridreferencenorthingtext"),
|
||||||
|
"Electricity should preserve current absence of grid reference fields"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("representation-support cluster preserves questionnaire, statement, final-comments, and proof/other-parties variants", () => {
|
||||||
|
const rows = getFieldSet("pinswg_rows");
|
||||||
|
const planningObligation = getFieldSet(
|
||||||
|
"pinswg_planningobligationappeals106s"
|
||||||
|
);
|
||||||
|
const householder = getFieldSet("pinswg_householderappealhases");
|
||||||
|
const commonLand = getFieldSet("pinswg_commonlands");
|
||||||
|
|
||||||
|
assertHasFields(
|
||||||
|
rows,
|
||||||
|
[
|
||||||
|
"pinswg_questionnaireduedate",
|
||||||
|
"pinswg_statementduedate",
|
||||||
|
"pinswg_finalcommentsduedate",
|
||||||
|
"pinswg_proofsofevidencewrittenstatementsofeviden",
|
||||||
|
"pinswg_otherpartiesstatement"
|
||||||
|
],
|
||||||
|
"ROW representation-support cluster"
|
||||||
|
);
|
||||||
|
|
||||||
|
assertHasFields(
|
||||||
|
planningObligation,
|
||||||
|
[
|
||||||
|
"pinswg_questionnaireduedate",
|
||||||
|
"pinswg_statementduedate",
|
||||||
|
"pinswg_finalcommentsduedate",
|
||||||
|
"pinswg_otherpartiesstatements"
|
||||||
|
],
|
||||||
|
"Planning obligation representation-support cluster"
|
||||||
|
);
|
||||||
|
|
||||||
|
assertHasFields(
|
||||||
|
householder,
|
||||||
|
[
|
||||||
|
"pinswg_questionnaireduedate",
|
||||||
|
"pinswg_statementduedate",
|
||||||
|
"pinswg_finalcommentsduedate",
|
||||||
|
"pinswg_otherpartiesstmt"
|
||||||
|
],
|
||||||
|
"Householder representation-support cluster"
|
||||||
|
);
|
||||||
|
|
||||||
|
assertHasFields(
|
||||||
|
commonLand,
|
||||||
|
[
|
||||||
|
"pinswg_questionnaireduedate",
|
||||||
|
"pinswg_statementsduedate",
|
||||||
|
"pinswg_finalcommentsduedate",
|
||||||
|
"pinswg_proofsofevidencewrittenstatementsofeviden",
|
||||||
|
"pinswg_otherpartiesstatement"
|
||||||
|
],
|
||||||
|
"Common land representation-support cluster"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.ok(
|
||||||
|
commonLand.has("pinswg_statementsduedate") &&
|
||||||
|
!commonLand.has("pinswg_statementduedate"),
|
||||||
|
"Common land should preserve plural statement-due-date variant"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("event-date cluster preserves coexistence of start-date/time variants across representative bundles", () => {
|
||||||
|
const callIns = getFieldSet("pinswg_callinss77s");
|
||||||
|
const adverts = getFieldSet("pinswg_advertses");
|
||||||
|
const planningS78Dev = getFieldSet(
|
||||||
|
"pinswg_planningappeals78s",
|
||||||
|
"dev-pedw-hc"
|
||||||
|
);
|
||||||
|
const listedBuilding = getFieldSet(
|
||||||
|
"pinswg_listedbuildingandconservationareaconsens"
|
||||||
|
);
|
||||||
|
const commonLand = getFieldSet("pinswg_commonlands");
|
||||||
|
|
||||||
|
assert.ok(
|
||||||
|
callIns.has("pinswg_startdateofevent") &&
|
||||||
|
!callIns.has("pinswg_startdateoftheevent"),
|
||||||
|
"Call-ins should preserve `pinswg_startdateofevent` variant"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.ok(
|
||||||
|
adverts.has("pinswg_startdateoftheevent") &&
|
||||||
|
!adverts.has("pinswg_startdateofevent"),
|
||||||
|
"Adverts should preserve `pinswg_startdateoftheevent` variant"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.ok(
|
||||||
|
planningS78Dev.has("pinswg_startdatetimeiftheevent"),
|
||||||
|
"S78 should preserve datetime event variant"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.ok(
|
||||||
|
listedBuilding.has("pinswg_starttimeofevent") &&
|
||||||
|
!listedBuilding.has("pinswg_starttimeoftheevent"),
|
||||||
|
"Listed building should preserve `pinswg_starttimeofevent` variant"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.ok(
|
||||||
|
commonLand.has("pinswg_starttimeoftheevent") &&
|
||||||
|
!commonLand.has("pinswg_starttimeofevent"),
|
||||||
|
"Common land should preserve `pinswg_starttimeoftheevent` variant"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("authority and LPA cluster preserves current coexistence of local, associated, and relevant authority fields", () => {
|
||||||
|
const planningS78Dev = getFieldSet(
|
||||||
|
"pinswg_planningappeals78s",
|
||||||
|
"dev-pedw-hc"
|
||||||
|
);
|
||||||
|
const rows = getFieldSet("pinswg_rows");
|
||||||
|
const dns = getFieldSet("pinswg_dnses");
|
||||||
|
|
||||||
|
assert.ok(
|
||||||
|
planningS78Dev.has("_pinswg_localplanningauthority_value") &&
|
||||||
|
!planningS78Dev.has("_pinswg_associatedlpa_value") &&
|
||||||
|
!planningS78Dev.has("pinswg_relevantauthorityname"),
|
||||||
|
"S78 should preserve local-planning-authority-only behaviour"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.ok(
|
||||||
|
rows.has("_pinswg_associatedlpa_value") &&
|
||||||
|
rows.has("pinswg_relevantauthorityname") &&
|
||||||
|
!rows.has("_pinswg_localplanningauthority_value"),
|
||||||
|
"ROW should preserve overlap of associated LPA and relevant authority"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.ok(
|
||||||
|
dns.has("_pinswg_associatedlpa_value") &&
|
||||||
|
!dns.has("pinswg_relevantauthorityname") &&
|
||||||
|
!dns.has("_pinswg_localplanningauthority_value"),
|
||||||
|
"DNS should preserve associated-LPA-only authority behaviour"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const run = async () => {
|
||||||
|
let passed = 0;
|
||||||
|
for (const currentTest of tests) {
|
||||||
|
await currentTest.fn();
|
||||||
|
passed += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`Phase 22 select-query-types duplicated-cluster tests passed (${passed}/${tests.length}).`
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = run;
|
||||||
|
|
||||||
|
if (require.main === module) {
|
||||||
|
run().catch((error) => {
|
||||||
|
console.error(error);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,395 @@
|
|||||||
|
const fs = require("fs");
|
||||||
|
const path = require("path");
|
||||||
|
const vm = require("vm");
|
||||||
|
const assert = require("assert");
|
||||||
|
|
||||||
|
const rootDir = path.resolve(__dirname, "..", "..");
|
||||||
|
|
||||||
|
const loadSelectQueryTypesModule = (relayPath) => {
|
||||||
|
const filePath = path.join(rootDir, "actions", "selectQueryTypes.js");
|
||||||
|
|
||||||
|
let source = fs.readFileSync(filePath, "utf8");
|
||||||
|
source = source.replace(
|
||||||
|
/export const\s+getSelectQuery\s*=\s*/,
|
||||||
|
"const getSelectQuery = "
|
||||||
|
);
|
||||||
|
source += `
|
||||||
|
module.exports = {
|
||||||
|
getSelectQuery
|
||||||
|
};
|
||||||
|
`;
|
||||||
|
|
||||||
|
const context = {
|
||||||
|
module: { exports: {} },
|
||||||
|
exports: {},
|
||||||
|
require,
|
||||||
|
process: {
|
||||||
|
env: {
|
||||||
|
RELAYPATH: relayPath
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
vm.runInNewContext(source, context, { filename: filePath });
|
||||||
|
return context.module.exports;
|
||||||
|
};
|
||||||
|
|
||||||
|
const tests = [];
|
||||||
|
const test = (name, fn) => tests.push({ name, fn });
|
||||||
|
|
||||||
|
const splitFields = (query) => {
|
||||||
|
assert.ok(
|
||||||
|
query.startsWith("&$select="),
|
||||||
|
`Expected query to start with &$select= but received: ${query}`
|
||||||
|
);
|
||||||
|
|
||||||
|
return query
|
||||||
|
.replace(/^&\$select=/, "")
|
||||||
|
.split(",")
|
||||||
|
.map((field) => field.trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
};
|
||||||
|
|
||||||
|
const assertIncludesAll = (fields, requiredFields, message) => {
|
||||||
|
for (const field of requiredFields) {
|
||||||
|
assert.ok(
|
||||||
|
fields.includes(field),
|
||||||
|
`${message}: expected field \`${field}\` to be included`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
test("Planning S78 bundle preserves base, typo, S78-specific, and relaypath-dependent fields", () => {
|
||||||
|
const devMod = loadSelectQueryTypesModule("dev-pedw-hc");
|
||||||
|
const nonDevMod = loadSelectQueryTypesModule("prod-like");
|
||||||
|
|
||||||
|
const devFields = splitFields(
|
||||||
|
devMod.getSelectQuery("pinswg_planningappeals78s")
|
||||||
|
);
|
||||||
|
const nonDevFields = splitFields(
|
||||||
|
nonDevMod.getSelectQuery("pinswg_planningappeals78s")
|
||||||
|
);
|
||||||
|
|
||||||
|
assertIncludesAll(
|
||||||
|
devFields,
|
||||||
|
[
|
||||||
|
"pinswg_eiarequired",
|
||||||
|
"modifiedon",
|
||||||
|
"_pinswg_appellant_value",
|
||||||
|
"pinswg_name",
|
||||||
|
"pinswg_siteaddressline1",
|
||||||
|
"pinswg_siteaddresspostcode",
|
||||||
|
"pinswg_siteaddresscounty",
|
||||||
|
"_pinswg_localplanningauthority_value",
|
||||||
|
"pinswg_casedecisiondate",
|
||||||
|
"pinswg_dicision",
|
||||||
|
"pinswg_questionnaireduedate",
|
||||||
|
"pinswg_startdate",
|
||||||
|
"pinswg_statementduedate",
|
||||||
|
"pinswg_startdatetimeiftheevent",
|
||||||
|
"pinswg_representation_period_end_date",
|
||||||
|
"pinswg_extend_representation_date",
|
||||||
|
"pinswg_typeofevent",
|
||||||
|
"statuscode"
|
||||||
|
],
|
||||||
|
"S78 dev bundle"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.ok(
|
||||||
|
!devFields.includes("pinswg_developmentdescription"),
|
||||||
|
"S78 dev bundle should preserve current omission of extended appeal-form fields"
|
||||||
|
);
|
||||||
|
|
||||||
|
assertIncludesAll(
|
||||||
|
nonDevFields,
|
||||||
|
[
|
||||||
|
"pinswg_eiarequired",
|
||||||
|
"modifiedon",
|
||||||
|
"_pinswg_appellant_value",
|
||||||
|
"pinswg_name",
|
||||||
|
"pinswg_dicision",
|
||||||
|
"pinswg_startdatetimeiftheevent",
|
||||||
|
"pinswg_developmentdescription",
|
||||||
|
"pinswg_whyisinspectorneededonsite",
|
||||||
|
"pinswg_outlinehealthandsafetyissuesonsite",
|
||||||
|
"pinswg_additionalappealsmadedetails",
|
||||||
|
"pinswg_chosenprocedureexplanation",
|
||||||
|
"pinswg_dateofapplication",
|
||||||
|
"pinswg_lpaapplicationreference",
|
||||||
|
"pinswg_caseworkreason",
|
||||||
|
"pinswg_dateoflpadecision",
|
||||||
|
"pinswg_areaofsiteinhectaresdec",
|
||||||
|
"pinswg_floorspaceinsquaremeters",
|
||||||
|
"pinswg_ownershipcertificate",
|
||||||
|
"pinswg_siteviewablefromroad",
|
||||||
|
"pinswg_costappliedforindicator",
|
||||||
|
"pinswg_s106unilateralsubmitted"
|
||||||
|
],
|
||||||
|
"S78 non-dev bundle"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.ok(
|
||||||
|
!nonDevFields.includes("pinswg_representation_period_end_date"),
|
||||||
|
"S78 non-dev bundle should preserve current omission of dev-only representation-period fields"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("DNS bundle preserves project, representation-window, and process fields", () => {
|
||||||
|
const { getSelectQuery } = loadSelectQueryTypesModule();
|
||||||
|
const fields = splitFields(getSelectQuery("pinswg_dnses"));
|
||||||
|
|
||||||
|
assertIncludesAll(
|
||||||
|
fields,
|
||||||
|
[
|
||||||
|
"pinswg_eiarequired",
|
||||||
|
"pinswg_anticipatedgridreferenceeastingtext",
|
||||||
|
"pinswg_anticipatedgridreferencenorthingtext",
|
||||||
|
"pinswg_name",
|
||||||
|
"pinswg_addressline1",
|
||||||
|
"pinswg_addresscounty",
|
||||||
|
"pinswg_applicationacceptedasvalid",
|
||||||
|
"pinswg_choiceofprocedure",
|
||||||
|
"pinswg_dateofdecision",
|
||||||
|
"pinswg_deadlineforsubmissionofapplication",
|
||||||
|
"pinswg_endofrepresentationperiod",
|
||||||
|
"pinswg_procedureconfirmed",
|
||||||
|
"pinswg_projectdescription",
|
||||||
|
"pinswg_projectlocation",
|
||||||
|
"pinswg_recommendation",
|
||||||
|
"pinswg_reportdueby",
|
||||||
|
"pinswg_suspensiondates",
|
||||||
|
"pinswg_suspensionenddates",
|
||||||
|
"pinswg_suspensionstartdates",
|
||||||
|
"pinswg_webaddress",
|
||||||
|
"pinswg_writtenacceptanceofnotification",
|
||||||
|
"_pinswg_dnsids_value",
|
||||||
|
"pinswg_startdateofevent",
|
||||||
|
"pinswg_typeofevent",
|
||||||
|
"statuscode"
|
||||||
|
],
|
||||||
|
"DNS bundle"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("SIPS bundle preserves consultation and milestone fields", () => {
|
||||||
|
const { getSelectQuery } = loadSelectQueryTypesModule();
|
||||||
|
const fields = splitFields(getSelectQuery("pinswg_sipses"));
|
||||||
|
|
||||||
|
assertIncludesAll(
|
||||||
|
fields,
|
||||||
|
[
|
||||||
|
"pinswg_casedeadline",
|
||||||
|
"_pinswg_projecttype_value",
|
||||||
|
"pinswg_eiarequired",
|
||||||
|
"pinswg_anticipatedgridrefeasting",
|
||||||
|
"pinswg_anticipatedgridrefnorthing",
|
||||||
|
"_pinswg_appellant_value",
|
||||||
|
"pinswg_name",
|
||||||
|
"_pinswg_associatedlpa_value",
|
||||||
|
"pinswg_choiceofprocedure",
|
||||||
|
"pinswg_projectdescription",
|
||||||
|
"pinswg_projectlocation",
|
||||||
|
"_pinswg_sipscase_value",
|
||||||
|
"pinswg_validnoticeofintentiontosubmit",
|
||||||
|
"pinswg_deadlineforsubmissionofapplication",
|
||||||
|
"pinswg_screeningstartdate",
|
||||||
|
"pinswg_eiatargetdate",
|
||||||
|
"pinswg_scopingstartdate",
|
||||||
|
"pinswg_scopingtargetdate",
|
||||||
|
"pinswg_scopingoutcomeissueddate",
|
||||||
|
"pinswg_consultationopen",
|
||||||
|
"pinswg_consultationclose",
|
||||||
|
"pinswg_reportissuedtowelshministers",
|
||||||
|
"pinswg_decisionissuedtoapplicant",
|
||||||
|
"pinswg_redeterminationconsultationdeadline"
|
||||||
|
],
|
||||||
|
"SIPS bundle"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("ROW bundle preserves canonical specialist-process field and date/representation fields", () => {
|
||||||
|
const { getSelectQuery } = loadSelectQueryTypesModule();
|
||||||
|
const fields = splitFields(getSelectQuery("pinswg_rows"));
|
||||||
|
|
||||||
|
assertIncludesAll(
|
||||||
|
fields,
|
||||||
|
[
|
||||||
|
"pinswg_detailedeiascreeningrequired",
|
||||||
|
"pinswg_specialistcaseprocess",
|
||||||
|
"modifiedon",
|
||||||
|
"_pinswg_appellant_value",
|
||||||
|
"pinswg_name",
|
||||||
|
"_pinswg_associatedlpa_value",
|
||||||
|
"pinswg_casedecisiondate",
|
||||||
|
"pinswg_finalcommentsduedate",
|
||||||
|
"pinswg_otherpartiesstatement",
|
||||||
|
"pinswg_procedure",
|
||||||
|
"pinswg_proofsofevidencewrittenstatementsofeviden",
|
||||||
|
"pinswg_questionnaireduedate",
|
||||||
|
"pinswg_relevantauthorityname",
|
||||||
|
"pinswg_startdate",
|
||||||
|
"pinswg_startdateoftheevent",
|
||||||
|
"pinswg_typeofevent",
|
||||||
|
"pinswg_statementduedate",
|
||||||
|
"statuscode"
|
||||||
|
],
|
||||||
|
"ROW bundle"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.ok(
|
||||||
|
!fields.includes("pinswg_speacialistcaseprocess"),
|
||||||
|
"ROW bundle should preserve current absence of misspelled specialist-process field"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Adverts bundle preserves misspelled specialist-process field and representation fields", () => {
|
||||||
|
const { getSelectQuery } = loadSelectQueryTypesModule();
|
||||||
|
const fields = splitFields(getSelectQuery("pinswg_advertses"));
|
||||||
|
|
||||||
|
assertIncludesAll(
|
||||||
|
fields,
|
||||||
|
[
|
||||||
|
"pinswg_detailedeiascreeningrequired",
|
||||||
|
"pinswg_speacialistcaseprocess",
|
||||||
|
"modifiedon",
|
||||||
|
"_pinswg_appellant_value",
|
||||||
|
"pinswg_name",
|
||||||
|
"_pinswg_associatedlpa_value",
|
||||||
|
"pinswg_casedecisiondate",
|
||||||
|
"pinswg_dateeventrequested",
|
||||||
|
"pinswg_procedure",
|
||||||
|
"pinswg_questionnaireduedate",
|
||||||
|
"pinswg_startdateoftheevent",
|
||||||
|
"pinswg_startdate",
|
||||||
|
"pinswg_otherpartiesstatement",
|
||||||
|
"pinswg_finalcommentsduedate",
|
||||||
|
"pinswg_proofsofevidencewrittenstatementsofeviden",
|
||||||
|
"pinswg_statementduedate",
|
||||||
|
"statuscode"
|
||||||
|
],
|
||||||
|
"Adverts bundle"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.ok(
|
||||||
|
!fields.includes("pinswg_specialistcaseprocess"),
|
||||||
|
"Adverts bundle should preserve current absence of canonical specialist-process field"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Householder bundle preserves reduced field set and other-parties variant", () => {
|
||||||
|
const { getSelectQuery } = loadSelectQueryTypesModule();
|
||||||
|
const fields = splitFields(getSelectQuery("pinswg_householderappealhases"));
|
||||||
|
|
||||||
|
assertIncludesAll(
|
||||||
|
fields,
|
||||||
|
[
|
||||||
|
"pinswg_eiarequired",
|
||||||
|
"modifiedon",
|
||||||
|
"_pinswg_appellant_value",
|
||||||
|
"pinswg_name",
|
||||||
|
"pinswg_siteaddressline1",
|
||||||
|
"pinswg_siteaddresspostcode",
|
||||||
|
"_pinswg_associatedlpa_value",
|
||||||
|
"pinswg_casedecisiondate",
|
||||||
|
"pinswg_dateeventrequested",
|
||||||
|
"pinswg_decision",
|
||||||
|
"pinswg_finalcommentsduedate",
|
||||||
|
"pinswg_otherpartiesstmt",
|
||||||
|
"pinswg_procedure",
|
||||||
|
"pinswg_questionnaireduedate",
|
||||||
|
"pinswg_startdate",
|
||||||
|
"pinswg_statementduedate",
|
||||||
|
"statuscode"
|
||||||
|
],
|
||||||
|
"Householder bundle"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.ok(
|
||||||
|
!fields.includes("pinswg_startdateoftheevent") &&
|
||||||
|
!fields.includes("pinswg_startdateofevent"),
|
||||||
|
"Householder bundle should preserve current omission of event-start fields"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Infrastructure-style harbour bundle preserves project/date fields and schema variants", () => {
|
||||||
|
const { getSelectQuery } = loadSelectQueryTypesModule();
|
||||||
|
const fields = splitFields(getSelectQuery("pinswg_harbourrevisionorders"));
|
||||||
|
|
||||||
|
assertIncludesAll(
|
||||||
|
fields,
|
||||||
|
[
|
||||||
|
"pinswg_eiarequired",
|
||||||
|
"pinswg_anticipatedgridreferenceeastingtext",
|
||||||
|
"pinswg_anticipatedgridreferencenorthingtext",
|
||||||
|
"pinswg_name",
|
||||||
|
"pinswg_addressline1",
|
||||||
|
"pinswg_addresscounty",
|
||||||
|
"pinswg_applicationacceptedasvalid",
|
||||||
|
"pinswg_choiceofprocedure",
|
||||||
|
"pinswg_dateofdecision",
|
||||||
|
"pinswg_endofrepresentationperiod",
|
||||||
|
"pinswg_projectdiscription",
|
||||||
|
"pinswg_projectlocation",
|
||||||
|
"pinswg_suspensionstartdate",
|
||||||
|
"pinswg_suspensionenddates",
|
||||||
|
"pinswg_validnoticeofintentiontosubmitanapplicati",
|
||||||
|
"pinswg_writtenacceptanceofnotification",
|
||||||
|
"pinswg_startdateofevent",
|
||||||
|
"pinswg_typeofevent",
|
||||||
|
"statuscode"
|
||||||
|
],
|
||||||
|
"Harbour bundle"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Minimal bundles preserve current nonvalidation and LDP behaviour", () => {
|
||||||
|
const { getSelectQuery } = loadSelectQueryTypesModule();
|
||||||
|
|
||||||
|
const nonValidationFields = splitFields(
|
||||||
|
getSelectQuery("pinswg_nonvalidations")
|
||||||
|
);
|
||||||
|
assertIncludesAll(
|
||||||
|
nonValidationFields,
|
||||||
|
[
|
||||||
|
"modifiedon",
|
||||||
|
"_pinswg_appellant_value",
|
||||||
|
"pinswg_name",
|
||||||
|
"pinswg_siteaddressline1",
|
||||||
|
"pinswg_siteaddressline2",
|
||||||
|
"pinswg_siteaddresspostcode",
|
||||||
|
"pinswg_siteaddresstown",
|
||||||
|
"_pinswg_associatedlpa_value",
|
||||||
|
"statuscode"
|
||||||
|
],
|
||||||
|
"Nonvalidation bundle"
|
||||||
|
);
|
||||||
|
assert.strictEqual(
|
||||||
|
nonValidationFields.includes("pinswg_casedecisiondate"),
|
||||||
|
false,
|
||||||
|
"Nonvalidation bundle should preserve current omission of decision-date fields"
|
||||||
|
);
|
||||||
|
|
||||||
|
const ldpFields = splitFields(getSelectQuery("pinswg_ldps"));
|
||||||
|
assert.deepStrictEqual(ldpFields, ["modifiedon", "pinswg_name"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
const run = async () => {
|
||||||
|
let passed = 0;
|
||||||
|
for (const currentTest of tests) {
|
||||||
|
await currentTest.fn();
|
||||||
|
passed += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`Phase 22 select-query-types field bundle tests passed (${passed}/${tests.length}).`
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = run;
|
||||||
|
|
||||||
|
if (require.main === module) {
|
||||||
|
run().catch((error) => {
|
||||||
|
console.error(error);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,405 @@
|
|||||||
|
const fs = require("fs");
|
||||||
|
const path = require("path");
|
||||||
|
const vm = require("vm");
|
||||||
|
const assert = require("assert");
|
||||||
|
|
||||||
|
const rootDir = path.resolve(__dirname, "..", "..");
|
||||||
|
|
||||||
|
const loadSelectQueryTypesModule = (relayPath) => {
|
||||||
|
const filePath = path.join(rootDir, "actions", "selectQueryTypes.js");
|
||||||
|
|
||||||
|
let source = fs.readFileSync(filePath, "utf8");
|
||||||
|
source = source.replace(
|
||||||
|
/export const\s+getSelectQuery\s*=\s*/,
|
||||||
|
"const getSelectQuery = "
|
||||||
|
);
|
||||||
|
source += `
|
||||||
|
module.exports = {
|
||||||
|
getSelectQuery
|
||||||
|
};
|
||||||
|
`;
|
||||||
|
|
||||||
|
const context = {
|
||||||
|
module: { exports: {} },
|
||||||
|
exports: {},
|
||||||
|
require,
|
||||||
|
process: {
|
||||||
|
env: {
|
||||||
|
RELAYPATH: relayPath
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
vm.runInNewContext(source, context, { filename: filePath });
|
||||||
|
return context.module.exports;
|
||||||
|
};
|
||||||
|
|
||||||
|
const tests = [];
|
||||||
|
const test = (name, fn) => tests.push({ name, fn });
|
||||||
|
|
||||||
|
const splitFields = (query) => {
|
||||||
|
assert.ok(
|
||||||
|
query.startsWith("&$select="),
|
||||||
|
`Expected query to start with &$select= but received: ${query}`
|
||||||
|
);
|
||||||
|
|
||||||
|
return query
|
||||||
|
.replace(/^&\$select=/, "")
|
||||||
|
.split(",")
|
||||||
|
.map((field) => field.trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getFieldSet = (appealTypeName, relayPath) =>
|
||||||
|
new Set(
|
||||||
|
splitFields(
|
||||||
|
loadSelectQueryTypesModule(relayPath).getSelectQuery(appealTypeName)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
const assertHasFields = (fieldSet, expectedFields, label) => {
|
||||||
|
for (const field of expectedFields) {
|
||||||
|
assert.ok(fieldSet.has(field), `${label}: expected field \`${field}\``);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
test("planning-style family preserves common base, location, decision, and status fields across representative bundles", () => {
|
||||||
|
const s78Dev = getFieldSet("pinswg_planningappeals78s", "dev-pedw-hc");
|
||||||
|
const rows = getFieldSet("pinswg_rows");
|
||||||
|
const householder = getFieldSet("pinswg_householderappealhases");
|
||||||
|
|
||||||
|
const planningBaseFamily = [
|
||||||
|
"modifiedon",
|
||||||
|
"_pinswg_appellant_value",
|
||||||
|
"pinswg_name",
|
||||||
|
"pinswg_dateeventrequested",
|
||||||
|
"statuscode"
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const [label, fields] of [
|
||||||
|
["S78 dev", s78Dev],
|
||||||
|
["ROW", rows],
|
||||||
|
["Householder", householder]
|
||||||
|
]) {
|
||||||
|
assertHasFields(fields, planningBaseFamily, label);
|
||||||
|
}
|
||||||
|
|
||||||
|
assertHasFields(
|
||||||
|
s78Dev,
|
||||||
|
[
|
||||||
|
"pinswg_siteaddressline1",
|
||||||
|
"pinswg_siteaddressline2",
|
||||||
|
"pinswg_siteaddresspostcode",
|
||||||
|
"pinswg_siteaddresstown",
|
||||||
|
"pinswg_siteaddresscounty",
|
||||||
|
"pinswg_casedecisiondate",
|
||||||
|
"pinswg_dicision"
|
||||||
|
],
|
||||||
|
"S78 dev planning family"
|
||||||
|
);
|
||||||
|
|
||||||
|
assertHasFields(
|
||||||
|
rows,
|
||||||
|
[
|
||||||
|
"pinswg_siteaddressline1",
|
||||||
|
"pinswg_siteaddressline2",
|
||||||
|
"pinswg_siteaddresspostcode",
|
||||||
|
"pinswg_siteaddresstown",
|
||||||
|
"pinswg_casedecisiondate",
|
||||||
|
"pinswg_decision"
|
||||||
|
],
|
||||||
|
"ROW planning family"
|
||||||
|
);
|
||||||
|
|
||||||
|
assertHasFields(
|
||||||
|
householder,
|
||||||
|
[
|
||||||
|
"pinswg_siteaddressline1",
|
||||||
|
"pinswg_siteaddressline2",
|
||||||
|
"pinswg_siteaddresspostcode",
|
||||||
|
"pinswg_siteaddresstown",
|
||||||
|
"pinswg_casedecisiondate",
|
||||||
|
"pinswg_decision"
|
||||||
|
],
|
||||||
|
"Householder planning family"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("representation-window family preserves current start and end date variants without normalization", () => {
|
||||||
|
const rows = getFieldSet("pinswg_rows");
|
||||||
|
const enforcementListed = getFieldSet(
|
||||||
|
"pinswg_enforcementlistedbuildingconservationaps"
|
||||||
|
);
|
||||||
|
const dns = getFieldSet("pinswg_dnses");
|
||||||
|
const callIns = getFieldSet("pinswg_callinss77s");
|
||||||
|
|
||||||
|
assertHasFields(
|
||||||
|
rows,
|
||||||
|
[
|
||||||
|
"pinswg_startdate",
|
||||||
|
"pinswg_statementduedate",
|
||||||
|
"pinswg_finalcommentsduedate"
|
||||||
|
],
|
||||||
|
"ROW representation-window family"
|
||||||
|
);
|
||||||
|
|
||||||
|
assertHasFields(
|
||||||
|
enforcementListed,
|
||||||
|
[
|
||||||
|
"pinswg_startdates",
|
||||||
|
"pinswg_statementduedate",
|
||||||
|
"pinswg_finalcommentsduedate"
|
||||||
|
],
|
||||||
|
"Enforcement listed representation-window family"
|
||||||
|
);
|
||||||
|
|
||||||
|
assertHasFields(
|
||||||
|
dns,
|
||||||
|
[
|
||||||
|
"pinswg_applicationacceptedasvalid",
|
||||||
|
"pinswg_endofrepresentationperiod"
|
||||||
|
],
|
||||||
|
"DNS representation-window family"
|
||||||
|
);
|
||||||
|
|
||||||
|
assertHasFields(
|
||||||
|
callIns,
|
||||||
|
[
|
||||||
|
"pinswg_startdate",
|
||||||
|
"pinswg_statementsduedate",
|
||||||
|
"pinswg_finalcommentsduedate"
|
||||||
|
],
|
||||||
|
"Call-ins representation-window family"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.ok(
|
||||||
|
!dns.has("pinswg_startdate") && !dns.has("pinswg_startdates"),
|
||||||
|
"DNS should preserve infrastructure-style start-date variant rather than planning-style start fields"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("specialist-process family preserves intentional field-variant divergence across bundles", () => {
|
||||||
|
const rows = getFieldSet("pinswg_rows");
|
||||||
|
const adverts = getFieldSet("pinswg_advertses");
|
||||||
|
const hedges = getFieldSet(
|
||||||
|
"pinswg_hedgeshedgerowstreepreservationreplacems"
|
||||||
|
);
|
||||||
|
const householder = getFieldSet("pinswg_householderappealhases");
|
||||||
|
|
||||||
|
assert.ok(
|
||||||
|
rows.has("pinswg_specialistcaseprocess") &&
|
||||||
|
!rows.has("pinswg_speacialistcaseprocess"),
|
||||||
|
"ROW should preserve canonical-only specialist-process selection"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.ok(
|
||||||
|
adverts.has("pinswg_speacialistcaseprocess") &&
|
||||||
|
!adverts.has("pinswg_specialistcaseprocess"),
|
||||||
|
"Adverts should preserve misspelled-only specialist-process selection"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.ok(
|
||||||
|
hedges.has("pinswg_specialistcaseprocess") &&
|
||||||
|
!hedges.has("pinswg_speacialistcaseprocess"),
|
||||||
|
"Hedges should preserve canonical-only specialist-process selection"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.ok(
|
||||||
|
!householder.has("pinswg_specialistcaseprocess") &&
|
||||||
|
!householder.has("pinswg_speacialistcaseprocess"),
|
||||||
|
"Householder should preserve absence of specialist-process fields"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("infrastructure-style family preserves project-oriented fields and schema variants across representative bundles", () => {
|
||||||
|
const dns = getFieldSet("pinswg_dnses");
|
||||||
|
const harbour = getFieldSet("pinswg_harbourrevisionorders");
|
||||||
|
const electricity = getFieldSet("pinswg_electricityacts");
|
||||||
|
|
||||||
|
const commonInfrastructureFields = [
|
||||||
|
"pinswg_eiarequired",
|
||||||
|
"pinswg_name",
|
||||||
|
"pinswg_addressline1",
|
||||||
|
"pinswg_addresstown",
|
||||||
|
"pinswg_postcode",
|
||||||
|
"pinswg_applicationacceptedasvalid",
|
||||||
|
"pinswg_endofrepresentationperiod",
|
||||||
|
"pinswg_projectlocation",
|
||||||
|
"pinswg_recommendation",
|
||||||
|
"pinswg_reportdueby",
|
||||||
|
"pinswg_validnoticeofintentiontosubmitanapplicati",
|
||||||
|
"pinswg_writtenacceptanceofnotification",
|
||||||
|
"pinswg_startdateofevent",
|
||||||
|
"pinswg_typeofevent",
|
||||||
|
"statuscode"
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const [label, fields] of [
|
||||||
|
["DNS", dns],
|
||||||
|
["Harbour", harbour],
|
||||||
|
["Electricity", electricity]
|
||||||
|
]) {
|
||||||
|
assertHasFields(fields, commonInfrastructureFields, label);
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.ok(
|
||||||
|
dns.has("pinswg_projectdescription") &&
|
||||||
|
!dns.has("pinswg_projectdiscription"),
|
||||||
|
"DNS should preserve canonical project description field"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.ok(
|
||||||
|
harbour.has("pinswg_projectdiscription") &&
|
||||||
|
!harbour.has("pinswg_projectdescription"),
|
||||||
|
"Harbour should preserve misspelled project description variant"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.ok(
|
||||||
|
dns.has("pinswg_suspensionstartdates") &&
|
||||||
|
!dns.has("pinswg_suspensionstartdate"),
|
||||||
|
"DNS should preserve plural suspension start field"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.ok(
|
||||||
|
harbour.has("pinswg_suspensionstartdate") &&
|
||||||
|
!harbour.has("pinswg_suspensionstartdates"),
|
||||||
|
"Harbour should preserve singular suspension start field variant"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.ok(
|
||||||
|
electricity.has("modifiedon") && !dns.has("modifiedon"),
|
||||||
|
"Electricity should preserve current modifiedon overlap while DNS preserves its current omission"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("authority and LPA family preserves current field distinctions rather than collapsing them", () => {
|
||||||
|
const s78Dev = getFieldSet("pinswg_planningappeals78s", "dev-pedw-hc");
|
||||||
|
const rows = getFieldSet("pinswg_rows");
|
||||||
|
const dns = getFieldSet("pinswg_dnses");
|
||||||
|
|
||||||
|
assert.ok(
|
||||||
|
s78Dev.has("_pinswg_localplanningauthority_value") &&
|
||||||
|
!s78Dev.has("_pinswg_associatedlpa_value"),
|
||||||
|
"S78 should preserve local-planning-authority field variant"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.ok(
|
||||||
|
rows.has("_pinswg_associatedlpa_value") &&
|
||||||
|
rows.has("pinswg_relevantauthorityname"),
|
||||||
|
"ROW should preserve overlap of associated LPA and relevant authority fields"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.ok(
|
||||||
|
dns.has("_pinswg_associatedlpa_value") &&
|
||||||
|
!dns.has("pinswg_relevantauthorityname"),
|
||||||
|
"DNS should preserve associated LPA without planning-style relevant-authority field"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("consultation family is currently specific to SIPS-style profiles", () => {
|
||||||
|
const sips = getFieldSet("pinswg_sipses");
|
||||||
|
const dns = getFieldSet("pinswg_dnses");
|
||||||
|
const rows = getFieldSet("pinswg_rows");
|
||||||
|
|
||||||
|
assertHasFields(
|
||||||
|
sips,
|
||||||
|
["pinswg_consultationopen", "pinswg_consultationclose"],
|
||||||
|
"SIPS consultation family"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.ok(
|
||||||
|
!dns.has("pinswg_consultationopen") &&
|
||||||
|
!dns.has("pinswg_consultationclose") &&
|
||||||
|
!rows.has("pinswg_consultationopen") &&
|
||||||
|
!rows.has("pinswg_consultationclose"),
|
||||||
|
"Consultation fields should preserve their current absence from non-SIPS representative bundles"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("minimal-profile family preserves intentionally small bundles and divergence from larger families", () => {
|
||||||
|
const nonValidation = getFieldSet("pinswg_nonvalidations");
|
||||||
|
const ldp = getFieldSet("pinswg_ldps");
|
||||||
|
const rows = getFieldSet("pinswg_rows");
|
||||||
|
|
||||||
|
assert.deepStrictEqual([...ldp], ["modifiedon", "pinswg_name"]);
|
||||||
|
|
||||||
|
assertHasFields(
|
||||||
|
nonValidation,
|
||||||
|
[
|
||||||
|
"modifiedon",
|
||||||
|
"_pinswg_appellant_value",
|
||||||
|
"pinswg_name",
|
||||||
|
"pinswg_siteaddressline1",
|
||||||
|
"pinswg_siteaddresspostcode",
|
||||||
|
"_pinswg_associatedlpa_value",
|
||||||
|
"statuscode"
|
||||||
|
],
|
||||||
|
"Nonvalidation minimal family"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.ok(
|
||||||
|
!nonValidation.has("pinswg_questionnaireduedate") &&
|
||||||
|
!nonValidation.has("pinswg_startdate") &&
|
||||||
|
!nonValidation.has("pinswg_applicationacceptedasvalid"),
|
||||||
|
"Nonvalidation should preserve absence of representation-window and questionnaire family fields"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.ok(
|
||||||
|
rows.size > nonValidation.size && nonValidation.size > ldp.size,
|
||||||
|
"Minimal bundles should preserve their intentionally smaller profiles relative to larger planning bundles"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("family overlaps and divergences are preserved across representative bundles", () => {
|
||||||
|
const rows = getFieldSet("pinswg_rows");
|
||||||
|
const dns = getFieldSet("pinswg_dnses");
|
||||||
|
const sips = getFieldSet("pinswg_sipses");
|
||||||
|
const nonValidation = getFieldSet("pinswg_nonvalidations");
|
||||||
|
|
||||||
|
assert.ok(
|
||||||
|
rows.has("pinswg_startdate") && rows.has("pinswg_finalcommentsduedate"),
|
||||||
|
"Planning + representation overlap should be preserved for ROW"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.ok(
|
||||||
|
dns.has("pinswg_applicationacceptedasvalid") &&
|
||||||
|
dns.has("pinswg_endofrepresentationperiod") &&
|
||||||
|
dns.has("pinswg_projectlocation"),
|
||||||
|
"Infrastructure + representation overlap should be preserved for DNS"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.ok(
|
||||||
|
sips.has("pinswg_consultationopen") &&
|
||||||
|
sips.has("pinswg_consultationclose") &&
|
||||||
|
!sips.has("pinswg_finalcommentsduedate"),
|
||||||
|
"SIPS + consultation overlap and planning-style divergence should be preserved"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.ok(
|
||||||
|
!nonValidation.has("pinswg_projectlocation") &&
|
||||||
|
!nonValidation.has("pinswg_consultationopen") &&
|
||||||
|
!nonValidation.has("pinswg_questionnaireduedate"),
|
||||||
|
"Minimal profiles should preserve divergence from infrastructure, consultation, and richer planning families"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const run = async () => {
|
||||||
|
let passed = 0;
|
||||||
|
for (const currentTest of tests) {
|
||||||
|
await currentTest.fn();
|
||||||
|
passed += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`Phase 22 select-query-types field family tests passed (${passed}/${tests.length}).`
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = run;
|
||||||
|
|
||||||
|
if (require.main === module) {
|
||||||
|
run().catch((error) => {
|
||||||
|
console.error(error);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user