Skip to content

Commit 09cd3fe

Browse files
committed
feat: Add Standard Schema validator to qwik-city
1 parent aa595d4 commit 09cd3fe

File tree

10 files changed

+229
-29
lines changed

10 files changed

+229
-29
lines changed

package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,7 @@
124124
"@node-rs/helper": "1.6.0",
125125
"@octokit/action": "6.1.0",
126126
"@playwright/test": "1.50.1",
127+
"@standard-schema/spec": "^1.0.0",
127128
"@types/brotli": "1.3.4",
128129
"@types/bun": "1.1.6",
129130
"@types/cross-spawn": "6.0.6",
@@ -181,7 +182,7 @@
181182
"vitest": "2.0.5",
182183
"watchlist": "0.3.1",
183184
"which-pm-runs": "1.1.0",
184-
"zod": "3.22.4"
185+
"zod": "3.24.2"
185186
},
186187
"engines": {
187188
"node": ">=16.8.0 <18.0.0 || >=18.11",

packages/docs/src/routes/api/qwik-city/api.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -838,7 +838,7 @@
838838
}
839839
],
840840
"kind": "TypeAlias",
841-
"content": "```typescript\nexport type TypedDataValidator = ValibotDataValidator | ZodDataValidator;\n```",
841+
"content": "```typescript\nexport type TypedDataValidator = StandardSchemaDataValidator | ValibotDataValidator | ZodDataValidator;\n```",
842842
"editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik-city/src/runtime/src/types.ts",
843843
"mdFile": "qwik-city.typeddatavalidator.md"
844844
},

packages/docs/src/routes/api/qwik-city/index.md

+4-1
Original file line numberDiff line numberDiff line change
@@ -2373,7 +2373,10 @@ export type StrictUnion<T> = Prettify<StrictUnionHelper<T, T>>;
23732373
## TypedDataValidator
23742374
23752375
```typescript
2376-
export type TypedDataValidator = ValibotDataValidator | ZodDataValidator;
2376+
export type TypedDataValidator =
2377+
| StandardSchemaDataValidator
2378+
| ValibotDataValidator
2379+
| ZodDataValidator;
23772380
```
23782381
23792382
[Edit this section](https://github.com/QwikDev/qwik/tree/main/packages/qwik-city/src/runtime/src/types.ts)

packages/qwik-city/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
"vfile": "6.0.2",
1414
"vite": "^5",
1515
"vite-imagetools": "^7",
16-
"zod": "3.22.4"
16+
"zod": "3.24.2"
1717
},
1818
"devDependencies": {
1919
"@azure/functions": "3.5.1",

packages/qwik-city/src/runtime/src/api.md

+4-1
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import { RequestEventCommon } from '@builder.io/qwik-city/middleware/request-han
2323
import { RequestEventLoader } from '@builder.io/qwik-city/middleware/request-handler';
2424
import { RequestHandler } from '@builder.io/qwik-city/middleware/request-handler';
2525
import type { ResolveSyncValue } from '@builder.io/qwik-city/middleware/request-handler';
26+
import type { StandardSchemaV1 } from '@standard-schema/spec';
2627
import type * as v from 'valibot';
2728
import type { ValueOrPromise } from '@builder.io/qwik';
2829
import { z } from 'zod';
@@ -468,8 +469,10 @@ export type StaticGenerateHandler = ({ env, }: {
468469
// @public (undocumented)
469470
export type StrictUnion<T> = Prettify<StrictUnionHelper<T, T>>;
470471

472+
// Warning: (ae-forgotten-export) The symbol "StandardSchemaDataValidator" needs to be exported by the entry point index.d.ts
473+
//
471474
// @public (undocumented)
472-
export type TypedDataValidator = ValibotDataValidator | ZodDataValidator;
475+
export type TypedDataValidator = StandardSchemaDataValidator | ValibotDataValidator | ZodDataValidator;
473476

474477
// Warning: (ae-forgotten-export) The symbol "ContentState" needs to be exported by the entry point index.d.ts
475478
//

packages/qwik-city/src/runtime/src/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ export { routeAction$, routeActionQrl } from './server-functions';
6161
export { globalAction$, globalActionQrl } from './server-functions';
6262
export { routeLoader$, routeLoaderQrl } from './server-functions';
6363
export { server$, serverQrl } from './server-functions';
64+
export { schema$, standardSchemaQrl } from './server-functions';
6465
export { valibot$, valibotQrl } from './server-functions';
6566
export { zod$, zodQrl } from './server-functions';
6667
export { validator$, validatorQrl } from './server-functions';

packages/qwik-city/src/runtime/src/server-functions.ts

+59
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
type ValueOrPromise,
1414
} from '@builder.io/qwik';
1515

16+
import type { StandardSchemaV1 } from '@standard-schema/spec';
1617
import * as v from 'valibot';
1718
import { z } from 'zod';
1819
import type { RequestEventLoader } from '../../middleware/request-handler/types';
@@ -47,6 +48,9 @@ import type {
4748
ZodConstructor,
4849
ZodConstructorQRL,
4950
ZodDataValidator,
51+
StandardSchemaDataValidator,
52+
StandardSchemaConstructorQRL,
53+
StandardSchemaConstructor,
5054
} from './types';
5155
import { useAction, useLocation, useQwikCityEnv } from './use-functions';
5256

@@ -253,6 +257,61 @@ const flattenValibotIssues = (issues: v.GenericIssue[]) => {
253257
}, {});
254258
};
255259

