From 6ed4746379e8b038e4a148ca6f6c4c1e52aa67a2 Mon Sep 17 00:00:00 2001 From: Pagan Gazzard Date: Tue, 12 Mar 2024 16:34:26 +0000 Subject: [PATCH] Improve validate typings Change-type: patch --- src/type-utils.ts | 26 ++++++--- src/types/big-integer.ts | 6 +- src/types/big-serial.ts | 6 +- src/types/boolean.ts | 27 +++++---- src/types/case-insensitive-text.ts | 6 +- src/types/color.ts | 91 +++++++++++++++--------------- src/types/concept-type.ts | 6 +- src/types/date-time.ts | 5 +- src/types/date.ts | 5 +- src/types/file.ts | 50 ++++++++-------- src/types/foreign-key.ts | 6 +- src/types/hashed.ts | 18 +++--- src/types/integer.ts | 6 +- src/types/interval.ts | 6 +- src/types/json.ts | 25 ++++---- src/types/real.ts | 18 +++--- src/types/serial.ts | 6 +- src/types/sha.ts | 16 ++++-- src/types/short-text.ts | 6 +- src/types/text.ts | 6 +- src/types/time.ts | 11 +++- src/types/web-resource.ts | 9 +-- 22 files changed, 225 insertions(+), 136 deletions(-) diff --git a/src/type-utils.ts b/src/type-utils.ts index 8ee552a..d51b9bf 100644 --- a/src/type-utils.ts +++ b/src/type-utils.ts @@ -3,7 +3,7 @@ export interface DatabaseTypeFn { castType: string; } export type DatabaseType = string | DatabaseTypeFn; -export interface SbvrType { +export interface SbvrType { types: { odata: { name: string; @@ -14,16 +14,24 @@ export interface SbvrType { websql: DatabaseType; }; fetchProcessing?: FetchProcessing; - validate: (value: any, required: boolean) => Promise; + validate: Validate; } export type FetchProcessing = (data: unknown) => Read | null | undefined; +export interface Validate { + (value: Write, required: true): Promise; + (value: Write, required: false): Promise; + (value: Write, required: boolean): Promise; +} -const checkRequired = (validateFn: (value: any) => T) => { - function runCheck(value: any, required: true): Promise; +const checkRequired = (validateFn: (value: any) => T | Promise) => { + function runCheck(value: unknown, required: true): Promise; function runCheck(value: undefined | null, required: false): Promise; - function runCheck(value: any, required: boolean): Promise; - async function runCheck(value: any, required: boolean): Promise { + function runCheck(value: unknown, required: boolean): Promise; + async function runCheck( + value: unknown, + required: boolean, + ): Promise { if (value == null) { if (required) { throw new Error('cannot be null'); @@ -53,7 +61,7 @@ export const nativeFactTypeTemplates = { export const validate = { checkRequired, - integer: checkRequired((value: any) => { + integer: checkRequired((value) => { const processedValue = parseInt(value, 10); if (Number.isNaN(processedValue)) { throw new Error('is not a number: ' + value); @@ -61,7 +69,7 @@ export const validate = { return processedValue; }), text: (length?: number) => - checkRequired((value: any) => { + checkRequired((value) => { if (typeof value !== 'string') { throw new Error('is not a string: ' + value); } @@ -72,7 +80,7 @@ export const validate = { } return value; }), - date: checkRequired((value: any) => { + date: checkRequired((value) => { let processedValue = Number(value); if (Number.isNaN(processedValue)) { processedValue = value; diff --git a/src/types/big-integer.ts b/src/types/big-integer.ts index e3761a2..e5c96a3 100644 --- a/src/types/big-integer.ts +++ b/src/types/big-integer.ts @@ -9,9 +9,13 @@ export const types = { }, }; +type WriteType = number; +type DbWriteType = number; + export const nativeFactTypes = { Integer: TypeUtils.nativeFactTypeTemplates.comparison, Real: TypeUtils.nativeFactTypeTemplates.comparison, }; -export const validate = TypeUtils.validate.integer; +export const validate: TypeUtils.Validate = + TypeUtils.validate.integer; diff --git a/src/types/big-serial.ts b/src/types/big-serial.ts index ecc76d9..a89f38f 100644 --- a/src/types/big-serial.ts +++ b/src/types/big-serial.ts @@ -23,4 +23,8 @@ export const types = { }, }; -export const validate = TypeUtils.validate.integer; +type WriteType = number; +type DbWriteType = number; + +export const validate: TypeUtils.Validate = + TypeUtils.validate.integer; diff --git a/src/types/boolean.ts b/src/types/boolean.ts index c3a4378..112e80b 100644 --- a/src/types/boolean.ts +++ b/src/types/boolean.ts @@ -17,20 +17,23 @@ export const types = { }; type ReadType = boolean; +type WriteType = boolean | 0 | 1; +type DbWriteType = boolean; // `BOOLEAN` on sqlite/websql is just an alias for `INTEGER` hence the `=== 1` check export const fetchProcessing: TypeUtils.FetchProcessing = (data) => data === true || data === 1; -export const validate = TypeUtils.validate.checkRequired((originalValue) => { - // We use Number rather than parseInt as it deals with booleans and will return NaN for things like "a1" - const value = Number(originalValue); - if (value !== 0 && value !== 1) { - throw new Error( - `is not a boolean: ${JSON.stringify( - originalValue, - )} (${typeof originalValue})`, - ); - } - return value === 1; -}); +export const validate: TypeUtils.Validate = + TypeUtils.validate.checkRequired((originalValue) => { + // We use Number rather than parseInt as it deals with booleans and will return NaN for things like "a1" + const value = Number(originalValue); + if (value !== 0 && value !== 1) { + throw new Error( + `is not a boolean: ${JSON.stringify( + originalValue, + )} (${typeof originalValue})`, + ); + } + return value === 1; + }); diff --git a/src/types/case-insensitive-text.ts b/src/types/case-insensitive-text.ts index 9b41bce..f314053 100644 --- a/src/types/case-insensitive-text.ts +++ b/src/types/case-insensitive-text.ts @@ -9,4 +9,8 @@ export const types = { }, }; -export const validate = TypeUtils.validate.text(); +type WriteType = string; +type DbWriteType = string; + +export const validate: TypeUtils.Validate = + TypeUtils.validate.text(); diff --git a/src/types/color.ts b/src/types/color.ts index a50cad3..2b58188 100644 --- a/src/types/color.ts +++ b/src/types/color.ts @@ -22,6 +22,8 @@ type ReadType = { b: number; a: number; }; +type WriteType = number | ReadType; +type DbWriteType = number; export const nativeProperties = { has: { @@ -58,48 +60,49 @@ export const fetchProcessing: TypeUtils.FetchProcessing = (data) => { /* eslint-enable no-bitwise */ }; -export const validate = TypeUtils.validate.checkRequired((value) => { - let processedValue: number; - if (typeof value !== 'object') { - processedValue = parseInt(value, 10); - if (Number.isNaN(processedValue)) { - throw new Error('is neither an integer or color object: ' + value); - } - } else { - processedValue = 0; - Object.keys(value).forEach((component) => { - const componentValue = value[component]; - if (Number.isNaN(componentValue) || componentValue > 255) { - throw new Error( - 'has invalid component value of ' + - componentValue + - ' for component ' + - component, - ); - } - /* eslint-disable no-bitwise */ - switch (component.toLowerCase()) { - case 'r': - case 'red': - processedValue |= componentValue << 16; - break; - case 'g': - case 'green': - processedValue |= componentValue << 8; - break; - case 'b': - case 'blue': - processedValue |= componentValue; - break; - case 'a': - case 'alpha': - processedValue |= componentValue << 24; - break; - default: - throw new Error('has an unknown component: ' + component); +export const validate: TypeUtils.Validate = + TypeUtils.validate.checkRequired((value) => { + let processedValue: number; + if (typeof value !== 'object') { + processedValue = parseInt(value, 10); + if (Number.isNaN(processedValue)) { + throw new Error('is neither an integer or color object: ' + value); } - /* eslint-enable no-bitwise */ - }); - } - return processedValue; -}); + } else { + processedValue = 0; + Object.keys(value).forEach((component) => { + const componentValue = value[component]; + if (Number.isNaN(componentValue) || componentValue > 255) { + throw new Error( + 'has invalid component value of ' + + componentValue + + ' for component ' + + component, + ); + } + /* eslint-disable no-bitwise */ + switch (component.toLowerCase()) { + case 'r': + case 'red': + processedValue |= componentValue << 16; + break; + case 'g': + case 'green': + processedValue |= componentValue << 8; + break; + case 'b': + case 'blue': + processedValue |= componentValue; + break; + case 'a': + case 'alpha': + processedValue |= componentValue << 24; + break; + default: + throw new Error('has an unknown component: ' + component); + } + /* eslint-enable no-bitwise */ + }); + } + return processedValue; + }); diff --git a/src/types/concept-type.ts b/src/types/concept-type.ts index 28f60ea..cb69f45 100644 --- a/src/types/concept-type.ts +++ b/src/types/concept-type.ts @@ -9,9 +9,13 @@ export const types = { }, }; +type WriteType = number; +type DbWriteType = number; + export const nativeFactTypes = { Integer: TypeUtils.nativeFactTypeTemplates.comparison, Real: TypeUtils.nativeFactTypeTemplates.comparison, }; -export const validate = TypeUtils.validate.integer; +export const validate: TypeUtils.Validate = + TypeUtils.validate.integer; diff --git a/src/types/date-time.ts b/src/types/date-time.ts index 15d33df..6410f2d 100644 --- a/src/types/date-time.ts +++ b/src/types/date-time.ts @@ -10,6 +10,8 @@ export const types = { }; type ReadType = string; +type WriteType = string | number; +type DbWriteType = Date; export const fetchProcessing: TypeUtils.FetchProcessing = (data) => { if (data == null) { @@ -37,4 +39,5 @@ export const nativeNames = { 'Current Time': ['Now'], }; -export const validate = TypeUtils.validate.date; +export const validate: TypeUtils.Validate = + TypeUtils.validate.date; diff --git a/src/types/date.ts b/src/types/date.ts index 633d5a1..acef75f 100644 --- a/src/types/date.ts +++ b/src/types/date.ts @@ -10,6 +10,8 @@ export const types = { }; type ReadType = string; +type WriteType = string | number; +type DbWriteType = Date; export const fetchProcessing: TypeUtils.FetchProcessing = (data) => { if (data == null) { @@ -33,4 +35,5 @@ export const nativeFactTypes = { }, }; -export const validate = TypeUtils.validate.date; +export const validate: TypeUtils.Validate = + TypeUtils.validate.date; diff --git a/src/types/file.ts b/src/types/file.ts index 98e7460..1110434 100644 --- a/src/types/file.ts +++ b/src/types/file.ts @@ -8,26 +8,30 @@ export const types = { }, }; -export const validate = TypeUtils.validate.checkRequired((value) => { - if (Buffer.isBuffer(value)) { - return value; - } - if (typeof value !== 'string') { - throw new Error(`could not be converted to binary: ${typeof value}`); - } - if (value.length % 2 !== 0) { - throw new Error( - 'could not be converted to binary: hex string must have an even length', - ); - } - if (!/^[a-fA-F0-9]*$/.test(value)) { - throw new Error( - 'could not be converted to binary: hex string must contain only hex characters', - ); - } - try { - return Buffer.from(value, 'hex'); - } catch (e: any) { - throw new Error(`could not be converted to binary: ${e.message}`); - } -}); +type WriteType = Buffer | string; +type DbWriteType = Buffer; + +export const validate: TypeUtils.Validate = + TypeUtils.validate.checkRequired((value) => { + if (Buffer.isBuffer(value)) { + return value; + } + if (typeof value !== 'string') { + throw new Error(`could not be converted to binary: ${typeof value}`); + } + if (value.length % 2 !== 0) { + throw new Error( + 'could not be converted to binary: hex string must have an even length', + ); + } + if (!/^[a-fA-F0-9]*$/.test(value)) { + throw new Error( + 'could not be converted to binary: hex string must contain only hex characters', + ); + } + try { + return Buffer.from(value, 'hex'); + } catch (e: any) { + throw new Error(`could not be converted to binary: ${e.message}`); + } + }); diff --git a/src/types/foreign-key.ts b/src/types/foreign-key.ts index 28f60ea..cb69f45 100644 --- a/src/types/foreign-key.ts +++ b/src/types/foreign-key.ts @@ -9,9 +9,13 @@ export const types = { }, }; +type WriteType = number; +type DbWriteType = number; + export const nativeFactTypes = { Integer: TypeUtils.nativeFactTypeTemplates.comparison, Real: TypeUtils.nativeFactTypeTemplates.comparison, }; -export const validate = TypeUtils.validate.integer; +export const validate: TypeUtils.Validate = + TypeUtils.validate.integer; diff --git a/src/types/hashed.ts b/src/types/hashed.ts index 96c930f..a07678b 100644 --- a/src/types/hashed.ts +++ b/src/types/hashed.ts @@ -18,12 +18,16 @@ export const types = { }, }; -export const validate = TypeUtils.validate.checkRequired(async (value) => { - if (typeof value !== 'string') { - throw new Error('is not a string'); - } - const salt = await bcrypt.genSalt(); - return bcrypt.hash(value, salt); -}); +type WriteType = string; +type DbWriteType = string; + +export const validate: TypeUtils.Validate = + TypeUtils.validate.checkRequired(async (value) => { + if (typeof value !== 'string') { + throw new Error('is not a string'); + } + const salt = await bcrypt.genSalt(); + return bcrypt.hash(value, salt); + }); export const compare = bcrypt.compare.bind(bcrypt); diff --git a/src/types/integer.ts b/src/types/integer.ts index e229f87..2f08c51 100644 --- a/src/types/integer.ts +++ b/src/types/integer.ts @@ -9,9 +9,13 @@ export const types = { }, }; +type WriteType = number; +type DbWriteType = number; + export const nativeFactTypes = { Integer: TypeUtils.nativeFactTypeTemplates.comparison, Real: TypeUtils.nativeFactTypeTemplates.comparison, }; -export const validate = TypeUtils.validate.integer; +export const validate: TypeUtils.Validate = + TypeUtils.validate.integer; diff --git a/src/types/interval.ts b/src/types/interval.ts index e8805ae..0852a5b 100644 --- a/src/types/interval.ts +++ b/src/types/interval.ts @@ -9,4 +9,8 @@ export const types = { }, }; -export const validate = TypeUtils.validate.integer; +type WriteType = number; +type DbWriteType = number; + +export const validate: TypeUtils.Validate = + TypeUtils.validate.integer; diff --git a/src/types/json.ts b/src/types/json.ts index f43f0f1..84ebdba 100644 --- a/src/types/json.ts +++ b/src/types/json.ts @@ -10,6 +10,8 @@ export const types = { }; type ReadType = Record | any[]; +type WriteType = Record | any[]; +type DbWriteType = string; export const fetchProcessing: TypeUtils.FetchProcessing = (data) => { if (typeof data === 'string') { @@ -18,15 +20,16 @@ export const fetchProcessing: TypeUtils.FetchProcessing = (data) => { return data; }; -export const validate = TypeUtils.validate.checkRequired((value) => { - // Disallow primitives - if (typeof value !== 'object') { - throw new Error(`is not an object/array: ${typeof value}`); - } +export const validate: TypeUtils.Validate = + TypeUtils.validate.checkRequired((value) => { + // Disallow primitives + if (typeof value !== 'object') { + throw new Error(`is not an object/array: ${typeof value}`); + } - try { - return JSON.stringify(value); - } catch { - throw new Error('cannot be turned into JSON: ' + value); - } -}); + try { + return JSON.stringify(value); + } catch { + throw new Error('cannot be turned into JSON: ' + value); + } + }); diff --git a/src/types/real.ts b/src/types/real.ts index 270b40e..277cad4 100644 --- a/src/types/real.ts +++ b/src/types/real.ts @@ -9,15 +9,19 @@ export const types = { }, }; +type WriteType = number; +type DbWriteType = number; + export const nativeFactTypes = { Integer: TypeUtils.nativeFactTypeTemplates.comparison, Real: TypeUtils.nativeFactTypeTemplates.comparison, }; -export const validate = TypeUtils.validate.checkRequired((value) => { - const processedValue = parseFloat(value); - if (Number.isNaN(processedValue)) { - throw new Error('is not a number: ' + value); - } - return processedValue; -}); +export const validate: TypeUtils.Validate = + TypeUtils.validate.checkRequired((value) => { + const processedValue = parseFloat(value); + if (Number.isNaN(processedValue)) { + throw new Error('is not a number: ' + value); + } + return processedValue; + }); diff --git a/src/types/serial.ts b/src/types/serial.ts index b149258..c596678 100644 --- a/src/types/serial.ts +++ b/src/types/serial.ts @@ -23,4 +23,8 @@ export const types = { }, }; -export const validate = TypeUtils.validate.integer; +type WriteType = number; +type DbWriteType = number; + +export const validate: TypeUtils.Validate = + TypeUtils.validate.integer; diff --git a/src/types/sha.ts b/src/types/sha.ts index cfe03d6..0fb8d17 100644 --- a/src/types/sha.ts +++ b/src/types/sha.ts @@ -30,14 +30,18 @@ export const types = { }, }; +type WriteType = string; +type DbWriteType = string; + export const validateSync = sha256; -export const validate = TypeUtils.validate.checkRequired((value) => { - if (typeof value !== 'string') { - throw new Error('is not a string'); - } - return sha256(value); -}); +export const validate: TypeUtils.Validate = + TypeUtils.validate.checkRequired((value) => { + if (typeof value !== 'string') { + throw new Error('is not a string'); + } + return sha256(value); + }); export const compare = async (value: string, result: string) => { return sha256(value) === result; diff --git a/src/types/short-text.ts b/src/types/short-text.ts index 3a740d9..8034acb 100644 --- a/src/types/short-text.ts +++ b/src/types/short-text.ts @@ -9,4 +9,8 @@ export const types = { }, }; -export const validate = TypeUtils.validate.text(255); +type WriteType = string; +type DbWriteType = string; + +export const validate: TypeUtils.Validate = + TypeUtils.validate.text(255); diff --git a/src/types/text.ts b/src/types/text.ts index 99e621d..2bfd6fa 100644 --- a/src/types/text.ts +++ b/src/types/text.ts @@ -9,6 +9,9 @@ export const types = { }, }; +type WriteType = string; +type DbWriteType = string; + export const nativeProperties = { has: { Length: (from: string) => ['CharacterLength', from], @@ -24,4 +27,5 @@ export const nativeFactTypes = { }, }; -export const validate = TypeUtils.validate.text(); +export const validate: TypeUtils.Validate = + TypeUtils.validate.text(); diff --git a/src/types/time.ts b/src/types/time.ts index c6ecf4e..ceac285 100644 --- a/src/types/time.ts +++ b/src/types/time.ts @@ -10,6 +10,8 @@ export const types = { }; type ReadType = string; +type WriteType = number | string; +type DbWriteType = string; export const fetchProcessing: TypeUtils.FetchProcessing = (data) => { if (data != null) { @@ -19,10 +21,17 @@ export const fetchProcessing: TypeUtils.FetchProcessing = (data) => { return data; }; -export const validate = async (value: any, required: boolean) => { +export const validate: TypeUtils.Validate = (async ( + value, + required, +) => { const date = await TypeUtils.validate.date(value, required); if (date == null) { return date; } return date.toLocaleTimeString(); +}) as { + (value: WriteType, required: true): Promise; + (value: WriteType, required: false): Promise; + (value: WriteType, required: boolean): Promise; }; diff --git a/src/types/web-resource.ts b/src/types/web-resource.ts index bc135e3..39defd4 100644 --- a/src/types/web-resource.ts +++ b/src/types/web-resource.ts @@ -33,6 +33,8 @@ export const types = { }; type ReadType = WebResource; +type WriteType = WebResource; +type DbWriteType = string; export const nativeProperties = { has: { @@ -121,8 +123,8 @@ export const fetchProcessing: TypeUtils.FetchProcessing = (data) => { * Returns a Stringified WebResource that will be persisted on the DB * */ -export const validate = TypeUtils.validate.checkRequired( - async (value: WebResource) => { +export const validate: TypeUtils.Validate = + TypeUtils.validate.checkRequired(async (value: WebResource) => { if (typeof value !== 'object') { throw new Error(`is not an object: ${typeof value}`); } @@ -158,5 +160,4 @@ export const validate = TypeUtils.validate.checkRequired( } catch (e: any) { throw new Error("can't stringify JSON content"); } - }, -); + });