Skip to content

Commit 57b35e8

Browse files
committed
feat: add validator in utils
1 parent 466827d commit 57b35e8

3 files changed

Lines changed: 120 additions & 14 deletions

File tree

packages/core/src/collection.ts

Lines changed: 28 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,13 @@ import type { ConditionalExcept, Simplify, UnionToIntersection, ValueOf } from '
33
import z from 'zod'
44

55
import type { MinimalContext } from './config'
6-
import type {
7-
ApiRoute,
8-
ApiRouteHandler,
9-
ApiRouter,
10-
ApiRouteSchema,
11-
ClientApiRouter,
6+
import {
7+
type ApiRoute,
8+
type ApiRouteHandler,
9+
type ApiRouter,
10+
type ApiRouteSchema,
11+
type ClientApiRouter,
12+
createEndpoint,
1213
} from './endpoint'
1314
import {
1415
type Field,
@@ -23,7 +24,7 @@ import {
2324
fieldsToZodObject,
2425
type FieldsWithFieldName,
2526
} from './field'
26-
import type { GetTableByTableTsName, ToZodObject } from './utils'
27+
import { type GetTableByTableTsName, type ToZodObject } from './utils'
2728

2829
type SimplifyConditionalExcept<Base, Condition> = Simplify<ConditionalExcept<Base, Condition>>
2930

@@ -724,7 +725,10 @@ export function getAllCollectionEndpoints<
724725
return { status: 200, body: response }
725726
}
726727

727-
return [endpointName, { schema, handler } satisfies ApiRoute<any, typeof schema>]
728+
return [
729+
endpointName,
730+
createEndpoint(schema, handler) satisfies ApiRoute<any, typeof schema>,
731+
]
728732
}
729733
case ApiDefaultMethod.FIND_ONE: {
730734
const response = fieldsToZodObject(fields)
@@ -752,7 +756,10 @@ export function getAllCollectionEndpoints<
752756
return { status: 200, body: response }
753757
}
754758

755-
return [endpointName, { schema, handler } satisfies ApiRoute<any, typeof schema>]
759+
return [
760+
endpointName,
761+
createEndpoint(schema, handler) satisfies ApiRoute<any, typeof schema>,
762+
]
756763
}
757764
case ApiDefaultMethod.FIND_MANY: {
758765
const response = fieldsToZodObject(fields)
@@ -786,7 +793,10 @@ export function getAllCollectionEndpoints<
786793
return { status: 200, body: response }
787794
}
788795

789-
return [endpointName, { schema, handler } satisfies ApiRoute<any, typeof schema>]
796+
return [
797+
endpointName,
798+
createEndpoint(schema, handler) satisfies ApiRoute<any, typeof schema>,
799+
]
790800
}
791801
case ApiDefaultMethod.UPDATE: {
792802
const body = fieldsToZodObject(fields)
@@ -820,7 +830,10 @@ export function getAllCollectionEndpoints<
820830
return { status: 200, body: response }
821831
}
822832

823-
return [endpointName, { schema, handler } satisfies ApiRoute<any, typeof schema>]
833+
return [
834+
endpointName,
835+
createEndpoint(schema, handler) satisfies ApiRoute<any, typeof schema>,
836+
]
824837
}
825838
case ApiDefaultMethod.DELETE: {
826839
const schema = {
@@ -846,7 +859,10 @@ export function getAllCollectionEndpoints<
846859
return { status: 200, body: { message: 'ok' } }
847860
}
848861

849-
return [endpointName, { schema, handler } satisfies ApiRoute<any, typeof schema>]
862+
return [
863+
endpointName,
864+
createEndpoint(schema, handler) satisfies ApiRoute<any, typeof schema>,
865+
]
850866
}
851867
default:
852868
throw new Error(`Unknown method: ${method}`)

packages/core/src/endpoint.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import type { z, ZodType } from 'zod'
44
import zodToJsonSchema from 'zod-to-json-schema'
55

66
import type { MaybePromise } from './collection'
7+
import { withValidator } from './utils'
78

89
export type ApiHttpStatus = 200 | 201 | 204 | 301 | 302 | 400 | 401 | 403 | 404 | 409 | 422 | 500
910

