Skip to content
This repository was archived by the owner on Mar 1, 2026. It is now read-only.

Commit c19427b

Browse files
committed
fix(delegate): filter fixes
1 parent 04959ad commit c19427b

7 files changed

Lines changed: 284 additions & 124 deletions

File tree

packages/runtime/src/client/crud/dialects/base.ts

Lines changed: 76 additions & 91 deletions
Large diffs are not rendered by default.

packages/runtime/src/client/crud/dialects/postgresql.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -81,17 +81,14 @@ export class PostgresCrudDialect<Schema extends SchemaDef> extends BaseCrudDiale
8181
// simple select by default
8282
let result = eb.selectFrom(`${relationModel} as ${joinTableName}`);
8383

84-
const joinBases: string[] = [];
85-
8684
// however if there're filter/orderBy/take/skip,
8785
// we need to build a subquery to handle them before aggregation
8886
result = eb.selectFrom(() => {
89-
let subQuery = eb.selectFrom(relationModel);
87+
let subQuery = this.buildSelectModel(eb, relationModel);
9088
subQuery = this.buildSelectAllFields(
9189
relationModel,
9290
subQuery,
9391
typeof payload === 'object' ? payload?.omit : undefined,
94-
joinBases,
9592
);
9693

9794
if (payload && typeof payload === 'object') {

packages/runtime/src/client/crud/dialects/sqlite.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -77,14 +77,12 @@ export class SqliteCrudDialect<Schema extends SchemaDef> extends BaseCrudDialect
7777
const subQueryName = `${parentName}$${relationField}`;
7878

7979
let tbl = eb.selectFrom(() => {
80-
let subQuery = eb.selectFrom(relationModel);
80+
let subQuery = this.buildSelectModel(eb, relationModel);
8181

82-
const joinBases: string[] = [];
8382
subQuery = this.buildSelectAllFields(
8483
relationModel,
8584
subQuery,
8685
typeof payload === 'object' ? payload?.omit : undefined,
87-
joinBases,
8886
);
8987

9088
if (payload && typeof payload === 'object') {

packages/runtime/src/client/crud/operations/base.ts

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,7 @@ export abstract class BaseOperationHandler<Schema extends SchemaDef> {
143143
args: FindArgs<Schema, GetModels<Schema>, true> | undefined,
144144
): Promise<any[]> {
145145
// table
146-
let query = kysely.selectFrom(model);
146+
let query = this.dialect.buildSelectModel(expressionBuilder(), model);
147147

148148
// where
149149
if (args?.where) {
@@ -182,22 +182,19 @@ export abstract class BaseOperationHandler<Schema extends SchemaDef> {
182182
}
183183
}
184184

185-
// for deduplicating base joins
186-
const joinedBases: string[] = [];
187-
188185
// select
189186
if (args && 'select' in args && args.select) {
190187
// select is mutually exclusive with omit
191-
query = this.buildFieldSelection(model, query, args.select, model, joinedBases);
188+
query = this.buildFieldSelection(model, query, args.select, model);
192189
} else {
193190
// include all scalar fields except those in omit
194-
query = this.dialect.buildSelectAllFields(model, query, (args as any)?.omit, joinedBases);
191+
query = this.dialect.buildSelectAllFields(model, query, (args as any)?.omit);
195192
}
196193

197194
// include
198195
if (args && 'include' in args && args.include) {
199196
// note that 'omit' is handled above already
200-
query = this.buildFieldSelection(model, query, args.include, model, joinedBases);
197+
query = this.buildFieldSelection(model, query, args.include, model);
201198
}
202199

203200
if (args?.cursor) {
@@ -207,13 +204,15 @@ export abstract class BaseOperationHandler<Schema extends SchemaDef> {
207204
query = query.modifyEnd(this.makeContextComment({ model, operation: 'read' }));
208205

209206
let result: any[] = [];
207+
const queryId = { queryId: `zenstack-${createId()}` };
208+
const compiled = kysely.getExecutor().compileQuery(query.toOperationNode(), queryId);
210209
try {
211-
result = await query.execute();
210+
const r = await kysely.getExecutor().executeQuery(compiled, queryId);
211+
result = r.rows;
212212
} catch (err) {
213-
const { sql, parameters } = query.compile();
214-
let message = `Failed to execute query: ${err}, sql: ${sql}`;
213+
let message = `Failed to execute query: ${err}, sql: ${compiled.sql}`;
215214
if (this.options.debug) {
216-
message += `, parameters: \n${parameters.map((p) => inspect(p)).join('\n')}`;
215+
message += `, parameters: \n${compiled.parameters.map((p) => inspect(p)).join('\n')}`;
217216
}
218217
throw new QueryError(message, err);
219218
}
@@ -248,7 +247,6 @@ export abstract class BaseOperationHandler<Schema extends SchemaDef> {
248247
query: SelectQueryBuilder<any, any, any>,
249248
selectOrInclude: Record<string, any>,
250249
parentAlias: string,
251-
joinedBases: string[],
252250
) {
253251
let result = query;
254252

@@ -265,17 +263,12 @@ export abstract class BaseOperationHandler<Schema extends SchemaDef> {
265263
const fieldDef = this.requireField(model, field);
266264
if (!fieldDef.relation) {
267265
// scalar field
268-
result = this.dialect.buildSelectField(result, model, parentAlias, field, joinedBases);
266+
result = this.dialect.buildSelectField(result, model, parentAlias, field);
269267
} else {
270268
if (!fieldDef.array && !fieldDef.optional && payload.where) {
271269
throw new QueryError(`Field "${field}" doesn't support filtering`);
272270
}
273271
if (fieldDef.originModel) {
274-
// relation is inherited from a delegate base model, need to build a join
275-
if (!joinedBases.includes(fieldDef.originModel)) {
276-
joinedBases.push(fieldDef.originModel);
277-
result = this.dialect.buildDelegateJoin(parentAlias, fieldDef.originModel, result);
278-
}
279272
result = this.dialect.buildRelationSelection(
280273
result,
281274
fieldDef.originModel,
@@ -399,8 +392,15 @@ export abstract class BaseOperationHandler<Schema extends SchemaDef> {
399392
model: GetModels<Schema>,
400393
data: any,
401394
fromRelation?: FromRelationContext<Schema>,
395+
creatingForDelegate = false,
402396
): Promise<unknown> {
403397
const modelDef = this.requireModel(model);
398+
399+
// additional validations
400+
if (modelDef.isDelegate && !creatingForDelegate) {
401+
throw new QueryError(`Model "${this.model}" is a delegate and cannot be created directly.`);
402+
}
403+
404404
let createFields: any = {};
405405
let parentUpdateTask: ((entity: any) => Promise<unknown>) | undefined = undefined;
406406

@@ -573,7 +573,7 @@ export abstract class BaseOperationHandler<Schema extends SchemaDef> {
573573
thisCreateFields[discriminatorField] = forModel;
574574

575575
// create base model entity
576-
const createResult = await this.create(kysely, model as GetModels<Schema>, thisCreateFields);
576+
const createResult = await this.create(kysely, model as GetModels<Schema>, thisCreateFields, undefined, true);
577577

578578
// copy over id fields from base model
579579
const idValues = extractIdFields(createResult, this.schema, model);

packages/runtime/src/client/crud/operations/create.ts

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,9 @@ import type { GetModels, SchemaDef } from '../../../schema';
44
import type { CreateArgs, CreateManyAndReturnArgs, CreateManyArgs, WhereInput } from '../../crud-types';
55
import { getIdValues } from '../../query-utils';
66
import { BaseOperationHandler } from './base';
7-
import { QueryError } from '../../errors';
87

98
export class CreateOperationHandler<Schema extends SchemaDef> extends BaseOperationHandler<Schema> {
109
async handle(operation: 'create' | 'createMany' | 'createManyAndReturn', args: unknown | undefined) {
11-
const modelDef = this.requireModel(this.model);
12-
if (modelDef.isDelegate) {
13-
throw new QueryError(`Model "${this.model}" is a delegate and cannot be created directly.`);
14-
}
15-
1610
// normalize args to strip `undefined` fields
1711
const normalizedArgs = this.normalizeArgs(args);
1812

packages/runtime/test/client-api/delegate.test.ts

Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,15 @@ model Gallery {
7878
},
7979
}),
8080
).rejects.toThrow('is a delegate');
81+
await expect(
82+
client.user.create({
83+
data: {
84+
assets: {
85+
create: { assetType: 'Video' },
86+
},
87+
},
88+
}),
89+
).rejects.toThrow('is a delegate');
8190

8291
// create entity with two levels of delegation
8392
await expect(
@@ -242,5 +251,183 @@ model Gallery {
242251
ratedVideos: [{ url: 'abc', rating: 5 }],
243252
});
244253
});
254+
255+
describe('Delegate filter tests', async () => {
256+
beforeEach(async () => {
257+
const u = await client.user.create({
258+
data: {
259+
email: 'u1@example.com',
260+
},
261+
});
262+
await client.ratedVideo.create({
263+
data: {
264+
viewCount: 0,
265+
duration: 100,
266+
url: 'v1',
267+
rating: 5,
268+
owner: { connect: { id: u.id } },
269+
user: { connect: { id: u.id } },
270+
},
271+
});
272+
await client.ratedVideo.create({
273+
data: {
274+
viewCount: 1,
275+
duration: 200,
276+
url: 'v2',
277+
rating: 4,
278+
owner: { connect: { id: u.id } },
279+
user: { connect: { id: u.id } },
280+
},
281+
});
282+
});
283+
284+
it('works with toplevel filters', async () => {
285+
await expect(
286+
client.asset.findMany({
287+
where: { viewCount: { gt: 0 } },
288+
}),
289+
).toResolveWithLength(1);
290+
291+
await expect(
292+
client.video.findMany({
293+
where: { viewCount: { gt: 0 }, url: 'v1' },
294+
}),
295+
).toResolveWithLength(0);
296+
297+
await expect(
298+
client.video.findMany({
299+
where: { viewCount: { gt: 0 }, url: 'v2' },
300+
}),
301+
).toResolveWithLength(1);
302+
303+
await expect(
304+
client.ratedVideo.findMany({
305+
where: { viewCount: { gt: 0 }, rating: 5 },
306+
}),
307+
).toResolveWithLength(0);
308+
309+
await expect(
310+
client.ratedVideo.findMany({
311+
where: { viewCount: { gt: 0 }, rating: 4 },
312+
}),
313+
).toResolveWithLength(1);
314+
});
315+
316+
it('works with filtering relations', async () => {
317+
await expect(
318+
client.user.findFirst({
319+
include: {
320+
assets: {
321+
where: { viewCount: { gt: 0 } },
322+
},
323+
},
324+
}),
325+
).resolves.toSatisfy((user) => user.assets.length === 1);
326+
327+
await expect(
328+
client.user.findFirst({
329+
include: {
330+
ratedVideos: {
331+
where: { viewCount: { gt: 0 }, url: 'v1' },
332+
},
333+
},
334+
}),
335+
).resolves.toSatisfy((user) => user.ratedVideos.length === 0);
336+
337+
await expect(
338+
client.user.findFirst({
339+
include: {
340+
ratedVideos: {
341+
where: { viewCount: { gt: 0 }, url: 'v2' },
342+
},
343+
},
344+
}),
345+
).resolves.toSatisfy((user) => user.ratedVideos.length === 1);
346+
347+
await expect(
348+
client.user.findFirst({
349+
include: {
350+
ratedVideos: {
351+
where: { viewCount: { gt: 0 }, rating: 5 },
352+
},
353+
},
354+
}),
355+
).resolves.toSatisfy((user) => user.ratedVideos.length === 0);
356+
357+
await expect(
358+
client.user.findFirst({
359+
include: {
360+
ratedVideos: {
361+
where: { viewCount: { gt: 0 }, rating: 4 },
362+
},
363+
},
364+
}),
365+
).resolves.toSatisfy((user) => user.ratedVideos.length === 1);
366+
});
367+
368+
it('works with filtering parents', async () => {
369+
await expect(
370+
client.user.findFirst({
371+
where: {
372+
assets: {
373+
some: { viewCount: { gt: 0 } },
374+
},
375+
},
376+
}),
377+
).toResolveTruthy();
378+
379+
await expect(
380+
client.user.findFirst({
381+
where: {
382+
assets: {
383+
some: { viewCount: { gt: 1 } },
384+
},
385+
},
386+
}),
387+
).toResolveFalsy();
388+
389+
await expect(
390+
client.user.findFirst({
391+
where: {
392+
ratedVideos: {
393+
some: { viewCount: { gt: 0 }, url: 'v1' },
394+
},
395+
},
396+
}),
397+
).toResolveFalsy();
398+
399+
await expect(
400+
client.user.findFirst({
401+
where: {
402+
ratedVideos: {
403+
some: { viewCount: { gt: 0 }, url: 'v2' },
404+
},
405+
},
406+
}),
407+
).toResolveTruthy();
408+
});
409+
410+
it('works with filtering with relations from base', async () => {
411+
await expect(
412+
client.video.findFirst({
413+
where: {
414+
owner: {
415+
email: 'u1@example.com',
416+
},
417+
},
418+
}),
419+
).toResolveTruthy();
420+
421+
await expect(
422+
client.video.findFirst({
423+
where: {
424+
owner: {
425+
email: 'u2@example.com',
426+
},
427+
},
428+
}),
429+
).toResolveFalsy();
430+
});
431+
});
245432
},
246433
);

packages/runtime/test/client-api/typed-json-fields.test.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@ model User {
2929
usePrismaPush: true,
3030
provider,
3131
dbName: provider === 'postgresql' ? PG_DB_NAME : undefined,
32-
log: ['query'],
3332
});
3433
});
3534

0 commit comments

Comments
 (0)