Allow for more API data configuration
This puts the data configuration in a dedicated file, and allows for separately allowing/disallowing editing and verification for each field, as well as specifying PSQL types to cast to on inserting
This commit is contained in:
parent
4bd3c147e3
commit
58bc11be04
242
app/src/api/config/dataFields.ts
Normal file
242
app/src/api/config/dataFields.ts
Normal file
@ -0,0 +1,242 @@
|
|||||||
|
import { valueType } from '../../helpers';
|
||||||
|
|
||||||
|
/** Configuration for a single data field */
|
||||||
|
export interface DataFieldConfig {
|
||||||
|
/**
|
||||||
|
* Allow editing the field?
|
||||||
|
*/
|
||||||
|
edit: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allow verifying the field value?
|
||||||
|
* Default: false;
|
||||||
|
*/
|
||||||
|
verify?: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Should the update value be formatted as JSON text?
|
||||||
|
* E.g. if the database column is of type json, the object coming from
|
||||||
|
* the HTTP request body needs to be formatted as json by pg-promise
|
||||||
|
* See more on formatting here https://vitaly-t.github.io/pg-promise/formatting.html#.format
|
||||||
|
* Default: false
|
||||||
|
*/
|
||||||
|
asJson?: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Should the formatted value be cast to a different type when inserting to db?
|
||||||
|
* Useful when the JS object from request needs to be mapped to a Postgres-specific column
|
||||||
|
* E.g. can map a complex object to jsonb (need to format it as text json, too, with the asJson option above)
|
||||||
|
* (Add more options here as they become necessary)
|
||||||
|
* Default: undefined
|
||||||
|
*/
|
||||||
|
sqlCast?: 'json' | 'jsonb';
|
||||||
|
}
|
||||||
|
|
||||||
|
export const dataFieldsConfig = valueType<DataFieldConfig>()({ /* eslint-disable @typescript-eslint/camelcase */
|
||||||
|
ref_osm_id: {
|
||||||
|
edit: true,
|
||||||
|
},
|
||||||
|
location_name: {
|
||||||
|
edit: false,
|
||||||
|
verify: true,
|
||||||
|
},
|
||||||
|
location_number: {
|
||||||
|
edit: true,
|
||||||
|
verify: true,
|
||||||
|
},
|
||||||
|
location_street: {
|
||||||
|
edit: false,
|
||||||
|
verify: true,
|
||||||
|
},
|
||||||
|
location_line_two: {
|
||||||
|
edit: false,
|
||||||
|
},
|
||||||
|
location_town: {
|
||||||
|
edit: false,
|
||||||
|
},
|
||||||
|
location_postcode: {
|
||||||
|
edit: false,
|
||||||
|
},
|
||||||
|
location_latitude: {
|
||||||
|
edit: true,
|
||||||
|
},
|
||||||
|
location_longitude: {
|
||||||
|
edit: true,
|
||||||
|
},
|
||||||
|
date_year: {
|
||||||
|
edit: true,
|
||||||
|
verify: true,
|
||||||
|
},
|
||||||
|
date_lower: {
|
||||||
|
edit: true,
|
||||||
|
},
|
||||||
|
date_upper: {
|
||||||
|
edit: true,
|
||||||
|
},
|
||||||
|
date_source: {
|
||||||
|
edit: true,
|
||||||
|
},
|
||||||
|
date_source_detail: {
|
||||||
|
edit: true,
|
||||||
|
},
|
||||||
|
date_link: {
|
||||||
|
edit: true,
|
||||||
|
},
|
||||||
|
facade_year: {
|
||||||
|
edit: true,
|
||||||
|
},
|
||||||
|
facade_upper: {
|
||||||
|
edit: false,
|
||||||
|
},
|
||||||
|
facade_lower: {
|
||||||
|
edit: false,
|
||||||
|
},
|
||||||
|
facade_source: {
|
||||||
|
edit: false,
|
||||||
|
},
|
||||||
|
facade_source_detail: {
|
||||||
|
edit: false,
|
||||||
|
},
|
||||||
|
size_storeys_attic: {
|
||||||
|
edit: true,
|
||||||
|
verify: true,
|
||||||
|
},
|
||||||
|
size_storeys_core: {
|
||||||
|
edit: true,
|
||||||
|
verify: true,
|
||||||
|
},
|
||||||
|
size_storeys_basement: {
|
||||||
|
edit: true,
|
||||||
|
verify: true,
|
||||||
|
},
|
||||||
|
size_height_apex: {
|
||||||
|
edit: true,
|
||||||
|
verify: true,
|
||||||
|
},
|
||||||
|
size_floor_area_ground: {
|
||||||
|
edit: true,
|
||||||
|
verify: true,
|
||||||
|
},
|
||||||
|
size_floor_area_total: {
|
||||||
|
edit: true,
|
||||||
|
verify: true,
|
||||||
|
},
|
||||||
|
size_width_frontage: {
|
||||||
|
edit: true,
|
||||||
|
verify: true,
|
||||||
|
},
|
||||||
|
construction_core_material: {
|
||||||
|
edit: true,
|
||||||
|
},
|
||||||
|
construction_secondary_materials: {
|
||||||
|
edit: false,
|
||||||
|
},
|
||||||
|
construction_roof_covering: {
|
||||||
|
edit: true,
|
||||||
|
},
|
||||||
|
planning_portal_link: {
|
||||||
|
edit: true,
|
||||||
|
verify: true,
|
||||||
|
},
|
||||||
|
planning_in_conservation_area: {
|
||||||
|
edit: true,
|
||||||
|
verify: true,
|
||||||
|
},
|
||||||
|
planning_conservation_area_name: {
|
||||||
|
edit: true,
|
||||||
|
verify: true,
|
||||||
|
},
|
||||||
|
planning_in_list: {
|
||||||
|
edit: false,
|
||||||
|
},
|
||||||
|
planning_list_id: {
|
||||||
|
edit: false,
|
||||||
|
},
|
||||||
|
planning_list_cat: {
|
||||||
|
edit: false,
|
||||||
|
},
|
||||||
|
planning_list_grade: {
|
||||||
|
edit: false,
|
||||||
|
},
|
||||||
|
planning_heritage_at_risk_id: {
|
||||||
|
edit: true,
|
||||||
|
verify: true,
|
||||||
|
},
|
||||||
|
planning_world_list_id: {
|
||||||
|
edit: true,
|
||||||
|
verify: true,
|
||||||
|
},
|
||||||
|
planning_in_glher: {
|
||||||
|
edit: true,
|
||||||
|
verify: true,
|
||||||
|
},
|
||||||
|
planning_glher_url: {
|
||||||
|
edit: true,
|
||||||
|
verify: true,
|
||||||
|
},
|
||||||
|
planning_in_apa: {
|
||||||
|
edit: true,
|
||||||
|
verify: true,
|
||||||
|
},
|
||||||
|
planning_apa_name: {
|
||||||
|
edit: true,
|
||||||
|
verify: true,
|
||||||
|
},
|
||||||
|
planning_apa_tier: {
|
||||||
|
edit: true,
|
||||||
|
verify: true,
|
||||||
|
},
|
||||||
|
planning_in_local_list: {
|
||||||
|
edit: true,
|
||||||
|
verify: true,
|
||||||
|
},
|
||||||
|
planning_local_list_url: {
|
||||||
|
edit: true,
|
||||||
|
verify: true,
|
||||||
|
},
|
||||||
|
planning_in_historic_area_assessment: {
|
||||||
|
edit: true,
|
||||||
|
verify: true,
|
||||||
|
},
|
||||||
|
planning_historic_area_assessment_url: {
|
||||||
|
edit: true,
|
||||||
|
verify: true,
|
||||||
|
},
|
||||||
|
sust_breeam_rating: {
|
||||||
|
edit: true,
|
||||||
|
verify: true,
|
||||||
|
},
|
||||||
|
sust_dec: {
|
||||||
|
edit: true,
|
||||||
|
verify: true,
|
||||||
|
},
|
||||||
|
sust_aggregate_estimate_epc: {
|
||||||
|
edit: false,
|
||||||
|
},
|
||||||
|
sust_retrofit_date: {
|
||||||
|
edit: true,
|
||||||
|
verify: true,
|
||||||
|
},
|
||||||
|
sust_life_expectancy: {
|
||||||
|
edit: false,
|
||||||
|
},
|
||||||
|
building_attachment_form: {
|
||||||
|
edit: true,
|
||||||
|
verify: true,
|
||||||
|
},
|
||||||
|
date_change_building_use: {
|
||||||
|
edit: true,
|
||||||
|
},
|
||||||
|
current_landuse_class: {
|
||||||
|
edit: false,
|
||||||
|
},
|
||||||
|
current_landuse_group: {
|
||||||
|
edit: true,
|
||||||
|
verify: true,
|
||||||
|
},
|
||||||
|
current_landuse_order: {
|
||||||
|
edit: false,
|
||||||
|
verify: false,
|
||||||
|
},
|
||||||
|
|
||||||
|
});
|
@ -2,6 +2,7 @@
|
|||||||
import { errors, ITask } from 'pg-promise';
|
import { errors, ITask } from 'pg-promise';
|
||||||
|
|
||||||
import db from '../../db';
|
import db from '../../db';
|
||||||
|
import { dataFieldsConfig } from '../config/dataFields';
|
||||||
import { ArgumentError, DatabaseError } from '../errors/general';
|
import { ArgumentError, DatabaseError } from '../errors/general';
|
||||||
|
|
||||||
export async function getBuildingData(
|
export async function getBuildingData(
|
||||||
@ -52,13 +53,26 @@ export async function insertEditHistoryRevision(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const columnConfigLookup = Object.assign(
|
||||||
|
{},
|
||||||
|
...Object.entries(dataFieldsConfig).filter(([, config]) => config.edit).map(([key, {
|
||||||
|
asJson = false,
|
||||||
|
sqlCast
|
||||||
|
}]) => ({ [key]: {
|
||||||
|
name: key,
|
||||||
|
mod: asJson ? ':json' : undefined,
|
||||||
|
cast: sqlCast
|
||||||
|
} }))
|
||||||
|
);
|
||||||
|
|
||||||
export async function updateBuildingData(
|
export async function updateBuildingData(
|
||||||
buildingId: number,
|
buildingId: number,
|
||||||
forwardPatch: object,
|
forwardPatch: object,
|
||||||
revisionId: string,
|
revisionId: string,
|
||||||
t?: ITask<any>
|
t?: ITask<any>
|
||||||
): Promise<object> {
|
): Promise<object> {
|
||||||
const sets = db.$config.pgp.helpers.sets(forwardPatch);
|
const columnConfig = Object.entries(forwardPatch).map(([key]) => columnConfigLookup[key]);
|
||||||
|
const sets = db.$config.pgp.helpers.sets(forwardPatch, columnConfig);
|
||||||
|
|
||||||
console.log('Setting', buildingId, sets);
|
console.log('Setting', buildingId, sets);
|
||||||
|
|
||||||
|
@ -5,10 +5,10 @@
|
|||||||
import * as _ from 'lodash';
|
import * as _ from 'lodash';
|
||||||
|
|
||||||
import { pickFields } from '../../../helpers';
|
import { pickFields } from '../../../helpers';
|
||||||
|
import { dataFieldsConfig } from '../../config/dataFields';
|
||||||
import * as buildingDataAccess from '../../dataAccess/building';
|
import * as buildingDataAccess from '../../dataAccess/building';
|
||||||
import { processBuildingUpdate } from '../domainLogic/processBuildingUpdate';
|
import { processBuildingUpdate } from '../domainLogic/processBuildingUpdate';
|
||||||
|
|
||||||
import { BUILDING_FIELD_WHITELIST } from './dataFields';
|
|
||||||
import { getBuildingEditHistory } from './history';
|
import { getBuildingEditHistory } from './history';
|
||||||
import { updateBuildingData } from './save';
|
import { updateBuildingData } from './save';
|
||||||
import { getBuildingVerifications } from './verify';
|
import { getBuildingVerifications } from './verify';
|
||||||
@ -30,6 +30,8 @@ export async function getBuildingById(id: number) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const FIELD_EDIT_WHITELIST = new Set(Object.entries(dataFieldsConfig).filter(([, value]) => value.edit).map(([key]) => key));
|
||||||
|
|
||||||
export async function editBuilding(buildingId: number, building: any, userId: string): Promise<object> { // TODO add proper building type
|
export async function editBuilding(buildingId: number, building: any, userId: string): Promise<object> { // TODO add proper building type
|
||||||
return await updateBuildingData(buildingId, userId, async () => {
|
return await updateBuildingData(buildingId, userId, async () => {
|
||||||
const processedBuilding = await processBuildingUpdate(buildingId, building);
|
const processedBuilding = await processBuildingUpdate(buildingId, building);
|
||||||
@ -40,7 +42,7 @@ export async function editBuilding(buildingId: number, building: any, userId: st
|
|||||||
delete processedBuilding.geometry_id;
|
delete processedBuilding.geometry_id;
|
||||||
|
|
||||||
// return whitelisted fields to update
|
// return whitelisted fields to update
|
||||||
return pickFields(processedBuilding, BUILDING_FIELD_WHITELIST);
|
return pickFields(processedBuilding, FIELD_EDIT_WHITELIST);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,61 +0,0 @@
|
|||||||
export const BUILDING_FIELD_WHITELIST = new Set([
|
|
||||||
'ref_osm_id',
|
|
||||||
// 'location_name',
|
|
||||||
'location_number',
|
|
||||||
// 'location_street',
|
|
||||||
// 'location_line_two',
|
|
||||||
// 'location_town',
|
|
||||||
// 'location_postcode',
|
|
||||||
'location_latitude',
|
|
||||||
'location_longitude',
|
|
||||||
'date_year',
|
|
||||||
'date_lower',
|
|
||||||
'date_upper',
|
|
||||||
'date_source',
|
|
||||||
'date_source_detail',
|
|
||||||
'date_link',
|
|
||||||
'facade_year',
|
|
||||||
'facade_upper',
|
|
||||||
'facade_lower',
|
|
||||||
'facade_source',
|
|
||||||
'facade_source_detail',
|
|
||||||
'size_storeys_attic',
|
|
||||||
'size_storeys_core',
|
|
||||||
'size_storeys_basement',
|
|
||||||
'size_height_apex',
|
|
||||||
'size_floor_area_ground',
|
|
||||||
'size_floor_area_total',
|
|
||||||
'size_width_frontage',
|
|
||||||
'construction_core_material',
|
|
||||||
'construction_secondary_materials',
|
|
||||||
'construction_roof_covering',
|
|
||||||
'planning_portal_link',
|
|
||||||
'planning_in_conservation_area',
|
|
||||||
'planning_conservation_area_name',
|
|
||||||
'planning_in_list',
|
|
||||||
'planning_list_id',
|
|
||||||
'planning_list_cat',
|
|
||||||
'planning_list_grade',
|
|
||||||
'planning_heritage_at_risk_id',
|
|
||||||
'planning_world_list_id',
|
|
||||||
'planning_in_glher',
|
|
||||||
'planning_glher_url',
|
|
||||||
'planning_in_apa',
|
|
||||||
'planning_apa_name',
|
|
||||||
'planning_apa_tier',
|
|
||||||
'planning_in_local_list',
|
|
||||||
'planning_local_list_url',
|
|
||||||
'planning_in_historic_area_assessment',
|
|
||||||
'planning_historic_area_assessment_url',
|
|
||||||
'sust_breeam_rating',
|
|
||||||
'sust_dec',
|
|
||||||
// 'sust_aggregate_estimate_epc',
|
|
||||||
'sust_retrofit_date',
|
|
||||||
// 'sust_life_expectancy',
|
|
||||||
'building_attachment_form',
|
|
||||||
'date_change_building_use',
|
|
||||||
|
|
||||||
// 'current_landuse_class',
|
|
||||||
'current_landuse_group',
|
|
||||||
'current_landuse_order'
|
|
||||||
]);
|
|
@ -1,8 +1,9 @@
|
|||||||
|
import { dataFieldsConfig } from '../../config/dataFields';
|
||||||
import * as buildingDataAccess from '../../dataAccess/building';
|
import * as buildingDataAccess from '../../dataAccess/building';
|
||||||
import * as verifyDataAccess from '../../dataAccess/verify';
|
import * as verifyDataAccess from '../../dataAccess/verify';
|
||||||
import { DatabaseError } from '../../errors/general';
|
import { DatabaseError } from '../../errors/general';
|
||||||
|
|
||||||
import { BUILDING_FIELD_WHITELIST } from './dataFields';
|
const FIELD_VERIFICATION_WHITELIST = new Set(Object.entries(dataFieldsConfig).filter(([, value]) => value.verify === true).map(([key]) => key));
|
||||||
|
|
||||||
export async function verifyBuildingAttributes(buildingId: number, userId: string, patch: object) {
|
export async function verifyBuildingAttributes(buildingId: number, userId: string, patch: object) {
|
||||||
// get current building attribute values for comparison
|
// get current building attribute values for comparison
|
||||||
@ -13,7 +14,7 @@ export async function verifyBuildingAttributes(buildingId: number, userId: strin
|
|||||||
// loop through attribute => value pairs to mark as verified
|
// loop through attribute => value pairs to mark as verified
|
||||||
for (let [key, value] of Object.entries(patch)) {
|
for (let [key, value] of Object.entries(patch)) {
|
||||||
// check key in whitelist
|
// check key in whitelist
|
||||||
if(BUILDING_FIELD_WHITELIST.has(key)) {
|
if(FIELD_VERIFICATION_WHITELIST.has(key)) {
|
||||||
// check value against current from database - JSON.stringify as hack for "any" data type
|
// check value against current from database - JSON.stringify as hack for "any" data type
|
||||||
if (JSON.stringify(value) == JSON.stringify(building[key])) {
|
if (JSON.stringify(value) == JSON.stringify(building[key])) {
|
||||||
try {
|
try {
|
||||||
@ -52,7 +53,7 @@ export async function getBuildingVerifications(building) {
|
|||||||
const verifications = await verifyDataAccess.getBuildingVerifiedAttributes(building.building_id);
|
const verifications = await verifyDataAccess.getBuildingVerifiedAttributes(building.building_id);
|
||||||
|
|
||||||
const verified = {};
|
const verified = {};
|
||||||
for (const element of BUILDING_FIELD_WHITELIST) {
|
for (const element of FIELD_VERIFICATION_WHITELIST) {
|
||||||
verified[element] = 0;
|
verified[element] = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -74,3 +74,26 @@ export function pickFields(obj: any, fieldWhitelist: Set<string>) {
|
|||||||
}
|
}
|
||||||
return subObject;
|
return subObject;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generic type for a function validating that the argument is a object with
|
||||||
|
* Used to enforce value types in a config object, but not obscuring the key names
|
||||||
|
* by using a TS lookup type
|
||||||
|
*/
|
||||||
|
type ValueTypeCheck<C> = <K extends string>(x: Record<K, C>) => Record<K, C>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a function that enforces all fields of its argument to be of type C
|
||||||
|
* Useful to create configs where each field must be of a set type,
|
||||||
|
* but the list of keys should be accessible to users of the config variable.
|
||||||
|
*/
|
||||||
|
export function valueType<C>(): ValueTypeCheck<C>{
|
||||||
|
return x => x;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Map all properties of object through a function
|
||||||
|
*/
|
||||||
|
export function mapObject<T, R>(x: T, fn: ([key, value]: [keyof T, T[keyof T]]) => R): Record<keyof T, R> {
|
||||||
|
return Object.assign({}, ...Object.entries(x).map(([key, value]) => ({ [key]: fn([key as keyof T, value]) })) );
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user