From cc6cbbfb070e3cbea913a1326746a9b34ac070dc Mon Sep 17 00:00:00 2001 From: J M <2364004+Blu-J@users.noreply.github.com> Date: Tue, 10 May 2022 11:00:56 -0600 Subject: [PATCH] chore: Convert from ajv to ts-matches (#1415) --- frontend/package-lock.json | 38 ++- frontend/package.json | 2 +- .../ui/src/app/util/properties.util.ts | 305 ++++++++---------- 3 files changed, 174 insertions(+), 171 deletions(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index ebc1f29b8..6b59f7197 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -22,7 +22,6 @@ "@start9labs/argon2": "^0.1.0", "@start9labs/emver": "^0.1.5", "aes-js": "^3.1.2", - "ajv": "^6.12.6", "ansi-to-html": "^0.7.2", "core-js": "^3.21.1", "dompurify": "^2.3.6", @@ -36,6 +35,7 @@ "patch-db-client": "file: ../../../patch-db/client", "pbkdf2": "^3.1.2", "rxjs": "^6.6.7", + "ts-matches": "^5.1.0", "tslib": "^2.3.0", "uuid": "^8.3.2", "zone.js": "^0.11.5" @@ -3881,6 +3881,7 @@ "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -6746,7 +6747,8 @@ "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true }, "node_modules/fast-glob": { "version": "3.2.11", @@ -6772,7 +6774,8 @@ "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true }, "node_modules/fast-levenshtein": { "version": "2.0.6", @@ -8469,7 +8472,8 @@ "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true }, "node_modules/json5": { "version": "2.2.1", @@ -11548,6 +11552,7 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "dev": true, "engines": { "node": ">=6" } @@ -13234,6 +13239,11 @@ "tree-kill": "cli.js" } }, + "node_modules/ts-matches": { + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/ts-matches/-/ts-matches-5.1.5.tgz", + "integrity": "sha512-PdashZCpn30SFH9mboHq3/rmC3wntKajw5IkQcSX9HhbcV7FoP3/nJzjFII6ZhRyoAV0mrDXWoblJulpMlh65g==" + }, "node_modules/ts-node": { "version": "10.7.0", "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.7.0.tgz", @@ -13549,6 +13559,7 @@ "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, "dependencies": { "punycode": "^2.1.0" } @@ -17135,6 +17146,7 @@ "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", @@ -19183,7 +19195,8 @@ "fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true }, "fast-glob": { "version": "3.2.11", @@ -19206,7 +19219,8 @@ "fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true }, "fast-levenshtein": { "version": "2.0.6", @@ -20460,7 +20474,8 @@ "json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true }, "json5": { "version": "2.2.1", @@ -22772,7 +22787,8 @@ "punycode": { "version": "2.1.1", "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 }, "qrcode": { "version": "1.5.0", @@ -24040,6 +24056,11 @@ "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", "dev": true }, + "ts-matches": { + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/ts-matches/-/ts-matches-5.1.5.tgz", + "integrity": "sha512-PdashZCpn30SFH9mboHq3/rmC3wntKajw5IkQcSX9HhbcV7FoP3/nJzjFII6ZhRyoAV0mrDXWoblJulpMlh65g==" + }, "ts-node": { "version": "10.7.0", "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.7.0.tgz", @@ -24272,6 +24293,7 @@ "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, "requires": { "punycode": "^2.1.0" } diff --git a/frontend/package.json b/frontend/package.json index 995df8163..f49d127b4 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -35,7 +35,6 @@ "@start9labs/argon2": "^0.1.0", "@start9labs/emver": "^0.1.5", "aes-js": "^3.1.2", - "ajv": "^6.12.6", "ansi-to-html": "^0.7.2", "core-js": "^3.21.1", "dompurify": "^2.3.6", @@ -50,6 +49,7 @@ "pbkdf2": "^3.1.2", "rxjs": "^6.6.7", "tslib": "^2.3.0", + "ts-matches": "^5.1.0", "uuid": "^8.3.2", "zone.js": "^0.11.5" }, diff --git a/frontend/projects/ui/src/app/util/properties.util.ts b/frontend/projects/ui/src/app/util/properties.util.ts index bf1890671..5fcad320e 100644 --- a/frontend/projects/ui/src/app/util/properties.util.ts +++ b/frontend/projects/ui/src/app/util/properties.util.ts @@ -1,82 +1,92 @@ -import * as Ajv from 'ajv' import { applyOperation } from 'fast-json-patch' - +import { + Parser, + shape, + string, + literal, + boolean, + object, + deferred, + dictionary, + anyOf, +} from 'ts-matches' export type ValidVersion = 1 | 2 -function has(obj: Obj, key: K): obj is (Obj & { [P in K]: unknown }) { +function has( + obj: Obj, + key: K, +): obj is Obj & { [P in K]: unknown } { return key in obj } - -const ajv = new Ajv({ jsonPointers: true, allErrors: true, nullable: true }) -const ajvWithDefaults = new Ajv({ jsonPointers: true, allErrors: true, useDefaults: true, nullable: true, removeAdditional:true }) - -const schemaV1 = { - 'type': 'object', - 'properties': { - 'name': { 'type': 'string' }, - 'value': { 'type': 'string' }, - 'description': { 'type': 'string', 'nullable': true, 'default': null }, - 'copyable': { 'type': 'boolean', 'default': false }, - 'qr': { 'type': 'boolean', 'default': false }, +const matchPropertiesV1 = shape( + { + name: string, + value: string, + description: string, + copyable: boolean, + qr: boolean, }, - 'required': ['name', 'value', 'copyable', 'qr'], - 'additionalProperties': false, -} -const schemaV1Compiled = ajv.compile(schemaV1) -function isSchemaV1(properties: unknown): properties is PropertiesV1 { - return schemaV1Compiled(properties) as any -} -const _schemaV1CompiledWithDefaults = ajvWithDefaults.compile(schemaV1) -function schemaV1CompiledWithDefaults(properties: unknown): properties is PropertiesV1 { - return _schemaV1CompiledWithDefaults(properties) as any -} -const schemaV2 = { - 'anyOf': [ - { - 'type': 'object', - 'properties': { - 'type': { 'type': 'string', 'const': 'string' }, - 'value': { 'type': 'string' }, - 'description': { 'type': 'string', 'nullable': true, 'default': null }, - 'copyable': { 'type': 'boolean', 'default': false }, - 'qr': { 'type': 'boolean', 'default': false }, - 'masked': { 'type': 'boolean', 'default': false }, - }, - 'required': ['type', 'value', 'description', 'copyable', 'qr', 'masked'], - 'additionalProperties': false, - }, - { - 'type': 'object', - 'properties': { - 'type': { 'type': 'string', 'const': 'object' }, - 'value': { - 'type': 'object', - 'patternProperties': { - '^.*$': { - '$ref': '#', - }, - }, - }, - 'description': { 'type': 'string', 'nullable': true, 'default': null }, + ['description', 'copyable', 'qr'], + { description: null as null, copyable: false, qr: false } as const, +) +type PropertiesV1 = typeof matchPropertiesV1._TYPE - }, - 'required': ['type', 'value', 'description'], - 'additionalProperties': false, - }, - ], +type PackagePropertiesV2 = { + [name: string]: PackagePropertyString | PackagePropertyObject } -const schemaV2Compiled = ajv.compile(schemaV2) -const schemaV2CompiledWithDefaults = ajvWithDefaults.compile(schemaV2) -export function parsePropertiesPermissive (properties: unknown, errorCallback: (err: Error) => any = console.warn): PackageProperties { +const [matchPackagePropertiesV2, setPPV2] = deferred() +const matchPackagePropertyString = shape( + { + type: literal('string'), + description: string, + value: string, + copyable: boolean, + qr: boolean, + masked: boolean, + }, + ['description', 'copyable', 'qr', 'masked'], + { + description: null as null, + copyable: false, + qr: false, + masked: false, + } as const, +) +type PackagePropertyString = typeof matchPackagePropertyString._TYPE +const matchPackagePropertyObject = shape( + { + type: literal('object'), + value: matchPackagePropertiesV2, + description: string.optional(), + }, + ['description'], + { description: null as null }, +) +const matchPropertyV2 = anyOf( + matchPackagePropertyString, + matchPackagePropertyObject, +) +type PackagePropertyObject = typeof matchPackagePropertyObject._TYPE +setPPV2(dictionary([string, matchPropertyV2])) + +export function parsePropertiesPermissive( + properties: unknown, + errorCallback: (err: Error) => any = console.warn, +): PackageProperties { if (typeof properties !== 'object' || properties === null) { errorCallback(new TypeError(`${properties} is not an object`)) return {} } // @TODO still need this conditional? - if (!has(properties, 'version') || !has(properties, 'data') || typeof properties.version !== 'number' || !properties.data) { + if ( + !has(properties, 'version') || + !has(properties, 'data') || + typeof properties.version !== 'number' || + !properties.data + ) { return Object.entries(properties) .filter(([_, value]) => { if (typeof value === 'string') { @@ -99,8 +109,7 @@ export function parsePropertiesPermissive (properties: unknown, errorCallback: ( .reduce((acc, { name, value }) => { acc[name] = value return acc - }, { }) - + }, {}) } switch (properties.version) { case 1: @@ -108,124 +117,96 @@ export function parsePropertiesPermissive (properties: unknown, errorCallback: ( case 2: return parsePropertiesV2Permissive(properties.data, errorCallback) default: - errorCallback(new Error(`unknown properties version ${properties.version}, attempting to parse as v2`)) + errorCallback( + new Error( + `unknown properties version ${properties.version}, attempting to parse as v2`, + ), + ) return parsePropertiesV2Permissive(properties.data, errorCallback) } } - - -function parsePropertiesV1Permissive (properties: unknown, errorCallback: (err: Error) => any): PackageProperties { +function parsePropertiesV1Permissive( + properties: unknown, + errorCallback: (err: Error) => any, +): PackageProperties { if (!Array.isArray(properties)) { errorCallback(new TypeError(`${properties} is not an array`)) return {} } - const parsedProperties : PackagePropertiesV2 = {}; - for(const idx in properties) { - const cur:unknown = properties[idx] - if(isSchemaV1(cur)) { - parsedProperties[cur.name] = { - type: 'string', - value: cur.value, - description: cur.description, - copyable: cur.copyable, - qr: cur.qr, - masked: false, - } - } - else if (schemaV1Compiled.errors) { - for (let err of schemaV1Compiled.errors) { - errorCallback(new Error(`/data/${idx}${err.dataPath}: ${err.message}`)) - if (err.dataPath) { - applyOperation(cur, { op: 'replace', path: err.dataPath, value: undefined }) + return properties.reduce( + (prev: PackagePropertiesV2, cur: unknown, idx: number) => { + const result = matchPropertiesV1.enumParsed(cur) + if ('value' in result) { + const value = result.value + prev[value.name] = { + type: 'string', + value: value.value, + description: value.description, + copyable: value.copyable, + qr: value.qr, + masked: false, + } + } else { + const error = result.error + const message = Parser.validatorErrorAsString(error) + let dataPath = error.keys.map(x => JSON.parse(x)).join('/') + errorCallback(new Error(`/data/${idx}: ${message}`)) + if (dataPath) { + applyOperation(cur, { + op: 'replace', + path: dataPath, + value: undefined, + }) } } - if (!schemaV1CompiledWithDefaults(cur)) { - for (let err of _schemaV1CompiledWithDefaults.errors) { - errorCallback(new Error(`/data/${idx}${err.dataPath}: ${err.message}`)) - } - continue - } - parsedProperties[cur.name] = { - - type: 'string', - value: cur.value, - description: cur.description, - copyable: cur.copyable, - qr: cur.qr, - masked: false, - } - } - - } - return parsedProperties + return prev + }, + {}, + ) } -function parsePropertiesV2Permissive (properties: unknown, errorCallback: (err: Error) => any): PackageProperties { - if (typeof properties !== 'object' || properties === null) { - errorCallback(new TypeError(`${properties} is not an object`)) +function parsePropertiesV2Permissive( + properties: unknown, + errorCallback: (err: Error) => any, +): PackageProperties { + if (!object.test(properties)) { return {} } - return Object.entries(properties).reduce((prev, [name, value], idx) => { - schemaV2Compiled(value) - if (schemaV2Compiled.errors) { - for (let err of schemaV2Compiled.errors) { - errorCallback(new Error(`/data/${idx}${err.dataPath}: ${err.message}`)) - if (err.dataPath) { - applyOperation(value, { op: 'replace', path: err.dataPath, value: undefined }) + return Object.entries(properties).reduce( + (prev: PackageProperties, [name, value], idx) => { + const result = matchPropertyV2.enumParsed(value) + if ('value' in result) { + prev[name] = result.value + } else { + const error = result.error + const message = Parser.validatorErrorAsString(error) + let dataPath = error.keys.map(x => JSON.parse(x)).join('/') + errorCallback(new Error(`/data/${idx}: ${message}`)) + if (dataPath) { + applyOperation(properties, { + op: 'replace', + path: dataPath, + value: undefined, + }) } } - if (!schemaV2CompiledWithDefaults(value)) { - for (let err of schemaV2CompiledWithDefaults.errors) { - errorCallback(new Error(`/data/${idx}${err.dataPath}: ${err.message}`)) - } - return prev - } - } - prev[name] = value - return prev - }, { }) -} -interface PropertiesV1 { - name: string - value: string - description: string | null - copyable: boolean - qr: boolean + return prev + }, + + {}, + ) } type PackagePropertiesV1 = PropertiesV1[] export type PackageProperties = PackagePropertiesV2 export type PackagePropertiesVersioned = { - version: T, + version: T data: PackagePropertiesVersionedData } -export type PackagePropertiesVersionedData = - T extends 1 ? PackagePropertiesV1 : - T extends 2 ? PackagePropertiesV2 : - never - - -interface PackagePropertiesV2 { - [name: string]: PackagePropertyString | PackagePropertyObject -} - -interface PackagePropertyBase { - type: 'string' | 'object' - description: string | null -} - -interface PackagePropertyString extends PackagePropertyBase { - type: 'string' - value: string - copyable: boolean - qr: boolean - masked: boolean -} - -interface PackagePropertyObject extends PackagePropertyBase { - type: 'object' - value: PackagePropertiesV2 -} - +export type PackagePropertiesVersionedData = T extends 1 + ? PackagePropertiesV1 + : T extends 2 + ? PackagePropertiesV2 + : never