260+
/** @public */
261+
export const standardSchemaQrl: StandardSchemaConstructorQRL = (
262+
qrl: QRL<StandardSchemaV1 | ((ev: RequestEvent) => StandardSchemaV1)>
263+
): StandardSchemaDataValidator => {
264+
if (isServer) {
265+
return {
266+
__brand: 'standard-schema',
267+
async validate(ev, inputData) {
268+
const schema: StandardSchemaV1 = await qrl
269+
.resolve()
270+
.then((obj) => ('~standard' in obj ? obj : obj(ev)));
271+
const data = inputData ?? (await ev.parseBody());
272+
const result = await schema['~standard'].validate(data);
273+
if (!result.issues) {
274+
return {
275+
success: true,
276+
data: result.value,
277+
};
278+
} else {
279+
if (isDev) {
280+
console.error('ERROR: Standard Schema validation failed', result.issues);
281+
}
282+
const formErrors: string[] = [];
283+
const fieldErrors: Partial<Record<string, string[]>> = {};
284+
for (const issue of result.issues) {
285+
const dotPath = issue.path
286+
?.map((item) => (typeof item === 'object' ? item.key : item))
287+
.join('.');
288+
if (dotPath) {
289+
const sub = fieldErrors[dotPath];
290+
if (sub) {
291+
sub.push(issue.message);
292+
} else {
293+
fieldErrors[dotPath] = [issue.message];
294+
}
295+
} else {
296+
formErrors.push(issue.message);
297+
}
298+
}
299+
return {
300+
success: false,
301+
status: 400,
302+
error: { formErrors, fieldErrors },
303+
};
304+
}
305+
},
306+
};
307+
}
308+
return undefined as never;
309+
};
310+
311+
/** @public */
312+
export const schema$: StandardSchemaConstructor =
313+
/*#__PURE__*/ implicit$FirstArg(standardSchemaQrl);
314+
256315
/** @alpha */
257316
export const valibotQrl: ValibotConstructorQRL = (
258317
qrl: QRL<

packages/qwik-city/src/runtime/src/server-functions.unit.ts

+85-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
import { describe, expectTypeOf, test } from 'vitest';
1+
import type { StandardSchemaV1 } from '@standard-schema/spec';
2+
import { describe, expectTypeOf, test, expect } from 'vitest';
23
import { z } from 'zod';
3-
import { server$ } from './server-functions';
4+
import { server$, schema$ } from './server-functions';
45
import type { RequestEventBase, ValidatorErrorType } from './types';
56

67
describe('types', () => {
@@ -161,4 +162,86 @@ describe('types', () => {
161162
someAnyType?: string;
162163
}>();
163164
});
165+
166+
describe('Standard schema type with zod', () => {
167+
const Schema = z.object({
168+
id: z.string().uuid(),
169+
username: z.string().min(4),
170+
password: z
171+
.string()
172+
.min(8)
173+
.regex(/^[a-zA-Z]+$/),
174+
verified: z.boolean().default(false),
175+
createdAt: z.date({ coerce: true }).default(new Date()),
176+
role: z.enum(['user', 'moderator', 'admin']).default('user'),
177+
});
178+
179+
test('Test types', () => {
180+
type ErrorType = ValidatorErrorType<
181+
StandardSchemaV1.InferOutput<typeof Schema>
182+
>['fieldErrors'];
183+
184+
type EqualType = {
185+
id?: string;
186+
username?: string;
187+
password?: string;
188+
verified?: string;
189+
role?: string;
190+
};
191+
192+
expectTypeOf<ErrorType>().toEqualTypeOf<EqualType>();
193+
expectTypeOf<ErrorType>().not.toEqualTypeOf<{
194+
someAnyType?: string;
195+
}>();
196+
});
197+
198+
test('Successful validation', async () => {
199+
const schema = schema$(Schema);
200+
const date = new Date();
201+
const okResult = await schema.validate(undefined as any, {
202+
id: '9ff695ee-6604-4db5-af25-98a6ac682705',
203+
username: 'test',
204+
password: 'testpassword',
205+
role: 'moderator',
206+
createdAt: date.toISOString(),
207+
});
208+
209+
expect(okResult).toEqual({
210+
success: true,
211+
data: {
212+
id: '9ff695ee-6604-4db5-af25-98a6ac682705',
213+
username: 'test',
214+
password: 'testpassword',
215+
verified: false,
216+
createdAt: date,
217+
role: 'moderator',
218+
},
219+
});
220+
});
221+
test('Failed validation', async () => {
222+
const schema = schema$(Schema);
223+
const failResult = await schema.validate(undefined as any, {
224+
id: 'invalid-uuid',
225+
password: 'short1',
226+
role: 'missing-role',
227+
date: 'Invalid date',
228+
});
229+
230+
expect(failResult).toEqual({
231+
success: false,
232+
status: 400,
233+
error: {
234+
formErrors: [],
235+
fieldErrors: {
236+
id: ['Invalid uuid'],
237+
password: ['String must contain at least 8 character(s)', 'Invalid'],
238+
username: ['Required'],
239+
role: [
240+
"Invalid enum value. Expected 'user' | 'moderator' | 'admin', received 'missing-role'",
241+
],
242+
},
243+
},
244+
});
245+
});
246+
});
164247
});

