diff --git a/packages/runtime/src/client/crud/dialects/base.ts b/packages/runtime/src/client/crud/dialects/base.ts index 3ce8e0f74..c253b9051 100644 --- a/packages/runtime/src/client/crud/dialects/base.ts +++ b/packages/runtime/src/client/crud/dialects/base.ts @@ -22,7 +22,6 @@ import { buildJoinPairs, flattenCompoundUniqueFilters, getDelegateDescendantModels, - getField, getIdFields, getManyToManyRelation, getRelationForeignKeyFieldPairs, @@ -46,6 +45,18 @@ export abstract class BaseCrudDialect { // #region common query builders + buildSelectModel(eb: ExpressionBuilder, model: string) { + const modelDef = requireModel(this.schema, model); + let result = eb.selectFrom(model); + // join all delegate bases + let joinBase = modelDef.baseModel; + while (joinBase) { + result = this.buildDelegateJoin(model, joinBase, result); + joinBase = requireModel(this.schema, joinBase).baseModel; + } + return result; + } + buildFilter( eb: ExpressionBuilder, model: string, @@ -78,12 +89,24 @@ export abstract class BaseCrudDialect { } const fieldDef = requireField(this.schema, model, key); + if (fieldDef.relation) { result = this.and(eb, result, this.buildRelationFilter(eb, model, modelAlias, key, fieldDef, payload)); - } else if (fieldDef.array) { - result = this.and(eb, result, this.buildArrayFilter(eb, model, modelAlias, key, fieldDef, payload)); } else { - result = this.and(eb, result, this.buildPrimitiveFilter(eb, model, modelAlias, key, fieldDef, payload)); + // if the field is from a base model, build a reference from that model + const fieldRef = buildFieldRef( + this.schema, + fieldDef.originModel ?? model, + key, + this.options, + eb, + fieldDef.originModel ?? modelAlias, + ); + if (fieldDef.array) { + result = this.and(eb, result, this.buildArrayFilter(eb, fieldRef, fieldDef, payload)); + } else { + result = this.and(eb, result, this.buildPrimitiveFilter(eb, fieldRef, fieldDef, payload)); + } } } @@ -137,7 +160,7 @@ export abstract class BaseCrudDialect { private buildToOneRelationFilter( eb: ExpressionBuilder, model: string, - table: string, + modelAlias: string, field: string, fieldDef: FieldDef, payload: any, @@ -145,17 +168,24 @@ export abstract class BaseCrudDialect { if (payload === null) { const { ownedByModel, keyPairs } = getRelationForeignKeyFieldPairs(this.schema, model, field); - if (ownedByModel) { + if (ownedByModel && !fieldDef.originModel) { // can be short-circuited to FK null check - return this.and(eb, ...keyPairs.map(({ fk }) => eb(sql.ref(`${table}.${fk}`), 'is', null))); + return this.and(eb, ...keyPairs.map(({ fk }) => eb(sql.ref(`${modelAlias}.${fk}`), 'is', null))); } else { // translate it to `{ is: null }` filter - return this.buildToOneRelationFilter(eb, model, table, field, fieldDef, { is: null }); + return this.buildToOneRelationFilter(eb, model, modelAlias, field, fieldDef, { is: null }); } } - const joinAlias = `${table}$${field}`; - const joinPairs = buildJoinPairs(this.schema, model, table, field, joinAlias); + const joinAlias = `${modelAlias}$${field}`; + const joinPairs = buildJoinPairs( + this.schema, + model, + // if field is from a base, use the base model to join + fieldDef.originModel ?? modelAlias, + field, + joinAlias, + ); const filterResultField = `${field}$filter`; const joinSelect = eb @@ -284,9 +314,8 @@ export abstract class BaseCrudDialect { eb, result, eb( - eb - .selectFrom(relationModel) - .select((eb1) => eb1.fn.count(eb1.lit(1)).as('count')) + this.buildSelectModel(eb, relationModel) + .select((eb1) => eb1.fn.count(eb1.lit(1)).as('$count')) .where(buildPkFkWhereRefs(eb)) .where((eb1) => this.buildFilter(eb1, relationModel, relationModel, subPayload)), '>', @@ -301,9 +330,8 @@ export abstract class BaseCrudDialect { eb, result, eb( - eb - .selectFrom(relationModel) - .select((eb1) => eb1.fn.count(eb1.lit(1)).as('count')) + this.buildSelectModel(eb, relationModel) + .select((eb1) => eb1.fn.count(eb1.lit(1)).as('$count')) .where(buildPkFkWhereRefs(eb)) .where((eb1) => eb1.not(this.buildFilter(eb1, relationModel, relationModel, subPayload)), @@ -320,9 +348,8 @@ export abstract class BaseCrudDialect { eb, result, eb( - eb - .selectFrom(relationModel) - .select((eb1) => eb1.fn.count(eb1.lit(1)).as('count')) + this.buildSelectModel(eb, relationModel) + .select((eb1) => eb1.fn.count(eb1.lit(1)).as('$count')) .where(buildPkFkWhereRefs(eb)) .where((eb1) => this.buildFilter(eb1, relationModel, relationModel, subPayload)), '=', @@ -339,15 +366,12 @@ export abstract class BaseCrudDialect { private buildArrayFilter( eb: ExpressionBuilder, - model: string, - modelAlias: string, - field: string, + fieldRef: Expression, fieldDef: FieldDef, payload: any, ) { const clauses: Expression[] = []; const fieldType = fieldDef.type as BuiltinType; - const fieldRef = buildFieldRef(this.schema, model, field, this.options, eb, modelAlias); for (const [key, _value] of Object.entries(payload)) { if (_value === undefined) { @@ -391,31 +415,24 @@ export abstract class BaseCrudDialect { return this.and(eb, ...clauses); } - buildPrimitiveFilter( - eb: ExpressionBuilder, - model: string, - modelAlias: string, - field: string, - fieldDef: FieldDef, - payload: any, - ) { + buildPrimitiveFilter(eb: ExpressionBuilder, fieldRef: Expression, fieldDef: FieldDef, payload: any) { if (payload === null) { - return eb(sql.ref(`${modelAlias}.${field}`), 'is', null); + return eb(fieldRef, 'is', null); } if (isEnum(this.schema, fieldDef.type)) { - return this.buildEnumFilter(eb, modelAlias, field, fieldDef, payload); + return this.buildEnumFilter(eb, fieldRef, fieldDef, payload); } return ( match(fieldDef.type as BuiltinType) - .with('String', () => this.buildStringFilter(eb, modelAlias, field, payload)) + .with('String', () => this.buildStringFilter(eb, fieldRef, payload)) .with(P.union('Int', 'Float', 'Decimal', 'BigInt'), (type) => - this.buildNumberFilter(eb, model, modelAlias, field, type, payload), + this.buildNumberFilter(eb, fieldRef, type, payload), ) - .with('Boolean', () => this.buildBooleanFilter(eb, modelAlias, field, payload)) - .with('DateTime', () => this.buildDateTimeFilter(eb, modelAlias, field, payload)) - .with('Bytes', () => this.buildBytesFilter(eb, modelAlias, field, payload)) + .with('Boolean', () => this.buildBooleanFilter(eb, fieldRef, payload)) + .with('DateTime', () => this.buildDateTimeFilter(eb, fieldRef, payload)) + .with('Bytes', () => this.buildBytesFilter(eb, fieldRef, payload)) // TODO: JSON filters .with('Json', () => { throw new InternalError('JSON filters are not supported yet'); @@ -496,15 +513,7 @@ export abstract class BaseCrudDialect { return { conditions, consumedKeys }; } - private buildStringFilter( - eb: ExpressionBuilder, - table: string, - field: string, - payload: StringFilter, - ) { - const fieldDef = getField(this.schema, table, field); - let fieldRef: Expression = fieldDef?.computed ? sql.ref(field) : sql.ref(`${table}.${field}`); - + private buildStringFilter(eb: ExpressionBuilder, fieldRef: Expression, payload: StringFilter) { let insensitive = false; if (payload && typeof payload === 'object' && 'mode' in payload && payload.mode === 'insensitive') { insensitive = true; @@ -517,7 +526,7 @@ export abstract class BaseCrudDialect { payload, fieldRef, (value) => this.prepStringCasing(eb, value, insensitive), - (value) => this.buildStringFilter(eb, table, field, value as StringFilter), + (value) => this.buildStringFilter(eb, fieldRef, value as StringFilter), ); if (payload && typeof payload === 'object') { @@ -568,9 +577,7 @@ export abstract class BaseCrudDialect { private buildNumberFilter( eb: ExpressionBuilder, - model: string, - modelAlias: string, - field: string, + fieldRef: Expression, type: BuiltinType, payload: any, ) { @@ -578,26 +585,25 @@ export abstract class BaseCrudDialect { eb, type, payload, - buildFieldRef(this.schema, model, field, this.options, eb, modelAlias), + fieldRef, (value) => this.transformPrimitive(value, type, false), - (value) => this.buildNumberFilter(eb, model, modelAlias, field, type, value), + (value) => this.buildNumberFilter(eb, fieldRef, type, value), ); return this.and(eb, ...conditions); } private buildBooleanFilter( eb: ExpressionBuilder, - table: string, - field: string, + fieldRef: Expression, payload: BooleanFilter, ) { const { conditions } = this.buildStandardFilter( eb, 'Boolean', payload, - sql.ref(`${table}.${field}`), + fieldRef, (value) => this.transformPrimitive(value, 'Boolean', false), - (value) => this.buildBooleanFilter(eb, table, field, value as BooleanFilter), + (value) => this.buildBooleanFilter(eb, fieldRef, value as BooleanFilter), true, ['equals', 'not'], ); @@ -606,35 +612,29 @@ export abstract class BaseCrudDialect { private buildDateTimeFilter( eb: ExpressionBuilder, - table: string, - field: string, + fieldRef: Expression, payload: DateTimeFilter, ) { const { conditions } = this.buildStandardFilter( eb, 'DateTime', payload, - sql.ref(`${table}.${field}`), + fieldRef, (value) => this.transformPrimitive(value, 'DateTime', false), - (value) => this.buildDateTimeFilter(eb, table, field, value as DateTimeFilter), + (value) => this.buildDateTimeFilter(eb, fieldRef, value as DateTimeFilter), true, ); return this.and(eb, ...conditions); } - private buildBytesFilter( - eb: ExpressionBuilder, - table: string, - field: string, - payload: BytesFilter, - ) { + private buildBytesFilter(eb: ExpressionBuilder, fieldRef: Expression, payload: BytesFilter) { const conditions = this.buildStandardFilter( eb, 'Bytes', payload, - sql.ref(`${table}.${field}`), + fieldRef, (value) => this.transformPrimitive(value, 'Bytes', false), - (value) => this.buildBytesFilter(eb, table, field, value as BytesFilter), + (value) => this.buildBytesFilter(eb, fieldRef, value as BytesFilter), true, ['equals', 'in', 'notIn', 'not'], ); @@ -643,8 +643,7 @@ export abstract class BaseCrudDialect { private buildEnumFilter( eb: ExpressionBuilder, - table: string, - field: string, + fieldRef: Expression, fieldDef: FieldDef, payload: any, ) { @@ -652,9 +651,9 @@ export abstract class BaseCrudDialect { eb, 'String', payload, - sql.ref(`${table}.${field}`), + fieldRef, (value) => value, - (value) => this.buildEnumFilter(eb, table, field, fieldDef, value), + (value) => this.buildEnumFilter(eb, fieldRef, fieldDef, value), true, ['equals', 'in', 'notIn', 'not'], ); @@ -747,7 +746,7 @@ export abstract class BaseCrudDialect { ); const sort = this.negateSort(value._count, negated); result = result.orderBy((eb) => { - let subQuery = eb.selectFrom(relationModel); + let subQuery = this.buildSelectModel(eb, relationModel); const joinPairs = buildJoinPairs(this.schema, model, modelAlias, field, relationModel); subQuery = subQuery.where(() => this.and( @@ -783,7 +782,6 @@ export abstract class BaseCrudDialect { model: string, query: SelectQueryBuilder, omit?: Record, - joinedBases: string[] = [], ) { const modelDef = requireModel(this.schema, model); let result = query; @@ -795,16 +793,13 @@ export abstract class BaseCrudDialect { if (omit?.[field] === true) { continue; } - result = this.buildSelectField(result, model, model, field, joinedBases); + result = this.buildSelectField(result, model, model, field); } // select all fields from delegate descendants and pack into a JSON field `$delegate$Model` const descendants = getDelegateDescendantModels(this.schema, model); for (const subModel of descendants) { - if (!joinedBases.includes(subModel.name)) { - joinedBases.push(subModel.name); - result = this.buildDelegateJoin(model, subModel.name, result); - } + result = this.buildDelegateJoin(model, subModel.name, result); result = result.select((eb) => { const jsonObject: Record> = {}; for (const field of Object.keys(subModel.fields)) { @@ -823,13 +818,7 @@ export abstract class BaseCrudDialect { return result; } - buildSelectField( - query: SelectQueryBuilder, - model: string, - modelAlias: string, - field: string, - joinedBases: string[], - ) { + buildSelectField(query: SelectQueryBuilder, model: string, modelAlias: string, field: string) { const fieldDef = requireField(this.schema, model, field); if (fieldDef.computed) { @@ -841,11 +830,7 @@ export abstract class BaseCrudDialect { } else { // field from delegate base, build a join let result = query; - if (!joinedBases.includes(fieldDef.originModel)) { - joinedBases.push(fieldDef.originModel); - result = this.buildDelegateJoin(model, fieldDef.originModel, result); - } - result = this.buildSelectField(result, fieldDef.originModel, fieldDef.originModel, field, joinedBases); + result = this.buildSelectField(result, fieldDef.originModel, fieldDef.originModel, field); return result; } } diff --git a/packages/runtime/src/client/crud/dialects/postgresql.ts b/packages/runtime/src/client/crud/dialects/postgresql.ts index be273e818..f91c7aad9 100644 --- a/packages/runtime/src/client/crud/dialects/postgresql.ts +++ b/packages/runtime/src/client/crud/dialects/postgresql.ts @@ -81,17 +81,14 @@ export class PostgresCrudDialect extends BaseCrudDiale // simple select by default let result = eb.selectFrom(`${relationModel} as ${joinTableName}`); - const joinBases: string[] = []; - // however if there're filter/orderBy/take/skip, // we need to build a subquery to handle them before aggregation result = eb.selectFrom(() => { - let subQuery = eb.selectFrom(relationModel); + let subQuery = this.buildSelectModel(eb, relationModel); subQuery = this.buildSelectAllFields( relationModel, subQuery, typeof payload === 'object' ? payload?.omit : undefined, - joinBases, ); if (payload && typeof payload === 'object') { diff --git a/packages/runtime/src/client/crud/dialects/sqlite.ts b/packages/runtime/src/client/crud/dialects/sqlite.ts index 2961b8647..3d74f82c0 100644 --- a/packages/runtime/src/client/crud/dialects/sqlite.ts +++ b/packages/runtime/src/client/crud/dialects/sqlite.ts @@ -77,14 +77,12 @@ export class SqliteCrudDialect extends BaseCrudDialect const subQueryName = `${parentName}$${relationField}`; let tbl = eb.selectFrom(() => { - let subQuery = eb.selectFrom(relationModel); + let subQuery = this.buildSelectModel(eb, relationModel); - const joinBases: string[] = []; subQuery = this.buildSelectAllFields( relationModel, subQuery, typeof payload === 'object' ? payload?.omit : undefined, - joinBases, ); if (payload && typeof payload === 'object') { diff --git a/packages/runtime/src/client/crud/operations/base.ts b/packages/runtime/src/client/crud/operations/base.ts index ca77245ae..43884f6b2 100644 --- a/packages/runtime/src/client/crud/operations/base.ts +++ b/packages/runtime/src/client/crud/operations/base.ts @@ -143,7 +143,7 @@ export abstract class BaseOperationHandler { args: FindArgs, true> | undefined, ): Promise { // table - let query = kysely.selectFrom(model); + let query = this.dialect.buildSelectModel(expressionBuilder(), model); // where if (args?.where) { @@ -182,22 +182,19 @@ export abstract class BaseOperationHandler { } } - // for deduplicating base joins - const joinedBases: string[] = []; - // select if (args && 'select' in args && args.select) { // select is mutually exclusive with omit - query = this.buildFieldSelection(model, query, args.select, model, joinedBases); + query = this.buildFieldSelection(model, query, args.select, model); } else { // include all scalar fields except those in omit - query = this.dialect.buildSelectAllFields(model, query, (args as any)?.omit, joinedBases); + query = this.dialect.buildSelectAllFields(model, query, (args as any)?.omit); } // include if (args && 'include' in args && args.include) { // note that 'omit' is handled above already - query = this.buildFieldSelection(model, query, args.include, model, joinedBases); + query = this.buildFieldSelection(model, query, args.include, model); } if (args?.cursor) { @@ -207,13 +204,15 @@ export abstract class BaseOperationHandler { query = query.modifyEnd(this.makeContextComment({ model, operation: 'read' })); let result: any[] = []; + const queryId = { queryId: `zenstack-${createId()}` }; + const compiled = kysely.getExecutor().compileQuery(query.toOperationNode(), queryId); try { - result = await query.execute(); + const r = await kysely.getExecutor().executeQuery(compiled, queryId); + result = r.rows; } catch (err) { - const { sql, parameters } = query.compile(); - let message = `Failed to execute query: ${err}, sql: ${sql}`; + let message = `Failed to execute query: ${err}, sql: ${compiled.sql}`; if (this.options.debug) { - message += `, parameters: \n${parameters.map((p) => inspect(p)).join('\n')}`; + message += `, parameters: \n${compiled.parameters.map((p) => inspect(p)).join('\n')}`; } throw new QueryError(message, err); } @@ -248,7 +247,6 @@ export abstract class BaseOperationHandler { query: SelectQueryBuilder, selectOrInclude: Record, parentAlias: string, - joinedBases: string[], ) { let result = query; @@ -265,17 +263,12 @@ export abstract class BaseOperationHandler { const fieldDef = this.requireField(model, field); if (!fieldDef.relation) { // scalar field - result = this.dialect.buildSelectField(result, model, parentAlias, field, joinedBases); + result = this.dialect.buildSelectField(result, model, parentAlias, field); } else { if (!fieldDef.array && !fieldDef.optional && payload.where) { throw new QueryError(`Field "${field}" doesn't support filtering`); } if (fieldDef.originModel) { - // relation is inherited from a delegate base model, need to build a join - if (!joinedBases.includes(fieldDef.originModel)) { - joinedBases.push(fieldDef.originModel); - result = this.dialect.buildDelegateJoin(parentAlias, fieldDef.originModel, result); - } result = this.dialect.buildRelationSelection( result, fieldDef.originModel, @@ -399,8 +392,15 @@ export abstract class BaseOperationHandler { model: GetModels, data: any, fromRelation?: FromRelationContext, + creatingForDelegate = false, ): Promise { const modelDef = this.requireModel(model); + + // additional validations + if (modelDef.isDelegate && !creatingForDelegate) { + throw new QueryError(`Model "${this.model}" is a delegate and cannot be created directly.`); + } + let createFields: any = {}; let parentUpdateTask: ((entity: any) => Promise) | undefined = undefined; @@ -573,7 +573,7 @@ export abstract class BaseOperationHandler { thisCreateFields[discriminatorField] = forModel; // create base model entity - const createResult = await this.create(kysely, model as GetModels, thisCreateFields); + const createResult = await this.create(kysely, model as GetModels, thisCreateFields, undefined, true); // copy over id fields from base model const idValues = extractIdFields(createResult, this.schema, model); diff --git a/packages/runtime/src/client/crud/operations/create.ts b/packages/runtime/src/client/crud/operations/create.ts index e097d4759..bc15bb36b 100644 --- a/packages/runtime/src/client/crud/operations/create.ts +++ b/packages/runtime/src/client/crud/operations/create.ts @@ -4,15 +4,9 @@ import type { GetModels, SchemaDef } from '../../../schema'; import type { CreateArgs, CreateManyAndReturnArgs, CreateManyArgs, WhereInput } from '../../crud-types'; import { getIdValues } from '../../query-utils'; import { BaseOperationHandler } from './base'; -import { QueryError } from '../../errors'; export class CreateOperationHandler extends BaseOperationHandler { async handle(operation: 'create' | 'createMany' | 'createManyAndReturn', args: unknown | undefined) { - const modelDef = this.requireModel(this.model); - if (modelDef.isDelegate) { - throw new QueryError(`Model "${this.model}" is a delegate and cannot be created directly.`); - } - // normalize args to strip `undefined` fields const normalizedArgs = this.normalizeArgs(args); diff --git a/packages/runtime/test/client-api/delegate.test.ts b/packages/runtime/test/client-api/delegate.test.ts index eb55ae227..8c3a79069 100644 --- a/packages/runtime/test/client-api/delegate.test.ts +++ b/packages/runtime/test/client-api/delegate.test.ts @@ -15,6 +15,13 @@ model User { ratedVideos RatedVideo[] @relation('direct') } +model Comment { + id Int @id @default(autoincrement()) + content String + asset Asset? @relation(fields: [assetId], references: [id]) + assetId Int? +} + model Asset { id Int @id @default(autoincrement()) createdAt DateTime @default(now()) @@ -22,6 +29,7 @@ model Asset { viewCount Int @default(0) owner User? @relation(fields: [ownerId], references: [id]) ownerId Int? + comments Comment[] assetType String @@delegate(assetType) @@ -78,6 +86,15 @@ model Gallery { }, }), ).rejects.toThrow('is a delegate'); + await expect( + client.user.create({ + data: { + assets: { + create: { assetType: 'Video' }, + }, + }, + }), + ).rejects.toThrow('is a delegate'); // create entity with two levels of delegation await expect( @@ -242,5 +259,249 @@ model Gallery { ratedVideos: [{ url: 'abc', rating: 5 }], }); }); + + describe('Delegate filter tests', async () => { + beforeEach(async () => { + const u = await client.user.create({ + data: { + email: 'u1@example.com', + }, + }); + await client.ratedVideo.create({ + data: { + viewCount: 0, + duration: 100, + url: 'v1', + rating: 5, + owner: { connect: { id: u.id } }, + user: { connect: { id: u.id } }, + comments: { create: { content: 'c1' } }, + }, + }); + await client.ratedVideo.create({ + data: { + viewCount: 1, + duration: 200, + url: 'v2', + rating: 4, + owner: { connect: { id: u.id } }, + user: { connect: { id: u.id } }, + comments: { create: { content: 'c2' } }, + }, + }); + }); + + it('works with toplevel filters', async () => { + await expect( + client.asset.findMany({ + where: { viewCount: { gt: 0 } }, + }), + ).toResolveWithLength(1); + + await expect( + client.video.findMany({ + where: { viewCount: { gt: 0 }, url: 'v1' }, + }), + ).toResolveWithLength(0); + + await expect( + client.video.findMany({ + where: { viewCount: { gt: 0 }, url: 'v2' }, + }), + ).toResolveWithLength(1); + + await expect( + client.ratedVideo.findMany({ + where: { viewCount: { gt: 0 }, rating: 5 }, + }), + ).toResolveWithLength(0); + + await expect( + client.ratedVideo.findMany({ + where: { viewCount: { gt: 0 }, rating: 4 }, + }), + ).toResolveWithLength(1); + }); + + it('works with filtering relations', async () => { + await expect( + client.user.findFirst({ + include: { + assets: { + where: { viewCount: { gt: 0 } }, + }, + }, + }), + ).resolves.toSatisfy((user) => user.assets.length === 1); + + await expect( + client.user.findFirst({ + include: { + ratedVideos: { + where: { viewCount: { gt: 0 }, url: 'v1' }, + }, + }, + }), + ).resolves.toSatisfy((user) => user.ratedVideos.length === 0); + + await expect( + client.user.findFirst({ + include: { + ratedVideos: { + where: { viewCount: { gt: 0 }, url: 'v2' }, + }, + }, + }), + ).resolves.toSatisfy((user) => user.ratedVideos.length === 1); + + await expect( + client.user.findFirst({ + include: { + ratedVideos: { + where: { viewCount: { gt: 0 }, rating: 5 }, + }, + }, + }), + ).resolves.toSatisfy((user) => user.ratedVideos.length === 0); + + await expect( + client.user.findFirst({ + include: { + ratedVideos: { + where: { viewCount: { gt: 0 }, rating: 4 }, + }, + }, + }), + ).resolves.toSatisfy((user) => user.ratedVideos.length === 1); + }); + + it('works with filtering parents', async () => { + await expect( + client.user.findFirst({ + where: { + assets: { + some: { viewCount: { gt: 0 } }, + }, + }, + }), + ).toResolveTruthy(); + + await expect( + client.user.findFirst({ + where: { + assets: { + some: { viewCount: { gt: 1 } }, + }, + }, + }), + ).toResolveFalsy(); + + await expect( + client.user.findFirst({ + where: { + ratedVideos: { + some: { viewCount: { gt: 0 }, url: 'v1' }, + }, + }, + }), + ).toResolveFalsy(); + + await expect( + client.user.findFirst({ + where: { + ratedVideos: { + some: { viewCount: { gt: 0 }, url: 'v2' }, + }, + }, + }), + ).toResolveTruthy(); + }); + + it('works with filtering with relations from base', async () => { + await expect( + client.video.findFirst({ + where: { + owner: { + email: 'u1@example.com', + }, + }, + }), + ).toResolveTruthy(); + + await expect( + client.video.findFirst({ + where: { + owner: { + email: 'u2@example.com', + }, + }, + }), + ).toResolveFalsy(); + + await expect( + client.video.findFirst({ + where: { + owner: null, + }, + }), + ).toResolveFalsy(); + + await expect( + client.video.findFirst({ + where: { + owner: { is: null }, + }, + }), + ).toResolveFalsy(); + + await expect( + client.video.findFirst({ + where: { + owner: { isNot: null }, + }, + }), + ).toResolveTruthy(); + + await expect( + client.video.findFirst({ + where: { + comments: { + some: { content: 'c1' }, + }, + }, + }), + ).toResolveTruthy(); + + await expect( + client.video.findFirst({ + where: { + comments: { + all: { content: 'c2' }, + }, + }, + }), + ).toResolveTruthy(); + + await expect( + client.video.findFirst({ + where: { + comments: { + none: { content: 'c1' }, + }, + }, + }), + ).toResolveTruthy(); + + await expect( + client.video.findFirst({ + where: { + comments: { + none: { content: { startsWith: 'c' } }, + }, + }, + }), + ).toResolveFalsy(); + }); + }); }, ); diff --git a/packages/runtime/test/client-api/typed-json-fields.test.ts b/packages/runtime/test/client-api/typed-json-fields.test.ts index fdf01f81d..53ca43e0f 100644 --- a/packages/runtime/test/client-api/typed-json-fields.test.ts +++ b/packages/runtime/test/client-api/typed-json-fields.test.ts @@ -29,7 +29,6 @@ model User { usePrismaPush: true, provider, dbName: provider === 'postgresql' ? PG_DB_NAME : undefined, - log: ['query'], }); });