Allow non-editable fields to be auto-derived

Some fields shouldn't be editable through the API
but can still be modified, because they are derived
from another field which is editable.
This change fixes a bug where a derived field
wouldn't be updated, because it was not on the
editable fields whitelist.
This commit is contained in:
Maciej Ziarkowski 2021-08-12 01:29:37 +01:00
parent 937ddcc4d2
commit 9722c173d8
5 changed files with 35 additions and 5 deletions

View File

@ -3,10 +3,19 @@ import { valueType } from '../../helpers';
/** Configuration for a single data field */ /** Configuration for a single data field */
export interface DataFieldConfig { export interface DataFieldConfig {
/** /**
* Allow editing the field? * Allow editing the field through the API?
*/ */
edit: boolean; edit: boolean;
/**
* Should the editing of the field be allowed - but only when
* the change is a result of an edit of another field, from which this field is derived.
* Example: editing Land Use Group modifies Land Use Order, too, because LU Order is automatically derived from LU Group.
* But Land Use Order itself cannot be modified directly by users.
* Default: false
*/
derivedEdit?: boolean;
/** /**
* Allow verifying the field value? * Allow verifying the field value?
* Default: false; * Default: false;
@ -236,6 +245,7 @@ export const dataFieldsConfig = valueType<DataFieldConfig>()({ /* eslint-disable
}, },
current_landuse_order: { current_landuse_order: {
edit: false, edit: false,
derivedEdit: true,
verify: false, verify: false,
}, },

View File

@ -55,7 +55,7 @@ export async function insertEditHistoryRevision(
const columnConfigLookup = Object.assign( const columnConfigLookup = Object.assign(
{}, {},
...Object.entries(dataFieldsConfig).filter(([, config]) => config.edit).map(([key, { ...Object.entries(dataFieldsConfig).filter(([, config]) => config.edit || config.derivedEdit).map(([key, {
asJson = false, asJson = false,
sqlCast sqlCast
}]) => ({ [key]: { }]) => ({ [key]: {

View File

@ -21,6 +21,15 @@ export class InvalidOperationError extends UserError {
} }
} }
export class InvalidFieldError extends UserError {
public fieldName: string;
constructor(message?: string, fieldName?: string) {
super(message);
this.name = 'InvalidFieldError';
this.fieldName = fieldName;
}
}
export class FieldTypeError extends UserError { export class FieldTypeError extends UserError {
public fieldName: string; public fieldName: string;
constructor(message?: string, fieldName?: string) { constructor(message?: string, fieldName?: string) {

View File

@ -31,7 +31,11 @@ export async function getBuildingById(id: number) {
} }
} }
const FIELD_EDIT_WHITELIST = new Set(Object.entries(dataFieldsConfig).filter(([, value]) => value.edit).map(([key]) => key)); /**
* List of fields for which modification is allowed
* (directly by the user, or for fields that are derived from others)
*/
const FINAL_FIELD_EDIT_ALLOWLIST = new Set(Object.entries(dataFieldsConfig).filter(([, value]) => value.edit || value.derivedEdit).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 () => {
@ -44,7 +48,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, FIELD_EDIT_WHITELIST); return pickFields(processedBuilding, FINAL_FIELD_EDIT_ALLOWLIST);
}); });
} }

View File

@ -2,7 +2,8 @@ import Ajv from 'ajv';
import addFormats from 'ajv-formats'; import addFormats from 'ajv-formats';
import { mapObject } from '../../../helpers'; import { mapObject } from '../../../helpers';
import { FieldTypeError } from '../../errors/general'; import { InvalidFieldError, FieldTypeError } from '../../errors/general';
import { dataFieldsConfig } from '../../config/dataFields';
import { fieldSchemaConfig } from '../../config/fieldSchemaConfig'; import { fieldSchemaConfig } from '../../config/fieldSchemaConfig';
const ajv = new Ajv(); const ajv = new Ajv();
@ -10,8 +11,14 @@ addFormats(ajv);
const compiledSchemas = mapObject(fieldSchemaConfig, ([, val]) => ajv.compile(val)) const compiledSchemas = mapObject(fieldSchemaConfig, ([, val]) => ajv.compile(val))
const EXTERNAL_FIELD_EDIT_ALLOWLIST = new Set(Object.entries(dataFieldsConfig).filter(([, value]) => value.edit).map(([key]) => key));
export function validateBuildingUpdate(buildingId: number, building: any) { export function validateBuildingUpdate(buildingId: number, building: any) {
for(const field of Object.keys(building)) { for(const field of Object.keys(building)) {
if(!EXTERNAL_FIELD_EDIT_ALLOWLIST.has(field)) {
throw new InvalidFieldError('Field is not editable', field);
}
if(field in compiledSchemas) { if(field in compiledSchemas) {
if(!compiledSchemas[field](building[field])) { if(!compiledSchemas[field](building[field])) {
throw new FieldTypeError('Invalid format of data sent', field); throw new FieldTypeError('Invalid format of data sent', field);