diff --git a/examples/usage.ts b/examples/usage.ts index 5854b42..73bf361 100644 --- a/examples/usage.ts +++ b/examples/usage.ts @@ -97,7 +97,7 @@ try { console.error('Failed to validate process.env variables:', error); if (error instanceof EnvSchemaValidationError) { - const e: EnvSchemaValidationError = error; + const e: EnvSchemaValidationError = error; // if you want to proceed anyway, the partial result is stored at: console.log('partial result:', e.values); console.log('errors:', JSON.stringify(e.errors, undefined, 2)); diff --git a/lib/convert.test.ts b/lib/convert.test.ts index cb6f981..d7416db 100644 --- a/lib/convert.test.ts +++ b/lib/convert.test.ts @@ -4,13 +4,10 @@ process.env.VALIDATED_ENV_SCHEMA_DEBUG = 'true'; import { commonSchemas, JSONSchema7, + TypeFromJSONSchema, } from '@profusion/json-schema-to-typescript-definitions'; -import { - EnvSchemaMaybeErrors, - EnvSchemaPartialValues, - schemaProperties, -} from './types'; +import { EnvSchemaMaybeErrors, schemaProperties } from './types'; import commonConvert from './common-convert'; import createConvert from './convert'; @@ -34,17 +31,17 @@ describe('createConvert', (): void => { required: ['REQ_VAR'], type: 'object', } as const; - type S = typeof schema; + type V = TypeFromJSONSchema; it('works without conversion', (): void => { - const values: EnvSchemaPartialValues = { + const values = { OPT_VAR: '0x1fffffffffffff', REQ_VAR: '2021-01-02T12:34:56.000Z', - }; - const container: Record = { + } as const satisfies V; + const container = { OPT_VAR: '0x1fffffffffffff', REQ_VAR: '2021-01-02T12:34:56.000Z', - }; + } as const satisfies V; const convert = createConvert(schema, schemaProperties(schema), undefined); const consoleSpy = getConsoleMock(); const [convertedValue, conversionErrors] = convert( @@ -66,22 +63,22 @@ describe('createConvert', (): void => { }); it('works with valid schema', (): void => { - const values: EnvSchemaPartialValues = { + const values = { OPT_VAR: '0x1fffffffffffff', REQ_VAR: '2021-01-02T12:34:56.000Z', - }; + } as const satisfies V; const container = { OPT_VAR: '0x1fffffffffffff', REQ_VAR: '2021-01-02T12:34:56.000Z', - } as const; + } as const satisfies V; const convert = createConvert(schema, schemaProperties(schema), { OPT_VAR: ( value: string | undefined, propertySchema: JSONSchema7, key: string, allSchema: JSONSchema7, - initialValues: EnvSchemaPartialValues, - errors: EnvSchemaMaybeErrors, + initialValues: Partial, + errors: EnvSchemaMaybeErrors, ): bigint | undefined => typeof value === 'string' && propertySchema === schema.properties.OPT_VAR && @@ -129,7 +126,7 @@ New Value.....: ${commonConvert.dateTime(container.REQ_VAR)} }); it('works with missing values (keep undefined)', (): void => { - const values: EnvSchemaPartialValues = {}; + const values: Partial = {}; const container: Record = {}; const convert = createConvert(schema, schemaProperties(schema), { OPT_VAR: ( @@ -137,8 +134,8 @@ New Value.....: ${commonConvert.dateTime(container.REQ_VAR)} propertySchema: JSONSchema7, key: string, allSchema: JSONSchema7, - initialValues: EnvSchemaPartialValues, - errors: EnvSchemaMaybeErrors, + initialValues: Partial, + errors: EnvSchemaMaybeErrors, ): bigint | undefined => typeof value === 'string' && propertySchema === schema.properties.OPT_VAR && @@ -167,7 +164,7 @@ New Value.....: ${commonConvert.dateTime(container.REQ_VAR)} }); it('works with missing values (return custom default)', (): void => { - const values: EnvSchemaPartialValues = {}; + const values: Partial = {}; const container: Record = {}; const convert = createConvert(schema, schemaProperties(schema), { OPT_VAR: ( @@ -175,8 +172,8 @@ New Value.....: ${commonConvert.dateTime(container.REQ_VAR)} propertySchema: JSONSchema7, key: string, allSchema: JSONSchema7, - initialValues: EnvSchemaPartialValues, - errors: EnvSchemaMaybeErrors, + initialValues: Partial, + errors: EnvSchemaMaybeErrors, ): bigint | undefined => typeof value === 'string' && propertySchema === schema.properties.OPT_VAR && @@ -222,14 +219,14 @@ New Value.....: ${new Date(0)} }); it('removes properties converted to undefined', (): void => { - const values: EnvSchemaPartialValues = { + const values = { OPT_VAR: '0x1fffffffffffff', REQ_VAR: '2021-01-02T12:34:56.000Z', - }; + } as const satisfies V; const container: Record = { OPT_VAR: '0x1fffffffffffff', REQ_VAR: '2021-01-02T12:34:56.000Z', - }; + } as const satisfies V; const convert = createConvert(schema, schemaProperties(schema), { OPT_VAR: (): bigint | undefined => undefined, REQ_VAR: (): Date | undefined => undefined, @@ -255,14 +252,14 @@ New Value.....: ${new Date(0)} }); it('removes properties that conversion did throw', (): void => { - const values: EnvSchemaPartialValues = { + const values = { OPT_VAR: '0x1fffffffffffff', REQ_VAR: '2021-01-02T12:34:56.000Z', - }; + } as const satisfies V; const container: Record = { OPT_VAR: '0x1fffffffffffff', REQ_VAR: '2021-01-02T12:34:56.000Z', - }; + } as const satisfies V; const error = new Error('forced error'); const convert = createConvert(schema, schemaProperties(schema), { OPT_VAR: (): bigint => { @@ -303,7 +300,7 @@ New Value.....: ${new Date(0)} }, type: 'object', } as const; - const values: EnvSchemaPartialValues = {}; + const values: TypeFromJSONSchema = {}; const container: Record = {}; const convert = createConvert( schemaNoRequired, diff --git a/lib/convert.ts b/lib/convert.ts index f9bded7..8e8bf90 100644 --- a/lib/convert.ts +++ b/lib/convert.ts @@ -1,12 +1,14 @@ +import type { TypeFromJSONSchema } from '@profusion/json-schema-to-typescript-definitions'; + import type { + BaseEnvParsed, BaseEnvSchema, EnvSchemaConvertedPartialValues, EnvSchemaConvertedPartialValuesWithConvert, EnvSchemaCustomConverters, EnvSchemaMaybeErrors, EnvSchemaProperties, - EnvSchemaPropertyValue, - EnvSchemaPartialValues, + KeyOf, } from './types'; import { addErrors } from './errors'; @@ -15,13 +17,14 @@ import dbg from './dbg'; // DO NOT THROW HERE! type EnvSchemaConvert< S extends BaseEnvSchema, - Converters extends EnvSchemaCustomConverters | undefined, + V extends BaseEnvParsed = TypeFromJSONSchema, + Converters extends EnvSchemaCustomConverters | undefined = undefined, > = ( - value: EnvSchemaPartialValues, + value: Partial, errors: EnvSchemaMaybeErrors, container: Record, ) => [ - EnvSchemaConvertedPartialValuesWithConvert, + EnvSchemaConvertedPartialValuesWithConvert, EnvSchemaMaybeErrors, ]; @@ -29,36 +32,35 @@ const noRequiredProperties: string[] = []; const createConvert = < S extends BaseEnvSchema, - Converters extends EnvSchemaCustomConverters, + V extends BaseEnvParsed = TypeFromJSONSchema, + Converters extends EnvSchemaCustomConverters< + S, + V + > = EnvSchemaCustomConverters, >( schema: S, properties: Readonly>, customize: Converters, -): EnvSchemaConvert => { +): EnvSchemaConvert => { const convertedProperties = properties.filter( ([key]) => customize[key] !== undefined, ); - type ConverterKey = Extract; - const requiredProperties: readonly ConverterKey[] = schema.required - ? (schema.required.filter( - key => customize[key] !== undefined, - ) as ConverterKey[]) - : (noRequiredProperties as ConverterKey[]); + type ConverterKey = KeyOf; + const requiredProperties = schema.required + ? schema.required.filter(key => customize[key] !== undefined) + : noRequiredProperties; return ( - initialValues: EnvSchemaPartialValues, + initialValues: Partial, initialErrors: EnvSchemaMaybeErrors, container: Record, ): [ - EnvSchemaConvertedPartialValuesWithConvert, + EnvSchemaConvertedPartialValuesWithConvert, EnvSchemaMaybeErrors, ] => { // alias the same object with a different type, save on casts - const values = initialValues as EnvSchemaConvertedPartialValuesWithConvert< - S, - Converters - >; + const values = initialValues; let errors = initialErrors; const removeValue = (key: ConverterKey): void => { @@ -68,13 +70,14 @@ const createConvert = < }; convertedProperties.forEach(([key, propertySchema]) => { - type K = typeof key; // it was filtered before - const convert = customize[key] as Exclude; + const convert = customize[key] as NonNullable< + (typeof customize)[typeof key] + >; const oldValue = values[key]; try { const newValue = convert( - values[key] as EnvSchemaPropertyValue | undefined, + values[key], propertySchema, key, schema, @@ -118,36 +121,39 @@ New Value.....: ${newValue} if (values[key] === undefined) { errors = addErrors( errors, - key as Extract, + key, new Error(`required property "${key}" is undefined`), ); } }); - return [values, errors]; + return [ + values as EnvSchemaConvertedPartialValuesWithConvert, + errors, + ]; }; }; -const noConversion = ( - values: EnvSchemaPartialValues, +const noConversion = < + S extends BaseEnvSchema, + V extends BaseEnvParsed = TypeFromJSONSchema, +>( + values: Partial, errors: EnvSchemaMaybeErrors, -): [EnvSchemaConvertedPartialValues, EnvSchemaMaybeErrors] => [ - values as EnvSchemaConvertedPartialValues, +): [EnvSchemaConvertedPartialValues, EnvSchemaMaybeErrors] => [ + values as EnvSchemaConvertedPartialValues, errors, ]; export default < S extends BaseEnvSchema, - Converters extends EnvSchemaCustomConverters | undefined, + V extends BaseEnvParsed = TypeFromJSONSchema, + Converters extends EnvSchemaCustomConverters | undefined = undefined, >( schema: Readonly, properties: Readonly>, customize: Converters, -): EnvSchemaConvert => +): EnvSchemaConvert => customize === undefined - ? (noConversion as unknown as EnvSchemaConvert) - : createConvert( - schema, - properties, - customize as Exclude, - ); + ? (noConversion as unknown as EnvSchemaConvert) + : createConvert(schema, properties, customize); diff --git a/lib/errors.ts b/lib/errors.ts index bcc3b6a..91c9eb1 100644 --- a/lib/errors.ts +++ b/lib/errors.ts @@ -1,9 +1,13 @@ +import type { TypeFromJSONSchema } from '@profusion/json-schema-to-typescript-definitions'; + import type { BaseEnvSchema, EnvSchemaConvertedPartialValues, EnvSchemaCustomizations, EnvSchemaMaybeErrors, EnvSchemaErrors, + BaseEnvParsed, + KeyOf, } from './types'; /* istanbul ignore next */ @@ -13,14 +17,12 @@ export const assertIsError: (e: unknown) => asserts e is Error = e => { export const addErrors = ( initialErrors: EnvSchemaMaybeErrors, - key: Extract | '$other', + key: KeyOf | '$other', exception: Error, ): EnvSchemaErrors => { - let errors = initialErrors; - if (errors === undefined) errors = {}; - let keyErrors = errors[key]; - if (keyErrors === undefined) { - keyErrors = []; + const errors: EnvSchemaErrors = initialErrors ?? {}; + const keyErrors = errors[key] ?? []; + if (!keyErrors.length) { errors[key] = keyErrors; } keyErrors.push(exception); @@ -40,11 +42,15 @@ export const addErrors = ( */ export class EnvSchemaValidationError< S extends BaseEnvSchema, - Customizations extends EnvSchemaCustomizations, + V extends BaseEnvParsed = TypeFromJSONSchema, + Customizations extends EnvSchemaCustomizations< + S, + V + > = EnvSchemaCustomizations, > extends Error { readonly schema: S; - values: EnvSchemaConvertedPartialValues; + values: EnvSchemaConvertedPartialValues; errors: EnvSchemaErrors; @@ -57,7 +63,7 @@ export class EnvSchemaValidationError< customize: Customizations, errors: EnvSchemaErrors, container: Record, - values: EnvSchemaConvertedPartialValues, + values: EnvSchemaConvertedPartialValues, ) { const names = Object.keys(errors).join(', '); super(`Failed to validate environment variables against schema: ${names}`); diff --git a/lib/index.test.ts b/lib/index.test.ts index ad424ea..c29a85e 100644 --- a/lib/index.test.ts +++ b/lib/index.test.ts @@ -66,7 +66,7 @@ describe('validateEnvSchema', (): void => { validateEnvSchema(schema, container); } catch (e) { expect(e).toBeInstanceOf(EnvSchemaValidationError); - const err = e as EnvSchemaValidationError; + const err = e as EnvSchemaValidationError; expect(err.schema).toBe(schema); expect(err.container).toBe(container); expect(err.values).toEqual({ @@ -88,10 +88,10 @@ describe('validateEnvSchema', (): void => { }); it('works with customizations', (): void => { - const container: Record = { + const container = { OPT_VAR: '1.23', REQ_VAR: '{"a": [2, 3], "s": "hello"}', - }; + } as const; const consoleSpy = getConsoleMock(); const values = validateEnvSchema(schema, container, { convert: { @@ -99,7 +99,7 @@ describe('validateEnvSchema', (): void => { value !== undefined ? BigInt(value * 1e6) : undefined, }, parse: { - OPT_VAR: (str: string): number => Number(str) * 1000, + OPT_VAR: str => Number(str) * 1000, }, postValidate: { OPT_VAR: (value: number | undefined): number | undefined => @@ -113,7 +113,7 @@ describe('validateEnvSchema', (): void => { } as const); type ValueType = typeof values; type ExpectedType = { - OPT_VAR: bigint | undefined; + OPT_VAR: number | undefined; REQ_VAR: { a?: number[]; s: string; @@ -183,7 +183,7 @@ New Value.....: 1000000000 } as const); } catch (e) { expect(e).toBeInstanceOf(EnvSchemaValidationError); - const err = e as EnvSchemaValidationError; + const err = e as EnvSchemaValidationError; expect(err.schema).toBe(schema); expect(err.container).toBe(container); expect(err.values).toEqual({ @@ -220,7 +220,7 @@ New Value.....: 1000000000 } as const); } catch (e) { expect(e).toBeInstanceOf(EnvSchemaValidationError); - const err = e as EnvSchemaValidationError; + const err = e as EnvSchemaValidationError; expect(err.schema).toBe(schema); expect(err.container).toBe(container); expect(err.values).toEqual({ @@ -256,7 +256,7 @@ New Value.....: 1000000000 } as const); } catch (e) { expect(e).toBeInstanceOf(EnvSchemaValidationError); - const err = e as EnvSchemaValidationError; + const err = e as EnvSchemaValidationError; expect(err.schema).toBe(schema); expect(err.container).toBe(container); expect(err.values).toEqual({ @@ -293,7 +293,7 @@ New Value.....: 1000000000 } as const); } catch (e) { expect(e).toBeInstanceOf(EnvSchemaValidationError); - const err = e as EnvSchemaValidationError; + const err = e as EnvSchemaValidationError; expect(err.schema).toBe(schema); expect(err.container).toBe(container); expect(err.values).toEqual({ diff --git a/lib/index.ts b/lib/index.ts index c86168f..a3ba12a 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -1,4 +1,7 @@ +import type { TypeFromJSONSchema } from '@profusion/json-schema-to-typescript-definitions'; + import type { + BaseEnvParsed, BaseEnvSchema, EnvSchemaConvertedValues, EnvSchemaConverters, @@ -28,10 +31,14 @@ export const commonConvert = providedConverters; type ValidateEnvSchema< S extends BaseEnvSchema, - Customizations extends EnvSchemaCustomizations, + V extends BaseEnvParsed = TypeFromJSONSchema, + Customizations extends EnvSchemaCustomizations< + S, + V + > = EnvSchemaCustomizations, > = ( container: Record, -) => EnvSchemaConvertedValues; +) => EnvSchemaConvertedValues; /** * Creates the validator based on JSON Schema 7 and customizations. @@ -53,23 +60,35 @@ type ValidateEnvSchema< */ export const createValidateEnvSchema = < S extends BaseEnvSchema, - Customizations extends EnvSchemaCustomizations, + V extends BaseEnvParsed = TypeFromJSONSchema, + Customizations extends EnvSchemaCustomizations< + S, + V + > = EnvSchemaCustomizations, >( schema: S, customize?: Customizations, -): ValidateEnvSchema => { +): ValidateEnvSchema => { const properties = schemaProperties(schema); - const parse = createParse(schema, properties, customize?.parse); - const validate = createValidate(schema, properties, customize?.postValidate); - const serialize = createSerialize(schema, properties, customize?.serialize); - const convert = createConvert>( + const parse = createParse(schema, properties, customize?.parse); + const validate = createValidate( + schema, + properties, + customize?.postValidate, + ); + const serialize = createSerialize( + schema, + properties, + customize?.serialize, + ); + const convert = createConvert( schema, properties, - customize?.convert as EnvSchemaConverters, + customize?.convert as EnvSchemaConverters, ); return ( container: Record, - ): EnvSchemaConvertedValues => { + ): EnvSchemaConvertedValues => { const [parsedValues, parseErrors] = parse(container); const [validatedValues, validationErrors] = validate( @@ -100,7 +119,7 @@ export const createValidateEnvSchema = < } // no errors means the partial object is actually complete - return values as EnvSchemaConvertedValues; + return values as EnvSchemaConvertedValues; }; }; @@ -135,12 +154,13 @@ export const createValidateEnvSchema = < */ export const validateEnvSchema = < S extends BaseEnvSchema, - Customizations extends EnvSchemaCustomizations, + V extends TypeFromJSONSchema, + Customizations extends EnvSchemaCustomizations, >( schema: S, container: Record = process.env, customize?: Customizations, -): EnvSchemaConvertedValues => - createValidateEnvSchema(schema, customize)(container); +): EnvSchemaConvertedValues => + createValidateEnvSchema(schema, customize)(container); export default validateEnvSchema; diff --git a/lib/parse.ts b/lib/parse.ts index 7a0117e..2e93cb4 100644 --- a/lib/parse.ts +++ b/lib/parse.ts @@ -1,16 +1,20 @@ import type { JSONSchema7Definition, JSONSchema7Type, + TypeFromJSONSchema, } from '@profusion/json-schema-to-typescript-definitions'; import dbg from './dbg'; -import { EnvSchemaMaybeErrors, EnvSchemaProperties } from './types'; +import { + BaseEnvParsed, + EnvSchemaMaybeErrors, + EnvSchemaProperties, +} from './types'; import type { BaseEnvSchema, EnvSchemaCustomParsers, EnvSchemaParserFn, - EnvSchemaPartialValues, } from './types'; import { addErrors, assertIsError } from './errors'; @@ -30,34 +34,34 @@ const defaultParser = ( // Do its best to parse values, however Ajv will handle most // of the specific conversions itself during validate() // DO NOT THROW HERE! -type EnvSchemaParse = ( +type EnvSchemaParse> = ( container: Readonly>, -) => [EnvSchemaPartialValues, EnvSchemaMaybeErrors]; +) => [Partial, EnvSchemaMaybeErrors]; -export default ( +export default < + S extends BaseEnvSchema, + V extends BaseEnvParsed = TypeFromJSONSchema, + >( schema: Readonly, properties: Readonly>, - customize: EnvSchemaCustomParsers | undefined, - ): EnvSchemaParse => + customize: EnvSchemaCustomParsers | undefined, + ): EnvSchemaParse => ( container: Readonly>, - ): [EnvSchemaPartialValues, EnvSchemaMaybeErrors] => - properties.reduce( + ): [Partial, EnvSchemaMaybeErrors] => + properties.reduce<[Partial, EnvSchemaMaybeErrors]>( ( [values, initialErrors], [key, propertySchema], - ): [EnvSchemaPartialValues, EnvSchemaMaybeErrors] => { - type K = typeof key; - const str = container[key]; + ): [Partial, EnvSchemaMaybeErrors] => { + const containerKey = container[key]; let errors = initialErrors; - if (typeof str === 'string') { - const parser = - (customize && customize[key]) || - (defaultParser as unknown as EnvSchemaParserFn); + if (typeof containerKey === 'string') { + const parser = (customize?.[key] || + defaultParser) as EnvSchemaParserFn; try { - const value = parser(str, propertySchema, key, schema); // eslint-disable-next-line no-param-reassign - values[key] = value; + values[key] = parser(containerKey, propertySchema, key, schema); } catch (e) { dbg(`failed to parse "${key}": ${e}`, e); assertIsError(e); @@ -66,5 +70,5 @@ export default ( } return [values, errors]; }, - [{}, undefined] as [EnvSchemaPartialValues, EnvSchemaMaybeErrors], + [{}, undefined], ); diff --git a/lib/serialize.test.ts b/lib/serialize.test.ts index 5829252..ff55b0f 100644 --- a/lib/serialize.test.ts +++ b/lib/serialize.test.ts @@ -28,10 +28,10 @@ describe('createSerialize', (): void => { }, type: 'object', } as const; - type S = typeof schema; - const values: TypeFromJSONSchema = { + type V = TypeFromJSONSchema; + const values = { MY_VAR: true, - } as const; + } as const satisfies V; const container: Record = {}; expect( createSerialize(schema, schemaProperties(schema), undefined)( @@ -50,10 +50,10 @@ describe('createSerialize', (): void => { }, type: 'object', } as const; - type S = typeof schema; - const values: TypeFromJSONSchema = { + type V = TypeFromJSONSchema; + const values = { MY_VAR: 123, - } as const; + } as const satisfies V; const container: Record = {}; expect( createSerialize(schema, schemaProperties(schema), undefined)( @@ -72,10 +72,10 @@ describe('createSerialize', (): void => { }, type: 'object', } as const; - type S = typeof schema; - const values: TypeFromJSONSchema = { + type V = TypeFromJSONSchema; + const values = { MY_VAR: 'hello', - } as const; + } as const satisfies V; const container: Record = {}; expect( createSerialize(schema, schemaProperties(schema), undefined)( @@ -94,12 +94,12 @@ describe('createSerialize', (): void => { }, type: 'object', } as const; - type S = typeof schema; - const values = { MY_VAR: 123 }; + type V = TypeFromJSONSchema; + const values = { MY_VAR: 123 } as const; const container: Record = {}; expect( createSerialize(schema, schemaProperties(schema), undefined)( - values as unknown as TypeFromJSONSchema, + values as unknown as V, container, undefined, ), @@ -114,10 +114,10 @@ describe('createSerialize', (): void => { }, type: 'object', } as const; - type S = typeof schema; - const values: TypeFromJSONSchema = { + type V = TypeFromJSONSchema; + const values = { MY_VAR: null, - } as const; + } as const satisfies V; const container: Record = {}; expect( createSerialize(schema, schemaProperties(schema), undefined)( @@ -136,10 +136,10 @@ describe('createSerialize', (): void => { }, type: 'object', } as const; - type S = typeof schema; - const values: TypeFromJSONSchema = { + type V = TypeFromJSONSchema; + const values = { MY_VAR: ['abc', 'def'] as string[], - } as const; + } as const satisfies V; const container: Record = {}; expect( createSerialize(schema, schemaProperties(schema), undefined)( @@ -164,13 +164,13 @@ describe('createSerialize', (): void => { }, type: 'object', } as const; - type S = typeof schema; - const values: TypeFromJSONSchema = { + type V = TypeFromJSONSchema; + const values = { MY_VAR: { b: true, s: 'hello', }, - } as const; + } as const satisfies V; const container: Record = {}; expect( createSerialize(schema, schemaProperties(schema), undefined)( @@ -189,8 +189,8 @@ describe('createSerialize', (): void => { }, type: 'object', } as const; - type S = typeof schema; - const values: Partial> = {} as const; + type V = TypeFromJSONSchema; + const values = {} as const satisfies V; const container: Record = { MY_VAR: 'will be removed', }; @@ -211,10 +211,10 @@ describe('createSerialize', (): void => { }, type: 'object', } as const; - type S = typeof schema; - const values: Partial> = { + type V = TypeFromJSONSchema; + const values = { MY_VAR: true, - } as const; + } as const satisfies V; const container: Record = {}; expect( createSerialize(schema, schemaProperties(schema), { @@ -243,10 +243,10 @@ describe('createSerialize', (): void => { }, type: 'object', } as const; - type S = typeof schema; - const values: Partial> = { + type V = TypeFromJSONSchema; + const values = { MY_VAR: true, - } as const; + } as const satisfies V; const container: Record = {}; const consoleSpy = getConsoleMock(); const error = new Error('forced error'); diff --git a/lib/serialize.ts b/lib/serialize.ts index ba3e025..40c53ff 100644 --- a/lib/serialize.ts +++ b/lib/serialize.ts @@ -1,63 +1,63 @@ import type { JSONSchema7Definition, - JSONSchema7Type, + TypeFromJSONSchema, } from '@profusion/json-schema-to-typescript-definitions'; -import { EnvSchemaProperties } from './types'; +import { + BaseEnvParsed, + EnvSchemaProperties, + EnvSchemaPropertyValue, +} from './types'; import type { BaseEnvSchema, EnvSchemaCustomSerializers, EnvSchemaMaybeErrors, - EnvSchemaPartialValues, } from './types'; import dbg from './dbg'; import { addErrors, assertIsError } from './errors'; const defaultSerialize = ( - value: JSONSchema7Type, + value: unknown, _propertySchema: JSONSchema7Definition, -): string => { - if (typeof value === 'string') return value; // no double-quotes - return JSON.stringify(value); -}; +): string => (typeof value === 'string' ? value : JSON.stringify(value)); // no double quotes // Serialize the parsed and validated values back to container. // DO NOT THROW HERE! -type EnvSchemaSerialize = ( - values: Readonly>, +type EnvSchemaSerialize< + S extends BaseEnvSchema, + V extends BaseEnvParsed = TypeFromJSONSchema, +> = ( + values: Readonly>, container: Record, errors: EnvSchemaMaybeErrors, -) => [EnvSchemaPartialValues, EnvSchemaMaybeErrors]; +) => [Partial, EnvSchemaMaybeErrors]; -export default ( - schema: Readonly, +export default < + S extends Readonly, + V extends BaseEnvParsed = TypeFromJSONSchema, + >( + schema: S, properties: Readonly>, - customize: EnvSchemaCustomSerializers | undefined, - ): EnvSchemaSerialize => + customize: EnvSchemaCustomSerializers | undefined, + ): EnvSchemaSerialize => ( - givenValues: Readonly>, + givenValues: Readonly>, container: Record, givenErrors: EnvSchemaMaybeErrors, - ): [EnvSchemaPartialValues, EnvSchemaMaybeErrors] => + ): [Partial, EnvSchemaMaybeErrors] => properties.reduce( ( [values, initialErrors], [key, propertySchema], - ): [EnvSchemaPartialValues, EnvSchemaMaybeErrors] => { - const value = values[key]; + ): [Partial, EnvSchemaMaybeErrors] => { + const value: EnvSchemaPropertyValue | undefined = values[key]; let errors = initialErrors; if (value !== undefined) { const serialize = customize?.[key] || defaultSerialize; try { // eslint-disable-next-line no-param-reassign - container[key] = serialize( - // we already checked for not undefined, but TS doesn't get it :-( - value as Parameters[0], - propertySchema, - key, - schema, - ); + container[key] = serialize(value, propertySchema, key, schema); } catch (e) { dbg(`failed to serialize "${key}": ${e}`, e); // eslint-disable-next-line no-param-reassign diff --git a/lib/types.ts b/lib/types.ts index 42e8339..b881a49 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -1,40 +1,47 @@ -import type { - TypeFromJSONSchema, - JSONSchema7, - JSONSchema7Definition, -} from '@profusion/json-schema-to-typescript-definitions'; +import type { JSONSchema7, JSONSchema7Definition } from 'json-schema'; +import type { DeepReadonly } from 'json-schema-to-ts/lib/types/type-utils/readonly'; + +type ReadonlyJSONSchema = DeepReadonly; +type ReadonlyJSONSchemaDefinition = DeepReadonly; + +// string-only keys +export type KeyOf = keyof T & string; export type BaseEnvSchema = { type: 'object'; - properties: Readonly<{ [key: string]: JSONSchema7 }>; + properties: Readonly<{ [key: string]: ReadonlyJSONSchema }>; required?: readonly string[]; - dependencies?: { - readonly [key: string]: JSONSchema7Definition | readonly string[]; - }; + dependencies?: Readonly<{ + [key: string]: ReadonlyJSONSchemaDefinition | string[]; + }>; additionalProperties?: true; // if provided, must be true, otherwise it may hurt process.env }; -export type EnvSchemaProperties = { - [K in Extract]: [K, S['properties'][K]]; -}[Extract][]; +export type BaseEnvParsed = Record< + KeyOf, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + any +>; -export const schemaProperties = ( - schema: S, -): Readonly> => - Object.entries(schema.properties) as EnvSchemaProperties; +export type EnvSchemaProperties< + S extends BaseEnvSchema, + Keys extends KeyOf = KeyOf, +> = { + [K in Keys]: [K, S['properties'][K]]; +}[Keys][]; -/** - * Subset of valid (post-validate) properties. If there are no - * errors, then all required properties should be present. - */ -export type EnvSchemaPartialValues = Partial< - TypeFromJSONSchema ->; +export const schemaProperties = < + S extends BaseEnvSchema, + Ret extends EnvSchemaProperties = EnvSchemaProperties, +>({ + properties, +}: S): Readonly => Object.entries(properties) as Ret; export type EnvSchemaPropertyValue< S extends BaseEnvSchema, - K extends keyof S['properties'], -> = K extends keyof TypeFromJSONSchema ? TypeFromJSONSchema[K] : never; + V extends BaseEnvParsed, + K extends KeyOf = KeyOf, +> = V[K]; /** * Errors are stored per-property/variable, if something @@ -43,27 +50,28 @@ export type EnvSchemaPropertyValue< * Multiple phases may produce errors, then it's stored as array. */ export type EnvSchemaErrors = Partial<{ - [K in keyof S['properties'] | '$other']: Error[]; + [K in KeyOf | '$other']: Error[]; }>; export type EnvSchemaMaybeErrors = | EnvSchemaErrors | undefined; /** - * Parse one property from string to the best JSONSchema7Type. + * Parse one property from string to the best ReadonlyJSONSchema. * * There is no need to coerce types as Ajv.validate() will do that * for you. */ export type EnvSchemaParserFn< S extends BaseEnvSchema, - K extends keyof S['properties'], + V extends BaseEnvParsed, + K extends KeyOf, > = ( str: string, propertySchema: Readonly, key: K, schema: Readonly, -) => EnvSchemaPropertyValue; +) => EnvSchemaPropertyValue; /** * Customize the parser to be used for each property. @@ -71,22 +79,26 @@ export type EnvSchemaParserFn< * If not provided, the default is to `JSON.parse()` and, if that fails, * keep the original value as a string. */ -export type EnvSchemaCustomParsers = Readonly< +export type EnvSchemaCustomParsers< + S extends BaseEnvSchema, + V extends BaseEnvParsed, +> = Readonly< Partial<{ - [K in keyof S['properties']]: EnvSchemaParserFn; + [K in KeyOf]: EnvSchemaParserFn; }> >; /** - * Serialize one property from validated JSONSchema7Type to string. + * Serialize one property from validated ReadonlyJSONSchema to string. * * The types will be validated by Ajv.validate() */ export type EnvSchemaSerializeFn< S extends BaseEnvSchema, - K extends keyof S['properties'], + V extends BaseEnvParsed, + K extends KeyOf, > = ( - value: Exclude, undefined>, + value: Exclude, undefined>, propertySchema: Readonly, key: K, schema: Readonly, @@ -98,9 +110,12 @@ export type EnvSchemaSerializeFn< * If not provided, the default is to `JSON.stringify()`, * unless it's already a string. */ -export type EnvSchemaCustomSerializers = Readonly< +export type EnvSchemaCustomSerializers< + S extends BaseEnvSchema, + V extends BaseEnvParsed, +> = Readonly< Partial<{ - [K in keyof S['properties']]: EnvSchemaSerializeFn; + [K in KeyOf]: EnvSchemaSerializeFn; }> >; @@ -118,15 +133,17 @@ export type EnvSchemaCustomSerializers = Readonly< */ export type EnvSchemaPostValidateFn< S extends BaseEnvSchema, - K extends keyof S['properties'], + V extends BaseEnvParsed, + K extends KeyOf, + PV extends EnvSchemaPropertyValue = EnvSchemaPropertyValue, > = ( - value: EnvSchemaPropertyValue | undefined, + value: PV | undefined, propertySchema: S['properties'][K], key: K, schema: Readonly, - allValues: EnvSchemaPartialValues, + allValues: Partial, errors: Readonly>, -) => EnvSchemaPropertyValue | undefined; +) => PV | undefined; /** * Customize the validator to be executed for each property, @@ -134,42 +151,49 @@ export type EnvSchemaPostValidateFn< * * Each validator will receive the other properties for convenience. */ -export type EnvSchemaCustomPostValidators = Readonly< +export type EnvSchemaCustomPostValidators< + S extends BaseEnvSchema, + V extends BaseEnvParsed, +> = Readonly< Partial<{ - [K in keyof S['properties']]: EnvSchemaPostValidateFn; + [K in KeyOf]: EnvSchemaPostValidateFn; }> >; /** - * Convert JSONSchema7Type to high-level values, such as 'Date'. + * Convert ReadonlyJSONSchema to high-level values, such as 'Date'. * * This is executed with the post-Validated data. */ export type EnvSchemaConvertFn< S extends BaseEnvSchema, - K extends keyof S['properties'], - R, + V extends BaseEnvParsed, + K extends KeyOf, + // we must match any return type + // eslint-disable-next-line @typescript-eslint/no-explicit-any + Ret = any, > = ( - value: EnvSchemaPropertyValue | undefined, + value: EnvSchemaPropertyValue | undefined, propertySchema: S['properties'][K], key: K, schema: Readonly, - allValues: EnvSchemaPartialValues, + allValues: Partial, errors: Readonly>, -) => R; +) => Ret; /** * Each converter will receive the other properties for convenience. * * If not provided, the value will be kept as the post-validated - * JSONSchema7Type that matches the type (TypeFromJSONSchema), + * ReadonlyJSONSchema that matches the type (TypeFromJSONSchema), * otherwise it may be converted to high-level type, such as `Date`. */ -export type EnvSchemaCustomConverters = Readonly< +export type EnvSchemaCustomConverters< + S extends BaseEnvSchema, + V extends BaseEnvParsed, +> = Readonly< Partial<{ - // we must match any return type - // eslint-disable-next-line @typescript-eslint/no-explicit-any - [K in keyof S['properties']]: EnvSchemaConvertFn; + [K in KeyOf]: EnvSchemaConvertFn; }> >; @@ -180,49 +204,61 @@ export type EnvSchemaCustomConverters = Readonly< * - serialize the post-validated value back to string * - convert the JSON type to native type (ie: `Date`) */ -export type EnvSchemaCustomizations = +export type EnvSchemaCustomizations< + S extends BaseEnvSchema, + V extends BaseEnvParsed, +> = | Readonly<{ - convert?: EnvSchemaCustomConverters; - parse?: EnvSchemaCustomParsers; - postValidate?: EnvSchemaCustomPostValidators; - serialize?: EnvSchemaCustomSerializers; + convert?: EnvSchemaCustomConverters; + parse?: EnvSchemaCustomParsers; + postValidate?: EnvSchemaCustomPostValidators; + serialize?: EnvSchemaCustomSerializers; }> | undefined; type EnvSchemaConvertedValue< S extends BaseEnvSchema, - K extends keyof S['properties'], + K extends KeyOf, Convert, -> = Convert extends EnvSchemaConvertFn - ? N - : K extends keyof TypeFromJSONSchema - ? TypeFromJSONSchema[K] - : never; + V extends BaseEnvParsed, +> = Convert extends EnvSchemaConvertFn ? N : V[K]; type EnvSchemaConvertedValuesWithConvertInternal< S extends BaseEnvSchema, - Converters extends EnvSchemaCustomConverters, + V extends BaseEnvParsed, + Converters extends EnvSchemaCustomConverters< + S, + V + > = EnvSchemaCustomConverters, > = { - -readonly [K in keyof S['properties']]: EnvSchemaConvertedValue< + -readonly [K in KeyOf]: EnvSchemaConvertedValue< S, K, - Converters[K] + Converters[K], + V >; }; export type EnvSchemaConverters< S extends BaseEnvSchema, - Customizations extends EnvSchemaCustomizations, -> = Customizations extends { readonly convert: EnvSchemaCustomConverters } + V extends BaseEnvParsed, + Customizations extends EnvSchemaCustomizations< + S, + V + > = EnvSchemaCustomizations, +> = Customizations extends { readonly convert: EnvSchemaCustomConverters } ? Customizations['convert'] : undefined; export type EnvSchemaConvertedPartialValuesWithConvert< S extends BaseEnvSchema, - Converters extends EnvSchemaCustomConverters | undefined, -> = Converters extends EnvSchemaCustomConverters - ? Partial> - : EnvSchemaPartialValues; + Converters, + V extends BaseEnvParsed, +> = Partial< + Converters extends EnvSchemaCustomConverters + ? EnvSchemaConvertedValuesWithConvertInternal + : V +>; /** * Subset of converted properties. If there are no @@ -230,18 +266,24 @@ export type EnvSchemaConvertedPartialValuesWithConvert< */ export type EnvSchemaConvertedPartialValues< S extends BaseEnvSchema, - Customizations extends EnvSchemaCustomizations, + V extends BaseEnvParsed, + Customizations extends EnvSchemaCustomizations< + S, + V + > = EnvSchemaCustomizations, > = EnvSchemaConvertedPartialValuesWithConvert< S, - EnvSchemaConverters + EnvSchemaConverters, + V >; -export type EnvSchemaConvertedValuesWithConvert< +type EnvSchemaConvertedValuesWithConvert< S extends BaseEnvSchema, - Converters extends EnvSchemaCustomConverters | undefined, -> = Converters extends EnvSchemaCustomConverters - ? EnvSchemaConvertedValuesWithConvertInternal - : TypeFromJSONSchema; + Converters, + V extends BaseEnvParsed, +> = Converters extends EnvSchemaCustomConverters + ? EnvSchemaConvertedValuesWithConvertInternal + : V; /** * All converted properties. It assumes there @@ -250,8 +292,13 @@ export type EnvSchemaConvertedValuesWithConvert< */ export type EnvSchemaConvertedValues< S extends BaseEnvSchema, - Customizations extends EnvSchemaCustomizations, + V extends BaseEnvParsed, + Customizations extends EnvSchemaCustomizations< + S, + V + > = EnvSchemaCustomizations, > = EnvSchemaConvertedValuesWithConvert< S, - EnvSchemaConverters + EnvSchemaConverters, + V >; diff --git a/lib/validate.test.ts b/lib/validate.test.ts index 516e659..f638979 100644 --- a/lib/validate.test.ts +++ b/lib/validate.test.ts @@ -3,11 +3,9 @@ process.env.VALIDATED_ENV_SCHEMA_DEBUG = 'true'; import Ajv from 'ajv'; -import { - EnvSchemaMaybeErrors, - EnvSchemaPartialValues, - schemaProperties, -} from './types'; +import type { TypeFromJSONSchema } from '@profusion/json-schema-to-typescript-definitions'; + +import { schemaProperties } from './types'; import createValidate from './validate'; const getConsoleMock = (): jest.SpyInstance => @@ -37,7 +35,7 @@ describe('createValidate', (): void => { required: ['REQ_VAR'], type: 'object', } as const; - type S = typeof schema; + type V = TypeFromJSONSchema; it('works with valid data', (): void => { expect( @@ -107,8 +105,7 @@ describe('createValidate', (): void => { a: ['2', '3'], s: true, }, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - } as any, + }, undefined, ), ).toEqual([ @@ -138,8 +135,7 @@ describe('createValidate', (): void => { a: [2, 3], s: 'hello', }, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - } as any, + }, undefined, ), ).toEqual([ @@ -171,8 +167,7 @@ describe('createValidate', (): void => { )( { OPT_VAR: 1, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - } as any, + }, undefined, ), ).toEqual([ @@ -201,8 +196,7 @@ describe('createValidate', (): void => { { OPT_VAR: 1, REQ_VAR: '{"bug":"not-an-object"}', - // eslint-disable-next-line @typescript-eslint/no-explicit-any - } as any, + }, undefined, ), ).toEqual([ @@ -233,8 +227,7 @@ describe('createValidate', (): void => { a: [2, 'bug'], s: 'hello', }, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - } as any, + }, undefined, ), ).toEqual([ @@ -264,8 +257,7 @@ describe('createValidate', (): void => { REQ_VAR: { a: [1, 2], }, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - } as any, + }, undefined, ), ).toEqual([ @@ -292,18 +284,17 @@ describe('createValidate', (): void => { a: ['2', '3'], s: true, }, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - } as any; + }; expect( createValidate(schema, schemaProperties(schema), { OPT_VAR: ( value: number | undefined, - propertySchema: S['properties']['OPT_VAR'], - key: string, - allSchema: S, - allValues: EnvSchemaPartialValues, - errors: Readonly>, - ): number | undefined => + propertySchema, + key, + allSchema, + allValues, + errors, + ) => value === 1 && propertySchema === schema.properties.OPT_VAR && key === 'OPT_VAR' && @@ -332,19 +323,18 @@ describe('createValidate', (): void => { a: ['2', '3'], s: true, }, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - } as any; + }; const consoleSpy = getConsoleMock(); expect( createValidate(schema, schemaProperties(schema), { OPT_VAR: ( value: number | undefined, - propertySchema: S['properties']['OPT_VAR'], - key: string, - allSchema: S, - allValues: EnvSchemaPartialValues, - errors: Readonly>, - ): number | undefined => + propertySchema, + key, + allSchema, + allValues, + errors, + ) => value === 1 && propertySchema === schema.properties.OPT_VAR && key === 'OPT_VAR' && @@ -382,19 +372,18 @@ New Value.....: 1234 a: ['2', '3'], s: true, }, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - } as any; + }; const consoleSpy = getConsoleMock(); expect( createValidate(schema, schemaProperties(schema), { OPT_VAR: ( value: number | undefined, - propertySchema: S['properties']['OPT_VAR'], - key: string, - allSchema: S, - allValues: EnvSchemaPartialValues, - errors: Readonly>, - ): number | undefined => + propertySchema, + key, + allSchema, + allValues, + errors, + ) => value === 1 && propertySchema === schema.properties.OPT_VAR && key === 'OPT_VAR' && @@ -425,8 +414,7 @@ New Value.....: 1234 a: ['2', '3'], s: true, }, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - } as any; + }; const error = new Error('forced error'); const consoleSpy = getConsoleMock(); expect( @@ -457,13 +445,12 @@ New Value.....: 1234 const values = { OPT_VAR: '1', REQ_VAR: '{"bug":"not-an-object"}', - // eslint-disable-next-line @typescript-eslint/no-explicit-any - } as any; + }; const error = new Error('forced error'); const consoleSpy = getConsoleMock(); expect( createValidate(schema, schemaProperties(schema), { - REQ_VAR: (): EnvSchemaPartialValues['REQ_VAR'] => { + REQ_VAR: (): V['REQ_VAR'] => { throw error; }, })(values, undefined), diff --git a/lib/validate.ts b/lib/validate.ts index ef5d59f..38aa139 100644 --- a/lib/validate.ts +++ b/lib/validate.ts @@ -1,13 +1,13 @@ import Ajv, { ErrorObject } from 'ajv'; +import type { TypeFromJSONSchema } from '@profusion/json-schema-to-typescript-definitions'; import type { + BaseEnvParsed, BaseEnvSchema, EnvSchemaCustomPostValidators, EnvSchemaMaybeErrors, - EnvSchemaPostValidateFn, EnvSchemaProperties, - EnvSchemaPropertyValue, - EnvSchemaPartialValues, + KeyOf, } from './types'; import { addErrors, assertIsError } from './errors'; @@ -42,60 +42,65 @@ try { } // DO NOT THROW HERE! -type EnvSchemaValidate = ( - value: EnvSchemaPartialValues, +type EnvSchemaValidate< + S extends BaseEnvSchema, + V extends BaseEnvParsed = TypeFromJSONSchema, +> = ( + value: Partial, errors: EnvSchemaMaybeErrors, -) => [EnvSchemaPartialValues, EnvSchemaMaybeErrors]; +) => [Partial, EnvSchemaMaybeErrors]; -const createPostValidation = ( +const createPostValidation = < + S extends BaseEnvSchema, + V extends BaseEnvParsed = TypeFromJSONSchema, +>( schema: S, properties: Readonly>, - customize: EnvSchemaCustomPostValidators, -): EnvSchemaValidate => { + customize: EnvSchemaCustomPostValidators, +): EnvSchemaValidate => { const postValidatedProperties = properties.filter( ([key]) => customize[key] !== undefined, ); return ( - values: EnvSchemaPartialValues, + values: Partial, initialErrors: EnvSchemaMaybeErrors, - ): [EnvSchemaPartialValues, EnvSchemaMaybeErrors] => { + ): [Partial, EnvSchemaMaybeErrors] => { let errors = initialErrors; postValidatedProperties.forEach(([key, propertySchema]) => { - type K = typeof key; // it was filtered before - const validate = customize[key] as EnvSchemaPostValidateFn; + const validate = customize[key] as NonNullable< + (typeof customize)[string] + >; const oldValue = values[key]; try { const newValue = validate( - oldValue as EnvSchemaPropertyValue | undefined, + oldValue, propertySchema, key, schema, values, errors, ); - if (oldValue !== newValue) { - if (newValue === undefined) { - dbg( - () => - `Post validation of "${key}" removed property. Was ${JSON.stringify( - oldValue, - )}`, - ); - // eslint-disable-next-line no-param-reassign - delete values[key]; - } else { - dbg( - () => - `\ + if (oldValue !== newValue && newValue === undefined) { + dbg( + () => + `Post validation of "${key}" removed property. Was ${JSON.stringify( + oldValue, + )}`, + ); + // eslint-disable-next-line no-param-reassign + delete values[key]; + } else { + dbg( + () => + `\ Post validation of "${key}" changed property from: Previous Value: ${JSON.stringify(oldValue)} New Value.....: ${JSON.stringify(newValue)} `, - ); - // eslint-disable-next-line no-param-reassign - values[key] = newValue; - } + ); + // eslint-disable-next-line no-param-reassign + values[key] = newValue; } } catch (e) { dbg( @@ -114,33 +119,39 @@ New Value.....: ${JSON.stringify(newValue)} }; }; -const noPostValidation = ( - values: EnvSchemaPartialValues, +const noPostValidation = < + S extends BaseEnvSchema, + V extends BaseEnvParsed = TypeFromJSONSchema, +>( + values: Partial, errors: EnvSchemaMaybeErrors, -): [EnvSchemaPartialValues, EnvSchemaMaybeErrors] => [values, errors]; +): [Partial, EnvSchemaMaybeErrors] => [values, errors]; const createExceptionForAjvError = (ajvError: ErrorObject): Error => new Ajv.ValidationError([ajvError]); -const processAjvTopLevelError = ( +const processAjvTopLevelError = < + S extends BaseEnvSchema, + V extends BaseEnvParsed = TypeFromJSONSchema, +>( schema: S, - key: Extract, + key: KeyOf, _path: string[], - values: EnvSchemaPartialValues, + values: Partial, ajvError: ErrorObject, ): void => { - const defVal = schema.properties[key].default; - if (defVal !== undefined) { + const defaultValue = schema.properties[key].default as V[KeyOf]; + if (defaultValue) { dbg( () => `Ajv failed the validation of "${key}": ${ajv.errorsText([ ajvError, - ])}. Use default ${JSON.stringify(defVal)}. Was ${JSON.stringify( + ])}. Use default ${JSON.stringify(defaultValue)}. Was ${JSON.stringify( values[key], )}`, ); // eslint-disable-next-line no-param-reassign - values[key] = defVal as EnvSchemaPropertyValue; + values[key] = defaultValue; return; } @@ -159,11 +170,14 @@ const processAjvTopLevelError = ( // array, const, oneOf/allOf/anyOf/not... const processAjvNestedError = processAjvTopLevelError; -const processSingleAjvError = ( +const processSingleAjvError = < + S extends BaseEnvSchema, + V extends BaseEnvParsed = TypeFromJSONSchema, +>( schema: S, - key: Extract, + key: KeyOf, path: string[], - values: EnvSchemaPartialValues, + values: Partial, ajvError: ErrorObject, errors: EnvSchemaMaybeErrors, ): EnvSchemaMaybeErrors => { @@ -187,10 +201,13 @@ const processSpuriousAjvError = ( return addErrors(errors, '$other', createExceptionForAjvError(ajvError)); }; -const processAjvErrors = ( +const processAjvErrors = < + S extends BaseEnvSchema, + V extends BaseEnvParsed = TypeFromJSONSchema, +>( schema: Readonly, - schemaKeys: Readonly>>, - values: EnvSchemaPartialValues, + schemaKeys: Readonly>>, + values: Partial, ajvErrors: readonly ErrorObject[], ): EnvSchemaMaybeErrors => ajvErrors.reduce( @@ -201,7 +218,7 @@ const processAjvErrors = ( /* istanbul ignore else */ if (ajvError.instancePath.startsWith('/')) { const path = ajvError.instancePath.substr(1).split('/'); - const key = path[0] as Extract; + const key = path[0]; /* istanbul ignore else */ if (schemaKeys.has(key)) { return processSingleAjvError( @@ -233,11 +250,14 @@ const processAjvErrors = ( undefined, ); -export default ( +export default < + S extends BaseEnvSchema, + V extends BaseEnvParsed = TypeFromJSONSchema, +>( schema: Readonly, properties: Readonly>, - customize: EnvSchemaCustomPostValidators | undefined, -): EnvSchemaValidate => { + customize: EnvSchemaCustomPostValidators | undefined, +): EnvSchemaValidate => { const validate = ajv.compile(schema); const postValidate = customize === undefined @@ -245,14 +265,19 @@ export default ( : createPostValidation(schema, properties, customize); const schemaKeys = new Set(properties.map(([key]) => key)); return ( - values: EnvSchemaPartialValues, + values: Partial, initialErrors: EnvSchemaMaybeErrors, - ): [EnvSchemaPartialValues, EnvSchemaMaybeErrors] => { + ): [Partial, EnvSchemaMaybeErrors] => { let errors = initialErrors; if (!validate(values)) { /* istanbul ignore else */ if (validate.errors && validate.errors.length > 0) { - errors = processAjvErrors(schema, schemaKeys, values, validate.errors); + errors = processAjvErrors( + schema, + schemaKeys, + values, + validate.errors, + ); } } return postValidate(values, errors); diff --git a/yarn.lock b/yarn.lock index 5245bb7..91b8fe8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -105,7 +105,7 @@ "@babel/traverse" "^7.21.5" "@babel/types" "^7.21.5" -"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.12.13", "@babel/helper-plugin-utils@^7.14.5", "@babel/helper-plugin-utils@^7.20.2", "@babel/helper-plugin-utils@^7.8.0": +"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.12.13", "@babel/helper-plugin-utils@^7.20.2", "@babel/helper-plugin-utils@^7.8.0": version "7.21.5" resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.21.5.tgz#345f2377d05a720a4e5ecfa39cbf4474a4daed56" integrity sha512-0WDaIlXKOX/3KfBK/dwP1oQGiPh6rjMkT7HIRv7i5RR2VUMwrx5ZL0dwBkKx7+SW1zwNdgjHd34IMk5ZjTeHVg== @@ -157,7 +157,7 @@ chalk "^2.0.0" js-tokens "^4.0.0" -"@babel/parser@^7.1.0", "@babel/parser@^7.14.7", "@babel/parser@^7.20.7", "@babel/parser@^7.21.5", "@babel/parser@^7.21.8": +"@babel/parser@^7.1.0", "@babel/parser@^7.12.13", "@babel/parser@^7.14.7", "@babel/parser@^7.20.7", "@babel/parser@^7.21.5", "@babel/parser@^7.21.8": version "7.21.8" resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.21.8.tgz#642af7d0333eab9c0ad70b14ac5e76dbde7bfdf8" integrity sha512-6zavDGdzG3gUqAdWvlLFfk+36RilI+Pwyuuh7HItyeScCWP3k6i8vKclAQ0bM/0y/Kz/xiwvxhMv9MgTJP5gmA== @@ -247,11 +247,11 @@ "@babel/helper-plugin-utils" "^7.8.0" "@babel/plugin-syntax-top-level-await@^7.8.3": - version "7.14.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz#c1cfdadc35a646240001f06138247b741c34d94c" - integrity sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw== + version "7.12.13" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.12.13.tgz#c5f0fa6e249f5b739727f923540cf7a806130178" + integrity sha512-A81F9pDwyS7yM//KwbCSDqy3Uj4NMIurtplxphWxoYtNPov7cJsDkAFNNyVlIZ3jwGycVsurZ+LtOA8gZ376iQ== dependencies: - "@babel/helper-plugin-utils" "^7.14.5" + "@babel/helper-plugin-utils" "^7.12.13" "@babel/plugin-syntax-typescript@^7.7.2": version "7.21.4" @@ -260,7 +260,7 @@ dependencies: "@babel/helper-plugin-utils" "^7.20.2" -"@babel/template@^7.20.7", "@babel/template@^7.3.3": +"@babel/template@^7.20.7": version "7.20.7" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.20.7.tgz#a15090c2839a83b02aa996c0b4994005841fd5a8" integrity sha512-8SegXApWe6VoNw0r9JHpSteLKTpTiLZ4rMlGIm9JQ18KiCtyQiAMEazujAHrUS5flrcqYZa75ukev3P6QmUwUw== @@ -269,6 +269,15 @@ "@babel/parser" "^7.20.7" "@babel/types" "^7.20.7" +"@babel/template@^7.3.3": + version "7.12.13" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.12.13.tgz#530265be8a2589dbb37523844c5bcb55947fb327" + integrity sha512-/7xxiGA57xMo/P2GVvdEumr8ONhFOhfgq2ihK3h1e6THqzTAkHbkXgB0xI9yeTfIUoH3+oAeHhqm/I43OTbbjA== + dependencies: + "@babel/code-frame" "^7.12.13" + "@babel/parser" "^7.12.13" + "@babel/types" "^7.12.13" + "@babel/traverse@^7.21.5", "@babel/traverse@^7.7.2": version "7.21.5" resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.21.5.tgz#ad22361d352a5154b498299d523cf72998a4b133" @@ -285,7 +294,7 @@ debug "^4.1.0" globals "^11.1.0" -"@babel/types@^7.0.0", "@babel/types@^7.18.6", "@babel/types@^7.20.7", "@babel/types@^7.21.0", "@babel/types@^7.21.4", "@babel/types@^7.21.5", "@babel/types@^7.3.0", "@babel/types@^7.3.3": +"@babel/types@^7.0.0", "@babel/types@^7.12.13", "@babel/types@^7.18.6", "@babel/types@^7.20.7", "@babel/types@^7.21.0", "@babel/types@^7.21.4", "@babel/types@^7.21.5", "@babel/types@^7.3.0", "@babel/types@^7.3.3": version "7.21.5" resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.21.5.tgz#18dfbd47c39d3904d5db3d3dc2cc80bedb60e5b6" integrity sha512-m4AfNvVF2mVC/F7fDEdH2El3HzUg9It/XsCxZiOTTA3m3qYfcSVSbTfM6Q9xG+hYDniZssYhlXKKUMD5m8tF4Q==