From d8a61cc504f7030751cad1b5d3349570704f172f Mon Sep 17 00:00:00 2001 From: Fabian Hiller Date: Sat, 25 Jan 2025 12:20:37 -0500 Subject: [PATCH 1/3] feat: start Standard Schema validator implementation --- packages/qwik-router/package.json | 6 +- packages/qwik-router/src/runtime/src/index.ts | 2 - .../src/runtime/src/server-functions.ts | 117 ++++++++++-------- packages/qwik-router/src/runtime/src/types.ts | 106 ++++++++-------- .../src/optimizer/src/qwik-binding-map.ts | 10 -- pnpm-lock.yaml | 54 +++++--- 6 files changed, 157 insertions(+), 138 deletions(-) diff --git a/packages/qwik-router/package.json b/packages/qwik-router/package.json index f68337b3b89..b7fd5548a45 100644 --- a/packages/qwik-router/package.json +++ b/packages/qwik-router/package.json @@ -5,15 +5,17 @@ "bugs": "https://github.com/QwikDev/qwik/issues", "dependencies": { "@mdx-js/mdx": "^3", + "@standard-schema/spec": "^1.0.0-rc.0", "@types/mdx": "^2", "source-map": "^0.7.4", "svgo": "^3.3", + "type-fest": "^4.33.0", "undici": "*", - "valibot": ">=0.36.0 <2", + "valibot": "^1.0.0-beta.14", "vfile": "6.0.2", "vite": "^5", "vite-imagetools": "^7", - "zod": "3.22.4" + "zod": "^3.24.1" }, "devDependencies": { "@azure/functions": "3.5.1", diff --git a/packages/qwik-router/src/runtime/src/index.ts b/packages/qwik-router/src/runtime/src/index.ts index b2517744519..b0028a30c0b 100644 --- a/packages/qwik-router/src/runtime/src/index.ts +++ b/packages/qwik-router/src/runtime/src/index.ts @@ -86,8 +86,6 @@ export { usePreventNavigateQrl, } from './use-functions'; -export { z } from 'zod'; - export { Form } from './form-component'; export type { FormProps } from './form-component'; diff --git a/packages/qwik-router/src/runtime/src/server-functions.ts b/packages/qwik-router/src/runtime/src/server-functions.ts index 120d35290b8..c6648be7bdc 100644 --- a/packages/qwik-router/src/runtime/src/server-functions.ts +++ b/packages/qwik-router/src/runtime/src/server-functions.ts @@ -40,6 +40,9 @@ import type { ServerConfig, ServerFunction, ServerQRL, + StandardSchemaConstructor, + StandardSchemaConstructorQRL, + StandardSchemaDataValidator, ValibotConstructor, ValibotConstructorQRL, ValibotDataValidator, @@ -56,6 +59,7 @@ import { isDev, isServer } from '@qwik.dev/core'; import type { FormSubmitCompletedDetail } from './form-component'; import { deepFreeze } from './utils'; +import type { StandardSchemaV1 } from '@standard-schema/spec'; /** @internal */ export const routeActionQrl = (( @@ -232,31 +236,60 @@ export const validatorQrl = (( /** @public */ export const validator$: ValidatorConstructor = /*#__PURE__*/ implicit$FirstArg(validatorQrl); -const flattenValibotIssues = (issues: v.GenericIssue[]) => { - return issues.reduce>((acc, issue) => { - if (issue.path) { - const hasArrayType = issue.path.some((path) => path.type === 'array'); - if (hasArrayType) { - const keySuffix = issue.expected === 'Array' ? '[]' : ''; - const key = - issue.path - .map((item) => (item.type === 'array' ? '*' : item.key)) - .join('.') - .replace(/\.\*/g, '[]') + keySuffix; - acc[key] = acc[key] || []; - if (Array.isArray(acc[key])) { - (acc[key] as string[]).push(issue.message); +/** @public */ +export const schemaQrl: StandardSchemaConstructorQRL = ( + qrl: QRL StandardSchemaV1)> +): StandardSchemaDataValidator => { + if (isServer) { + return { + __brand: 'standard-schema', + async validate(ev, inputData) { + const schema: StandardSchemaV1 = await qrl + .resolve() + .then((obj) => (typeof obj === 'function' ? obj(ev) : obj)); + const data = inputData ?? (await ev.parseBody()); + const result = await schema['~standard'].validate(data); + if (!result.issues) { + return { + success: true, + data: result.value, + }; + } else { + if (isDev) { + console.error('ERROR: Standard Schema validation failed', result.issues); + } + const formErrors: string[] = []; + const fieldErrors: Partial> = {}; + for (const issue of result.issues) { + const dotPath = issue.path + ?.map((item) => (typeof item === 'object' ? item.key : item)) + .join('.'); + if (dotPath) { + if (fieldErrors[dotPath]) { + fieldErrors[dotPath]!.push(issue.message); + } else { + fieldErrors[dotPath] = [issue.message]; + } + } else { + formErrors.push(issue.message); + } + } + return { + success: false, + status: 400, + error: { formErrors, fieldErrors }, + }; } - return acc; - } else { - acc[issue.path.map((item) => item.key).join('.')] = issue.message; - } - } - return acc; - }, {}); + }, + }; + } + return undefined as never; }; -/** @internal */ +/** @public */ +export const schema$: StandardSchemaConstructor = /*#__PURE__*/ implicit$FirstArg(schemaQrl); + +/** @deprecated */ export const valibotQrl: ValibotConstructorQRL = ( qrl: QRL< | v.GenericSchema @@ -287,12 +320,13 @@ export const valibotQrl: ValibotConstructorQRL = ( if (isDev) { console.error('ERROR: Valibot validation failed', result.issues); } + const flattErrors = v.flatten(result.issues); return { success: false, status: 400, error: { - formErrors: v.flatten(result.issues).root ?? [], - fieldErrors: flattenValibotIssues(result.issues), + formErrors: flattErrors.root ?? [], + fieldErrors: flattErrors.nested ?? {}, }, }; } @@ -302,34 +336,10 @@ export const valibotQrl: ValibotConstructorQRL = ( return undefined as never; }; -/** @beta */ +/** @deprecated */ export const valibot$: ValibotConstructor = /*#__PURE__*/ implicit$FirstArg(valibotQrl); -const flattenZodIssues = (issues: z.ZodIssue | z.ZodIssue[]) => { - issues = Array.isArray(issues) ? issues : [issues]; - return issues.reduce>((acc, issue) => { - const isExpectingArray = 'expected' in issue && issue.expected === 'array'; - const hasArrayType = issue.path.some((path) => typeof path === 'number') || isExpectingArray; - if (hasArrayType) { - const keySuffix = 'expected' in issue && issue.expected === 'array' ? '[]' : ''; - const key = - issue.path - .map((path) => (typeof path === 'number' ? '*' : path)) - .join('.') - .replace(/\.\*/g, '[]') + keySuffix; - acc[key] = acc[key] || []; - if (Array.isArray(acc[key])) { - (acc[key] as string[]).push(issue.message); - } - return acc; - } else { - acc[issue.path.join('.')] = issue.message; - } - return acc; - }, {}); -}; - -/** @internal */ +/** @deprecated */ export const zodQrl: ZodConstructorQRL = ( qrl: QRL< z.ZodRawShape | z.Schema | ((z: typeof import('zod').z, ev: RequestEvent) => z.ZodRawShape) @@ -357,12 +367,13 @@ export const zodQrl: ZodConstructorQRL = ( if (isDev) { console.error('ERROR: Zod validation failed', result.error.issues); } + const flattErrors = result.error.flatten(); return { success: false, status: 400, error: { - formErrors: result.error.flatten().formErrors, - fieldErrors: flattenZodIssues(result.error.issues), + formErrors: flattErrors.formErrors, + fieldErrors: flattErrors.fieldErrors, }, }; } @@ -372,7 +383,7 @@ export const zodQrl: ZodConstructorQRL = ( return undefined as never; }; -/** @public */ +/** @deprecated */ export const zod$: ZodConstructor = /*#__PURE__*/ implicit$FirstArg(zodQrl); /** @internal */ diff --git a/packages/qwik-router/src/runtime/src/types.ts b/packages/qwik-router/src/runtime/src/types.ts index 522673f081e..6f2642ad303 100644 --- a/packages/qwik-router/src/runtime/src/types.ts +++ b/packages/qwik-router/src/runtime/src/types.ts @@ -15,6 +15,8 @@ import type { RequestHandler, ResolveSyncValue, } from '@qwik.dev/router/middleware/request-handler'; +import type { StandardSchemaV1 } from '@standard-schema/spec'; +import type { Paths, Simplify } from 'type-fest'; import type * as v from 'valibot'; import type * as z from 'zod'; @@ -355,11 +357,7 @@ type StrictUnionHelper = T extends any : never; /** @public */ -export type StrictUnion = Prettify>; - -type Prettify = {} & { - [K in keyof T]: T[K]; -}; +export type StrictUnion = Simplify>; /** @public */ export type JSONValue = string | number | boolean | { [x: string]: JSONValue } | Array; @@ -369,19 +367,23 @@ export type JSONObject = { [x: string]: JSONValue }; /** @public */ export type GetValidatorInputType = - VALIDATOR extends ValibotDataValidator - ? v.InferInput - : VALIDATOR extends ZodDataValidator - ? z.input - : never; + VALIDATOR extends StandardSchemaDataValidator + ? StandardSchemaV1.InferInput + : VALIDATOR extends ValibotDataValidator + ? v.InferInput + : VALIDATOR extends ZodDataValidator + ? z.input + : never; /** @public */ export type GetValidatorOutputType = - VALIDATOR extends ValibotDataValidator - ? v.InferOutput - : VALIDATOR extends ZodDataValidator - ? z.output - : never; + VALIDATOR extends StandardSchemaDataValidator + ? StandardSchemaV1.InferOutput + : VALIDATOR extends ValibotDataValidator + ? v.InferOutput + : VALIDATOR extends ZodDataValidator + ? z.output + : never; /** @public */ export type GetValidatorType = @@ -400,36 +402,10 @@ export type FailOfRest = REST extends rea ? ERROR : never; -type IsAny = 0 extends 1 & Type ? true : false; - -/** @public */ -export type ValidatorErrorKeyDotNotation = - IsAny extends true - ? never - : T extends object - ? { - [K in keyof T & string]: IsAny extends true - ? never - : T[K] extends (infer U)[] - ? IsAny extends true - ? never - : U extends object - ? `${Prefix}${K}[]` | ValidatorErrorKeyDotNotation - : `${Prefix}${K}[]` - : T[K] extends object - ? ValidatorErrorKeyDotNotation - : `${Prefix}${K}`; - }[keyof T & string] - : never; - -/** @public */ -export type ValidatorErrorType = { - formErrors: U[]; - fieldErrors: Partial<{ - [K in ValidatorErrorKeyDotNotation]: K extends `${infer _Prefix}[]${infer _Suffix}` - ? U[] - : U; - }>; +/** @public */ +export type ValidatorErrorType = { + formErrors: string[]; + fieldErrors: Simplify : string, string[]>>>; }; /** @public */ @@ -832,7 +808,30 @@ export type ValidatorConstructorQRL = { ): T extends ValidatorReturnFail ? DataValidator : DataValidator; }; -/** @beta */ +/** @public */ +export type StandardSchemaDataValidator = { + readonly __brand: 'standard-schema'; + validate( + ev: RequestEvent, + data: unknown + ): Promise>>>; +}; + +/** @public */ +export type StandardSchemaConstructor = { + (schema: T): StandardSchemaDataValidator; + (schema: (ev: RequestEvent) => T): StandardSchemaDataValidator; +}; + +/** @public */ +export type StandardSchemaConstructorQRL = { + (schema: QRL): StandardSchemaDataValidator; + ( + schema: QRL<(ev: RequestEvent) => T> + ): StandardSchemaDataValidator; +}; + +/** @deprecated */ export type ValibotDataValidator< T extends v.GenericSchema | v.GenericSchemaAsync = v.GenericSchema | v.GenericSchemaAsync, > = { @@ -843,7 +842,7 @@ export type ValibotDataValidator< ): Promise>>>; }; -/** @beta */ +/** @deprecated */ export type ValibotConstructor = { (schema: T): ValibotDataValidator; ( @@ -851,7 +850,7 @@ export type ValibotConstructor = { ): ValibotDataValidator; }; -/** @beta */ +/** @deprecated */ export type ValibotConstructorQRL = { (schema: QRL): ValibotDataValidator; ( @@ -859,7 +858,7 @@ export type ValibotConstructorQRL = { ): ValibotDataValidator; }; -/** @public */ +/** @deprecated */ export type ZodDataValidator = { readonly __brand: 'zod'; validate( @@ -868,7 +867,7 @@ export type ZodDataValidator = { ): Promise>>>; }; -/** @public */ +/** @deprecated */ export type ZodConstructor = { (schema: T): ZodDataValidator>; ( @@ -878,7 +877,7 @@ export type ZodConstructor = { (schema: (zod: typeof z.z, ev: RequestEvent) => T): ZodDataValidator; }; -/** @public */ +/** @deprecated */ export type ZodConstructorQRL = { (schema: QRL): ZodDataValidator>; ( @@ -889,7 +888,10 @@ export type ZodConstructorQRL = { }; /** @public */ -export type TypedDataValidator = ValibotDataValidator | ZodDataValidator; +export type TypedDataValidator = + | StandardSchemaDataValidator + | ValibotDataValidator + | ZodDataValidator; /** @public */ export interface ServerConfig { diff --git a/packages/qwik/src/optimizer/src/qwik-binding-map.ts b/packages/qwik/src/optimizer/src/qwik-binding-map.ts index cceeb273668..b1233616d76 100644 --- a/packages/qwik/src/optimizer/src/qwik-binding-map.ts +++ b/packages/qwik/src/optimizer/src/qwik-binding-map.ts @@ -30,15 +30,5 @@ export const QWIK_BINDING_MAP = { "platformArchABI": "qwik.win32-x64-msvc.node" } ] - }, - "linux": { - "x64": [ - { - "platform": "linux", - "arch": "x64", - "abi": "gnu", - "platformArchABI": "qwik.linux-x64-gnu.node" - } - ] } }; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index dca841cc07a..b98b372af15 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -615,6 +615,9 @@ importers: '@mdx-js/mdx': specifier: ^3 version: 3.0.1 + '@standard-schema/spec': + specifier: ^1.0.0-rc.0 + version: 1.0.0-rc.0 '@types/mdx': specifier: ^2 version: 2.0.13 @@ -624,12 +627,15 @@ importers: svgo: specifier: ^3.3 version: 3.3.2 + type-fest: + specifier: ^4.33.0 + version: 4.33.0 undici: specifier: '*' version: 6.18.2 valibot: - specifier: '>=0.36.0 <2' - version: 0.42.1(typescript@5.4.5) + specifier: ^1.0.0-beta.14 + version: 1.0.0-beta.14(typescript@5.4.5) vfile: specifier: 6.0.2 version: 6.0.2 @@ -640,8 +646,8 @@ importers: specifier: ^7 version: 7.0.4(rollup@4.24.2) zod: - specifier: 3.22.4 - version: 3.22.4 + specifier: ^3.24.1 + version: 3.24.1 devDependencies: '@azure/functions': specifier: 3.5.1 @@ -3258,6 +3264,9 @@ packages: resolution: {integrity: sha512-doH1gimEu3A46VX6aVxpHTeHrytJAG6HgdxntYnCFiIFHEM/ZGpG8KiZGBChchjQmG0XFIBL552kBTjVcMZXwQ==} engines: {node: '>=12'} + '@standard-schema/spec@1.0.0-rc.0': + resolution: {integrity: sha512-DcY/ICFcZIESNTLTexIT108HOqd1FtxsiLV4ZYGluySWyjF6TZ6troNyXjiqoHU6j0wN3A6SmYnTA5CHQp9blw==} + '@supabase/auth-helpers-shared@0.6.3': resolution: {integrity: sha512-xYQRLFeFkL4ZfwC7p9VKcarshj3FB2QJMgJPydvOY7J5czJe6xSG5/wM1z63RmAzGbCkKg+dzpq61oeSyWiGBQ==} peerDependencies: @@ -9183,14 +9192,14 @@ packages: resolution: {integrity: sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==} engines: {node: '>=12.20'} - type-fest@4.19.0: - resolution: {integrity: sha512-CN2l+hWACRiejlnr68vY0/7734Kzu+9+TOslUXbSCQ1ruY9XIHDBSceVXCcHm/oXrdzhtLMMdJEKfemf1yXiZQ==} - engines: {node: '>=16'} - type-fest@4.30.0: resolution: {integrity: sha512-G6zXWS1dLj6eagy6sVhOMQiLtJdxQBHIA9Z6HFUNLOlr6MFOgzV8wvmidtPONfPtEUv0uZsy77XJNzTAfwPDaA==} engines: {node: '>=16'} + type-fest@4.33.0: + resolution: {integrity: sha512-s6zVrxuyKbbAsSAD5ZPTB77q4YIdRctkTbJ2/Dqlinwz+8ooH2gd+YA7VA6Pa93KML9GockVvoxjZ2vHP+mu8g==} + engines: {node: '>=16'} + type-is@1.6.18: resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} engines: {node: '>= 0.6'} @@ -9421,8 +9430,8 @@ packages: valibot@0.33.3: resolution: {integrity: sha512-/fuY1DlX8uiQ7aphlzrrI2DbG0YJk84JMgvz2qKpUIdXRNsS53varfo4voPjSrjUr5BSV2K0miSEJUOlA5fQFg==} - valibot@0.42.1: - resolution: {integrity: sha512-3keXV29Ar5b//Hqi4MbSdV7lfVp6zuYLZuA9V1PvQUsXqogr+u5lvLPLk3A4f74VUXDnf/JfWMN6sB+koJ/FFw==} + valibot@1.0.0-beta.14: + resolution: {integrity: sha512-tLyV2rE5QL6U29MFy3xt4AqMrn+/HErcp2ZThASnQvPMwfSozjV1uBGKIGiegtZIGjinJqn0SlBdannf18wENA==} peerDependencies: typescript: 5.4.5 peerDependenciesMeta: @@ -9841,6 +9850,9 @@ packages: zod@3.23.8: resolution: {integrity: sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==} + zod@3.24.1: + resolution: {integrity: sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A==} + zwitch@2.0.4: resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} @@ -11807,7 +11819,7 @@ snapshots: unixify: 1.0.0 urlpattern-polyfill: 8.0.2 yargs: 17.7.2 - zod: 3.23.8 + zod: 3.24.1 transitivePeerDependencies: - encoding - supports-color @@ -12179,6 +12191,8 @@ snapshots: dependencies: escape-string-regexp: 5.0.0 + '@standard-schema/spec@1.0.0-rc.0': {} + '@supabase/auth-helpers-shared@0.6.3(@supabase/supabase-js@2.44.4)': dependencies: '@supabase/supabase-js': 2.44.4 @@ -14065,7 +14079,7 @@ snapshots: dot-prop@9.0.0: dependencies: - type-fest: 4.19.0 + type-fest: 4.30.0 dotenv@16.4.5: {} @@ -16927,7 +16941,7 @@ snapshots: workerd: 1.20240718.0 ws: 8.18.0(bufferutil@4.0.8)(utf-8-validate@5.0.10) youch: 3.3.3 - zod: 3.22.4 + zod: 3.24.1 transitivePeerDependencies: - bufferutil - supports-color @@ -17606,7 +17620,7 @@ snapshots: dependencies: '@babel/code-frame': 7.24.6 index-to-position: 0.1.2 - type-fest: 4.19.0 + type-fest: 4.30.0 parse-ms@3.0.0: {} @@ -18053,7 +18067,7 @@ snapshots: dependencies: find-up-simple: 1.0.0 read-pkg: 9.0.1 - type-fest: 4.19.0 + type-fest: 4.30.0 read-pkg-up@9.1.0: dependencies: @@ -18073,7 +18087,7 @@ snapshots: '@types/normalize-package-data': 2.4.4 normalize-package-data: 6.0.1 parse-json: 8.1.0 - type-fest: 4.19.0 + type-fest: 4.30.0 unicorn-magic: 0.1.0 read-yaml-file@1.1.0: @@ -19221,10 +19235,10 @@ snapshots: type-fest@2.19.0: {} - type-fest@4.19.0: {} - type-fest@4.30.0: {} + type-fest@4.33.0: {} + type-is@1.6.18: dependencies: media-typer: 0.3.0 @@ -19462,7 +19476,7 @@ snapshots: valibot@0.33.3: {} - valibot@0.42.1(typescript@5.4.5): + valibot@1.0.0-beta.14(typescript@5.4.5): optionalDependencies: typescript: 5.4.5 @@ -19945,4 +19959,6 @@ snapshots: zod@3.23.8: {} + zod@3.24.1: {} + zwitch@2.0.4: {} From 2ecadd38254efc9d7f8a74415a523722ad2b63ae Mon Sep 17 00:00:00 2001 From: Fabian Hiller Date: Sat, 25 Jan 2025 18:16:11 -0500 Subject: [PATCH 2/3] fix: remove deleted type from exports --- packages/qwik-router/src/runtime/src/index.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/qwik-router/src/runtime/src/index.ts b/packages/qwik-router/src/runtime/src/index.ts index b0028a30c0b..badff2a824b 100644 --- a/packages/qwik-router/src/runtime/src/index.ts +++ b/packages/qwik-router/src/runtime/src/index.ts @@ -41,7 +41,6 @@ export type { RouteNavigate, StaticGenerate, StaticGenerateHandler, - ValidatorErrorKeyDotNotation, ValidatorErrorType, ZodConstructor, } from './types'; From 1a25c54ead8c98d9c53f87cc351c2052340f1f81 Mon Sep 17 00:00:00 2001 From: Fabian Hiller Date: Sun, 26 Jan 2025 11:55:37 -0500 Subject: [PATCH 3/3] fix: revert changes in qwik-binding-map.ts of optimizer --- packages/qwik/src/optimizer/src/qwik-binding-map.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/packages/qwik/src/optimizer/src/qwik-binding-map.ts b/packages/qwik/src/optimizer/src/qwik-binding-map.ts index b1233616d76..cceeb273668 100644 --- a/packages/qwik/src/optimizer/src/qwik-binding-map.ts +++ b/packages/qwik/src/optimizer/src/qwik-binding-map.ts @@ -30,5 +30,15 @@ export const QWIK_BINDING_MAP = { "platformArchABI": "qwik.win32-x64-msvc.node" } ] + }, + "linux": { + "x64": [ + { + "platform": "linux", + "arch": "x64", + "abi": "gnu", + "platformArchABI": "qwik.linux-x64-gnu.node" + } + ] } };