packages/qwik-city/src/runtime/src/types.ts

+42-11
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import type {
1717
} from '@builder.io/qwik-city/middleware/request-handler';
1818
import type * as v from 'valibot';
1919
import type * as z from 'zod';
20+
import type { StandardSchemaV1 } from '@standard-schema/spec';
2021

2122
export type {
2223
Cookie,
@@ -363,19 +364,23 @@ export type JSONObject = { [x: string]: JSONValue };
363364

364365
/** @public */
365366
export type GetValidatorInputType<VALIDATOR extends TypedDataValidator> =
366-
VALIDATOR extends ValibotDataValidator<infer TYPE>
367-
? v.InferInput<TYPE>
368-
: VALIDATOR extends ZodDataValidator<infer TYPE>
369-
? z.input<TYPE>
370-
: never;
367+
VALIDATOR extends StandardSchemaDataValidator<infer TYPE>
368+
? StandardSchemaV1.InferInput<TYPE>
369+
: VALIDATOR extends ValibotDataValidator<infer TYPE>
370+
? v.InferInput<TYPE>
371+
: VALIDATOR extends ZodDataValidator<infer TYPE>
372+
? z.input<TYPE>
373+
: never;
371374

372375
/** @public */
373376
export type GetValidatorOutputType<VALIDATOR extends TypedDataValidator> =
374-
VALIDATOR extends ValibotDataValidator<infer TYPE>
375-
? v.InferOutput<TYPE>
376-
: VALIDATOR extends ZodDataValidator<infer TYPE>
377-
? z.output<TYPE>
378-
: never;
377+
VALIDATOR extends StandardSchemaDataValidator<infer TYPE>
378+
? StandardSchemaV1.InferOutput<TYPE>
379+
: VALIDATOR extends ValibotDataValidator<infer TYPE>
380+
? v.InferOutput<TYPE>
381+
: VALIDATOR extends ZodDataValidator<infer TYPE>
382+
? z.output<TYPE>
383+
: never;
379384

380385
/** @public */
381386
export type GetValidatorType<VALIDATOR extends TypedDataValidator> =
@@ -826,6 +831,29 @@ export type ValidatorConstructorQRL = {
826831
): T extends ValidatorReturnFail<infer ERROR> ? DataValidator<ERROR> : DataValidator<never>;
827832
};
828833

834+
/** @public */
835+
export type StandardSchemaDataValidator<T extends StandardSchemaV1 = StandardSchemaV1> = {
836+
readonly __brand: 'standard-schema';
837+
validate(
838+
ev: RequestEvent,
839+
data: unknown
840+
): Promise<ValidatorReturn<ValidatorErrorType<StandardSchemaV1.InferInput<T>>>>;
841+
};
842+
843+
/** @public */
844+
export type StandardSchemaConstructor = {
845+
<T extends StandardSchemaV1>(schema: T): StandardSchemaDataValidator<T>;
846+
<T extends StandardSchemaV1>(schema: (ev: RequestEvent) => T): StandardSchemaDataValidator<T>;
847+
};
848+
849+
/** @public */
850+
export type StandardSchemaConstructorQRL = {
851+
<T extends StandardSchemaV1>(schema: QRL<T>): StandardSchemaDataValidator<T>;
852+
<T extends StandardSchemaV1>(
853+
schema: QRL<(ev: RequestEvent) => T>
854+
): StandardSchemaDataValidator<T>;
855+
};
856+
829857
/** @alpha */
830858
export type ValibotDataValidator<
831859
T extends v.GenericSchema | v.GenericSchemaAsync = v.GenericSchema | v.GenericSchemaAsync,
@@ -883,7 +911,10 @@ export type ZodConstructorQRL = {
883911
};
884912

885913
/** @public */
886-
export type TypedDataValidator = ValibotDataValidator | ZodDataValidator;
914+
export type TypedDataValidator =
915+
| StandardSchemaDataValidator
916+
| ValibotDataValidator
917+
| ZodDataValidator;
887918

888919
/** @public */
889920
export interface ServerConfig {

0 commit comments

Comments
 (0)