@@ -166,7 +167,7 @@ export function createEndpoint<
166167
>(schema: TApiEndpointSchema, handler: ApiRouteHandler<TContext, TApiEndpointSchema>) {
167168
return {
168169
schema,
169-
handler,
170+
handler: withValidator(schema, handler),
170171
}
171172
}
172173

packages/core/src/utils.ts

Lines changed: 90 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import type { Column, TableRelationalConfig } from 'drizzle-orm'
22
import { is, Table } from 'drizzle-orm'
33
import type { IsNever, Simplify, ValueOf } from 'type-fest'
4-
import type { ZodObject, ZodOptional, ZodType } from 'zod'
4+
import type { ZodError, ZodObject, ZodOptional, ZodType } from 'zod'
55

6+
import type { MaybePromise } from './collection'
7+
import type { ApiHttpStatus, ApiRouteHandlerPayloadWithContext, ApiRouteSchema } from './endpoint'
68
import type { Field, FieldRelation, Fields, FieldsInitial, FieldsWithFieldName } from './field'
79

810
export function isRelationField(field: Field): field is FieldRelation {
@@ -115,6 +117,93 @@ export function mapValueToTsValue(
115117
return Object.fromEntries(mappedEntries.filter((r) => r.length > 0))
116118
}
117119

120+
export async function validateRequestBody<
121+
TApiRouteSchema extends ApiRouteSchema = any,
122+
TContext extends Record<string, unknown> = Record<string, unknown>,
123+
>(schema: TApiRouteSchema, payload: ApiRouteHandlerPayloadWithContext<TApiRouteSchema, TContext>) {
124+
const zodErrors: ZodError[] = []
125+
if (schema.query) {
126+
const err = await schema.query.safeParseAsync((payload as any).query)
127+
if (!err.success) {
128+
zodErrors.push(err.error)
129+
}
130+
}
131+
132+
if (schema.pathParams) {
133+
const err = await schema.pathParams.safeParseAsync((payload as any).pathParams)
134+
if (!err.success) {
135+
zodErrors.push(err.error)
136+
}
137+
}
138+
139+
if (schema.headers) {
140+
const err = await schema.headers.safeParseAsync(payload.headers)
141+
if (!err.success) {
142+
zodErrors.push(err.error)
143+
}
144+
}
145+
146+
if (schema.method !== 'GET' && schema.body) {
147+
const err = await schema.body.safeParseAsync((payload as any).body)
148+
if (!err.success) {
149+
zodErrors.push(err.error)
150+
}
151+
}
152+
153+
return zodErrors
154+
}
155+
156+
export function validateResponseBody<TApiRouteSchema extends ApiRouteSchema = any>(
157+
schema: TApiRouteSchema,
158+
statusCode: ApiHttpStatus,
159+
response: any
160+
) {
161+
if (!schema.responses[statusCode]) {
162+
throw new Error(`No response schema defined for status code ${statusCode}`)
163+
}
164+
165+
const result = schema.responses[statusCode].safeParse(response)
166+
return result.error
167+
}
168+
169+
export function withValidator<
170+
TApiRouteSchema extends ApiRouteSchema,
171+
TContext extends Record<string, unknown>,
172+
>(
173+
schema: TApiRouteSchema,
174+
handler: (
175+
payload: ApiRouteHandlerPayloadWithContext<TApiRouteSchema, TContext>
176+
) => MaybePromise<any>
177+
): (payload: ApiRouteHandlerPayloadWithContext<TApiRouteSchema, TContext>) => MaybePromise<any> {
178+
return async (payload: ApiRouteHandlerPayloadWithContext<TApiRouteSchema, TContext>) => {
179+
const zodErrors = await validateRequestBody(schema, payload)
180+
if (zodErrors.length > 0) {
181+
return {
182+
status: 400,
183+
body: {
184+
error: 'Validation failed',
185+
details: zodErrors.map((e) => e.message),
186+
},
187+
}
188+
}
189+
190+
const response = await handler(payload)
191+
192+
const validationError = validateResponseBody(schema, response.status, response.body)
193+
if (validationError) {
194+
return {
195+
status: 500,
196+
body: {
197+
error: 'Response validation failed',
198+
details: validationError.errors.map((e) => e.message),
199+
},
200+
}
201+
}
202+
203+
return response
204+
}
205+
}
206+
118207
export type JoinArrays<T extends any[]> = Simplify<
119208
T extends [infer A]
120209
? IsNever<A> extends true

0 commit comments

Comments
 (0)