From 7ca4b64c24c894bb9c79e791340d16971ddff04b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Costa?= Date: Thu, 11 Apr 2024 21:31:29 -0700 Subject: [PATCH] feat: Create vine adapter Summary: Test Plan: --- .gitattributes | 6 ++ README.md | 10 +++ packages/all/package.json | 3 + packages/all/src/__tests__/vine.test.ts | 82 +++++++++++++++++++++++ packages/main/package.json | 7 ++ packages/main/src/__tests__/vine.test.ts | 82 +++++++++++++++++++++++ packages/main/src/adapters.ts | 2 + packages/main/src/selector.ts | 2 + packages/main/src/serialization.ts | 1 + packages/main/src/validation.ts | 6 ++ packages/vine/README.md | 49 ++++++++++++++ packages/vine/package.json | 77 +++++++++++++++++++++ packages/vine/src/__tests__/example.ts | 14 ++++ packages/vine/src/__tests__/tsconfig.json | 5 ++ packages/vine/src/__tests__/vine.test.ts | 78 +++++++++++++++++++++ packages/vine/src/index.ts | 37 ++++++++++ packages/vine/src/resolver.ts | 12 ++++ packages/vine/src/validation.ts | 37 ++++++++++ packages/vine/tsconfig.json | 5 ++ pnpm-lock.yaml | 68 +++++++++++++++++++ tsconfig.test.json | 2 +- turbo/generators/config.ts | 6 ++ 22 files changed, 590 insertions(+), 1 deletion(-) create mode 100644 packages/all/src/__tests__/vine.test.ts create mode 100644 packages/main/src/__tests__/vine.test.ts create mode 100644 packages/vine/README.md create mode 100644 packages/vine/package.json create mode 100644 packages/vine/src/__tests__/example.ts create mode 100644 packages/vine/src/__tests__/tsconfig.json create mode 100644 packages/vine/src/__tests__/vine.test.ts create mode 100644 packages/vine/src/index.ts create mode 100644 packages/vine/src/resolver.ts create mode 100644 packages/vine/src/validation.ts create mode 100644 packages/vine/tsconfig.json diff --git a/.gitattributes b/.gitattributes index 7415f784..e84f95f5 100644 --- a/.gitattributes +++ b/.gitattributes @@ -20,6 +20,7 @@ packages/all/src/__tests__/tsconfig.json linguist-generated packages/all/src/__tests__/typebox.test.ts linguist-generated packages/all/src/__tests__/valibot.test.ts linguist-generated packages/all/src/__tests__/valita.test.ts linguist-generated +packages/all/src/__tests__/vine.test.ts linguist-generated packages/all/src/__tests__/yup.test.ts linguist-generated packages/all/src/__tests__/zod.test.ts linguist-generated packages/all/src/index.ts linguist-generated @@ -79,6 +80,7 @@ packages/main/src/__tests__/tsconfig.json linguist-generated packages/main/src/__tests__/typebox.test.ts linguist-generated packages/main/src/__tests__/valibot.test.ts linguist-generated packages/main/src/__tests__/valita.test.ts linguist-generated +packages/main/src/__tests__/vine.test.ts linguist-generated packages/main/src/__tests__/yup.test.ts linguist-generated packages/main/src/__tests__/zod.test.ts linguist-generated packages/main/src/adapters.ts linguist-generated @@ -114,6 +116,10 @@ packages/valita/README.md linguist-generated packages/valita/src/__tests__/tsconfig.json linguist-generated packages/valita/src/index.ts linguist-generated packages/valita/tsconfig.json linguist-generated +packages/vine/README.md linguist-generated +packages/vine/src/__tests__/tsconfig.json linguist-generated +packages/vine/src/index.ts linguist-generated +packages/vine/tsconfig.json linguist-generated packages/yup/README.md linguist-generated packages/yup/src/__tests__/tsconfig.json linguist-generated packages/yup/src/index.ts linguist-generated diff --git a/README.md b/README.md index bcd59982..839669f9 100644 --- a/README.md +++ b/README.md @@ -234,6 +234,16 @@ We value flexibility, which is why there are multiple ways of using TypeSchema: @typeschema/fastest-validator npm downloads + + vine + GitHub stars + ✅ + ✅ + ✅ + 🧐 + @typeschema/vine + npm downloads + suretype GitHub stars diff --git a/packages/all/package.json b/packages/all/package.json index bac758bf..2f34e819 100644 --- a/packages/all/package.json +++ b/packages/all/package.json @@ -29,6 +29,7 @@ "typebox", "valibot", "valita", + "vine", "yup", "zod" ], @@ -91,6 +92,7 @@ "@typeschema/typebox": "workspace:*", "@typeschema/valibot": "workspace:*", "@typeschema/valita": "workspace:*", + "@typeschema/vine": "workspace:*", "@typeschema/yup": "workspace:*", "@typeschema/zod": "workspace:*" }, @@ -118,6 +120,7 @@ "@gcornut/valibot-json-schema": "^0.0.25", "valibot": "^0.30.0", "@badrap/valita": "^0.3.6", + "@vinejs/vine": "^2.0.0", "@sodaru/yup-to-json-schema": "^2.0.1", "yup": "^1.4.0", "zod": "^3.22.4", diff --git a/packages/all/src/__tests__/vine.test.ts b/packages/all/src/__tests__/vine.test.ts new file mode 100644 index 00000000..0bfa1c5c --- /dev/null +++ b/packages/all/src/__tests__/vine.test.ts @@ -0,0 +1,82 @@ +/** + * This file is generated. Do not modify it manually! + */ + +import type {Infer, InferIn} from '..'; + +import {initTRPC} from '@trpc/server'; +import vine from '@vinejs/vine'; +import {expectTypeOf} from 'expect-type'; +import {describe, expect, test} from 'vitest'; + +import {assert, validate, wrap} from '..'; + +describe('vine', () => { + const schema = vine.object({ + age: vine.number(), + createdAt: vine.date({formats: {utc: true}}), + email: vine.string().email(), + id: vine.string(), + name: vine.string(), + updatedAt: vine.date({formats: {utc: true}}), + }); + + const data = { + age: 123 as string | number, + createdAt: '2021-01-01T00:00:00.000Z' as string | number, + email: 'john.doe@test.com', + id: 'c4a760a8-dbcf-4e14-9f39-645a8e933d74', + name: 'John Doe', + updatedAt: '2021-01-01T00:00:00.000Z' as string | number, + }; + const outputData = { + age: 123, + createdAt: new Date('2021-01-01T00:00:00.000Z'), + email: 'john.doe@test.com', + id: 'c4a760a8-dbcf-4e14-9f39-645a8e933d74', + name: 'John Doe', + updatedAt: new Date('2021-01-01T00:00:00.000Z'), + }; + const badData = { + age: '123a', + createdAt: '2021-01-01T00:00:00.000Z', + email: 'john.doe@test.com', + id: 'c4a760a8-dbcf-4e14-9f39-645a8e933d74', + name: 'John Doe', + updatedAt: '2021-01-01T00:00:00.000Z', + }; + + test('infer', () => { + expectTypeOf>().toEqualTypeOf(outputData); + expectTypeOf>().toEqualTypeOf(data); + }); + + test('validate', async () => { + expect(await validate(schema, data)).toStrictEqual({ + data: outputData, + success: true, + }); + expect(await validate(schema, badData)).toStrictEqual({ + issues: [{message: 'The age field must be a number', path: ['age']}], + success: false, + }); + }); + + test('assert', async () => { + expect(await assert(schema, data)).toStrictEqual(outputData); + await expect(assert(schema, badData)).rejects.toThrow(); + }); + + test('wrap', async () => { + const tRPC = initTRPC.create(); + const router = tRPC.router({ + hello: tRPC.procedure.input(wrap(schema)).query(({input}) => { + expectTypeOf().toEqualTypeOf(outputData); + return input; + }), + }); + const createCaller = tRPC.createCallerFactory(router); + const caller = createCaller({}); + expect(await caller.hello(data)).toStrictEqual(outputData); + }); +}); diff --git a/packages/main/package.json b/packages/main/package.json index a584ad13..2d473fcf 100644 --- a/packages/main/package.json +++ b/packages/main/package.json @@ -29,6 +29,7 @@ "typebox", "valibot", "valita", + "vine", "yup", "zod" ], @@ -115,6 +116,8 @@ "valibot": "^0.30.0", "@typeschema/valita": "workspace:*", "@badrap/valita": "^0.3.6", + "@typeschema/vine": "workspace:*", + "@vinejs/vine": "^2.0.0", "@typeschema/yup": "workspace:*", "@sodaru/yup-to-json-schema": "^2.0.1", "yup": "^1.4.0", @@ -139,6 +142,7 @@ "@typeschema/typebox": "workspace:*", "@typeschema/valibot": "workspace:*", "@typeschema/valita": "workspace:*", + "@typeschema/vine": "workspace:*", "@typeschema/yup": "workspace:*", "@typeschema/zod": "workspace:*" }, @@ -191,6 +195,9 @@ "@typeschema/valita": { "optional": true }, + "@typeschema/vine": { + "optional": true + }, "@typeschema/yup": { "optional": true }, diff --git a/packages/main/src/__tests__/vine.test.ts b/packages/main/src/__tests__/vine.test.ts new file mode 100644 index 00000000..0bfa1c5c --- /dev/null +++ b/packages/main/src/__tests__/vine.test.ts @@ -0,0 +1,82 @@ +/** + * This file is generated. Do not modify it manually! + */ + +import type {Infer, InferIn} from '..'; + +import {initTRPC} from '@trpc/server'; +import vine from '@vinejs/vine'; +import {expectTypeOf} from 'expect-type'; +import {describe, expect, test} from 'vitest'; + +import {assert, validate, wrap} from '..'; + +describe('vine', () => { + const schema = vine.object({ + age: vine.number(), + createdAt: vine.date({formats: {utc: true}}), + email: vine.string().email(), + id: vine.string(), + name: vine.string(), + updatedAt: vine.date({formats: {utc: true}}), + }); + + const data = { + age: 123 as string | number, + createdAt: '2021-01-01T00:00:00.000Z' as string | number, + email: 'john.doe@test.com', + id: 'c4a760a8-dbcf-4e14-9f39-645a8e933d74', + name: 'John Doe', + updatedAt: '2021-01-01T00:00:00.000Z' as string | number, + }; + const outputData = { + age: 123, + createdAt: new Date('2021-01-01T00:00:00.000Z'), + email: 'john.doe@test.com', + id: 'c4a760a8-dbcf-4e14-9f39-645a8e933d74', + name: 'John Doe', + updatedAt: new Date('2021-01-01T00:00:00.000Z'), + }; + const badData = { + age: '123a', + createdAt: '2021-01-01T00:00:00.000Z', + email: 'john.doe@test.com', + id: 'c4a760a8-dbcf-4e14-9f39-645a8e933d74', + name: 'John Doe', + updatedAt: '2021-01-01T00:00:00.000Z', + }; + + test('infer', () => { + expectTypeOf>().toEqualTypeOf(outputData); + expectTypeOf>().toEqualTypeOf(data); + }); + + test('validate', async () => { + expect(await validate(schema, data)).toStrictEqual({ + data: outputData, + success: true, + }); + expect(await validate(schema, badData)).toStrictEqual({ + issues: [{message: 'The age field must be a number', path: ['age']}], + success: false, + }); + }); + + test('assert', async () => { + expect(await assert(schema, data)).toStrictEqual(outputData); + await expect(assert(schema, badData)).rejects.toThrow(); + }); + + test('wrap', async () => { + const tRPC = initTRPC.create(); + const router = tRPC.router({ + hello: tRPC.procedure.input(wrap(schema)).query(({input}) => { + expectTypeOf().toEqualTypeOf(outputData); + return input; + }), + }); + const createCaller = tRPC.createCallerFactory(router); + const caller = createCaller({}); + expect(await caller.hello(data)).toStrictEqual(outputData); + }); +}); diff --git a/packages/main/src/adapters.ts b/packages/main/src/adapters.ts index 972067bb..33b6ba51 100644 --- a/packages/main/src/adapters.ts +++ b/packages/main/src/adapters.ts @@ -18,6 +18,7 @@ import type {AdapterResolver as SuretypeResolver} from '@typeschema/suretype'; import type {AdapterResolver as TypeboxResolver} from '@typeschema/typebox'; import type {AdapterResolver as ValibotResolver} from '@typeschema/valibot'; import type {AdapterResolver as ValitaResolver} from '@typeschema/valita'; +import type {AdapterResolver as VineResolver} from '@typeschema/vine'; import type {AdapterResolver as YupResolver} from '@typeschema/yup'; import type {AdapterResolver as ZodResolver} from '@typeschema/zod'; @@ -38,6 +39,7 @@ export type AdapterResolvers = { typebox: TypeboxResolver; valibot: ValibotResolver; valita: ValitaResolver; + vine: VineResolver; yup: YupResolver; zod: ZodResolver; }; diff --git a/packages/main/src/selector.ts b/packages/main/src/selector.ts index 8a9a8566..09a1d21f 100644 --- a/packages/main/src/selector.ts +++ b/packages/main/src/selector.ts @@ -92,6 +92,7 @@ export type Select = : TSchema extends {kind: unknown} ? 'deepkit' : TSchema extends {addValidator: unknown} ? 'ow' : TSchema extends {toTerminals: unknown} ? 'valita' + : TSchema extends {bail: unknown} ? 'vine' : IsJSONSchema extends true ? 'json' : 'fastestValidator' : never; @@ -129,6 +130,7 @@ export const select: < if ('kind' in schema) return is.deepkit(notJSON(schema)); if ('addValidator' in schema) return is.ow(notJSON(schema)); if ('toTerminals' in schema) return is.valita(notJSON(schema)); + if ('bail' in schema) return is.vine(notJSON(schema)); if (isJSONSchema(schema)) return is.json(schema); return is.fastestValidator(schema); } diff --git a/packages/main/src/serialization.ts b/packages/main/src/serialization.ts index 5aec484c..a232a999 100644 --- a/packages/main/src/serialization.ts +++ b/packages/main/src/serialization.ts @@ -69,6 +69,7 @@ export const serializationAdapter: SerializationAdapter = selec typebox: async schema => (await importTypeboxSerializationAdapter())(schema), valibot: async schema => (await importValibotSerializationAdapter())(schema), valita: unsupportedAdapter('@typeschema/valita'), + vine: unsupportedAdapter('@typeschema/vine'), yup: async schema => (await importYupSerializationAdapter())(schema), zod: async schema => (await importZodSerializationAdapter())(schema), }); diff --git a/packages/main/src/validation.ts b/packages/main/src/validation.ts index 9ce525ab..8c301acf 100644 --- a/packages/main/src/validation.ts +++ b/packages/main/src/validation.ts @@ -92,6 +92,11 @@ const importValitaValidationAdapter = memoize(async () => { return validationAdapter; }); +const importVineValidationAdapter = memoize(async () => { + const {validationAdapter} = await import('@typeschema/vine'); + return validationAdapter; +}); + const importYupValidationAdapter = memoize(async () => { const {validationAdapter} = await import('@typeschema/yup'); return validationAdapter; @@ -119,6 +124,7 @@ export const validationAdapter: ValidationAdapter = select({ typebox: async schema => (await importTypeboxValidationAdapter())(schema), valibot: async schema => (await importValibotValidationAdapter())(schema), valita: async schema => (await importValitaValidationAdapter())(schema), + vine: async schema => (await importVineValidationAdapter())(schema), yup: async schema => (await importYupValidationAdapter())(schema), zod: async schema => (await importZodValidationAdapter())(schema), }); diff --git a/packages/vine/README.md b/packages/vine/README.md new file mode 100644 index 00000000..db42a5a9 --- /dev/null +++ b/packages/vine/README.md @@ -0,0 +1,49 @@ + + +TypeSchema +

@typeschema/vine

+

+ License + Bundle size + npm downloads + GitHub stars +

+

+ Reusable adapter for VineJS schemas +
+ https://typeschema.com ✨ +

+ +```ts +import {initTRPC} from '@trpc/server'; +import vine from '@vinejs/vine'; + +import {wrap} from '@typeschema/vine'; + +const schema = vine.object({name: vine.string()}); + +const t = initTRPC.create(); +const appRouter = t.router({ + hello: t.procedure + .input(wrap(schema)) + .query(({input}) => `Hello, ${input.name}!`), + // ^? {name: string} +}); + +``` + +Use it directly or through [`@typeschema/main`](https://github.com/decs/typeschema/tree/main/packages/main) + +## Dependencies +- [`@vinejs/vine`](https://www.npmjs.com/package/@vinejs/vine): Required for inference and validation (`^2.0.0`) + +## API + +### Inference +- `Infer`: Extracts the output type of a schema +- `InferIn`: Extracts the input type of a schema + +### Validation +- `wrap(schema)`: Returns the wrapped schema with access to its operations +- `validate(schema, data)`: Returns the validated data or a list of validation issues +- `assert(schema, data)`: Returns the validated data or throws an `AggregateError` diff --git a/packages/vine/package.json b/packages/vine/package.json new file mode 100644 index 00000000..8ff36790 --- /dev/null +++ b/packages/vine/package.json @@ -0,0 +1,77 @@ +{ + "//": "This file is partially generated. Only some fields can be modified manually!", + "name": "@typeschema/vine", + "//version": "This field is manually maintained.", + "version": "0.0.0", + "//description": "This field is manually maintained.", + "description": "Reusable adapter for VineJS schemas", + "keywords": [ + "typescript", + "type", + "schema", + "adapter", + "validation", + "inference", + "assert" + ], + "homepage": "https://typeschema.com", + "license": "MIT", + "author": { + "name": "André Costa", + "email": "andrefonsecacosta@gmail.com" + }, + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org/" + }, + "files": [ + "/dist" + ], + "main": "dist/index.js", + "module": "dist/index.mjs", + "types": "dist/index.d.ts", + "exports": { + ".": { + "import": { + "types": "./dist/index.d.mts", + "default": "./dist/index.mjs" + }, + "require": { + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + } + } + }, + "sideEffects": false, + "repository": { + "type": "git", + "url": "https://github.com/decs/typeschema.git" + }, + "scripts": { + "build": "tsup --config ../../tsup.config.ts", + "lint": "eslint src --fix", + "lint:package": "publint && attw --pack", + "test": "vitest --config ../../vitest.config.ts", + "upgrade:deps": "ncu -u --dep=dev,peer --reject ow" + }, + "dependencies": { + "@typeschema/core": "workspace:*" + }, + "//devDependencies": "This field is manually maintained.", + "devDependencies": { + "@vinejs/vine": "^2.0.0" + }, + "//peerDependencies": { + "//": "This field is manually maintained.", + "@vinejs/vine": "Required for inference and validation" + }, + "peerDependencies": { + "@vinejs/vine": "^2.0.0" + }, + "//peerDependenciesMeta": "This field is manually maintained.", + "peerDependenciesMeta": { + "@vinejs/vine": { + "optional": true + } + } +} diff --git a/packages/vine/src/__tests__/example.ts b/packages/vine/src/__tests__/example.ts new file mode 100644 index 00000000..14882d61 --- /dev/null +++ b/packages/vine/src/__tests__/example.ts @@ -0,0 +1,14 @@ +import {initTRPC} from '@trpc/server'; +import vine from '@vinejs/vine'; + +import {wrap} from '..'; + +const schema = vine.object({name: vine.string()}); + +const t = initTRPC.create(); +const appRouter = t.router({ + hello: t.procedure + .input(wrap(schema)) + .query(({input}) => `Hello, ${input.name}!`), + // ^? {name: string} +}); diff --git a/packages/vine/src/__tests__/tsconfig.json b/packages/vine/src/__tests__/tsconfig.json new file mode 100644 index 00000000..6a86fc8c --- /dev/null +++ b/packages/vine/src/__tests__/tsconfig.json @@ -0,0 +1,5 @@ +{ + "//": "This file is generated. Do not modify it manually!", + "extends": "../../../../tsconfig.test.json", + "include": ["*.ts"] +} diff --git a/packages/vine/src/__tests__/vine.test.ts b/packages/vine/src/__tests__/vine.test.ts new file mode 100644 index 00000000..d666c1b5 --- /dev/null +++ b/packages/vine/src/__tests__/vine.test.ts @@ -0,0 +1,78 @@ +import type {Infer, InferIn} from '..'; + +import {initTRPC} from '@trpc/server'; +import vine from '@vinejs/vine'; +import {expectTypeOf} from 'expect-type'; +import {describe, expect, test} from 'vitest'; + +import {assert, validate, wrap} from '..'; + +describe('vine', () => { + const schema = vine.object({ + age: vine.number(), + createdAt: vine.date({formats: {utc: true}}), + email: vine.string().email(), + id: vine.string(), + name: vine.string(), + updatedAt: vine.date({formats: {utc: true}}), + }); + + const data = { + age: 123 as string | number, + createdAt: '2021-01-01T00:00:00.000Z' as string | number, + email: 'john.doe@test.com', + id: 'c4a760a8-dbcf-4e14-9f39-645a8e933d74', + name: 'John Doe', + updatedAt: '2021-01-01T00:00:00.000Z' as string | number, + }; + const outputData = { + age: 123, + createdAt: new Date('2021-01-01T00:00:00.000Z'), + email: 'john.doe@test.com', + id: 'c4a760a8-dbcf-4e14-9f39-645a8e933d74', + name: 'John Doe', + updatedAt: new Date('2021-01-01T00:00:00.000Z'), + }; + const badData = { + age: '123a', + createdAt: '2021-01-01T00:00:00.000Z', + email: 'john.doe@test.com', + id: 'c4a760a8-dbcf-4e14-9f39-645a8e933d74', + name: 'John Doe', + updatedAt: '2021-01-01T00:00:00.000Z', + }; + + test('infer', () => { + expectTypeOf>().toEqualTypeOf(outputData); + expectTypeOf>().toEqualTypeOf(data); + }); + + test('validate', async () => { + expect(await validate(schema, data)).toStrictEqual({ + data: outputData, + success: true, + }); + expect(await validate(schema, badData)).toStrictEqual({ + issues: [{message: 'The age field must be a number', path: ['age']}], + success: false, + }); + }); + + test('assert', async () => { + expect(await assert(schema, data)).toStrictEqual(outputData); + await expect(assert(schema, badData)).rejects.toThrow(); + }); + + test('wrap', async () => { + const tRPC = initTRPC.create(); + const router = tRPC.router({ + hello: tRPC.procedure.input(wrap(schema)).query(({input}) => { + expectTypeOf().toEqualTypeOf(outputData); + return input; + }), + }); + const createCaller = tRPC.createCallerFactory(router); + const caller = createCaller({}); + expect(await caller.hello(data)).toStrictEqual(outputData); + }); +}); diff --git a/packages/vine/src/index.ts b/packages/vine/src/index.ts new file mode 100644 index 00000000..c5a15869 --- /dev/null +++ b/packages/vine/src/index.ts @@ -0,0 +1,37 @@ +/** + * This file is generated. Do not modify it manually! + */ + +import type { + InputFrom, + OutputFrom, + SchemaFrom, + UnknownIfNever, +} from '@typeschema/core'; + +import { + createAssert, + createValidate, + createWrap, +} from '@typeschema/core'; + +import {AdapterResolver} from './resolver'; +import {validationAdapter} from './validation'; + +export type Schema = SchemaFrom; +export type Infer = UnknownIfNever< + OutputFrom +>; +export type InferIn = UnknownIfNever< + InputFrom +>; + +export const validate = createValidate(validationAdapter); +export const assert = createAssert(validate); +export const wrap = createWrap(assert, validate); + + +export { + AdapterResolver, + validationAdapter, +}; diff --git a/packages/vine/src/resolver.ts b/packages/vine/src/resolver.ts new file mode 100644 index 00000000..5c8679a0 --- /dev/null +++ b/packages/vine/src/resolver.ts @@ -0,0 +1,12 @@ +import type {IfDefined, Resolver} from '@typeschema/core'; +import type {BaseType} from '@vinejs/vine'; +import type {Infer, InferInput} from '@vinejs/vine/types'; + +export interface AdapterResolver extends Resolver { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + base: IfDefined, '@vinejs/vine'>; + input: this['schema'] extends this['base'] + ? InferInput + : never; + output: this['schema'] extends this['base'] ? Infer : never; +} diff --git a/packages/vine/src/validation.ts b/packages/vine/src/validation.ts new file mode 100644 index 00000000..77651ffc --- /dev/null +++ b/packages/vine/src/validation.ts @@ -0,0 +1,37 @@ +import type {AdapterResolver} from './resolver'; +import type {ValidationAdapter} from '@typeschema/core'; + +import {memoize} from '@typeschema/core'; + +const importValidationModule = memoize(async () => { + const {errors, Vine} = await import('@vinejs/vine'); + return {errors, vine: new Vine()}; +}); + +export const validationAdapter: ValidationAdapter< + AdapterResolver +> = async schema => { + const {errors, vine} = await importValidationModule(); + const validator = vine.compile(schema); + return async data => { + try { + return { + data: await validator.validate(data), + success: true, + }; + } catch (error) { + if (error instanceof errors.E_VALIDATION_ERROR) { + return { + issues: error.messages.map( + ({message, field}: {message: string; field: string}) => ({ + message, + path: field != null ? field.split('.') : undefined, + }), + ), + success: false, + }; + } + throw error; + } + }; +}; diff --git a/packages/vine/tsconfig.json b/packages/vine/tsconfig.json new file mode 100644 index 00000000..caf27170 --- /dev/null +++ b/packages/vine/tsconfig.json @@ -0,0 +1,5 @@ +{ + "//": "This file is generated. Do not modify it manually!", + "extends": "../../tsconfig.json", + "include": ["src"] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f4dfd5b7..4f8ca992 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -281,6 +281,9 @@ importers: '@typeschema/valita': specifier: workspace:* version: link:../valita + '@typeschema/vine': + specifier: workspace:* + version: link:../vine '@typeschema/yup': specifier: workspace:* version: link:../yup @@ -309,6 +312,9 @@ importers: '@sodaru/yup-to-json-schema': specifier: ^2.0.1 version: 2.0.1 + '@vinejs/vine': + specifier: ^2.0.0 + version: 2.0.0 ajv: specifier: ^8.12.0 version: 8.12.0 @@ -559,12 +565,18 @@ importers: '@typeschema/valita': specifier: workspace:* version: link:../valita + '@typeschema/vine': + specifier: workspace:* + version: link:../vine '@typeschema/yup': specifier: workspace:* version: link:../yup '@typeschema/zod': specifier: workspace:* version: link:../zod + '@vinejs/vine': + specifier: ^2.0.0 + version: 2.0.0 ajv: specifier: ^8.12.0 version: 8.12.0 @@ -699,6 +711,16 @@ importers: specifier: ^0.3.6 version: 0.3.6 + packages/vine: + dependencies: + '@typeschema/core': + specifier: workspace:* + version: link:../core + devDependencies: + '@vinejs/vine': + specifier: ^2.0.0 + version: 2.0.0 + packages/yup: dependencies: '@typeschema/core': @@ -3486,6 +3508,11 @@ packages: write-yaml-file: 5.0.0 dev: true + /@poppinss/macroable@1.0.2: + resolution: {integrity: sha512-xhhEcEvhQC8mP5oOr5hbE4CmUgmw/IPV1jhpGg2xSkzoFrt9i8YVqBQt9744EFesi5F7pBheWozg63RUBM/5JA==} + engines: {node: '>=18.16.0'} + dev: true + /@rollup/plugin-node-resolve@15.2.3(rollup@4.12.0): resolution: {integrity: sha512-j/lym8nf5E21LwBT4Df1VD6hRO2L2iwUeUmP7litikRsVp1H6NWx20NEp0Y7su+7XGc476GnXXc4kFeZNGmaSQ==} engines: {node: '>=14.0.0'} @@ -4366,6 +4393,25 @@ packages: resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} dev: true + /@vinejs/compiler@2.5.0: + resolution: {integrity: sha512-hg4ekaB5Y2zh+IWzBiC/WCDWrIfpVnKu/ubUvelKlidc/VbulsexoFRw5kJGHZenPVI5YzNnDeTdYSALkTV7jQ==} + engines: {node: '>=18.0.0'} + dev: true + + /@vinejs/vine@2.0.0: + resolution: {integrity: sha512-NqgT4B2uo4mMsGI8LJdpuXNnan7F3xm10+kHaXpqI0PCYpn7+Xiic6av586mmj747/qZ3iR8o4C9cL54WU1fWw==} + engines: {node: '>=18.16.0'} + dependencies: + '@poppinss/macroable': 1.0.2 + '@types/validator': 13.11.9 + '@vinejs/compiler': 2.5.0 + camelcase: 8.0.0 + dayjs: 1.11.10 + dlv: 1.1.3 + normalize-url: 8.0.1 + validator: 13.11.0 + dev: true + /@vitest/expect@1.3.1: resolution: {integrity: sha512-xofQFwIzfdmLLlHa6ag0dPV8YsnKOCP1KdAeVVh34vSjN2dcUiXYCD9htu/9eM7t8Xln4v03U9HLxLpPlsXdZw==} dependencies: @@ -5255,6 +5301,11 @@ packages: engines: {node: '>=14.16'} dev: true + /camelcase@8.0.0: + resolution: {integrity: sha512-8WB3Jcas3swSvjIeA2yvCJ+Miyz5l1ZmB6HFb9R1317dt9LCQoswg/BGrmAmkWVEszSrrg4RwmO46qIm2OEnSA==} + engines: {node: '>=16'} + dev: true + /can-write-to-dir@1.1.1: resolution: {integrity: sha512-eOgiEWqjppB+3DN/5E82EQ8dTINus8d9GXMCbEsUnp2hcUIcXmBvzWmD3tXMk3CuBK0v+ddK9qw0EAF+JVRMjQ==} engines: {node: '>=10.13'} @@ -5749,6 +5800,10 @@ packages: engines: {node: '>= 14'} dev: true + /dayjs@1.11.10: + resolution: {integrity: sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ==} + dev: true + /debug@3.2.7: resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} peerDependencies: @@ -5908,6 +5963,10 @@ packages: path-type: 4.0.0 dev: true + /dlv@1.1.3: + resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==} + dev: true + /doctrine@2.1.0: resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} engines: {node: '>=0.10.0'} @@ -9047,6 +9106,11 @@ packages: engines: {node: '>=14.16'} dev: true + /normalize-url@8.0.1: + resolution: {integrity: sha512-IO9QvjUMWxPQQhs60oOu10CRkWCiZzSUkzbXGGV9pviYl1fXYcvkzQ5jV9z8Y6un8ARoVRl4EtC6v6jNqbaJ/w==} + engines: {node: '>=14.16'} + dev: true + /npm-bundled@2.0.1: resolution: {integrity: sha512-gZLxXdjEzE/+mOstGDqR6b0EkhJ+kM6fxM6vUuckuctuVPh80Q6pw/rSZj9s4Gex9GxWtIicO1pc8DB9KZWudw==} engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} @@ -12382,6 +12446,7 @@ packages: '@typeschema/typebox': link:packages/typebox '@typeschema/valibot': link:packages/valibot '@typeschema/valita': link:packages/valita + '@typeschema/vine': link:packages/vine '@typeschema/yup': link:packages/yup '@typeschema/zod': link:packages/zod dev: false @@ -12407,6 +12472,7 @@ packages: '@typeschema/typebox': workspace:* '@typeschema/valibot': workspace:* '@typeschema/valita': workspace:* + '@typeschema/vine': workspace:* '@typeschema/yup': workspace:* '@typeschema/zod': workspace:* peerDependenciesMeta: @@ -12442,6 +12508,8 @@ packages: optional: true '@typeschema/valita': optional: true + '@typeschema/vine': + optional: true '@typeschema/yup': optional: true '@typeschema/zod': diff --git a/tsconfig.test.json b/tsconfig.test.json index 092fbded..1c82ee8b 100644 --- a/tsconfig.test.json +++ b/tsconfig.test.json @@ -1,7 +1,7 @@ { "extends": "./tsconfig.json", "compilerOptions": { - "moduleResolution": "Node", + "moduleResolution": "Bundler", "rootDir": "..", "plugins": [{"transform": "typia/lib/transform"}], "experimentalDecorators": true, diff --git a/turbo/generators/config.ts b/turbo/generators/config.ts index abdada09..163e5fc5 100644 --- a/turbo/generators/config.ts +++ b/turbo/generators/config.ts @@ -304,6 +304,12 @@ export default function generator(plop: PlopTypes.NodePlopAPI): void { name: 'fastest-validator', url: 'https://github.com/icebob/fastest-validator', }, + { + adapter: adapters.find(adapter => adapter.name === 'vine'), + github: 'vinejs/vine', + name: 'vine', + url: 'https://vinejs.dev', + }, { adapter: adapters.find(adapter => adapter.name === 'suretype'), github: 'grantila/suretype',