From b621799caf7ed14d4567bbdf4d3a61f0d51b5eb0 Mon Sep 17 00:00:00 2001 From: Vitali Haradkou Date: Tue, 10 Sep 2024 00:18:08 +0300 Subject: [PATCH 1/7] chore: thee shake --- UPCOMING.md | 1 + src/builder.ts | 24 +- src/builders/base.ts | 556 +++++++++++++++++++++++++++++++++++++++++ src/builders/number.ts | 0 src/builders/type.ts | 0 tests/array.test.ts | 3 +- tests/string.test.ts | 7 + 7 files changed, 580 insertions(+), 11 deletions(-) create mode 100644 src/builders/base.ts create mode 100644 src/builders/number.ts create mode 100644 src/builders/type.ts diff --git a/UPCOMING.md b/UPCOMING.md index cad621d..5a98545 100644 --- a/UPCOMING.md +++ b/UPCOMING.md @@ -9,3 +9,4 @@ - [#61](https://github.com/vitalics/ajv-ts/issues/61) ## 🏡 Chore/Infra +- integer checking for parse methods diff --git a/src/builder.ts b/src/builder.ts index c1bb480..aeb18d9 100644 --- a/src/builder.ts +++ b/src/builder.ts @@ -876,6 +876,7 @@ function integer() { export type StringBuilderOpts = { minLength?: number maxLength?: number, + pattern?: string | RegExp } class StringSchemaBuilder< const S extends string = string, @@ -925,10 +926,14 @@ class StringSchemaBuilder< * const str1 = prefixS.parse("qwe") // Error * const str2 = prefixS.parse("S_Some") // OK */ - pattern( - pattern: string, - ): StringSchemaBuilder { - this.schema.pattern = pattern; + pattern( + pattern: In, + ): StringSchemaBuilder> { + if (typeof pattern === 'string') { + this.schema.pattern = pattern; + } else if (pattern instanceof RegExp) { + this.schema.pattern = pattern.source + } return this as never; } @@ -950,12 +955,12 @@ class StringSchemaBuilder< minLength< const L extends number, Valid = IsPositiveInteger, - MinLengthValid extends boolean = GreaterThan + MinLengthValid = GreaterThan, >( value: Valid extends true - ? MinLengthValid extends true + ? Opts['maxLength'] extends undefined ? L - : TRangeGenericError<`MinLength are greater than MaxLength. MinLength: ${L}. MaxLength: ${Opts['maxLength']}`> + : MinLengthValid extends true ? L : TRangeGenericError<`MinLength are greater than MaxLength. MinLength: ${L}. MaxLength: ${Opts['maxLength']}`> : TTypeGenericError< `Only Positive and non floating numbers are supported. Received: '${L}'` > @@ -980,9 +985,10 @@ class StringSchemaBuilder< maxLength< const L extends number, Valid = IsPositiveInteger, - MinLengthValid = LessThan>( + MinLengthValid = GreaterThan>( value: Valid extends true - ? MinLengthValid extends true ? L + ? Opts['minLength'] extends undefined ? L + : MinLengthValid extends true ? L : TRangeGenericError<`MinLength are greater than MaxLength. MinLength: ${Opts['minLength']}. MaxLength: ${L}`> : TTypeGenericError<`Expected positive integer. Received: '${L}'`>, ): StringSchemaBuilder { diff --git a/src/builders/base.ts b/src/builders/base.ts new file mode 100644 index 0000000..502f766 --- /dev/null +++ b/src/builders/base.ts @@ -0,0 +1,556 @@ +import Ajv from 'ajv'; +import { AnySchemaOrAnnotation } from '../schema/types' +export class SchemaBuilder< + Input = unknown, + Schema extends AnySchemaOrAnnotation = AnySchemaOrAnnotation, + Output = Input, +> { + /** + * type helper. Do Not Use! + */ + _input!: Input; + + /** + * type helper. Do Not Use! + */ + _output!: Output; + private _schema: Schema; + private _shape: Schema; + + /** + * returns JSON-schema representation + */ + get schema() { + return this._schema; + } + /** + * returns JSON-schema representation. same as `schema` does. + * @satisfies zod API + */ + get shape() { + return this._shape; + } + + /** + * Set custom JSON-Schema representation. + * Updates `shape` property too + */ + set schema(schema) { + this._schema = schema; + this._shape = schema; + } + + /** + * Set custom JSON-Schema representation. + * Updates `schema` property too + * @satisfies zod API + */ + set shape(schema) { + this._schema = schema; + this._shape = schema; + } + + /** Returns your ajv instance */ + get ajv() { + return this._ajv; + } + + /** + * Set Ajv Instance. + * @throws `TypeError` if not ajv instance comes + */ + set ajv(instance: Ajv) { + if (!(instance instanceof Ajv)) { + throw new TypeError(`Cannot set ajv variable for non-ajv instance.`, { + cause: { type: typeof instance, value: instance }, + }); + } + this._ajv = instance; + } + + constructor( + schema: Schema, + private _ajv = DEFAULT_AJV, + ) { + this._schema = schema; + this._shape = schema; + } + protected isNullable = false; + + /** + * set custom JSON-schema field. Useful if you need to declare something but no api founded for built-in solution. + * + * Example: `If-Then-Else` you cannot declare without `custom` method. + * @example + * const myObj = s.object({ + * foo: s.string(), + * bar: s.string() + * }).custom('if', { + * "properties": { + * "foo": { "const": "bar" } + * }, + * "required": ["foo"] + * }).custom('then', { "required": ["bar"] }) + */ + custom( + key: string, + value: V, + ): Result { + (this.schema as Record)[key] = value; + return this as never; + } + + /** + * Marks your property as nullable (`undefined`) + * + * **NOTES**: json-schema not accept `undefined` type. It's just `nullable` as typescript `undefined` type. + */ + optional(): SchemaBuilder { + return this.nullable() as never; + } + + /** + * Marks your property as nullable (`null`). + * + * Updates `type` property for your schema. + * @example + * const schemaDef = s.string().nullable() + * schemaDef.schema // { type: ['string', 'null'], nullable: true } + */ + nullable(): SchemaBuilder { + this.isNullable = true; + (this.schema as any).nullable = true; + const type = (this.schema as any).type; + if (Array.isArray(type)) { + (this.schema as any).type = [...new Set([...type, "null"])]; + } else { + (this.schema as any).type = [...new Set([type, "null"])]; + } + return this as never; + } + + private preFns: Function[] = []; + + /** + * pre process function for incoming result. Transform input **BEFORE** calling `parse`, `safeParse`, `validate` functions + * + * **NOTE:** this functions works BEFORE parsing. use it at own risk. (e.g. transform Date object into string) + * @see {@link SchemaBuilder.parse parse} method + * @see {@link SchemaBuilder.safeParse safe parse} method + * @see {@link SchemaBuilder.validate validate} method + * @example + * const myString = s.string().preprocess(v => { + * // if date => transform to ISO string + * if(v instanceof Date) { + * return Date.toISOString() + * } + * // do nothing if not a date + * return v + * }) + * const res = myString.parse(new Date()) // '2023-09-23T07:10:57.881Z' + * const res = myString.parse('qwe') // 'qwe' + * const res = myString.parse({}) // error: not a string + */ + preprocess< + const In, + const Out, + const F extends Fn, + >(fn: F): this { + if (typeof fn !== "function") { + throw new TypeError(`Cannot use not a function for pre processing.`, { + cause: { type: typeof fn, value: fn }, + }); + } + this.preFns.push(fn); + return this as never; + } + + private postFns: { fn: Function; schema: AnySchemaBuilder }[] = []; + /** + * Post process. Use it when you would like to transform result after parsing is happens. + * + * **NOTE:** this function override your `input` variable for `safeParse` calling. + * @see {@link SchemaBuilder.safeParse safeParse method} + */ + postprocess( + fn: Fn, + schema: S, + ): this { + if (typeof fn !== "function") { + throw new TypeError(`Cannot use not a function for pre processing.`, { + cause: { type: typeof fn, value: fn }, + }); + } + this.postFns.push({ fn, schema }); + return this as never; + } + + private refineFns: Function[] = []; + /** + * Set custom validation. Any result exept `undefined` will throws. + * @param fn function that will be called after `safeParse`. Any result will throws + * @example + * import s from 'ajv-ts' + * // example: object with only 1 "active element" + * const Schema = s.object({ + * active: s.boolean(), + * name: s.string() + * }).array().refine((arr) => { + * const subArr = arr.filter(el => el.active === true) + * if (subArr.length > 1) throw new Error('Array should contains only 1 "active" element') + * }) + * + * Schema.parse([{ active: true, name: 'some 1' }, { active: true, name: 'some 2' }]) // throws Error + */ + refine(fn: (output: Output) => any) { + if (typeof fn !== "function") { + throw new TypeError( + `Cannot set for not a function for refine. Expect "function", Got: ${typeof fn}`, + { cause: { fn, type: typeof fn } }, + ); + } + this.refineFns.push(fn); + return this; + } + + /** + * Meta object. Adds meta information fields in your schema, such as `deprecated`, `description`, `$id`, `title` and more! + */ + meta(obj: MetaObject) { + Object.entries(obj).forEach(([key, value]) => { + this.custom(key, value); + }); + return this; + } + + /** + * Option `default` keywords throws exception during schema compilation when used in: + * + * - not in `properties` or `items` subschemas + * - in schemas inside `anyOf`, `oneOf` and `not` ({@link https://github.com/ajv-validator/ajv/issues/42 #42}) + * - in `if` schema + * - in schemas generated by user-defined _macro_ keywords + * This means only `object()` and `array()` buidlers are supported. + * @see {@link object} + * @see {@link array} + * @example + * import s from 'ajv-ts' + * const Person = s.object({ + * age: s.int().default(18) + * }) + * Person.parse({}) // { age: 18 } + */ + default(value: Output) { + (this.schema as AnySchema).default = value; + return this; + } + /** + * Defines custom error message for invalid schema. + * + * Set `schema.errorMessage = message` under the hood. + * @example + * // number example + * const numberSchema = s.number().error('Not a number') + * numberSchema.parse('qwe') // error: Not a number + */ + error(messageOrOptions: string | ErrorMessageParams) { + (this.schema as AnySchema).errorMessage = messageOrOptions; + return this; + } + + /** + * set `description` for your schema. + * You can use `meta` method to provide information in more consistant way. + * @see {@link SchemaBuilder.meta meta} method + * @satisfies `zod` API + */ + describe(message: string) { + return this.meta({ description: message }); + } + + /** + * set `$async=true` for your current schema. + * + * @see {@link https://ajv.js.org/guide/async-validation.html ajv async validation} + */ + async() { + (this.schema as Record).$async = true; + return this; + } + + /** + * set `$async=false` for your current schema. + * @param [remove=false] applies `delete` operator for `schema.$async` property. + */ + sync(remove: boolean = false) { + (this.schema as AnySchema).$async = false; + if (remove) { + delete (this.schema as AnySchema).$async; + } + return this; + } + + /** + * Construct Array schema. Same as `s.array(s.number())` + * + * @see {@link array} + */ + array>(): ArraySchemaBuilder { + return array(this) as never; + } + + /** + * Same as `s.and()`. Combine current type with another. Logical "AND" + * + * Typescript `A & B` + */ + intersection: typeof this.and = this.and; + /** + * Same as `s.and()`. Combine current type with another. Logical "AND" + * + * Typescript `A & B` + */ + and< + S extends AnySchemaBuilder[] = AnySchemaBuilder[], + Arr extends AnySchemaBuilder[] = [this, ...S], + // @ts-ignore - IntersectionSchemaBuilder circular return itself 2577 + >(...others: S): IntersectionSchemaBuilder { + return and(this, ...others) as never; + } + /** + * Same as `s.or()`. Combine current type with another type. Logical "OR" + * + * Typescript: `A | B` + */ + or( + ...others: S + ): UnionSchemaBuilder<[this, ...S]> { + return or(this, ...others); + } + /** + * Same as `s.or()`. Combine current type with another type. Logical "OR" + * + * Typescript: `A | B` + */ + union: typeof this.or = this.or; + + /** + * Exclude given subschema. + * + * Append `not` keyword for your schema + * + * @see {@link not} + * @see {@link SchemaBuilder.not not method} + * @example + * cosnt res = s + * .string<'Jerry' | 'Martin'>() + * .exclude(s.const('Jerry')) + * .schema // {type: "string", not: {const: "Jerry"} } + * type Res = s.infer // 'Martin' + */ + exclude< + S extends SchemaBuilder = SchemaBuilder, + Excl = Exclude, + This = this extends StringSchemaBuilder + ? StringSchemaBuilder + : this extends NumberSchemaBuilder + ? NumberSchemaBuilder + : this extends BooleanSchemaBuilder + ? BooleanSchemaBuilder + : this extends ArraySchemaBuilder + ? ArraySchemaBuilder> + : this extends ObjectSchemaBuilder< + infer Def extends ObjectDefinition + > + ? ObjectSchemaBuilder> + : this, + >(s: S): This { + (this.schema as AnySchema).not = s.schema; + return this as never; + } + + /** + * Exclude self schema. + * + * Wrap your schema with `not` keyword + * + * `s.not(s.string())` === `s.string().not()` + * + * If you need to append `not` keyword instead of wrap you might need to use {@link SchemaBuilder.exclude `exclude`} method + * + * @see {@link not} + * @see {@link SchemaBuilder.exclude exclude method} + * + * @example + * // not string + * s + * .string() + * .not() + * .schema // {not: { type: "string" }}, + */ + not(): NotSchemaBuilder { + return not(this) as never; + } + + private _transform( + input?: unknown, + arr: ({ fn: Function; schema: AnySchemaBuilder } | Function)[] = [], + ): SafeParseResult { + let output; + if (Array.isArray(arr) && arr.length > 0) { + try { + output = arr.reduce( + (prevResult, el) => { + if (!prevResult.success) { + throw prevResult.error; + } + let fnTransform; + let result: SafeParseResult = { + data: input, + input, + success: true, + }; + if (typeof el === "function") { + fnTransform = el(prevResult.data, this); + result.data = fnTransform; + } else { + fnTransform = el.fn(prevResult.data, this); + result = el.schema.safeParse(fnTransform); + } + return result; + }, + { input, data: input, success: true } as SafeParseResult, + ); + } catch (e) { + return { + success: false, + error: new Error((e as Error).message, { cause: e }), + input, + }; + } + return output as SafeParseResult; + } + output = input; + return { data: output as Out, input, success: true }; + } + + private _safeParseRaw(input?: unknown): SafeParseResult { + let success = false; + try { + const validateFn = this.ajv.compile(this.schema); + success = validateFn(input); + if (!success) { + const firstError = validateFn.errors?.at(0); + return { + error: new Error(firstError?.message, { + cause: validateFn.errors, + }), + success, + input, + }; + } + } catch (e) { + return { + error: new Error((e as Error).message, { cause: e }), + success: success as false, + input, + }; + } + return { + input, + data: input, + success, + }; + } + + /** + * Parse you input result. Used `ajv.validate` under the hood + * + * It also applies your `postProcess` functions if parsing was successfull + */ + safeParse(input?: I): SafeParseResult { + // need to remove schema, or we get precompiled result. It's bad for `extend` and `merge` in object schema + // TODO: investigate merge and add in ajv + // this.ajv.removeSchema(this.schema); + let preTransformedResult = this._transform(input, this.preFns); + preTransformedResult.input = input; + if (!preTransformedResult.success) { + return preTransformedResult as never; + } + + const parseResult = this._safeParseRaw(preTransformedResult.data); + parseResult.input = input; + if (!parseResult.success) { + return parseResult as never; + } + + const postTransformedResult = this._transform( + parseResult.data, + this.postFns, + ); + postTransformedResult.input = input; + if (!postTransformedResult.success) { + return postTransformedResult; + } + if ( + this.refineFns && + Array.isArray(this.refineFns) && + this.refineFns.length > 0 + ) { + for (const refine of this.refineFns) { + try { + const res = refine(postTransformedResult.data); + if (res !== undefined) { + return { + success: false, + error: new Error(`refine error`, { + cause: { + refine: res, + debug: { + input, + preTransformedResult, + parseResult, + postTransformedResult, + }, + }, + }), + input: postTransformedResult.data, + }; + } + } catch (e) { + return { + success: false, + error: e as Error, + input: postTransformedResult.data, + }; + } + } + } + return postTransformedResult; + } + /** + * Validate your schema. + * + * @returns {boolean} Validity of your schema + */ + validate(input?: unknown): input is Output { + const { success } = this.safeParse(input); + return success; + } + + /** + * Parse input for given schema. + * + * @returns {Output} parsed output result. + * @throws `Error` when input not match given schema + */ + parse< + const I, + >(input?: I): Output { + const result = this.safeParse(input); + if (!result.success) { + throw result.error; + } + return result.data as never; + } +} \ No newline at end of file diff --git a/src/builders/number.ts b/src/builders/number.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/builders/type.ts b/src/builders/type.ts new file mode 100644 index 0000000..e69de29 diff --git a/tests/array.test.ts b/tests/array.test.ts index ce77336..935743f 100644 --- a/tests/array.test.ts +++ b/tests/array.test.ts @@ -10,13 +10,12 @@ const intNum = s.array(s.string()).nonEmpty(); const nonEmptyMax = s.array(s.string()).nonEmpty().maxLength(2); const nonEmpty = s.array(s.string()).nonEmpty(); - test('types', () => { type t0 = s.infer assertType([]) type t1 = s.infer; - type A= typeof nonEmptyMax + type A = typeof nonEmptyMax assertType(['string', 'sd']); diff --git a/tests/string.test.ts b/tests/string.test.ts index 416b5ca..2dd866b 100644 --- a/tests/string.test.ts +++ b/tests/string.test.ts @@ -22,3 +22,10 @@ test('should pass validation', () => { optionalStr.parse(null) expect(() => optionalStr.parse(undefined)).toThrow() }) + +test.todo('pattern should be applied for RegExp instance', () => { + const etalon = s.string().pattern('^(\\([0-9]{3}\\))?[0-9]{3}-[0-9]{4}$') + const withRegExp = s.string().pattern(new RegExp('^(\\([0-9]{3}\\))?[0-9]{3}-[0-9]{4}$')) + + etalon.parse() +}) From 5173eddc050ddc0fa91bbc0253c384519a1b81c6 Mon Sep 17 00:00:00 2001 From: Vitali Haradkou Date: Tue, 10 Sep 2024 08:48:51 +0300 Subject: [PATCH 2/7] feat: strict number --- .changeset/chilled-seahorses-serve.md | 5 + README.md | 77 +++- package.json | 5 +- pnpm-lock.yaml | 9 + src/builder.ts | 225 ++++++++--- src/builders/base.ts | 556 -------------------------- src/builders/number.ts | 0 src/builders/type.ts | 0 src/types/array.d.ts | 2 +- src/types/misc.d.ts | 2 + src/types/number.d.ts | 18 +- tests/number.test.ts | 26 ++ 12 files changed, 302 insertions(+), 623 deletions(-) create mode 100644 .changeset/chilled-seahorses-serve.md delete mode 100644 src/builders/base.ts delete mode 100644 src/builders/number.ts delete mode 100644 src/builders/type.ts diff --git a/.changeset/chilled-seahorses-serve.md b/.changeset/chilled-seahorses-serve.md new file mode 100644 index 0000000..791c93a --- /dev/null +++ b/.changeset/chilled-seahorses-serve.md @@ -0,0 +1,5 @@ +--- +"ajv-ts": minor +--- + +new: make number more stricter diff --git a/README.md b/README.md index 34c0016..e859582 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,15 @@ - [String](#string) - [Typescript features](#typescript-features) - [Numbers](#numbers) + - [Types](#types) + - [Number](#number) + - [Int](#int) + - [Formats](#formats) + - [int32](#int32) + - [int64](#int64) + - [float](#float) + - [double](#double) + - [Typescript features](#typescript-features-1) - [BigInts](#bigints) - [NaNs](#nans) - [Dates](#dates) @@ -40,7 +49,7 @@ - [`.element`](#element) - [`.nonempty`](#nonempty) - [`.min`/`.max`/`.length`/`.minLength`/`.maxLength`](#minmaxlengthminlengthmaxlength) - - [Typescript features](#typescript-features-1) + - [Typescript features](#typescript-features-2) - [`.unique`](#unique) - [`.contains`/`.minContains`](#containsmincontains) - [Tuples](#tuples) @@ -276,6 +285,72 @@ s.number().nonpositive(); // <= 0 s.number().multipleOf(5); // Evenly divisible by 5. Alias .step(5) ``` +### Types + +#### Number + +Number - any number type + +```ts +s.number() +// same as +s.number().number() +``` + +#### Int + +Only integers values. + +Note: we check in runtime non-integer format (`float`, `double`) and give an error. + +```ts +s.number().int() +// or +s.number().integer() +// or +s.int() +``` + +### Formats + +Defines in [ajv-formats](https://ajv.js.org/packages/ajv-formats.html#formats) package + +#### int32 + +Signed 32 bits integer according to the [openApi 3.0.0 specification](https://spec.openapis.org/oas/v3.0.0#data-types) + +#### int64 + +Signed 64 bits according to the [openApi 3.0.0 specification](https://spec.openapis.org/oas/v3.0.0#data-types) + +#### float + +float: float according to the [openApi 3.0.0 specification](https://spec.openapis.org/oas/v3.0.0#data-types) + +#### double + +double: double according to the [openApi 3.0.0 specification](https://spec.openapis.org/oas/v3.0.0#data-types) + +### Typescript features + +> from 0.8 + +We make validation for number `type`, `format`, `minValue` and `maxValue` fields. That means we handle it in our side so you get an error for invalid values. + +Examples: + +```ts +s.number().format('float').int() // error in type! +s.int().const(3.4) // error in type! +s.number().int().format('float') // error in format! +s.number().int().format('double') // error in format! + +// ranges are also check for possibility + +s.number().min(5).max(3) // error in range! +s.number().min(3).max(5).const(10) // error in constant! +``` + ## BigInts Not supported diff --git a/package.json b/package.json index f7015b3..3f7bb2c 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "main": "dist/index.cjs", "module": "dist/index.mjs", "types": "dist/index.d.ts", - "packageManager": "pnpm@9.4.0", + "packageManager": "pnpm@9.9.0+sha256.7a4261e50d9a44d9240baf6c9d6e10089dcf0a79d0007f2a26985a6927324177", "scripts": { "build": "tsup", "test": "vitest run", @@ -62,6 +62,7 @@ "dependencies": { "ajv": "8.16.0", "ajv-errors": "3.0.0", - "ajv-formats": "3.0.1" + "ajv-formats": "3.0.1", + "type-fest": "4.26.0" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0cce9e6..dc6259b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -17,6 +17,9 @@ importers: ajv-formats: specifier: 3.0.1 version: 3.0.1(ajv@8.16.0) + type-fest: + specifier: 4.26.0 + version: 4.26.0 devDependencies: '@biomejs/biome': specifier: 1.8.3 @@ -2390,6 +2393,10 @@ packages: resolution: {integrity: sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==} engines: {node: '>=8'} + type-fest@4.26.0: + resolution: {integrity: sha512-OduNjVJsFbifKb57UqZ2EMP1i4u64Xwow3NYXUtBbD4vIwJdQd4+xl8YDou1dlm4DVrtwT/7Ky8z8WyCULVfxw==} + engines: {node: '>=16'} + typed-array-buffer@1.0.2: resolution: {integrity: sha512-gEymJYKZtKXzzBzM4jqa9w6Q1Jjm7x2d+sh19AdsD4wqnMPDYyvwpsIc2Q/835kHuo3BEQ7CjelGhfTsoBb2MQ==} engines: {node: '>= 0.4'} @@ -5003,6 +5010,8 @@ snapshots: type-fest@0.8.1: {} + type-fest@4.26.0: {} + typed-array-buffer@1.0.2: dependencies: call-bind: 1.0.7 diff --git a/src/builder.ts b/src/builder.ts index aeb18d9..c6b06f7 100644 --- a/src/builder.ts +++ b/src/builder.ts @@ -2,6 +2,17 @@ import Ajv from "ajv"; import ajvErrors from "ajv-errors"; import addFormats from "ajv-formats"; +import type { + And, + GreaterThan, + GreaterThanOrEqual, + IsFloat, + IsInteger, + LessThan, + LessThanOrEqual, + Or, +} from 'type-fest' + import type { AnySchema, AnySchemaOrAnnotation, @@ -15,18 +26,18 @@ import type { ObjectSchema, StringSchema, } from "./schema/types"; -import { Create, MakeReadonly, Optional, Push } from "./types/array"; +import type { Create, MakeReadonly, Optional } from "./types/array"; import type { + // Debug, Fn, Object as ObjectTypes, UnionToIntersection, UnionToTuple, } from "./types/index"; -import type { GreaterThan, GreaterThanOrEqual, IsPositiveInteger, LessThan } from "./types/number"; import type { OmitByValue, OmitMany, PickMany, Prettify } from "./types/object"; import type { Email, UUID } from "./types/string"; import type { TRangeGenericError, TTypeGenericError } from './types/errors'; - +import type { IsPositiveInteger, NumericStringifyType } from './types/number' /** * Default Ajv instance. * @@ -663,27 +674,40 @@ export class SchemaBuilder< } } +type NumberSchemaOpts = SchemaBuilderOpts & { + const?: number, + type?: 'integer' | 'number', + format?: NumberSchema["format"], + minValue?: number, + maxValue?: number, +} class NumberSchemaBuilder< const N extends number = number, - Opts extends SchemaBuilderOpts = { + Opts extends NumberSchemaOpts = { _preProcesses: [], _postProcesses: [], + const: undefined, + type: 'number', + format: undefined, + minValue: undefined, + maxValue: undefined, } -> extends SchemaBuilder { +> extends SchemaBuilder { constructor() { super({ type: "number" }); } /** - * change schema type from `any integer number` to `any number`. - * + * Define schema as `any number`. * Set schema `{type: 'number'}` * - * This is default behavior + * @note This is default behavior */ - number() { + number(): NumberSchemaBuilder & { + type: 'number', + }>> { this.schema.type = "number"; - return this; + return this as never; } /** @@ -693,32 +717,77 @@ class NumberSchemaBuilder< * a.schema // {type: "number", const: 5} * s.infer // 5 */ - const(value: N): NumberSchemaBuilder { + const< + const N extends number, + TypeValid = Opts['type'] extends 'integer' ? IsInteger : + Or, IsInteger>, + FormatValid = Opts['format'] extends 'int32' ? IsInteger : + Opts['format'] extends 'int64' ? IsInteger + : Or, IsInteger>, + ValueValid = Opts['maxValue'] extends number ? + Opts['minValue'] extends number ? + And, GreaterThanOrEqual>: LessThanOrEqual : true + >(value: TypeValid extends true ? + FormatValid extends true ? + ValueValid extends true ? + N + : TTypeGenericError<`Constant cannot be more than "MaxValue" and less than "MinValue"`, [`MinValue:`, Opts['minValue'], 'MaxValue:', Opts['maxValue']]> + : TTypeGenericError<`Format invalid. Expected ${Opts['format']}. Got "${N}" (${NumericStringifyType})`> + : TTypeGenericError<`Type invalid. Expected ${Opts['type']}. Got "${N}" (${NumericStringifyType})`> + ): NumberSchemaBuilder & { + const: N, + }>> { this.schema.const = value; return this as never; } - /** Set schema `{type: 'integer'}` */ - integer() { + /** + * Define schema as `any integer number`. + * Set schema `{type: 'integer'}` + */ + integer(): NumberSchemaBuilder & { + type: 'integer', + }>> { this.schema.type = "integer"; - return this; + return this as never; } + /** + * Set schema `{type: 'integer'}`. Same as `integer` method + * @see {@link integer integer} method + */ + int = this.integer + + /** * Appends format for your number schema. */ - format(type: NumberSchema["format"]) { - this.schema.format = type; - return this; + format< + const Format extends 'int32' | 'double' | 'int64' | 'float', + FormatValid = Opts['type'] extends 'integer' ? + Format extends 'int32' ? true : + Format extends 'int64' ? true + // type=int. Format float or doouble + : false + // Rest + : true + >(format: FormatValid extends true ? + Format : + TTypeGenericError<`Wrong format for given type. Expected "int32" or "int64". Given: "${Format}"`> + ): NumberSchemaBuilder & { + format: Format, + }>> { + this.schema.format = format as Format; + return this as never; } /** Getter. Retuns `minimum` or `exclusiveMinimum` depends on your schema definition */ - get minValue() { + get minValue(): Opts['minValue'] extends number ? Opts['minValue'] : number { return this.schema.minimum ?? (this.schema.exclusiveMinimum as number); } /** Getter. Retuns `maximum` or `exclusiveMaximum` depends on your schema definition */ - get maxValue() { + get maxValue(): Opts['maxValue'] extends number ? Opts['maxValue'] : number { return this.schema.maximum ?? (this.schema.exclusiveMaximum as number); } @@ -731,13 +800,34 @@ class NumberSchemaBuilder< * s.number().min(2, true) // > 2 * s.number().min(2) // >= 2 */ - minimum(value: number, exclusive = false) { + minimum< + const Min extends number = number, + Exclusive extends boolean = false, + MinLengthValid = Opts['maxValue'] extends number ? GreaterThan : true, + TypeValid = Opts['type'] extends 'integer' ? IsInteger : + Or, IsInteger>, + FormatValid = Opts['format'] extends undefined ? true : Opts['format'] extends 'int32' ? IsInteger : + Opts['format'] extends 'int64' ? IsInteger + : Or, IsInteger>, + >( + value: MinLengthValid extends true ? + TypeValid extends true ? + FormatValid extends true ? + Min + : TTypeGenericError<`Format invalid. Expected ${Opts['format']}. Got "${Min}" (${NumericStringifyType})`> + : TTypeGenericError<`Type invalid. Expected ${Opts['type']}. Got "${Min}" (${NumericStringifyType})`> + : TTypeGenericError<`"MaxValue" is less than "MinValue"`, ['MaxValue:', Opts['maxValue'], "MinValue:", Min]>, + exclusive = false as Exclusive + ): NumberSchemaBuilder & { + minValue: Min, + }> + > { if (exclusive) { - this.schema.exclusiveMinimum = value; + this.schema.exclusiveMinimum = value as Min; } else { - this.schema.minimum = value; + this.schema.minimum = value as Min; } - return this; + return this as never; } step = this.multipleOf; @@ -747,7 +837,7 @@ class NumberSchemaBuilder< * It may be set to any positive number. Same as `step`. * * **NOTE**: Since JSON schema odes not allow to use `multipleOf` with negative value - we use `Math.abs` to transform negative values into positive - * @see {@link NumberSchemaBuilder.step step} + * @see {@link step step} method * @example * const a = s.number().multipleOf(10) * @@ -767,52 +857,87 @@ class NumberSchemaBuilder< /** * marks you number maximum value */ - maximum(value: number, exclusive = false) { + maximum< + const Max extends number = number, + Exclusive extends boolean = false, + FormatValid = Opts['format'] extends undefined ? true : IsInteger extends true ? Opts['format'] extends 'int32' ? true : Opts['format'] extends 'int64' ? true : false : true, + TypeValid = Opts['type'] extends 'integer' ? IsInteger : + Or, IsInteger>, + MinLengthValid = Opts['minValue'] extends number ? LessThan : true, + >( + value: MinLengthValid extends true ? + TypeValid extends true ? + FormatValid extends true ? + Max + : TTypeGenericError<`Format invalid. Expected ${Opts['format']}. Got "${Max}" (${NumericStringifyType})`> + : TTypeGenericError<`Type invalid. Expected ${Opts['type']}. Got "${Max}" (${NumericStringifyType})`> + : TTypeGenericError<`"MinValue" greater than "MaxValue"`, ['MinValue:', Opts['minValue'], 'MaxValue:', Max]>, + exclusive = false as Exclusive + ): NumberSchemaBuilder< + N, Prettify & { + maxValue: Max, + } + >> { if (exclusive) { - this.schema.exclusiveMaximum = value; + this.schema.exclusiveMaximum = value as Max; } else { - this.schema.maximum = value; + this.schema.maximum = value as Max; } - return this; + return this as never; } /** * Greater than * - * @see {@link NumberSchemaBuilder.maximum} - * @see {@link NumberSchemaBuilder.gte} + * Range: `(value; Infinity)` + * @see {@link maximum} method + * @see {@link gte} method */ - gt(value: number) { - return this.minimum(value, true); + gt(value: V): NumberSchemaBuilder & { + minValue: V, + }>> { + return this.minimum(value as never, true); } /** * Greater than or equal * * Range: `[value; Infinity)` - * @see {@link NumberSchemaBuilder.maximum maximum} - * @see {@link NumberSchemaBuilder.gt gt} + * @see {@link maximum maximum} + * @see {@link gt gt} */ - gte(value: number) { - return this.minimum(value); + gte(value: V): NumberSchemaBuilder & { + minValue: V, + }>> { + return this.minimum(value as never); } + /** * Less than * * Range: `(value; Infinity)` - * @see {@link NumberSchemaBuilder.minimum minimum} - * @see {@link NumberSchemaBuilder.lte lte} + * @see {@link minimum minimum} method + * @see {@link lte lte} method */ - lt(value: number) { - return this.max(value, true); + lt(value: V): NumberSchemaBuilder< + N, Prettify & { + maxValue: V, + } + >> { + return this.max(value as never, true); } /** * Less than or Equal * * Range: `[value; Infinity)` - * @see {@link NumberSchemaBuilder.minimum} - * @see {@link NumberSchemaBuilder.lt} + * @see {@link minimum} method + * @see {@link lt} method */ - lte(value: number) { - return this.max(value); + lte(value: V): NumberSchemaBuilder & { + maxValue: V, + }>> { + return this.max(value as never); } /** Any positive number (greater than `0`) * Range: `(0; Infinity)` @@ -843,7 +968,7 @@ class NumberSchemaBuilder< } /** Marks incoming number between `MAX_SAFE_INTEGER` and `MIN_SAFE_INTEGER` */ safe() { - return this.lte(Number.MAX_SAFE_INTEGER).gte(Number.MIN_SAFE_INTEGER); + return this.lte(Number.MAX_SAFE_INTEGER as 9007199254740991).gte(Number.MIN_SAFE_INTEGER as -9007199254740991); } } /** @@ -869,8 +994,8 @@ function number() { * * **NOTE:** By default Ajv fails `{"type": "integer"}` validation for `Infinity` and `NaN`. */ -function integer() { - return new NumberSchemaBuilder().integer(); +function integer() { + return new NumberSchemaBuilder().integer(); } export type StringBuilderOpts = { @@ -949,8 +1074,8 @@ class StringSchemaBuilder< /** * Define minimum string length. * - * Same as `min` - * @see {@link StringSchemaBuilder.min min} + * Same as `min` method + * @see {@link min min} method */ minLength< const L extends number, @@ -980,7 +1105,7 @@ class StringSchemaBuilder< * Define maximum string length. * * Same as `max` - * @see {@link StringSchemaBuilder.max max} + * @see {@link max max} method */ maxLength< const L extends number, diff --git a/src/builders/base.ts b/src/builders/base.ts deleted file mode 100644 index 502f766..0000000 --- a/src/builders/base.ts +++ /dev/null @@ -1,556 +0,0 @@ -import Ajv from 'ajv'; -import { AnySchemaOrAnnotation } from '../schema/types' -export class SchemaBuilder< - Input = unknown, - Schema extends AnySchemaOrAnnotation = AnySchemaOrAnnotation, - Output = Input, -> { - /** - * type helper. Do Not Use! - */ - _input!: Input; - - /** - * type helper. Do Not Use! - */ - _output!: Output; - private _schema: Schema; - private _shape: Schema; - - /** - * returns JSON-schema representation - */ - get schema() { - return this._schema; - } - /** - * returns JSON-schema representation. same as `schema` does. - * @satisfies zod API - */ - get shape() { - return this._shape; - } - - /** - * Set custom JSON-Schema representation. - * Updates `shape` property too - */ - set schema(schema) { - this._schema = schema; - this._shape = schema; - } - - /** - * Set custom JSON-Schema representation. - * Updates `schema` property too - * @satisfies zod API - */ - set shape(schema) { - this._schema = schema; - this._shape = schema; - } - - /** Returns your ajv instance */ - get ajv() { - return this._ajv; - } - - /** - * Set Ajv Instance. - * @throws `TypeError` if not ajv instance comes - */ - set ajv(instance: Ajv) { - if (!(instance instanceof Ajv)) { - throw new TypeError(`Cannot set ajv variable for non-ajv instance.`, { - cause: { type: typeof instance, value: instance }, - }); - } - this._ajv = instance; - } - - constructor( - schema: Schema, - private _ajv = DEFAULT_AJV, - ) { - this._schema = schema; - this._shape = schema; - } - protected isNullable = false; - - /** - * set custom JSON-schema field. Useful if you need to declare something but no api founded for built-in solution. - * - * Example: `If-Then-Else` you cannot declare without `custom` method. - * @example - * const myObj = s.object({ - * foo: s.string(), - * bar: s.string() - * }).custom('if', { - * "properties": { - * "foo": { "const": "bar" } - * }, - * "required": ["foo"] - * }).custom('then', { "required": ["bar"] }) - */ - custom( - key: string, - value: V, - ): Result { - (this.schema as Record)[key] = value; - return this as never; - } - - /** - * Marks your property as nullable (`undefined`) - * - * **NOTES**: json-schema not accept `undefined` type. It's just `nullable` as typescript `undefined` type. - */ - optional(): SchemaBuilder { - return this.nullable() as never; - } - - /** - * Marks your property as nullable (`null`). - * - * Updates `type` property for your schema. - * @example - * const schemaDef = s.string().nullable() - * schemaDef.schema // { type: ['string', 'null'], nullable: true } - */ - nullable(): SchemaBuilder { - this.isNullable = true; - (this.schema as any).nullable = true; - const type = (this.schema as any).type; - if (Array.isArray(type)) { - (this.schema as any).type = [...new Set([...type, "null"])]; - } else { - (this.schema as any).type = [...new Set([type, "null"])]; - } - return this as never; - } - - private preFns: Function[] = []; - - /** - * pre process function for incoming result. Transform input **BEFORE** calling `parse`, `safeParse`, `validate` functions - * - * **NOTE:** this functions works BEFORE parsing. use it at own risk. (e.g. transform Date object into string) - * @see {@link SchemaBuilder.parse parse} method - * @see {@link SchemaBuilder.safeParse safe parse} method - * @see {@link SchemaBuilder.validate validate} method - * @example - * const myString = s.string().preprocess(v => { - * // if date => transform to ISO string - * if(v instanceof Date) { - * return Date.toISOString() - * } - * // do nothing if not a date - * return v - * }) - * const res = myString.parse(new Date()) // '2023-09-23T07:10:57.881Z' - * const res = myString.parse('qwe') // 'qwe' - * const res = myString.parse({}) // error: not a string - */ - preprocess< - const In, - const Out, - const F extends Fn, - >(fn: F): this { - if (typeof fn !== "function") { - throw new TypeError(`Cannot use not a function for pre processing.`, { - cause: { type: typeof fn, value: fn }, - }); - } - this.preFns.push(fn); - return this as never; - } - - private postFns: { fn: Function; schema: AnySchemaBuilder }[] = []; - /** - * Post process. Use it when you would like to transform result after parsing is happens. - * - * **NOTE:** this function override your `input` variable for `safeParse` calling. - * @see {@link SchemaBuilder.safeParse safeParse method} - */ - postprocess( - fn: Fn, - schema: S, - ): this { - if (typeof fn !== "function") { - throw new TypeError(`Cannot use not a function for pre processing.`, { - cause: { type: typeof fn, value: fn }, - }); - } - this.postFns.push({ fn, schema }); - return this as never; - } - - private refineFns: Function[] = []; - /** - * Set custom validation. Any result exept `undefined` will throws. - * @param fn function that will be called after `safeParse`. Any result will throws - * @example - * import s from 'ajv-ts' - * // example: object with only 1 "active element" - * const Schema = s.object({ - * active: s.boolean(), - * name: s.string() - * }).array().refine((arr) => { - * const subArr = arr.filter(el => el.active === true) - * if (subArr.length > 1) throw new Error('Array should contains only 1 "active" element') - * }) - * - * Schema.parse([{ active: true, name: 'some 1' }, { active: true, name: 'some 2' }]) // throws Error - */ - refine(fn: (output: Output) => any) { - if (typeof fn !== "function") { - throw new TypeError( - `Cannot set for not a function for refine. Expect "function", Got: ${typeof fn}`, - { cause: { fn, type: typeof fn } }, - ); - } - this.refineFns.push(fn); - return this; - } - - /** - * Meta object. Adds meta information fields in your schema, such as `deprecated`, `description`, `$id`, `title` and more! - */ - meta(obj: MetaObject) { - Object.entries(obj).forEach(([key, value]) => { - this.custom(key, value); - }); - return this; - } - - /** - * Option `default` keywords throws exception during schema compilation when used in: - * - * - not in `properties` or `items` subschemas - * - in schemas inside `anyOf`, `oneOf` and `not` ({@link https://github.com/ajv-validator/ajv/issues/42 #42}) - * - in `if` schema - * - in schemas generated by user-defined _macro_ keywords - * This means only `object()` and `array()` buidlers are supported. - * @see {@link object} - * @see {@link array} - * @example - * import s from 'ajv-ts' - * const Person = s.object({ - * age: s.int().default(18) - * }) - * Person.parse({}) // { age: 18 } - */ - default(value: Output) { - (this.schema as AnySchema).default = value; - return this; - } - /** - * Defines custom error message for invalid schema. - * - * Set `schema.errorMessage = message` under the hood. - * @example - * // number example - * const numberSchema = s.number().error('Not a number') - * numberSchema.parse('qwe') // error: Not a number - */ - error(messageOrOptions: string | ErrorMessageParams) { - (this.schema as AnySchema).errorMessage = messageOrOptions; - return this; - } - - /** - * set `description` for your schema. - * You can use `meta` method to provide information in more consistant way. - * @see {@link SchemaBuilder.meta meta} method - * @satisfies `zod` API - */ - describe(message: string) { - return this.meta({ description: message }); - } - - /** - * set `$async=true` for your current schema. - * - * @see {@link https://ajv.js.org/guide/async-validation.html ajv async validation} - */ - async() { - (this.schema as Record).$async = true; - return this; - } - - /** - * set `$async=false` for your current schema. - * @param [remove=false] applies `delete` operator for `schema.$async` property. - */ - sync(remove: boolean = false) { - (this.schema as AnySchema).$async = false; - if (remove) { - delete (this.schema as AnySchema).$async; - } - return this; - } - - /** - * Construct Array schema. Same as `s.array(s.number())` - * - * @see {@link array} - */ - array>(): ArraySchemaBuilder { - return array(this) as never; - } - - /** - * Same as `s.and()`. Combine current type with another. Logical "AND" - * - * Typescript `A & B` - */ - intersection: typeof this.and = this.and; - /** - * Same as `s.and()`. Combine current type with another. Logical "AND" - * - * Typescript `A & B` - */ - and< - S extends AnySchemaBuilder[] = AnySchemaBuilder[], - Arr extends AnySchemaBuilder[] = [this, ...S], - // @ts-ignore - IntersectionSchemaBuilder circular return itself 2577 - >(...others: S): IntersectionSchemaBuilder { - return and(this, ...others) as never; - } - /** - * Same as `s.or()`. Combine current type with another type. Logical "OR" - * - * Typescript: `A | B` - */ - or( - ...others: S - ): UnionSchemaBuilder<[this, ...S]> { - return or(this, ...others); - } - /** - * Same as `s.or()`. Combine current type with another type. Logical "OR" - * - * Typescript: `A | B` - */ - union: typeof this.or = this.or; - - /** - * Exclude given subschema. - * - * Append `not` keyword for your schema - * - * @see {@link not} - * @see {@link SchemaBuilder.not not method} - * @example - * cosnt res = s - * .string<'Jerry' | 'Martin'>() - * .exclude(s.const('Jerry')) - * .schema // {type: "string", not: {const: "Jerry"} } - * type Res = s.infer // 'Martin' - */ - exclude< - S extends SchemaBuilder = SchemaBuilder, - Excl = Exclude, - This = this extends StringSchemaBuilder - ? StringSchemaBuilder - : this extends NumberSchemaBuilder - ? NumberSchemaBuilder - : this extends BooleanSchemaBuilder - ? BooleanSchemaBuilder - : this extends ArraySchemaBuilder - ? ArraySchemaBuilder> - : this extends ObjectSchemaBuilder< - infer Def extends ObjectDefinition - > - ? ObjectSchemaBuilder> - : this, - >(s: S): This { - (this.schema as AnySchema).not = s.schema; - return this as never; - } - - /** - * Exclude self schema. - * - * Wrap your schema with `not` keyword - * - * `s.not(s.string())` === `s.string().not()` - * - * If you need to append `not` keyword instead of wrap you might need to use {@link SchemaBuilder.exclude `exclude`} method - * - * @see {@link not} - * @see {@link SchemaBuilder.exclude exclude method} - * - * @example - * // not string - * s - * .string() - * .not() - * .schema // {not: { type: "string" }}, - */ - not(): NotSchemaBuilder { - return not(this) as never; - } - - private _transform( - input?: unknown, - arr: ({ fn: Function; schema: AnySchemaBuilder } | Function)[] = [], - ): SafeParseResult { - let output; - if (Array.isArray(arr) && arr.length > 0) { - try { - output = arr.reduce( - (prevResult, el) => { - if (!prevResult.success) { - throw prevResult.error; - } - let fnTransform; - let result: SafeParseResult = { - data: input, - input, - success: true, - }; - if (typeof el === "function") { - fnTransform = el(prevResult.data, this); - result.data = fnTransform; - } else { - fnTransform = el.fn(prevResult.data, this); - result = el.schema.safeParse(fnTransform); - } - return result; - }, - { input, data: input, success: true } as SafeParseResult, - ); - } catch (e) { - return { - success: false, - error: new Error((e as Error).message, { cause: e }), - input, - }; - } - return output as SafeParseResult; - } - output = input; - return { data: output as Out, input, success: true }; - } - - private _safeParseRaw(input?: unknown): SafeParseResult { - let success = false; - try { - const validateFn = this.ajv.compile(this.schema); - success = validateFn(input); - if (!success) { - const firstError = validateFn.errors?.at(0); - return { - error: new Error(firstError?.message, { - cause: validateFn.errors, - }), - success, - input, - }; - } - } catch (e) { - return { - error: new Error((e as Error).message, { cause: e }), - success: success as false, - input, - }; - } - return { - input, - data: input, - success, - }; - } - - /** - * Parse you input result. Used `ajv.validate` under the hood - * - * It also applies your `postProcess` functions if parsing was successfull - */ - safeParse(input?: I): SafeParseResult { - // need to remove schema, or we get precompiled result. It's bad for `extend` and `merge` in object schema - // TODO: investigate merge and add in ajv - // this.ajv.removeSchema(this.schema); - let preTransformedResult = this._transform(input, this.preFns); - preTransformedResult.input = input; - if (!preTransformedResult.success) { - return preTransformedResult as never; - } - - const parseResult = this._safeParseRaw(preTransformedResult.data); - parseResult.input = input; - if (!parseResult.success) { - return parseResult as never; - } - - const postTransformedResult = this._transform( - parseResult.data, - this.postFns, - ); - postTransformedResult.input = input; - if (!postTransformedResult.success) { - return postTransformedResult; - } - if ( - this.refineFns && - Array.isArray(this.refineFns) && - this.refineFns.length > 0 - ) { - for (const refine of this.refineFns) { - try { - const res = refine(postTransformedResult.data); - if (res !== undefined) { - return { - success: false, - error: new Error(`refine error`, { - cause: { - refine: res, - debug: { - input, - preTransformedResult, - parseResult, - postTransformedResult, - }, - }, - }), - input: postTransformedResult.data, - }; - } - } catch (e) { - return { - success: false, - error: e as Error, - input: postTransformedResult.data, - }; - } - } - } - return postTransformedResult; - } - /** - * Validate your schema. - * - * @returns {boolean} Validity of your schema - */ - validate(input?: unknown): input is Output { - const { success } = this.safeParse(input); - return success; - } - - /** - * Parse input for given schema. - * - * @returns {Output} parsed output result. - * @throws `Error` when input not match given schema - */ - parse< - const I, - >(input?: I): Output { - const result = this.safeParse(input); - if (!result.success) { - throw result.error; - } - return result.data as never; - } -} \ No newline at end of file diff --git a/src/builders/number.ts b/src/builders/number.ts deleted file mode 100644 index e69de29..0000000 diff --git a/src/builders/type.ts b/src/builders/type.ts deleted file mode 100644 index e69de29..0000000 diff --git a/src/types/array.d.ts b/src/types/array.d.ts index 3701703..be54e50 100644 --- a/src/types/array.d.ts +++ b/src/types/array.d.ts @@ -1,4 +1,4 @@ -import type { GreaterThan, GreaterThanOrEqual, LessThan, IsFloat, IsNegative, IsPositiveInteger, LessThanOrEqual } from './number'; +import type { GreaterThan, GreaterThanOrEqual, LessThan, IsFloat, IsPositiveInteger, LessThanOrEqual } from './number'; export type Create< L extends number, diff --git a/src/types/misc.d.ts b/src/types/misc.d.ts index e0b17b4..7bfa87c 100644 --- a/src/types/misc.d.ts +++ b/src/types/misc.d.ts @@ -55,3 +55,5 @@ export type Fn< export type Return> = F extends Fn ? Res : never export type Param = F extends Fn ? Args : never + +export type Debug = [Name, T] diff --git a/src/types/number.d.ts b/src/types/number.d.ts index ba1a24f..de4a627 100644 --- a/src/types/number.d.ts +++ b/src/types/number.d.ts @@ -1,5 +1,7 @@ +import { NumberSchema } from '../schema/types'; import { Create } from './array' import { Reverse } from './string'; +import { IsInteger, IsNegative } from 'type-fest' /** `T > U` */ export type GreaterThan = Create extends [...Create, ...infer _] ? false : true; @@ -23,22 +25,12 @@ export type IsFloat = N extends number : N extends `${number}.${number extends 0 ? '' : number}` ? true : false - -export type IsNegative = N extends number - ? IsNegative<`${N}`> - : N extends `-${number}` - ? true - : false - -export type IsPositiveInteger = IsFloat extends true - ? false - : IsNegative extends true - ? false - : true - +export type IsPositiveInteger = IsInteger extends true ? IsNegative extends false ? true : false : false export type Negative = `${N}` extends `-${infer V extends number}` ? N : V export type IsNumberSubset = GreaterThanOrEqual extends false ? LessThanOrEqual extends false ? true : [false, 'less than '] : [false, 'greater than'] + +export type NumericStringifyType = IsFloat extends true ? "Float" : IsInteger extends true ? "Int" : "Unknown" \ No newline at end of file diff --git a/tests/number.test.ts b/tests/number.test.ts index faa8cc2..1ac393d 100644 --- a/tests/number.test.ts +++ b/tests/number.test.ts @@ -122,3 +122,29 @@ test('integer should supports only integers', () => { expect(schema.validate(400)).toBe(false) expect(schema.validate(12.4)).toBe(false) }) + +test('incompatible format should fail type', () => { + // @ts-expect-error should fails + const schema1 = s.int().format('double') + // @ts-expect-error should fails + const schema2 = s.int().format('float') + // @ts-expect-error should fails + const schema3 = s.int().const(3.4) + + // @ts-expect-error should fails + const schema4 = s.int().max(3.4) + // @ts-expect-error should fails + const schema5 = s.int().min(3.4) + // @ts-expect-error should fails + const schema6 = s.int().const(3.4) +}) + +test('ranges should fails for out of range', () => { + // @ts-expect-error should fails + s.int().min(1).max(3).const(-1) + + // @ts-expect-error should fails + s.int().min(5).max(3) + // @ts-expect-error should fails + s.int().max(2).min(3) +}) From c12b57a38464dc106d9c748b93bde0b84c3188b7 Mon Sep 17 00:00:00 2001 From: Vitali Haradkou Date: Tue, 10 Sep 2024 08:48:52 +0300 Subject: [PATCH 3/7] chore: rebase --- .npmignore | 2 +- CHANGELOG.md | 12 ++++ UPCOMING.md | 26 ++++++++- package.json | 5 +- pnpm-lock.yaml | 151 ++++++++++++++++++++++++++++++++++++++---------- tsup.config.mts | 37 ++++++++++++ tsup.config.ts | 19 ------ 7 files changed, 198 insertions(+), 54 deletions(-) create mode 100644 tsup.config.mts delete mode 100644 tsup.config.ts diff --git a/.npmignore b/.npmignore index f0268f9..fb8ac4f 100644 --- a/.npmignore +++ b/.npmignore @@ -8,7 +8,7 @@ tests # Files bun.lockb yarn.lock -tsup.config.ts +tsup.config.*ts tsconfig.json .eslintrc.json CONTRIBUTING.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 3adca71..24410fc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,17 @@ # ajv-ts +## 0.8.0 + +### Minor Changes + +- 0fb0e0e: new: make number more stricter + + fix #61 + +### Patch Changes + +- 37a7b1d: fix # 61 + ## 0.7.1 ### Patch Changes diff --git a/UPCOMING.md b/UPCOMING.md index 5a98545..cecade5 100644 --- a/UPCOMING.md +++ b/UPCOMING.md @@ -4,9 +4,33 @@ ## ✅ New Features +- [strict number](#strict-numbers) + +### Strict numbers + +We make validation for number `type`, `format`, `minValue` and `maxValue` fields. That means we handle it in our side so you get an error for invalid values. + +Examples: + +```ts +s.number().format('float').int() // error in type! +s.int().const(3.4) // error in type! +s.number().int().format('float') // error in format! +s.number().int().format('double') // error in format! + +// ranges are also check for possibility + +s.number().min(5).max(3) // error in range! +s.number().min(3).max(5).const(10) // error in constant - out of range! +``` + ## 🐛 Bug Fixes - [#61](https://github.com/vitalics/ajv-ts/issues/61) ## 🏡 Chore/Infra -- integer checking for parse methods + +- add [type-fest](https://www.npmjs.com/package/type-fest) library for correct type checking +- add [tsx](https://www.npmjs.com/package/tsx) package +- add minified files for cjs and esm modules in `dist` folder +- remove `bun-types` dependency diff --git a/package.json b/package.json index 3f7bb2c..8249967 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "types": "dist/index.d.ts", "packageManager": "pnpm@9.9.0+sha256.7a4261e50d9a44d9240baf6c9d6e10089dcf0a79d0007f2a26985a6927324177", "scripts": { - "build": "tsup", + "build": "tsx ./tsup.config.mts", "test": "vitest run", "test:watch": "vitest", "ci:version": "changeset version", @@ -47,7 +47,6 @@ "@typescript-eslint/eslint-plugin": "6.4.0", "@vitest/ui": "1.6.0", "benchmark": "2.1.4", - "bun-types": "1.1.18", "eslint": "8.0.1", "eslint-plugin-import": "2.25.2", "eslint-plugin-n": "15.0.0", @@ -65,4 +64,4 @@ "ajv-formats": "3.0.1", "type-fest": "4.26.0" } -} +} \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index dc6259b..24b9ba9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -39,9 +39,6 @@ importers: benchmark: specifier: 2.1.4 version: 2.1.4 - bun-types: - specifier: 1.1.18 - version: 1.1.18 eslint: specifier: 8.0.1 version: 8.0.1 @@ -53,7 +50,7 @@ importers: version: 15.0.0(eslint@8.0.1) tsup: specifier: 8.1.0 - version: 8.1.0(postcss@8.4.38)(typescript@5.5.3) + version: 8.1.0(postcss@8.4.38)(ts-node@10.9.2(@types/node@20.14.2)(typescript@5.5.3))(typescript@5.5.3) tsx: specifier: 4.17.0 version: 4.17.0 @@ -187,6 +184,10 @@ packages: '@changesets/write@0.2.3': resolution: {integrity: sha512-Dbamr7AIMvslKnNYsLFafaVORx4H0pvCA2MHqgtNCySMe1blImEyAEOzDmcgKAkgz4+uwoLz7demIrX+JBr/Xw==} + '@cspotcode/source-map-support@0.8.1': + resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} + engines: {node: '>=12'} + '@esbuild/aix-ppc64@0.21.5': resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} engines: {node: '>=12'} @@ -518,6 +519,9 @@ packages: '@jridgewell/trace-mapping@0.3.25': resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} + '@jridgewell/trace-mapping@0.3.9': + resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} + '@manypkg/find-root@1.1.0': resolution: {integrity: sha512-mki5uBvhHzO8kYYix/WRy2WX8S3B5wdVSc9D6KcU5lQNglP2yt58/VfLuAK49glRXChosY8ap2oJ1qgma3GUVA==} @@ -626,6 +630,18 @@ packages: '@sinclair/typebox@0.27.8': resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} + '@tsconfig/node10@1.0.11': + resolution: {integrity: sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==} + + '@tsconfig/node12@1.0.11': + resolution: {integrity: sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==} + + '@tsconfig/node14@1.0.3': + resolution: {integrity: sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==} + + '@tsconfig/node16@1.0.4': + resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==} + '@types/benchmark@2.1.5': resolution: {integrity: sha512-cKio2eFB3v7qmKcvIHLUMw/dIx/8bhWPuzpzRT4unCPRTD8VdA9Zb0afxpcxOqR4PixRS7yT42FqGS8BYL8g1w==} @@ -647,9 +663,6 @@ packages: '@types/node@12.20.55': resolution: {integrity: sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==} - '@types/node@20.12.14': - resolution: {integrity: sha512-scnD59RpYD91xngrQQLGkE+6UrHUPzeKZWhhjBSa3HSkwjbQc38+q3RoIVEwxQGRw3M+j5hpNAM+lgV3cVormg==} - '@types/node@20.14.2': resolution: {integrity: sha512-xyu6WAMVwv6AKFLB+e/7ySZVr/0zLCzOa7rSpq6jNwpqOrUbcACDWC+53d4n2QHOnDou0fbIsg8wZu/sxrnI4Q==} @@ -659,9 +672,6 @@ packages: '@types/semver@7.5.8': resolution: {integrity: sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==} - '@types/ws@8.5.10': - resolution: {integrity: sha512-vmQSUcfalpIq0R9q7uTo2lXs6eGIpt9wtnLdMv9LVpIjCA/+ufZRozlVoVelIYixx1ugCBKDhn89vnsEGOCx9A==} - '@typescript-eslint/eslint-plugin@6.4.0': resolution: {integrity: sha512-62o2Hmc7Gs3p8SLfbXcipjWAa6qk2wZGChXG2JbBtYpwSRmti/9KHLqfbLs9uDigOexG+3PaQ9G2g3201FWLKg==} engines: {node: ^16.0.0 || >=18.0.0} @@ -829,6 +839,9 @@ packages: resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} engines: {node: '>= 8'} + arg@4.1.3: + resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==} + argparse@1.0.10: resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} @@ -893,9 +906,6 @@ packages: breakword@1.0.6: resolution: {integrity: sha512-yjxDAYyK/pBvws9H4xKYpLDpYKEH6CzrBPAuXq3x18I+c/2MkVtT3qAr7Oloi6Dss9qNhPVueAAVU1CSeNDIXw==} - bun-types@1.1.18: - resolution: {integrity: sha512-m5GnQrIpQdRyfWRoa5pvwpVAMDiQR1GTgMMZNvBWzJ+k2/fC55NRFZCEsXFE38HLFpM57o/diAjP3rgacdA4Eg==} - bundle-require@4.2.1: resolution: {integrity: sha512-7Q/6vkyYAwOmQNRw75x+4yRtZCZJXUDmHHlFdkiV0wgv/reNjtJwpu1jPJ0w2kbEpIM0uoKI3S4/f39dU7AjSA==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -982,6 +992,9 @@ packages: confbox@0.1.7: resolution: {integrity: sha512-uJcB/FKZtBMCJpK8MQji6bJHgu1tixKPxRLeGkNzBoOZzpnZUJm0jm2/sBDWcuBx1dYgxV4JU+g5hmNxCyAmdA==} + create-require@1.1.1: + resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} + cross-spawn@5.1.0: resolution: {integrity: sha512-pTgQJ5KC0d2hcY8eyL1IzlBPYjTkyH72XRZPnLyKus2mBfNjQs3klqbJU2VILqZryAZUt9JOb3h/mWMy23/f5A==} @@ -1073,6 +1086,10 @@ packages: resolution: {integrity: sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + diff@4.0.2: + resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==} + engines: {node: '>=0.3.1'} + dir-glob@3.0.1: resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} engines: {node: '>=8'} @@ -1726,6 +1743,9 @@ packages: magic-string@0.30.10: resolution: {integrity: sha512-iIRwTIf0QKV3UAnYK4PU8uiEc4SRh5jX0mwpIwETPpHdhVM4f53RSwS/vXvN1JhGX+Cs7B8qIq3d6AH49O5fAQ==} + make-error@1.3.6: + resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} + map-obj@1.0.1: resolution: {integrity: sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg==} engines: {node: '>=0.10.0'} @@ -2337,6 +2357,20 @@ packages: ts-interface-checker@0.1.13: resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} + ts-node@10.9.2: + resolution: {integrity: sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==} + hasBin: true + peerDependencies: + '@swc/core': '>=1.2.50' + '@swc/wasm': '>=1.2.50' + '@types/node': '*' + typescript: '>=2.7' + peerDependenciesMeta: + '@swc/core': + optional: true + '@swc/wasm': + optional: true + tsconfig-paths@3.15.0: resolution: {integrity: sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==} @@ -2434,6 +2468,9 @@ packages: uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + v8-compile-cache-lib@3.0.1: + resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} + v8-compile-cache@2.4.0: resolution: {integrity: sha512-ocyWc3bAHBB/guyqJQVI5o4BZkPhznPYUG2ea80Gond/BgNWpap8TOmLSeeQG7bnh2KMISxskdADG59j7zruhw==} @@ -2585,6 +2622,10 @@ packages: resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} engines: {node: '>=12'} + yn@3.1.1: + resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==} + engines: {node: '>=6'} + yocto-queue@0.1.0: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} @@ -2797,6 +2838,11 @@ snapshots: human-id: 1.0.2 prettier: 2.8.8 + '@cspotcode/source-map-support@0.8.1': + dependencies: + '@jridgewell/trace-mapping': 0.3.9 + optional: true + '@esbuild/aix-ppc64@0.21.5': optional: true @@ -2999,6 +3045,12 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.4.15 + '@jridgewell/trace-mapping@0.3.9': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.4.15 + optional: true + '@manypkg/find-root@1.1.0': dependencies: '@babel/runtime': 7.24.7 @@ -3082,6 +3134,18 @@ snapshots: '@sinclair/typebox@0.27.8': {} + '@tsconfig/node10@1.0.11': + optional: true + + '@tsconfig/node12@1.0.11': + optional: true + + '@tsconfig/node14@1.0.3': + optional: true + + '@tsconfig/node16@1.0.4': + optional: true + '@types/benchmark@2.1.5': {} '@types/estree@1.0.5': {} @@ -3098,22 +3162,15 @@ snapshots: '@types/node@12.20.55': {} - '@types/node@20.12.14': - dependencies: - undici-types: 5.26.5 - '@types/node@20.14.2': dependencies: undici-types: 5.26.5 + optional: true '@types/normalize-package-data@2.4.4': {} '@types/semver@7.5.8': {} - '@types/ws@8.5.10': - dependencies: - '@types/node': 20.14.2 - '@typescript-eslint/eslint-plugin@6.4.0(@typescript-eslint/parser@6.21.0(eslint@8.0.1)(typescript@5.5.3))(eslint@8.0.1)(typescript@5.5.3)': dependencies: '@eslint-community/regexpp': 4.10.1 @@ -3323,6 +3380,9 @@ snapshots: normalize-path: 3.0.0 picomatch: 2.3.1 + arg@4.1.3: + optional: true + argparse@1.0.10: dependencies: sprintf-js: 1.0.3 @@ -3401,11 +3461,6 @@ snapshots: dependencies: wcwidth: 1.0.1 - bun-types@1.1.18: - dependencies: - '@types/node': 20.12.14 - '@types/ws': 8.5.10 - bundle-require@4.2.1(esbuild@0.21.5): dependencies: esbuild: 0.21.5 @@ -3504,6 +3559,9 @@ snapshots: confbox@0.1.7: {} + create-require@1.1.1: + optional: true + cross-spawn@5.1.0: dependencies: lru-cache: 4.1.5 @@ -3592,6 +3650,9 @@ snapshots: diff-sequences@29.6.3: {} + diff@4.0.2: + optional: true + dir-glob@3.0.1: dependencies: path-type: 4.0.0 @@ -4359,6 +4420,9 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.4.15 + make-error@1.3.6: + optional: true + map-obj@1.0.1: {} map-obj@4.3.0: {} @@ -4584,12 +4648,13 @@ snapshots: possible-typed-array-names@1.0.0: {} - postcss-load-config@4.0.2(postcss@8.4.38): + postcss-load-config@4.0.2(postcss@8.4.38)(ts-node@10.9.2(@types/node@20.14.2)(typescript@5.5.3)): dependencies: lilconfig: 3.1.2 yaml: 2.4.5 optionalDependencies: postcss: 8.4.38 + ts-node: 10.9.2(@types/node@20.14.2)(typescript@5.5.3) postcss@8.4.38: dependencies: @@ -4949,6 +5014,25 @@ snapshots: ts-interface-checker@0.1.13: {} + ts-node@10.9.2(@types/node@20.14.2)(typescript@5.5.3): + dependencies: + '@cspotcode/source-map-support': 0.8.1 + '@tsconfig/node10': 1.0.11 + '@tsconfig/node12': 1.0.11 + '@tsconfig/node14': 1.0.3 + '@tsconfig/node16': 1.0.4 + '@types/node': 20.14.2 + acorn: 8.12.0 + acorn-walk: 8.3.3 + arg: 4.1.3 + create-require: 1.1.1 + diff: 4.0.2 + make-error: 1.3.6 + typescript: 5.5.3 + v8-compile-cache-lib: 3.0.1 + yn: 3.1.1 + optional: true + tsconfig-paths@3.15.0: dependencies: '@types/json5': 0.0.29 @@ -4956,7 +5040,7 @@ snapshots: minimist: 1.2.8 strip-bom: 3.0.0 - tsup@8.1.0(postcss@8.4.38)(typescript@5.5.3): + tsup@8.1.0(postcss@8.4.38)(ts-node@10.9.2(@types/node@20.14.2)(typescript@5.5.3))(typescript@5.5.3): dependencies: bundle-require: 4.2.1(esbuild@0.21.5) cac: 6.7.14 @@ -4966,7 +5050,7 @@ snapshots: execa: 5.1.1 globby: 11.1.0 joycon: 3.1.1 - postcss-load-config: 4.0.2(postcss@8.4.38) + postcss-load-config: 4.0.2(postcss@8.4.38)(ts-node@10.9.2(@types/node@20.14.2)(typescript@5.5.3)) resolve-from: 5.0.0 rollup: 4.18.0 source-map: 0.8.0-beta.0 @@ -5055,7 +5139,8 @@ snapshots: has-symbols: 1.0.3 which-boxed-primitive: 1.0.2 - undici-types@5.26.5: {} + undici-types@5.26.5: + optional: true universalify@0.1.2: {} @@ -5063,6 +5148,9 @@ snapshots: dependencies: punycode: 2.3.1 + v8-compile-cache-lib@3.0.1: + optional: true + v8-compile-cache@2.4.0: {} validate-npm-package-license@3.0.4: @@ -5239,6 +5327,9 @@ snapshots: y18n: 5.0.8 yargs-parser: 21.1.1 + yn@3.1.1: + optional: true + yocto-queue@0.1.0: {} yocto-queue@1.0.0: {} diff --git a/tsup.config.mts b/tsup.config.mts new file mode 100644 index 0000000..dcd43a7 --- /dev/null +++ b/tsup.config.mts @@ -0,0 +1,37 @@ +import { build, Options } from 'tsup' + +const common: Options = { + entry: ['src/index.ts'], + format: ['cjs', 'esm'], + external: [], + splitting: true, + cjsInterop: true, + dts: true, + target: ['node18'], + shims: true, + tsconfig: './tsconfig.json', + +} +// minify +await build({ + ...common, + clean: true, + minify: true, + minifySyntax: true, + minifyWhitespace: true, + minifyIdentifiers: true, + outExtension({ format }) { + return { + js: format === 'cjs' ? '.min.cjs' : format === 'esm' ? `.min.mjs` : '.min.js', + } + }, +}) + +await build({ + ...common, + outExtension({ format }) { + return { + js: format === 'cjs' ? '.cjs' : format === 'esm' ? `.mjs` : '.js', + } + }, +}) diff --git a/tsup.config.ts b/tsup.config.ts deleted file mode 100644 index aa3d29d..0000000 --- a/tsup.config.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { defineConfig } from 'tsup' - -export default defineConfig(() => ({ - entry: ['src/index.ts'], - format: ['cjs', 'esm'], - external: [], - splitting: false, - clean: true, - cjsInterop: true, - dts: true, - target: ['node18'], - shims: true, - tsconfig: './tsconfig.json', - outExtension({ format }) { - return { - js: format === 'cjs' ? '.cjs' : format === 'esm' ? `.mjs` : '.js', - } - }, -})); \ No newline at end of file From bd8397ab73ab4e231bdbc95cb0921f3f74d79746 Mon Sep 17 00:00:00 2001 From: Vitali Haradkou Date: Tue, 10 Sep 2024 08:48:52 +0300 Subject: [PATCH 4/7] fix: changelog --- .changeset/chilled-seahorses-serve.md | 27 ++++++++++++++++++++++++++- .changeset/fair-dolls-lick.md | 2 +- CHANGELOG.md | 12 ------------ 3 files changed, 27 insertions(+), 14 deletions(-) diff --git a/.changeset/chilled-seahorses-serve.md b/.changeset/chilled-seahorses-serve.md index 791c93a..54f52c2 100644 --- a/.changeset/chilled-seahorses-serve.md +++ b/.changeset/chilled-seahorses-serve.md @@ -2,4 +2,29 @@ "ajv-ts": minor --- -new: make number more stricter +Make [strict numbers](#strict-numbers) + +### Strict numbers + +We make validation for number `type`, `format`, `minValue` and `maxValue` fields. That means we handle it in our side so you get an error for invalid values. + +Examples: + +```ts +s.number().format('float').int() // error in type! +s.int().const(3.4) // error in type! +s.number().int().format('float') // error in format! +s.number().int().format('double') // error in format! + +// ranges are also check for possibility + +s.number().min(5).max(3) // error in range! +s.number().min(3).max(5).const(10) // error in constant - out of range! +``` + +## 🏡 Chore/Infra + +- add [type-fest](https://www.npmjs.com/package/type-fest) library for correct type checking +- add [tsx](https://www.npmjs.com/package/tsx) package +- add minified files for cjs and esm modules in `dist` folder +- remove `bun-types` dependency diff --git a/.changeset/fair-dolls-lick.md b/.changeset/fair-dolls-lick.md index e49a8b9..7cac892 100644 --- a/.changeset/fair-dolls-lick.md +++ b/.changeset/fair-dolls-lick.md @@ -2,4 +2,4 @@ "ajv-ts": patch --- -fix # 61 +fix #61 diff --git a/CHANGELOG.md b/CHANGELOG.md index 24410fc..3adca71 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,17 +1,5 @@ # ajv-ts -## 0.8.0 - -### Minor Changes - -- 0fb0e0e: new: make number more stricter - - fix #61 - -### Patch Changes - -- 37a7b1d: fix # 61 - ## 0.7.1 ### Patch Changes From 4560553164533667bc1c6a877fdf8aca4c609f59 Mon Sep 17 00:00:00 2001 From: Vitali Haradkou Date: Tue, 10 Sep 2024 08:48:52 +0300 Subject: [PATCH 5/7] fix: readme --- README.md | 2 +- SECURITY.md | 1 - tsconfig.json | 3 --- 3 files changed, 1 insertion(+), 5 deletions(-) diff --git a/README.md b/README.md index e859582..3432085 100644 --- a/README.md +++ b/README.md @@ -333,7 +333,7 @@ double: double according to the [openApi 3.0.0 specification](https://spec.opena ### Typescript features -> from 0.8 +> from >= 0.8 We make validation for number `type`, `format`, `minValue` and `maxValue` fields. That means we handle it in our side so you get an error for invalid values. diff --git a/SECURITY.md b/SECURITY.md index b50f14e..2649ffd 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -9,7 +9,6 @@ currently being supported with security updates. | ------- | ------------------ | | < 0.x | :white_check_mark: | - ## Reporting a Vulnerability Use this section to tell people how to report a vulnerability. diff --git a/tsconfig.json b/tsconfig.json index 015837b..4d11d12 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -16,9 +16,6 @@ "allowSyntheticDefaultImports": true, "forceConsistentCasingInFileNames": true, "allowJs": true, - "types": [ - "bun-types" // add Bun global - ] }, "include": [ "src", From 720b24a88e34e6cb131bd77cdcd5238015904ae8 Mon Sep 17 00:00:00 2001 From: Vitali Haradkou Date: Tue, 10 Sep 2024 08:48:52 +0300 Subject: [PATCH 6/7] fix: workflows --- .github/workflows/pr.yaml | 2 +- .github/workflows/release.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pr.yaml b/.github/workflows/pr.yaml index 13c446d..564e531 100644 --- a/.github/workflows/pr.yaml +++ b/.github/workflows/pr.yaml @@ -14,7 +14,7 @@ jobs: strategy: matrix: node_version: [18, 20, 22, latest] - pnpm_version: [9.4.0] + pnpm_version: [9.9.0] steps: - name: Clone repository uses: actions/checkout@v3 diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index f2780db..92a928f 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -22,7 +22,7 @@ jobs: strategy: matrix: node_version: [20] - pnpm_version: [9.4.0] + pnpm_version: [9.9.0] steps: - name: Clone repository uses: actions/checkout@v3 From 1187384744c65907805544e14acaa57ce231a713 Mon Sep 17 00:00:00 2001 From: Vitali Haradkou Date: Tue, 10 Sep 2024 08:48:52 +0300 Subject: [PATCH 7/7] fix: package manager version in package.json file --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 8249967..1e7e898 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "main": "dist/index.cjs", "module": "dist/index.mjs", "types": "dist/index.d.ts", - "packageManager": "pnpm@9.9.0+sha256.7a4261e50d9a44d9240baf6c9d6e10089dcf0a79d0007f2a26985a6927324177", + "packageManager": "pnpm@9.9.0", "scripts": { "build": "tsx ./tsup.config.mts", "test": "vitest run",