Enable jsonb fields with json schema
This adds the ability to verify JSON/JSONB fields using json-schema, and adds a simple edit history JSON formatting.
This commit is contained in:
parent
58bc11be04
commit
3726a0b81c
110
app/package-lock.json
generated
110
app/package-lock.json
generated
@ -2631,15 +2631,21 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ajv": {
|
"ajv": {
|
||||||
"version": "6.10.0",
|
"version": "7.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.0.tgz",
|
"resolved": "https://registry.npmjs.org/ajv/-/ajv-7.1.1.tgz",
|
||||||
"integrity": "sha512-nffhOpkymDECQyR0mnsUtoCE8RlX38G0rYP+wgLWFyZuUyuuojSSvi/+euOiQBIn63whYwYVIIH1TvE3tu4OEg==",
|
"integrity": "sha512-ga/aqDYnUy/o7vbsRTFhhTsNeXiYb5JWDIcRIeZfwRNCefwjNTVYCGdGSUrEmiu3yDK3vFvNbgJxvrQW4JXrYQ==",
|
||||||
"dev": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"fast-deep-equal": "^2.0.1",
|
"fast-deep-equal": "^3.1.1",
|
||||||
"fast-json-stable-stringify": "^2.0.0",
|
"json-schema-traverse": "^1.0.0",
|
||||||
"json-schema-traverse": "^0.4.1",
|
"require-from-string": "^2.0.2",
|
||||||
"uri-js": "^4.2.2"
|
"uri-js": "^4.2.2"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"json-schema-traverse": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ajv-errors": {
|
"ajv-errors": {
|
||||||
@ -2648,6 +2654,14 @@
|
|||||||
"integrity": "sha512-DCRfO/4nQ+89p/RK43i8Ezd41EqdGIU4ld7nGF8OQ14oc/we5rEntLCUa7+jrn3nn83BosfwZA0wb4pon2o8iQ==",
|
"integrity": "sha512-DCRfO/4nQ+89p/RK43i8Ezd41EqdGIU4ld7nGF8OQ14oc/we5rEntLCUa7+jrn3nn83BosfwZA0wb4pon2o8iQ==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"ajv-formats": {
|
||||||
|
"version": "1.5.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-1.5.1.tgz",
|
||||||
|
"integrity": "sha512-s1RBVF4HZd2UjGkb6t6uWoXjf6o7j7dXPQIL7vprcIT/67bTD6+5ocsU0UKShS2qWxueGDWuGfKHfOxHWrlTQg==",
|
||||||
|
"requires": {
|
||||||
|
"ajv": "^7.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"ajv-keywords": {
|
"ajv-keywords": {
|
||||||
"version": "3.5.2",
|
"version": "3.5.2",
|
||||||
"resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz",
|
"resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz",
|
||||||
@ -5971,6 +5985,18 @@
|
|||||||
"text-table": "^0.2.0"
|
"text-table": "^0.2.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"ajv": {
|
||||||
|
"version": "6.12.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
|
||||||
|
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"fast-deep-equal": "^3.1.1",
|
||||||
|
"fast-json-stable-stringify": "^2.0.0",
|
||||||
|
"json-schema-traverse": "^0.4.1",
|
||||||
|
"uri-js": "^4.2.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"ansi-regex": {
|
"ansi-regex": {
|
||||||
"version": "4.1.0",
|
"version": "4.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz",
|
||||||
@ -6577,10 +6603,9 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"fast-deep-equal": {
|
"fast-deep-equal": {
|
||||||
"version": "2.0.1",
|
"version": "3.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
||||||
"integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=",
|
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"fast-glob": {
|
"fast-glob": {
|
||||||
"version": "2.2.7",
|
"version": "2.2.7",
|
||||||
@ -9393,6 +9418,20 @@
|
|||||||
"ajv": "^6.1.0",
|
"ajv": "^6.1.0",
|
||||||
"ajv-errors": "^1.0.0",
|
"ajv-errors": "^1.0.0",
|
||||||
"ajv-keywords": "^3.1.0"
|
"ajv-keywords": "^3.1.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"ajv": {
|
||||||
|
"version": "6.12.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
|
||||||
|
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"fast-deep-equal": "^3.1.1",
|
||||||
|
"fast-json-stable-stringify": "^2.0.0",
|
||||||
|
"json-schema-traverse": "^0.4.1",
|
||||||
|
"uri-js": "^4.2.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -11116,6 +11155,20 @@
|
|||||||
"ajv": "^6.1.0",
|
"ajv": "^6.1.0",
|
||||||
"ajv-errors": "^1.0.0",
|
"ajv-errors": "^1.0.0",
|
||||||
"ajv-keywords": "^3.1.0"
|
"ajv-keywords": "^3.1.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"ajv": {
|
||||||
|
"version": "6.12.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
|
||||||
|
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"fast-deep-equal": "^3.1.1",
|
||||||
|
"fast-json-stable-stringify": "^2.0.0",
|
||||||
|
"json-schema-traverse": "^0.4.1",
|
||||||
|
"uri-js": "^4.2.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -11976,8 +12029,7 @@
|
|||||||
"punycode": {
|
"punycode": {
|
||||||
"version": "2.1.1",
|
"version": "2.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
|
||||||
"integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==",
|
"integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A=="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"q": {
|
"q": {
|
||||||
"version": "1.5.1",
|
"version": "1.5.1",
|
||||||
@ -12905,6 +12957,11 @@
|
|||||||
"integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=",
|
"integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"require-from-string": {
|
||||||
|
"version": "2.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
|
||||||
|
"integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="
|
||||||
|
},
|
||||||
"require-main-filename": {
|
"require-main-filename": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz",
|
||||||
@ -14194,6 +14251,18 @@
|
|||||||
"string-width": "^3.0.0"
|
"string-width": "^3.0.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"ajv": {
|
||||||
|
"version": "6.12.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
|
||||||
|
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"fast-deep-equal": "^3.1.1",
|
||||||
|
"fast-json-stable-stringify": "^2.0.0",
|
||||||
|
"json-schema-traverse": "^0.4.1",
|
||||||
|
"uri-js": "^4.2.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"ansi-regex": {
|
"ansi-regex": {
|
||||||
"version": "4.1.0",
|
"version": "4.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz",
|
||||||
@ -14890,7 +14959,6 @@
|
|||||||
"version": "4.2.2",
|
"version": "4.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz",
|
||||||
"integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==",
|
"integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==",
|
||||||
"dev": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"punycode": "^2.1.0"
|
"punycode": "^2.1.0"
|
||||||
}
|
}
|
||||||
@ -15551,6 +15619,20 @@
|
|||||||
"ajv": "^6.1.0",
|
"ajv": "^6.1.0",
|
||||||
"ajv-errors": "^1.0.0",
|
"ajv-errors": "^1.0.0",
|
||||||
"ajv-keywords": "^3.1.0"
|
"ajv-keywords": "^3.1.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"ajv": {
|
||||||
|
"version": "6.12.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
|
||||||
|
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"fast-deep-equal": "^3.1.1",
|
||||||
|
"fast-json-stable-stringify": "^2.0.0",
|
||||||
|
"json-schema-traverse": "^0.4.1",
|
||||||
|
"uri-js": "^4.2.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"semver": {
|
"semver": {
|
||||||
|
@ -17,6 +17,8 @@
|
|||||||
"@fortawesome/free-solid-svg-icons": "^5.14.0",
|
"@fortawesome/free-solid-svg-icons": "^5.14.0",
|
||||||
"@fortawesome/react-fontawesome": "^0.1.11",
|
"@fortawesome/react-fontawesome": "^0.1.11",
|
||||||
"@mapbox/sphericalmercator": "^1.1.0",
|
"@mapbox/sphericalmercator": "^1.1.0",
|
||||||
|
"ajv": "^7.1.1",
|
||||||
|
"ajv-formats": "^1.5.1",
|
||||||
"babel-runtime": "^6.26.0",
|
"babel-runtime": "^6.26.0",
|
||||||
"body-parser": "^1.19.0",
|
"body-parser": "^1.19.0",
|
||||||
"bootstrap": "^4.5.0",
|
"bootstrap": "^4.5.0",
|
||||||
|
7
app/src/api/config/fieldSchemaConfig.ts
Normal file
7
app/src/api/config/fieldSchemaConfig.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import { JSONSchemaType } from 'ajv';
|
||||||
|
import { SomeJSONSchema } from 'ajv/dist/types/json-schema';
|
||||||
|
import { dataFieldsConfig } from './dataFields';
|
||||||
|
|
||||||
|
export const fieldSchemaConfig: { [key in keyof typeof dataFieldsConfig]?: SomeJSONSchema} = { /*eslint-disable @typescript-eslint/camelcase */
|
||||||
|
|
||||||
|
} as const;
|
@ -21,6 +21,15 @@ export class InvalidOperationError extends UserError {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class FieldTypeError extends UserError {
|
||||||
|
public fieldName: string;
|
||||||
|
constructor(message?: string, fieldName?: string) {
|
||||||
|
super(message);
|
||||||
|
this.name = 'FieldTypeError';
|
||||||
|
this.fieldName = fieldName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export class DatabaseError extends Error {
|
export class DatabaseError extends Error {
|
||||||
public detail: any;
|
public detail: any;
|
||||||
constructor(detail?: string) {
|
constructor(detail?: string) {
|
||||||
|
@ -8,6 +8,7 @@ import { pickFields } from '../../../helpers';
|
|||||||
import { dataFieldsConfig } from '../../config/dataFields';
|
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 { validateBuildingUpdate } from '../domainLogic/validateBuildingUpdate';
|
||||||
|
|
||||||
import { getBuildingEditHistory } from './history';
|
import { getBuildingEditHistory } from './history';
|
||||||
import { updateBuildingData } from './save';
|
import { updateBuildingData } from './save';
|
||||||
@ -34,6 +35,7 @@ const FIELD_EDIT_WHITELIST = new Set(Object.entries(dataFieldsConfig).filter(([,
|
|||||||
|
|
||||||
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 () => {
|
||||||
|
validateBuildingUpdate(buildingId, building);
|
||||||
const processedBuilding = await processBuildingUpdate(buildingId, building);
|
const processedBuilding = await processBuildingUpdate(buildingId, building);
|
||||||
|
|
||||||
// remove read-only fields from consideration
|
// remove read-only fields from consideration
|
||||||
|
21
app/src/api/services/domainLogic/validateBuildingUpdate.ts
Normal file
21
app/src/api/services/domainLogic/validateBuildingUpdate.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import Ajv from 'ajv';
|
||||||
|
import addFormats from 'ajv-formats';
|
||||||
|
|
||||||
|
import { mapObject } from '../../../helpers';
|
||||||
|
import { FieldTypeError } from '../../errors/general';
|
||||||
|
import { fieldSchemaConfig } from '../../config/fieldSchemaConfig';
|
||||||
|
|
||||||
|
const ajv = new Ajv();
|
||||||
|
addFormats(ajv);
|
||||||
|
|
||||||
|
const compiledSchemas = mapObject(fieldSchemaConfig, ([, val]) => ajv.compile(val))
|
||||||
|
|
||||||
|
export function validateBuildingUpdate(buildingId: number, building: any) {
|
||||||
|
for(const field of Object.keys(building)) {
|
||||||
|
if(field in compiledSchemas) {
|
||||||
|
if(!compiledSchemas[field](building[field])) {
|
||||||
|
throw new FieldTypeError('Invalid format of data sent', field);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -16,12 +16,33 @@ function formatValue(value: any) {
|
|||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isComplex(x: any): boolean {
|
||||||
|
return x != undefined && (Array.isArray(x) || typeof x === 'object');
|
||||||
|
}
|
||||||
|
|
||||||
|
const ObjectDiffSummary: React.FC<{ oldValue: any; newValue: any}> = ({oldValue, newValue}) => (
|
||||||
|
<>
|
||||||
|
<pre title="Value before edit" className='edit-history-diff old'>{JSON.stringify(oldValue, null, 4)}</pre>
|
||||||
|
<pre title="Value after edit" className='edit-history-diff new'>{JSON.stringify(newValue, null, 4)}</pre>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
const SimpleSummary: React.FC<{ oldValue: any; newValue: any}> = (props) => (
|
||||||
|
<>
|
||||||
|
<code title="Value before edit" className='edit-history-diff old'>{formatValue(props.oldValue)}</code>
|
||||||
|
|
||||||
|
<code title="Value after edit" className='edit-history-diff new'>{formatValue(props.newValue)}</code>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
const FieldEditSummary: React.FunctionComponent<FieldEditSummaryProps> = props => (
|
const FieldEditSummary: React.FunctionComponent<FieldEditSummaryProps> = props => (
|
||||||
<>
|
<>
|
||||||
{props.title}:
|
{props.title}:
|
||||||
<code title="Value before edit" className='edit-history-diff old'>{formatValue(props.oldValue)}</code>
|
{
|
||||||
|
(isComplex(props.oldValue) || isComplex(props.value)) ?
|
||||||
<code title="Value after edit" className='edit-history-diff new'>{formatValue(props.value)}</code>
|
<ObjectDiffSummary oldValue={props.oldValue} newValue={props.value} /> :
|
||||||
|
<SimpleSummary oldValue={props.oldValue} newValue={props.value} />
|
||||||
|
}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user