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 db from '../../db';
|
||||
import { dataFieldsConfig } from '../config/dataFields';
|
||||
import { ArgumentError, DatabaseError } from '../errors/general';
|
||||
|
||||
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(
|
||||
buildingId: number,
|
||||
forwardPatch: object,
|
||||
revisionId: string,
|
||||
t?: ITask<any>
|
||||
): 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);
|
||||
|
||||
|
@ -5,10 +5,10 @@
|
||||
import * as _ from 'lodash';
|
||||
|
||||
import { pickFields } from '../../../helpers';
|
||||
import { dataFieldsConfig } from '../../config/dataFields';
|
||||
import * as buildingDataAccess from '../../dataAccess/building';
|
||||
import { processBuildingUpdate } from '../domainLogic/processBuildingUpdate';
|
||||
|
||||
import { BUILDING_FIELD_WHITELIST } from './dataFields';
|
||||
import { getBuildingEditHistory } from './history';
|
||||
import { updateBuildingData } from './save';
|
||||
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
|
||||
return await updateBuildingData(buildingId, userId, async () => {
|
||||
const processedBuilding = await processBuildingUpdate(buildingId, building);
|
||||
@ -40,7 +42,7 @@ export async function editBuilding(buildingId: number, building: any, userId: st
|
||||
delete processedBuilding.geometry_id;
|
||||
|
||||
// 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 verifyDataAccess from '../../dataAccess/verify';
|
||||
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) {
|
||||
// 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
|
||||
for (let [key, value] of Object.entries(patch)) {
|
||||
// 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
|
||||
if (JSON.stringify(value) == JSON.stringify(building[key])) {
|
||||
try {
|
||||
@ -52,7 +53,7 @@ export async function getBuildingVerifications(building) {
|
||||
const verifications = await verifyDataAccess.getBuildingVerifiedAttributes(building.building_id);
|
||||
|
||||
const verified = {};
|
||||
for (const element of BUILDING_FIELD_WHITELIST) {
|
||||
for (const element of FIELD_VERIFICATION_WHITELIST) {
|
||||
verified[element] = 0;
|
||||
}
|
||||
|
||||
|
@ -74,3 +74,26 @@ export function pickFields(obj: any, fieldWhitelist: Set<string>) {
|
||||
}
|
||||
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