Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions packages/runtime/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
14 changes: 9 additions & 5 deletions packages/runtime/src/client/client-impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,9 @@ export class ClientImpl<Schema extends SchemaDef> {
function createClientProxy<Schema extends SchemaDef>(
client: ClientContract<Schema>
): ClientImpl<Schema> {
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('$')) {
Expand All @@ -261,7 +264,9 @@ function createClientProxy<Schema extends SchemaDef>(
if (model) {
return createModelCrudHandler(
client,
model as GetModels<Schema>
model as GetModels<Schema>,
inputValidator,
resultProcessor
);
}
}
Expand All @@ -276,11 +281,10 @@ function createModelCrudHandler<
Model extends GetModels<Schema>
>(
client: ClientContract<Schema>,
model: Model
model: Model,
inputValidator: InputValidator<Schema>,
resultProcessor: ResultProcessor<Schema>
): ModelOperations<Schema, Model> {
const inputValidator = new InputValidator(client.$schema);
const resultProcessor = new ResultProcessor(client.$schema);

const createPromise = (
operation: CrudOperation,
args: unknown,
Expand Down
113 changes: 89 additions & 24 deletions packages/runtime/src/client/crud/validator.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -33,104 +34,169 @@ import {
requireModel,
} from '../query-utils';

type GetSchemaFunc<Schema extends SchemaDef, Options> = (
model: GetModels<Schema>,
options: Options
) => ZodSchema;

export class InputValidator<Schema extends SchemaDef> {
private schemaCache = new Map<string, ZodSchema>();

constructor(private readonly schema: Schema) {}

validateFindArgs(model: GetModels<Schema>, unique: boolean, args: unknown) {
return this.validate<FindArgs<Schema, GetModels<Schema>, true>>(
this.makeFindSchema(model, unique, true),
return this.validate<
FindArgs<Schema, GetModels<Schema>, true>,
Parameters<typeof this.makeFindSchema>[1]
>(
model,
'find',
{ unique, collection: true },
(model, options) => this.makeFindSchema(model, options),
args
);
}
Comment on lines 47 to 58
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

collection flag hard-coded to true

validateFindArgs always sets collection: true, enabling skip/take/orderBy even for findUnique and findFirst.
If this is unintended and parity with Prisma is desired, derive the flag from the concrete operation:

-        { unique, collection: true },
+        { unique, collection: !unique },  // findMany → true, findUnique/First → false

Double-check all call-sites (e.g. FindOperationHandler) after the signature change.


I’d like to inspect how makeFindSchema and the other validateFind* helpers use the collection flag. Let me run a few searches in validator.ts:


🏁 Script executed:

#!/bin/bash
# Locate validateFind* methods and makeFindSchema in the validator
rg -n "validateFindArgs" packages/runtime/src/client/crud/validator.ts
rg -n "validateFindUniqueArgs" packages/runtime/src/client/crud/validator.ts || true
rg -n "validateFindFirstArgs" packages/runtime/src/client/crud/validator.ts || true
rg -n "makeFindSchema" -n -A5 -B5 packages/runtime/src/client/crud/validator.ts

Length of output: 1325


I’ll inspect the full makeFindSchema implementation and how options.collection is used:


🏁 Script executed:

#!/bin/bash
# Show the body of makeFindSchema
sed -n '207,350p' packages/runtime/src/client/crud/validator.ts

# Highlight where options.collection is referenced
rg -n "options.collection" -A5 -B5 packages/runtime/src/client/crud/validator.ts

Length of output: 6229


Derive the collection flag from unique

In packages/runtime/src/client/crud/validator.ts (around lines 52–58), the call to makeFindSchema always passes collection: true, which allows pagination and ordering on unique queries (findUnique). To align with Prisma semantics, update it to:

-        { unique, collection: true },
+        { unique, collection: !unique },  // collections only for non‐unique queries

This change will disable skip/take/orderBy for findUnique while still permitting them for findMany and findFirst. After applying, review any call-sites of validateFindArgs to ensure your higher-level handlers continue to work correctly.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
validateFindArgs(model: GetModels<Schema>, unique: boolean, args: unknown) {
return this.validate<FindArgs<Schema, GetModels<Schema>, true>>(
this.makeFindSchema(model, unique, true),
return this.validate<
FindArgs<Schema, GetModels<Schema>, true>,
Parameters<typeof this.makeFindSchema>[1]
>(
model,
'find',
{ unique, collection: true },
(model, options) => this.makeFindSchema(model, options),
args
);
}
validateFindArgs(model: GetModels<Schema>, unique: boolean, args: unknown) {
return this.validate<
FindArgs<Schema, GetModels<Schema>, true>,
Parameters<typeof this.makeFindSchema>[1]
>(
model,
'find',
- { unique, collection: true },
+ { unique, collection: !unique }, // collections only for non‐unique queries
(model, options) => this.makeFindSchema(model, options),
args
);
}
🤖 Prompt for AI Agents
In packages/runtime/src/client/crud/validator.ts around lines 47 to 58, the
validateFindArgs method currently hard-codes the collection flag to true when
calling makeFindSchema, which incorrectly enables pagination and ordering
options for unique queries like findUnique. To fix this, derive the collection
flag from the unique parameter by setting collection to !unique, so that
collection is false for unique queries and true otherwise. After making this
change, verify all call-sites of validateFindArgs to ensure they handle the
updated signature correctly.


validateCreateArgs(model: GetModels<Schema>, args: unknown) {
return this.validate<CreateArgs<Schema, GetModels<Schema>>>(
this.makeCreateSchema(model),
model,
'create',
undefined,
(model) => this.makeCreateSchema(model),
args
);
}

validateCreateManyArgs(model: GetModels<Schema>, args: unknown) {
return this.validate<
CreateManyArgs<Schema, GetModels<Schema>> | undefined
>(this.makeCreateManySchema(model), 'createMany', args);
CreateManyArgs<Schema, GetModels<Schema>>,
undefined
>(
model,
'createMany',
undefined,
(model) => this.makeCreateManySchema(model),
args
);
}

validateCreateManyAndReturnArgs(model: GetModels<Schema>, args: unknown) {
return this.validate<
CreateManyAndReturnArgs<Schema, GetModels<Schema>> | undefined
>(
this.makeCreateManyAndReturnSchema(model),
model,
'createManyAndReturn',
undefined,
(model) => this.makeCreateManyAndReturnSchema(model),
args
);
}

validateUpdateArgs(model: GetModels<Schema>, args: unknown) {
return this.validate<UpdateArgs<Schema, GetModels<Schema>>>(
this.makeUpdateSchema(model),
model,
'update',
undefined,
(model) => this.makeUpdateSchema(model),
args
);
}

validateUpdateManyArgs(model: GetModels<Schema>, args: unknown) {
return this.validate<UpdateManyArgs<Schema, GetModels<Schema>>>(
this.makeUpdateManySchema(model),
model,
'updateMany',
undefined,
(model) => this.makeUpdateManySchema(model),
args
);
}

validateUpsertArgs(model: GetModels<Schema>, args: unknown) {
return this.validate<UpsertArgs<Schema, GetModels<Schema>>>(
this.makeUpsertSchema(model),
model,
'upsert',
undefined,
(model) => this.makeUpsertSchema(model),
args
);
}

validateDeleteArgs(model: GetModels<Schema>, args: unknown) {
return this.validate<DeleteArgs<Schema, GetModels<Schema>>>(
this.makeDeleteSchema(model),
model,
'delete',
undefined,
(model) => this.makeDeleteSchema(model),
args
);
}

validateDeleteManyArgs(model: GetModels<Schema>, args: unknown) {
return this.validate<
DeleteManyArgs<Schema, GetModels<Schema>> | undefined
>(this.makeDeleteManySchema(model), 'deleteMany', args);
>(
model,
'deleteMany',
undefined,
(model) => this.makeDeleteManySchema(model),
args
);
}

validateCountArgs(model: GetModels<Schema>, args: unknown) {
return this.validate<CountArgs<Schema, GetModels<Schema>> | undefined>(
this.makeCountSchema(model),
return this.validate<
CountArgs<Schema, GetModels<Schema>> | undefined,
undefined
>(
model,
'count',
undefined,
(model) => this.makeCountSchema(model),
args
);
}

validateAggregateArgs(model: GetModels<Schema>, args: unknown) {
return this.validate<AggregateArgs<Schema, GetModels<Schema>>>(
this.makeAggregateSchema(model),
return this.validate<
AggregateArgs<Schema, GetModels<Schema>>,
undefined
>(
model,
'aggregate',
undefined,
(model) => this.makeAggregateSchema(model),
args
);
}

validateGroupByArgs(model: GetModels<Schema>, args: unknown) {
return this.validate<GroupByArgs<Schema, GetModels<Schema>>>(
this.makeGroupBySchema(model),
return this.validate<GroupByArgs<Schema, GetModels<Schema>>, undefined>(
model,
'groupBy',
undefined,
(model) => this.makeGroupBySchema(model),
args
);
}

private validate<T>(schema: ZodSchema, operation: string, args: unknown) {
private validate<T, Options = undefined>(
model: GetModels<Schema>,
operation: string,
options: Options,
getSchema: GetSchemaFunc<Schema, Options>,
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}`);
Expand All @@ -142,12 +208,11 @@ export class InputValidator<Schema extends SchemaDef> {

private makeFindSchema(
model: string,
unique: boolean,
collection: boolean
options: { unique: boolean; collection: boolean }
) {
const fields: Record<string, z.ZodSchema> = {};
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();
Expand All @@ -159,7 +224,7 @@ export class InputValidator<Schema extends SchemaDef> {
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(
Expand All @@ -172,7 +237,7 @@ export class InputValidator<Schema extends SchemaDef> {
result = this.refineForSelectIncludeMutuallyExclusive(result);
result = this.refineForSelectOmitMutuallyExclusive(result);

if (!unique) {
if (!options.unique) {
result = result.optional();
}
return result;
Expand Down
20 changes: 20 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.