From de11bc5c58a09951f68d90f74393ac1069b2fbab Mon Sep 17 00:00:00 2001 From: ymc9 <104139426+ymc9@users.noreply.github.com> Date: Fri, 13 Jun 2025 10:53:06 +0800 Subject: [PATCH 1/2] chore: cache schema validators --- packages/runtime/package.json | 1 + packages/runtime/src/client/client-impl.ts | 14 ++- packages/runtime/src/client/crud/validator.ts | 113 ++++++++++++++---- pnpm-lock.yaml | 20 ++++ 4 files changed, 119 insertions(+), 29 deletions(-) diff --git a/packages/runtime/package.json b/packages/runtime/package.json index d4890d05..95d4a49b 100644 --- a/packages/runtime/package.json +++ b/packages/runtime/package.json @@ -87,6 +87,7 @@ "@paralleldrive/cuid2": "^2.2.2", "decimal.js": "^10.4.3", "is-plain-object": "^5.0.0", + "json-stable-stringify": "^1.3.0", "kysely": "^0.27.5", "nanoid": "^5.0.9", "pg-connection-string": "^2.9.0", diff --git a/packages/runtime/src/client/client-impl.ts b/packages/runtime/src/client/client-impl.ts index 16fa1761..3d7109a6 100644 --- a/packages/runtime/src/client/client-impl.ts +++ b/packages/runtime/src/client/client-impl.ts @@ -248,6 +248,9 @@ export class ClientImpl { function createClientProxy( client: ClientContract ): ClientImpl { + const inputValidator = new InputValidator(client.$schema); + const resultProcessor = new ResultProcessor(client.$schema); + return new Proxy(client, { get: (target, prop, receiver) => { if (typeof prop === 'string' && prop.startsWith('$')) { @@ -261,7 +264,9 @@ function createClientProxy( if (model) { return createModelCrudHandler( client, - model as GetModels + model as GetModels, + inputValidator, + resultProcessor ); } } @@ -276,11 +281,10 @@ function createModelCrudHandler< Model extends GetModels >( client: ClientContract, - model: Model + model: Model, + inputValidator: InputValidator, + resultProcessor: ResultProcessor ): ModelOperations { - const inputValidator = new InputValidator(client.$schema); - const resultProcessor = new ResultProcessor(client.$schema); - const createPromise = ( operation: CrudOperation, args: unknown, diff --git a/packages/runtime/src/client/crud/validator.ts b/packages/runtime/src/client/crud/validator.ts index 2a7b13fe..5f9aec77 100644 --- a/packages/runtime/src/client/crud/validator.ts +++ b/packages/runtime/src/client/crud/validator.ts @@ -1,4 +1,5 @@ import Decimal from 'decimal.js'; +import stableStringify from 'json-stable-stringify'; import { match, P } from 'ts-pattern'; import { z, ZodSchema } from 'zod'; import type { @@ -33,69 +34,100 @@ import { requireModel, } from '../query-utils'; +type GetSchemaFunc = ( + model: GetModels, + options: Options +) => ZodSchema; + export class InputValidator { + private schemaCache = new Map(); + constructor(private readonly schema: Schema) {} validateFindArgs(model: GetModels, unique: boolean, args: unknown) { - return this.validate, true>>( - this.makeFindSchema(model, unique, true), + return this.validate< + FindArgs, true>, + Parameters[1] + >( + model, 'find', + { unique, collection: true }, + (model, options) => this.makeFindSchema(model, options), args ); } validateCreateArgs(model: GetModels, args: unknown) { return this.validate>>( - this.makeCreateSchema(model), + model, 'create', + undefined, + (model) => this.makeCreateSchema(model), args ); } validateCreateManyArgs(model: GetModels, args: unknown) { return this.validate< - CreateManyArgs> | undefined - >(this.makeCreateManySchema(model), 'createMany', args); + CreateManyArgs>, + undefined + >( + model, + 'createMany', + undefined, + (model) => this.makeCreateManySchema(model), + args + ); } validateCreateManyAndReturnArgs(model: GetModels, args: unknown) { return this.validate< CreateManyAndReturnArgs> | undefined >( - this.makeCreateManyAndReturnSchema(model), + model, 'createManyAndReturn', + undefined, + (model) => this.makeCreateManyAndReturnSchema(model), args ); } validateUpdateArgs(model: GetModels, args: unknown) { return this.validate>>( - this.makeUpdateSchema(model), + model, 'update', + undefined, + (model) => this.makeUpdateSchema(model), args ); } validateUpdateManyArgs(model: GetModels, args: unknown) { return this.validate>>( - this.makeUpdateManySchema(model), + model, 'updateMany', + undefined, + (model) => this.makeUpdateManySchema(model), args ); } validateUpsertArgs(model: GetModels, args: unknown) { return this.validate>>( - this.makeUpsertSchema(model), + model, 'upsert', + undefined, + (model) => this.makeUpsertSchema(model), args ); } validateDeleteArgs(model: GetModels, args: unknown) { return this.validate>>( - this.makeDeleteSchema(model), + model, 'delete', + undefined, + (model) => this.makeDeleteSchema(model), args ); } @@ -103,34 +135,68 @@ export class InputValidator { validateDeleteManyArgs(model: GetModels, args: unknown) { return this.validate< DeleteManyArgs> | undefined - >(this.makeDeleteManySchema(model), 'deleteMany', args); + >( + model, + 'deleteMany', + undefined, + (model) => this.makeDeleteManySchema(model), + args + ); } validateCountArgs(model: GetModels, args: unknown) { - return this.validate> | undefined>( - this.makeCountSchema(model), + return this.validate< + CountArgs> | undefined, + undefined + >( + model, 'count', + undefined, + (model) => this.makeCountSchema(model), args ); } validateAggregateArgs(model: GetModels, args: unknown) { - return this.validate>>( - this.makeAggregateSchema(model), + return this.validate< + AggregateArgs>, + undefined + >( + model, 'aggregate', + undefined, + (model) => this.makeAggregateSchema(model), args ); } validateGroupByArgs(model: GetModels, args: unknown) { - return this.validate>>( - this.makeGroupBySchema(model), + return this.validate>, undefined>( + model, 'groupBy', + undefined, + (model) => this.makeGroupBySchema(model), args ); } - private validate(schema: ZodSchema, operation: string, args: unknown) { + private validate( + model: GetModels, + operation: string, + options: Options, + getSchema: GetSchemaFunc, + args: unknown + ) { + const cacheKey = stableStringify({ + model, + operation, + options, + }); + let schema = this.schemaCache.get(cacheKey!); + if (!schema) { + schema = getSchema(model, options); + this.schemaCache.set(cacheKey!, schema); + } const { error } = schema.safeParse(args); if (error) { throw new QueryError(`Invalid ${operation} args: ${error.message}`); @@ -142,12 +208,11 @@ export class InputValidator { private makeFindSchema( model: string, - unique: boolean, - collection: boolean + options: { unique: boolean; collection: boolean } ) { const fields: Record = {}; - const where = this.makeWhereSchema(model, unique); - if (unique) { + const where = this.makeWhereSchema(model, options.unique); + if (options.unique) { fields['where'] = where; } else { fields['where'] = where.optional(); @@ -159,7 +224,7 @@ export class InputValidator { fields['distinct'] = this.makeDistinctSchema(model).optional(); fields['cursor'] = this.makeCursorSchema(model).optional(); - if (collection) { + if (options.collection) { fields['skip'] = z.number().int().nonnegative().optional(); fields['take'] = z.number().int().optional(); fields['orderBy'] = this.orArray( @@ -172,7 +237,7 @@ export class InputValidator { result = this.refineForSelectIncludeMutuallyExclusive(result); result = this.refineForSelectOmitMutuallyExclusive(result); - if (!unique) { + if (!options.unique) { result = result.optional(); } return result; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 716dee3a..11336816 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -142,6 +142,9 @@ importers: is-plain-object: specifier: ^5.0.0 version: 5.0.0 + json-stable-stringify: + specifier: ^1.3.0 + version: 1.3.0 kysely: specifier: ^0.27.5 version: 0.27.6 @@ -1920,9 +1923,16 @@ packages: json-stable-stringify-without-jsonify@1.0.1: resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + json-stable-stringify@1.3.0: + resolution: {integrity: sha512-qtYiSSFlwot9XHtF9bD9c7rwKjr+RecWT//ZnPvSmEjpV5mmPOCN4j8UjY5hbjNkOwZ/jQv3J6R1/pL7RwgMsg==} + engines: {node: '>= 0.4'} + jsonfile@6.1.0: resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==} + jsonify@0.0.1: + resolution: {integrity: sha512-2/Ki0GcmuqSrgFyelQq9M05y7PS0mEwuIzrf3f1fPqkVDVRvZrPZtVSMHxdgo8Aq0sxAOb/cr2aqqA3LeWHVPg==} + jsonschema@1.4.1: resolution: {integrity: sha512-S6cATIPVv1z0IlxdN+zUk5EPjkGCdnhN4wVSBlvoUO1tOLJootbo9CquNJmbIh4yikWHiUedhRYrNPn1arpEmQ==} @@ -4523,12 +4533,22 @@ snapshots: json-stable-stringify-without-jsonify@1.0.1: {} + json-stable-stringify@1.3.0: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + isarray: 2.0.5 + jsonify: 0.0.1 + object-keys: 1.1.1 + jsonfile@6.1.0: dependencies: universalify: 2.0.1 optionalDependencies: graceful-fs: 4.2.11 + jsonify@0.0.1: {} + jsonschema@1.4.1: {} keyv@4.5.4: From 6287d46bcde28836f227cce651a5de719a306479 Mon Sep 17 00:00:00 2001 From: ymc9 <104139426+ymc9@users.noreply.github.com> Date: Fri, 13 Jun 2025 10:53:22 +0800 Subject: [PATCH 2/2] update TODOs --- TODO.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/TODO.md b/TODO.md index d3a1be9a..b0729094 100644 --- a/TODO.md +++ b/TODO.md @@ -55,7 +55,7 @@ - [x] Computed fields - [ ] Prisma client extension - [ ] Misc - - [ ] Cache validation schemas + - [x] Cache validation schemas - [x] Compound ID - [ ] Cross field comparison - [x] Many-to-many relation