From 192cc01537fbc43feb3c671b6e361882857b6389 Mon Sep 17 00:00:00 2001 From: Yiming Cao Date: Tue, 5 Aug 2025 14:27:08 +0800 Subject: [PATCH 1/6] fix: using _count in relation selection (#143) --- packages/runtime/src/client/crud-types.ts | 13 ++++- .../runtime/src/client/crud/dialects/base.ts | 50 +++++++++++++++++ .../src/client/crud/dialects/postgresql.ts | 30 +++++++---- .../src/client/crud/dialects/sqlite.ts | 42 +++++++++------ .../src/client/crud/operations/base.ts | 53 +------------------ packages/runtime/src/client/crud/validator.ts | 6 +-- packages/runtime/src/client/query-utils.ts | 16 +++--- packages/runtime/test/client-api/find.test.ts | 25 +++++++++ 8 files changed, 145 insertions(+), 90 deletions(-) diff --git a/packages/runtime/src/client/crud-types.ts b/packages/runtime/src/client/crud-types.ts index abe011f4..20fd4be9 100644 --- a/packages/runtime/src/client/crud-types.ts +++ b/packages/runtime/src/client/crud-types.ts @@ -393,7 +393,12 @@ export type SelectInput< [Key in NonRelationFields]?: true; } & (AllowRelation extends true ? IncludeInput : {}) & // relation fields // relation count - (AllowCount extends true ? { _count?: SelectCount } : {}); + (AllowCount extends true + ? // _count is only allowed if the model has to-many relations + HasToManyRelations extends true + ? { _count?: SelectCount } + : {} + : {}); type SelectCount> = | true @@ -1181,4 +1186,10 @@ type NonOwnedRelationFields> = keyof { + [Key in RelationFields as FieldIsArray extends true ? Key : never]: true; +} extends never + ? false + : true; + // #endregion diff --git a/packages/runtime/src/client/crud/dialects/base.ts b/packages/runtime/src/client/crud/dialects/base.ts index 369c1539..c1bc7660 100644 --- a/packages/runtime/src/client/crud/dialects/base.ts +++ b/packages/runtime/src/client/crud/dialects/base.ts @@ -847,6 +847,56 @@ export abstract class BaseCrudDialect { return query; } + buildCountJson(model: string, eb: ExpressionBuilder, parentAlias: string, payload: any) { + const modelDef = requireModel(this.schema, model); + const toManyRelations = Object.entries(modelDef.fields).filter(([, field]) => field.relation && field.array); + + const selections = + payload === true + ? { + select: toManyRelations.reduce( + (acc, [field]) => { + acc[field] = true; + return acc; + }, + {} as Record, + ), + } + : payload; + + const jsonObject: Record> = {}; + + for (const [field, value] of Object.entries(selections.select)) { + const fieldDef = requireField(this.schema, model, field); + const fieldModel = fieldDef.type; + const joinPairs = buildJoinPairs(this.schema, model, parentAlias, field, fieldModel); + + // build a nested query to count the number of records in the relation + let fieldCountQuery = eb.selectFrom(fieldModel).select(eb.fn.countAll().as(`_count$${field}`)); + + // join conditions + for (const [left, right] of joinPairs) { + fieldCountQuery = fieldCountQuery.whereRef(left, '=', right); + } + + // merge _count filter + if ( + value && + typeof value === 'object' && + 'where' in value && + value.where && + typeof value.where === 'object' + ) { + const filter = this.buildFilter(eb, fieldModel, fieldModel, value.where); + fieldCountQuery = fieldCountQuery.where(filter); + } + + jsonObject[field] = fieldCountQuery; + } + + return this.buildJsonObject(eb, jsonObject); + } + // #endregion // #region utils diff --git a/packages/runtime/src/client/crud/dialects/postgresql.ts b/packages/runtime/src/client/crud/dialects/postgresql.ts index f3408820..5cb9c5de 100644 --- a/packages/runtime/src/client/crud/dialects/postgresql.ts +++ b/packages/runtime/src/client/crud/dialects/postgresql.ts @@ -200,7 +200,7 @@ export class PostgresCrudDialect extends BaseCrudDiale relationField: string, eb: ExpressionBuilder, payload: true | FindArgs, true>, - parentName: string, + parentAlias: string, ) { const relationModelDef = requireModel(this.schema, relationModel); const objArgs: Array< @@ -238,14 +238,24 @@ export class PostgresCrudDialect extends BaseCrudDiale objArgs.push( ...Object.entries(payload.select) .filter(([, value]) => value) - .map(([field]) => { - const fieldDef = requireField(this.schema, relationModel, field); - const fieldValue = fieldDef.relation - ? // reference the synthesized JSON field - eb.ref(`${parentName}$${relationField}$${field}.$j`) - : // reference a plain field - buildFieldRef(this.schema, relationModel, field, this.options, eb); - return [sql.lit(field), fieldValue]; + .map(([field, value]) => { + if (field === '_count') { + const subJson = this.buildCountJson( + relationModel as GetModels, + eb, + `${parentAlias}$${relationField}`, + value, + ); + return [sql.lit(field), subJson]; + } else { + const fieldDef = requireField(this.schema, relationModel, field); + const fieldValue = fieldDef.relation + ? // reference the synthesized JSON field + eb.ref(`${parentAlias}$${relationField}$${field}.$j`) + : // reference a plain field + buildFieldRef(this.schema, relationModel, field, this.options, eb); + return [sql.lit(field), fieldValue]; + } }) .flatMap((v) => v), ); @@ -259,7 +269,7 @@ export class PostgresCrudDialect extends BaseCrudDiale .map(([field]) => [ sql.lit(field), // reference the synthesized JSON field - eb.ref(`${parentName}$${relationField}$${field}.$j`), + eb.ref(`${parentAlias}$${relationField}$${field}.$j`), ]) .flatMap((v) => v), ); diff --git a/packages/runtime/src/client/crud/dialects/sqlite.ts b/packages/runtime/src/client/crud/dialects/sqlite.ts index 695795ab..9277af48 100644 --- a/packages/runtime/src/client/crud/dialects/sqlite.ts +++ b/packages/runtime/src/client/crud/dialects/sqlite.ts @@ -67,14 +67,14 @@ export class SqliteCrudDialect extends BaseCrudDialect model: string, eb: ExpressionBuilder, relationField: string, - parentName: string, + parentAlias: string, payload: true | FindArgs, true>, ) { const relationFieldDef = requireField(this.schema, model, relationField); const relationModel = relationFieldDef.type as GetModels; const relationModelDef = requireModel(this.schema, relationModel); - const subQueryName = `${parentName}$${relationField}`; + const subQueryName = `${parentAlias}$${relationField}`; let tbl = eb.selectFrom(() => { let subQuery = this.buildSelectModel(eb, relationModel); @@ -129,7 +129,7 @@ export class SqliteCrudDialect extends BaseCrudDialect eb .selectFrom(m2m.joinTable) .select(`${m2m.joinTable}.${m2m.otherFkName}`) - .whereRef(`${parentName}.${parentIds[0]}`, '=', `${m2m.joinTable}.${m2m.parentFkName}`), + .whereRef(`${parentAlias}.${parentIds[0]}`, '=', `${m2m.joinTable}.${m2m.parentFkName}`), ), ); } else { @@ -137,10 +137,10 @@ export class SqliteCrudDialect extends BaseCrudDialect keyPairs.forEach(({ fk, pk }) => { if (ownedByModel) { // the parent model owns the fk - subQuery = subQuery.whereRef(`${relationModel}.${pk}`, '=', `${parentName}.${fk}`); + subQuery = subQuery.whereRef(`${relationModel}.${pk}`, '=', `${parentAlias}.${fk}`); } else { // the relation side owns the fk - subQuery = subQuery.whereRef(`${relationModel}.${fk}`, '=', `${parentName}.${pk}`); + subQuery = subQuery.whereRef(`${relationModel}.${fk}`, '=', `${parentAlias}.${pk}`); } }); } @@ -183,21 +183,31 @@ export class SqliteCrudDialect extends BaseCrudDialect ...Object.entries(payload.select) .filter(([, value]) => value) .map(([field, value]) => { - const fieldDef = requireField(this.schema, relationModel, field); - if (fieldDef.relation) { - const subJson = this.buildRelationJSON( + if (field === '_count') { + const subJson = this.buildCountJson( relationModel as GetModels, eb, - field, - `${parentName}$${relationField}`, + `${parentAlias}$${relationField}`, value, ); - return [sql.lit(field), subJson as ArgsType]; + return [sql.lit(field), subJson]; } else { - return [ - sql.lit(field), - buildFieldRef(this.schema, relationModel, field, this.options, eb) as ArgsType, - ]; + const fieldDef = requireField(this.schema, relationModel, field); + if (fieldDef.relation) { + const subJson = this.buildRelationJSON( + relationModel as GetModels, + eb, + field, + `${parentAlias}$${relationField}`, + value, + ); + return [sql.lit(field), subJson]; + } else { + return [ + sql.lit(field), + buildFieldRef(this.schema, relationModel, field, this.options, eb) as ArgsType, + ]; + } } }) .flatMap((v) => v), @@ -214,7 +224,7 @@ export class SqliteCrudDialect extends BaseCrudDialect relationModel as GetModels, eb, field, - `${parentName}$${relationField}`, + `${parentAlias}$${relationField}`, value, ); return [sql.lit(field), subJson]; diff --git a/packages/runtime/src/client/crud/operations/base.ts b/packages/runtime/src/client/crud/operations/base.ts index bc43e2af..58fe3759 100644 --- a/packages/runtime/src/client/crud/operations/base.ts +++ b/packages/runtime/src/client/crud/operations/base.ts @@ -8,7 +8,6 @@ import { UpdateResult, type Compilable, type IsolationLevel, - type Expression as KyselyExpression, type QueryResult, type SelectQueryBuilder, } from 'kysely'; @@ -31,7 +30,6 @@ import { InternalError, NotFoundError, QueryError } from '../../errors'; import type { ToKysely } from '../../query-builder'; import { buildFieldRef, - buildJoinPairs, ensureArray, extractIdFields, flattenCompoundUniqueFilters, @@ -298,56 +296,7 @@ export abstract class BaseOperationHandler { parentAlias: string, payload: any, ) { - const modelDef = requireModel(this.schema, model); - const toManyRelations = Object.entries(modelDef.fields).filter(([, field]) => field.relation && field.array); - - const selections = - payload === true - ? { - select: toManyRelations.reduce( - (acc, [field]) => { - acc[field] = true; - return acc; - }, - {} as Record, - ), - } - : payload; - - const eb = expressionBuilder(); - const jsonObject: Record> = {}; - - for (const [field, value] of Object.entries(selections.select)) { - const fieldDef = requireField(this.schema, model, field); - const fieldModel = fieldDef.type; - const joinPairs = buildJoinPairs(this.schema, model, parentAlias, field, fieldModel); - - // build a nested query to count the number of records in the relation - let fieldCountQuery = eb.selectFrom(fieldModel).select(eb.fn.countAll().as(`_count$${field}`)); - - // join conditions - for (const [left, right] of joinPairs) { - fieldCountQuery = fieldCountQuery.whereRef(left, '=', right); - } - - // merge _count filter - if ( - value && - typeof value === 'object' && - 'where' in value && - value.where && - typeof value.where === 'object' - ) { - const filter = this.dialect.buildFilter(eb, fieldModel, fieldModel, value.where); - fieldCountQuery = fieldCountQuery.where(filter); - } - - jsonObject[field] = fieldCountQuery; - } - - query = query.select((eb) => this.dialect.buildJsonObject(eb, jsonObject).as('_count')); - - return query; + return query.select((eb) => this.dialect.buildCountJson(model, eb, parentAlias, payload).as('_count')); } private buildCursorFilter( diff --git a/packages/runtime/src/client/crud/validator.ts b/packages/runtime/src/client/crud/validator.ts index c4c7a9d1..32ab09cb 100644 --- a/packages/runtime/src/client/crud/validator.ts +++ b/packages/runtime/src/client/crud/validator.ts @@ -544,7 +544,7 @@ export class InputValidator { } } - const toManyRelations = Object.entries(modelDef.fields).filter(([, value]) => value.relation && value.array); + const toManyRelations = Object.values(modelDef.fields).filter((def) => def.relation && def.array); if (toManyRelations.length > 0) { fields['_count'] = z @@ -552,9 +552,9 @@ export class InputValidator { z.literal(true), z.object( toManyRelations.reduce( - (acc, [name, fieldDef]) => ({ + (acc, fieldDef) => ({ ...acc, - [name]: z + [fieldDef.name]: z .union([ z.boolean(), z.object({ diff --git a/packages/runtime/src/client/query-utils.ts b/packages/runtime/src/client/query-utils.ts index c4cd78f5..7302933b 100644 --- a/packages/runtime/src/client/query-utils.ts +++ b/packages/runtime/src/client/query-utils.ts @@ -97,23 +97,23 @@ export function getRelationForeignKeyFieldPairs(schema: SchemaDef, model: string } export function isScalarField(schema: SchemaDef, model: string, field: string): boolean { - const fieldDef = requireField(schema, model, field); - return !fieldDef.relation && !fieldDef.foreignKeyFor; + const fieldDef = getField(schema, model, field); + return !fieldDef?.relation && !fieldDef?.foreignKeyFor; } export function isForeignKeyField(schema: SchemaDef, model: string, field: string): boolean { - const fieldDef = requireField(schema, model, field); - return !!fieldDef.foreignKeyFor; + const fieldDef = getField(schema, model, field); + return !!fieldDef?.foreignKeyFor; } export function isRelationField(schema: SchemaDef, model: string, field: string): boolean { - const fieldDef = requireField(schema, model, field); - return !!fieldDef.relation; + const fieldDef = getField(schema, model, field); + return !!fieldDef?.relation; } export function isInheritedField(schema: SchemaDef, model: string, field: string): boolean { - const fieldDef = requireField(schema, model, field); - return !!fieldDef.originModel; + const fieldDef = getField(schema, model, field); + return !!fieldDef?.originModel; } export function getUniqueFields(schema: SchemaDef, model: string) { diff --git a/packages/runtime/test/client-api/find.test.ts b/packages/runtime/test/client-api/find.test.ts index e1a05be1..3cb85495 100644 --- a/packages/runtime/test/client-api/find.test.ts +++ b/packages/runtime/test/client-api/find.test.ts @@ -832,6 +832,31 @@ describe.each(createClientSpecs(PG_DB_NAME))('Client find tests for $provider', _count: { posts: 2 }, }); + await expect( + client.user.findUnique({ + where: { id: user1.id }, + select: { + id: true, + posts: { + select: { _count: true }, + }, + }, + }), + ).resolves.toMatchObject({ + id: user1.id, + posts: [{ _count: { comments: 0 } }, { _count: { comments: 0 } }], + }); + + client.comment.findFirst({ + // @ts-expect-error Comment has no to-many relations to count + select: { _count: true }, + }); + + client.post.findFirst({ + // @ts-expect-error Comment has no to-many relations to count + select: { comments: { _count: true } }, + }); + await expect( client.user.findUnique({ where: { id: user1.id }, From 6a62a2c9d7b6695d41975bf9367c359cb414070d Mon Sep 17 00:00:00 2001 From: Yiming Cao Date: Tue, 5 Aug 2025 17:34:16 +0800 Subject: [PATCH 2/6] fix: issue with in memory distinct when distinct fields are not selected (#144) * fix: issue with in memory distinct when distinct fields are not selected * addressing PR comments --- .../src/client/crud/operations/base.ts | 22 ++++++++++++++++--- packages/runtime/test/client-api/find.test.ts | 19 +++++++++++++++- 2 files changed, 37 insertions(+), 4 deletions(-) diff --git a/packages/runtime/src/client/crud/operations/base.ts b/packages/runtime/src/client/crud/operations/base.ts index 58fe3759..cc79057f 100644 --- a/packages/runtime/src/client/crud/operations/base.ts +++ b/packages/runtime/src/client/crud/operations/base.ts @@ -177,12 +177,21 @@ export abstract class BaseOperationHandler { // distinct let inMemoryDistinct: string[] | undefined = undefined; if (args?.distinct) { - const distinct = ensureArray(args.distinct); + const distinct = ensureArray(args.distinct) as string[]; if (this.dialect.supportsDistinctOn) { - query = query.distinctOn(distinct.map((f: any) => sql.ref(`${model}.${f}`))); + query = query.distinctOn(distinct.map((f) => sql.ref(`${model}.${f}`))); } else { // in-memory distinct after fetching all results inMemoryDistinct = distinct; + + // make sure distinct fields are selected + query = distinct.reduce( + (acc, field) => + acc.select((eb) => + buildFieldRef(this.schema, model, field, this.options, eb).as(`$distinct$${field}`), + ), + query, + ); } } @@ -225,13 +234,20 @@ export abstract class BaseOperationHandler { const distinctResult: Record[] = []; const seen = new Set(); for (const r of result as any[]) { - const key = safeJSONStringify(inMemoryDistinct.map((f) => r[f]))!; + const key = safeJSONStringify(inMemoryDistinct.map((f) => r[`$distinct$${f}`]))!; if (!seen.has(key)) { distinctResult.push(r); seen.add(key); } } result = distinctResult; + + // clean up distinct utility fields + for (const r of result) { + Object.keys(r) + .filter((k) => k.startsWith('$distinct$')) + .forEach((k) => delete r[k]); + } } return result; diff --git a/packages/runtime/test/client-api/find.test.ts b/packages/runtime/test/client-api/find.test.ts index 3cb85495..9b588a03 100644 --- a/packages/runtime/test/client-api/find.test.ts +++ b/packages/runtime/test/client-api/find.test.ts @@ -244,6 +244,7 @@ describe.each(createClientSpecs(PG_DB_NAME))('Client find tests for $provider', await createUser(client, 'u1@test.com', { name: 'Admin1', role: 'ADMIN', + profile: { create: { bio: 'Bio1' } }, }); await createUser(client, 'u3@test.com', { name: 'User', @@ -252,6 +253,7 @@ describe.each(createClientSpecs(PG_DB_NAME))('Client find tests for $provider', await createUser(client, 'u2@test.com', { name: 'Admin2', role: 'ADMIN', + profile: { create: { bio: 'Bio1' } }, }); await createUser(client, 'u4@test.com', { name: 'User', @@ -259,7 +261,7 @@ describe.each(createClientSpecs(PG_DB_NAME))('Client find tests for $provider', }); // single field distinct - let r = await client.user.findMany({ distinct: ['role'] }); + let r: any = await client.user.findMany({ distinct: ['role'] }); expect(r).toHaveLength(2); expect(r).toEqual( expect.arrayContaining([ @@ -268,6 +270,21 @@ describe.each(createClientSpecs(PG_DB_NAME))('Client find tests for $provider', ]), ); + // distinct with include + r = await client.user.findMany({ distinct: ['role'], include: { profile: true } }); + expect(r).toHaveLength(2); + expect(r).toEqual( + expect.arrayContaining([ + expect.objectContaining({ role: 'ADMIN', profile: expect.any(Object) }), + expect.objectContaining({ role: 'USER', profile: null }), + ]), + ); + + // distinct with select + r = await client.user.findMany({ distinct: ['role'], select: { email: true } }); + expect(r).toHaveLength(2); + expect(r).toEqual(expect.arrayContaining([{ email: expect.any(String) }, { email: expect.any(String) }])); + // multiple fields distinct r = await client.user.findMany({ distinct: ['role', 'name'], From 3a4997331287d29d7e59d3bd6969d2c8eeeaec39 Mon Sep 17 00:00:00 2001 From: Yiming Cao Date: Wed, 6 Aug 2025 17:08:45 +0800 Subject: [PATCH 3/6] fix: tighten up query input validation, fixed case-sensitivity compatibility with Prisma (#147) * fix: tighten up query input validation, fixed case-sensitivity compatibility with Prisma * update --- packages/runtime/src/client/crud-types.ts | 39 ++- .../runtime/src/client/crud/dialects/base.ts | 63 ++-- packages/runtime/src/client/crud/validator.ts | 312 ++++++++++-------- .../runtime/test/client-api/filter.test.ts | 130 +++++++- 4 files changed, 343 insertions(+), 201 deletions(-) diff --git a/packages/runtime/src/client/crud-types.ts b/packages/runtime/src/client/crud-types.ts index 20fd4be9..4c4986b7 100644 --- a/packages/runtime/src/client/crud-types.ts +++ b/packages/runtime/src/client/crud-types.ts @@ -223,7 +223,7 @@ export type WhereInput< : FieldIsArray extends true ? ArrayFilter> : // primitive - PrimitiveFilter, ModelFieldIsOptional>; + PrimitiveFilter, ModelFieldIsOptional>; } & { $expr?: (eb: ExpressionBuilder, Model>) => OperandExpression; } & { @@ -249,21 +249,21 @@ type ArrayFilter = { isEmpty?: boolean; }; -type PrimitiveFilter = T extends 'String' - ? StringFilter +type PrimitiveFilter = T extends 'String' + ? StringFilter : T extends 'Int' | 'Float' | 'Decimal' | 'BigInt' - ? NumberFilter + ? NumberFilter : T extends 'Boolean' ? BooleanFilter : T extends 'DateTime' - ? DateTimeFilter + ? DateTimeFilter : T extends 'Bytes' ? BytesFilter : T extends 'Json' ? 'Not implemented yet' // TODO: Json filter : never; -type CommonPrimitiveFilter = { +type CommonPrimitiveFilter = { equals?: NullableIf; in?: DataType[]; notIn?: DataType[]; @@ -271,25 +271,30 @@ type CommonPrimitiveFilter; + not?: PrimitiveFilter; }; -export type StringFilter = +export type StringFilter = | NullableIf - | (CommonPrimitiveFilter & { + | (CommonPrimitiveFilter & { contains?: string; startsWith?: string; endsWith?: string; - mode?: 'default' | 'insensitive'; - }); + } & (ProviderSupportsCaseSensitivity extends true + ? { + mode?: 'default' | 'insensitive'; + } + : {})); -export type NumberFilter = - | NullableIf - | CommonPrimitiveFilter; +export type NumberFilter< + Schema extends SchemaDef, + T extends 'Int' | 'Float' | 'Decimal' | 'BigInt', + Nullable extends boolean, +> = NullableIf | CommonPrimitiveFilter; -export type DateTimeFilter = +export type DateTimeFilter = | NullableIf - | CommonPrimitiveFilter; + | CommonPrimitiveFilter; export type BytesFilter = | NullableIf @@ -1192,4 +1197,6 @@ type HasToManyRelations = Schema['provider'] extends 'postgresql' ? true : false; + // #endregion diff --git a/packages/runtime/src/client/crud/dialects/base.ts b/packages/runtime/src/client/crud/dialects/base.ts index c1bc7660..d6bb705e 100644 --- a/packages/runtime/src/client/crud/dialects/base.ts +++ b/packages/runtime/src/client/crud/dialects/base.ts @@ -457,6 +457,7 @@ export abstract class BaseCrudDialect { recurse: (value: unknown) => Expression, throwIfInvalid = false, onlyForKeys: string[] | undefined = undefined, + excludeKeys: string[] = [], ) { if (payload === null || !isPlainObject(payload)) { return { @@ -472,6 +473,9 @@ export abstract class BaseCrudDialect { if (onlyForKeys && !onlyForKeys.includes(op)) { continue; } + if (excludeKeys.includes(op)) { + continue; + } const rhs = Array.isArray(value) ? value.map(getRhs) : getRhs(value); const condition = match(op) .with('equals', () => (rhs === null ? eb(lhs, 'is', null) : eb(lhs, '=', rhs))) @@ -513,20 +517,23 @@ export abstract class BaseCrudDialect { return { conditions, consumedKeys }; } - private buildStringFilter(eb: ExpressionBuilder, fieldRef: Expression, payload: StringFilter) { - let insensitive = false; - if (payload && typeof payload === 'object' && 'mode' in payload && payload.mode === 'insensitive') { - insensitive = true; - fieldRef = eb.fn('lower', [fieldRef]); + private buildStringFilter( + eb: ExpressionBuilder, + fieldRef: Expression, + payload: StringFilter, + ) { + let mode: 'default' | 'insensitive' | undefined; + if (payload && typeof payload === 'object' && 'mode' in payload) { + mode = payload.mode; } const { conditions, consumedKeys } = this.buildStandardFilter( eb, 'String', payload, - fieldRef, - (value) => this.prepStringCasing(eb, value, insensitive), - (value) => this.buildStringFilter(eb, fieldRef, value as StringFilter), + mode === 'insensitive' ? eb.fn('lower', [fieldRef]) : fieldRef, + (value) => this.prepStringCasing(eb, value, mode), + (value) => this.buildStringFilter(eb, fieldRef, value as StringFilter), ); if (payload && typeof payload === 'object') { @@ -538,22 +545,22 @@ export abstract class BaseCrudDialect { const condition = match(key) .with('contains', () => - insensitive - ? eb(fieldRef, 'ilike', sql.lit(`%${value}%`)) - : eb(fieldRef, 'like', sql.lit(`%${value}%`)), + mode === 'insensitive' + ? eb(fieldRef, 'ilike', sql.val(`%${value}%`)) + : eb(fieldRef, 'like', sql.val(`%${value}%`)), ) .with('startsWith', () => - insensitive - ? eb(fieldRef, 'ilike', sql.lit(`${value}%`)) - : eb(fieldRef, 'like', sql.lit(`${value}%`)), + mode === 'insensitive' + ? eb(fieldRef, 'ilike', sql.val(`${value}%`)) + : eb(fieldRef, 'like', sql.val(`${value}%`)), ) .with('endsWith', () => - insensitive - ? eb(fieldRef, 'ilike', sql.lit(`%${value}`)) - : eb(fieldRef, 'like', sql.lit(`%${value}`)), + mode === 'insensitive' + ? eb(fieldRef, 'ilike', sql.val(`%${value}`)) + : eb(fieldRef, 'like', sql.val(`%${value}`)), ) .otherwise(() => { - throw new Error(`Invalid string filter key: ${key}`); + throw new QueryError(`Invalid string filter key: ${key}`); }); if (condition) { @@ -565,13 +572,21 @@ export abstract class BaseCrudDialect { return this.and(eb, ...conditions); } - private prepStringCasing(eb: ExpressionBuilder, value: unknown, toLower: boolean = true): any { + private prepStringCasing( + eb: ExpressionBuilder, + value: unknown, + mode: 'default' | 'insensitive' | undefined, + ): any { + if (!mode || mode === 'default') { + return value === null ? value : sql.val(value); + } + if (typeof value === 'string') { - return toLower ? eb.fn('lower', [sql.lit(value)]) : sql.lit(value); + return eb.fn('lower', [sql.val(value)]); } else if (Array.isArray(value)) { - return value.map((v) => this.prepStringCasing(eb, v, toLower)); + return value.map((v) => this.prepStringCasing(eb, v, mode)); } else { - return value === null ? null : sql.lit(value); + return value === null ? null : sql.val(value); } } @@ -613,7 +628,7 @@ export abstract class BaseCrudDialect { private buildDateTimeFilter( eb: ExpressionBuilder, fieldRef: Expression, - payload: DateTimeFilter, + payload: DateTimeFilter, ) { const { conditions } = this.buildStandardFilter( eb, @@ -621,7 +636,7 @@ export abstract class BaseCrudDialect { payload, fieldRef, (value) => this.transformPrimitive(value, 'DateTime', false), - (value) => this.buildDateTimeFilter(eb, fieldRef, value as DateTimeFilter), + (value) => this.buildDateTimeFilter(eb, fieldRef, value as DateTimeFilter), true, ); return this.and(eb, ...conditions); diff --git a/packages/runtime/src/client/crud/validator.ts b/packages/runtime/src/client/crud/validator.ts index 32ab09cb..c586abfd 100644 --- a/packages/runtime/src/client/crud/validator.ts +++ b/packages/runtime/src/client/crud/validator.ts @@ -210,12 +210,12 @@ export class InputValidator { fields['cursor'] = this.makeCursorSchema(model).optional(); if (options.collection) { - fields['skip'] = z.number().int().nonnegative().optional(); - fields['take'] = z.number().int().optional(); + fields['skip'] = this.makeSkipSchema().optional(); + fields['take'] = this.makeTakeSchema().optional(); fields['orderBy'] = this.orArray(this.makeOrderBySchema(model, true, false), true).optional(); } - let result: ZodType = z.object(fields).strict(); + let result: ZodType = z.strictObject(fields); result = this.refineForSelectIncludeMutuallyExclusive(result); result = this.refineForSelectOmitMutuallyExclusive(result); @@ -292,7 +292,7 @@ export class InputValidator { // to-many relation fieldSchema = z.union([ fieldSchema, - z.object({ + z.strictObject({ some: fieldSchema.optional(), every: fieldSchema.optional(), none: fieldSchema.optional(), @@ -302,7 +302,7 @@ export class InputValidator { // to-one relation fieldSchema = z.union([ fieldSchema, - z.object({ + z.strictObject({ is: fieldSchema.optional(), isNot: fieldSchema.optional(), }), @@ -381,7 +381,7 @@ export class InputValidator { true, ).optional(); - const baseWhere = z.object(fields).strict(); + const baseWhere = z.strictObject(fields); let result: ZodType = baseWhere; if (unique) { @@ -414,7 +414,7 @@ export class InputValidator { ); return z.union([ this.nullableIf(baseSchema, optional), - z.object({ + z.strictObject({ equals: components.equals, in: components.in, notIn: components.notIn, @@ -424,7 +424,7 @@ export class InputValidator { } private makeArrayFilterSchema(type: BuiltinType) { - return z.object({ + return z.strictObject({ equals: this.makePrimitiveSchema(type).array().optional(), has: this.makePrimitiveSchema(type).optional(), hasEvery: this.makePrimitiveSchema(type).array().optional(), @@ -468,7 +468,7 @@ export class InputValidator { private makeBooleanFilterSchema(optional: boolean): ZodType { return z.union([ this.nullableIf(z.boolean(), optional), - z.object({ + z.strictObject({ equals: this.nullableIf(z.boolean(), optional).optional(), not: z.lazy(() => this.makeBooleanFilterSchema(optional)).optional(), }), @@ -482,7 +482,7 @@ export class InputValidator { ); return z.union([ this.nullableIf(baseSchema, optional), - z.object({ + z.strictObject({ equals: components.equals, in: components.in, notIn: components.notIn, @@ -508,7 +508,7 @@ export class InputValidator { private makeCommonPrimitiveFilterSchema(baseSchema: ZodType, optional: boolean, makeThis: () => ZodType) { return z.union([ this.nullableIf(baseSchema, optional), - z.object(this.makeCommonPrimitiveFilterComponents(baseSchema, optional, makeThis)), + z.strictObject(this.makeCommonPrimitiveFilterComponents(baseSchema, optional, makeThis)), ]); } @@ -519,9 +519,26 @@ export class InputValidator { } private makeStringFilterSchema(optional: boolean): ZodType { - return this.makeCommonPrimitiveFilterSchema(z.string(), optional, () => - z.lazy(() => this.makeStringFilterSchema(optional)), - ); + return z.union([ + this.nullableIf(z.string(), optional), + z.strictObject({ + ...this.makeCommonPrimitiveFilterComponents(z.string(), optional, () => + z.lazy(() => this.makeStringFilterSchema(optional)), + ), + startsWith: z.string().optional(), + endsWith: z.string().optional(), + contains: z.string().optional(), + ...(this.providerSupportsCaseSensitivity + ? { + mode: this.makeStringModeSchema().optional(), + } + : {}), + }), + ]); + } + + private makeStringModeSchema() { + return z.union([z.literal('default'), z.literal('insensitive')]); } private makeSelectSchema(model: string) { @@ -533,7 +550,7 @@ export class InputValidator { fields[field] = z .union([ z.literal(true), - z.object({ + z.strictObject({ select: z.lazy(() => this.makeSelectSchema(fieldDef.type)).optional(), include: z.lazy(() => this.makeIncludeSchema(fieldDef.type)).optional(), }), @@ -550,27 +567,29 @@ export class InputValidator { fields['_count'] = z .union([ z.literal(true), - z.object( - toManyRelations.reduce( - (acc, fieldDef) => ({ - ...acc, - [fieldDef.name]: z - .union([ - z.boolean(), - z.object({ - where: this.makeWhereSchema(fieldDef.type, false, false), - }), - ]) - .optional(), - }), - {} as Record, + z.strictObject({ + select: z.strictObject( + toManyRelations.reduce( + (acc, fieldDef) => ({ + ...acc, + [fieldDef.name]: z + .union([ + z.boolean(), + z.strictObject({ + where: this.makeWhereSchema(fieldDef.type, false, false), + }), + ]) + .optional(), + }), + {} as Record, + ), ), - ), + }), ]) .optional(); } - return z.object(fields).strict(); + return z.strictObject(fields); } private makeOmitSchema(model: string) { @@ -582,7 +601,7 @@ export class InputValidator { fields[field] = z.boolean().optional(); } } - return z.object(fields).strict(); + return z.strictObject(fields); } private makeIncludeSchema(model: string) { @@ -594,17 +613,22 @@ export class InputValidator { fields[field] = z .union([ z.literal(true), - z.object({ + z.strictObject({ select: z.lazy(() => this.makeSelectSchema(fieldDef.type)).optional(), include: z.lazy(() => this.makeIncludeSchema(fieldDef.type)).optional(), + omit: z.lazy(() => this.makeOmitSchema(fieldDef.type)).optional(), where: z.lazy(() => this.makeWhereSchema(fieldDef.type, false)).optional(), + orderBy: z.lazy(() => this.makeOrderBySchema(fieldDef.type, true, false)).optional(), + skip: this.makeSkipSchema().optional(), + take: this.makeTakeSchema().optional(), + distinct: this.makeDistinctSchema(fieldDef.type).optional(), }), ]) .optional(); } } - return z.object(fields).strict(); + return z.strictObject(fields); } private makeOrderBySchema(model: string, withRelation: boolean, WithAggregation: boolean) { @@ -616,9 +640,15 @@ export class InputValidator { if (fieldDef.relation) { // relations if (withRelation) { - fields[field] = z.lazy(() => - this.makeOrderBySchema(fieldDef.type, withRelation, WithAggregation).optional(), - ); + fields[field] = z.lazy(() => { + let relationOrderBy = this.makeOrderBySchema(fieldDef.type, withRelation, WithAggregation); + if (fieldDef.array) { + relationOrderBy = relationOrderBy.extend({ + _count: sort, + }); + } + return relationOrderBy.optional(); + }); } } else { // scalars @@ -626,7 +656,7 @@ export class InputValidator { fields[field] = z .union([ sort, - z.object({ + z.strictObject({ sort, nulls: z.union([z.literal('first'), z.literal('last')]), }), @@ -646,7 +676,7 @@ export class InputValidator { } } - return z.object(fields); + return z.strictObject(fields); } private makeDistinctSchema(model: string) { @@ -665,14 +695,12 @@ export class InputValidator { private makeCreateSchema(model: string) { const dataSchema = this.makeCreateDataSchema(model, false); - const schema = z - .object({ - data: dataSchema, - select: this.makeSelectSchema(model).optional(), - include: this.makeIncludeSchema(model).optional(), - omit: this.makeOmitSchema(model).optional(), - }) - .strict(); + const schema = z.object({ + data: dataSchema, + select: this.makeSelectSchema(model).optional(), + include: this.makeIncludeSchema(model).optional(), + omit: this.makeOmitSchema(model).optional(), + }); return this.refineForSelectIncludeMutuallyExclusive(schema); } @@ -683,7 +711,7 @@ export class InputValidator { private makeCreateManyAndReturnSchema(model: string) { const base = this.makeCreateManyDataSchema(model, []); const result = base.merge( - z.object({ + z.strictObject({ select: this.makeSelectSchema(model).optional(), omit: this.makeOmitSchema(model).optional(), }), @@ -769,7 +797,7 @@ export class InputValidator { fieldSchema = z .union([ z.array(fieldSchema), - z.object({ + z.strictObject({ set: z.array(fieldSchema), }), ]) @@ -793,13 +821,13 @@ export class InputValidator { }); if (!hasRelation) { - return this.orArray(z.object(uncheckedVariantFields).strict(), canBeArray); + return this.orArray(z.strictObject(uncheckedVariantFields), canBeArray); } else { return z.union([ - z.object(uncheckedVariantFields).strict(), - z.object(checkedVariantFields).strict(), - ...(canBeArray ? [z.array(z.object(uncheckedVariantFields).strict())] : []), - ...(canBeArray ? [z.array(z.object(checkedVariantFields).strict())] : []), + z.strictObject(uncheckedVariantFields), + z.strictObject(checkedVariantFields), + ...(canBeArray ? [z.array(z.strictObject(uncheckedVariantFields))] : []), + ...(canBeArray ? [z.array(z.strictObject(checkedVariantFields))] : []), ]); } } @@ -838,7 +866,7 @@ export class InputValidator { fields['update'] = array ? this.orArray( - z.object({ + z.strictObject({ where: this.makeWhereSchema(fieldType, true), data: this.makeUpdateDataSchema(fieldType, withoutFields), }), @@ -846,7 +874,7 @@ export class InputValidator { ).optional() : z .union([ - z.object({ + z.strictObject({ where: this.makeWhereSchema(fieldType, true), data: this.makeUpdateDataSchema(fieldType, withoutFields), }), @@ -855,7 +883,7 @@ export class InputValidator { .optional(); fields['upsert'] = this.orArray( - z.object({ + z.strictObject({ where: this.makeWhereSchema(fieldType, true), create: this.makeCreateDataSchema(fieldType, false, withoutFields), update: this.makeUpdateDataSchema(fieldType, withoutFields), @@ -868,7 +896,7 @@ export class InputValidator { fields['set'] = this.makeSetDataSchema(fieldType, true).optional(); fields['updateMany'] = this.orArray( - z.object({ + z.strictObject({ where: this.makeWhereSchema(fieldType, false, true), data: this.makeUpdateDataSchema(fieldType, withoutFields), }), @@ -879,7 +907,7 @@ export class InputValidator { } } - return z.object(fields).strict(); + return z.strictObject(fields); } private makeSetDataSchema(model: string, canBeArray: boolean) { @@ -911,23 +939,19 @@ export class InputValidator { const whereSchema = this.makeWhereSchema(model, true); const createSchema = this.makeCreateDataSchema(model, false, withoutFields); return this.orArray( - z - .object({ - where: whereSchema, - create: createSchema, - }) - .strict(), + z.object({ + where: whereSchema, + create: createSchema, + }), canBeArray, ); } private makeCreateManyDataSchema(model: string, withoutFields: string[]) { - return z - .object({ - data: this.makeCreateDataSchema(model, true, withoutFields, true), - skipDuplicates: z.boolean().optional(), - }) - .strict(); + return z.object({ + data: this.makeCreateDataSchema(model, true, withoutFields, true), + skipDuplicates: z.boolean().optional(), + }); } // #endregion @@ -935,33 +959,28 @@ export class InputValidator { // #region Update private makeUpdateSchema(model: string) { - const schema = z - .object({ - where: this.makeWhereSchema(model, true), - data: this.makeUpdateDataSchema(model), - select: this.makeSelectSchema(model).optional(), - include: this.makeIncludeSchema(model).optional(), - omit: this.makeOmitSchema(model).optional(), - }) - .strict(); - + const schema = z.object({ + where: this.makeWhereSchema(model, true), + data: this.makeUpdateDataSchema(model), + select: this.makeSelectSchema(model).optional(), + include: this.makeIncludeSchema(model).optional(), + omit: this.makeOmitSchema(model).optional(), + }); return this.refineForSelectIncludeMutuallyExclusive(schema); } private makeUpdateManySchema(model: string) { - return z - .object({ - where: this.makeWhereSchema(model, false).optional(), - data: this.makeUpdateDataSchema(model, [], true), - limit: z.number().int().nonnegative().optional(), - }) - .strict(); + return z.object({ + where: this.makeWhereSchema(model, false).optional(), + data: this.makeUpdateDataSchema(model, [], true), + limit: z.number().int().nonnegative().optional(), + }); } private makeUpdateManyAndReturnSchema(model: string) { const base = this.makeUpdateManySchema(model); const result = base.merge( - z.object({ + z.strictObject({ select: this.makeSelectSchema(model).optional(), omit: this.makeOmitSchema(model).optional(), }), @@ -970,17 +989,14 @@ export class InputValidator { } private makeUpsertSchema(model: string) { - const schema = z - .object({ - where: this.makeWhereSchema(model, true), - create: this.makeCreateDataSchema(model, false), - update: this.makeUpdateDataSchema(model), - select: this.makeSelectSchema(model).optional(), - include: this.makeIncludeSchema(model).optional(), - omit: this.makeOmitSchema(model).optional(), - }) - .strict(); - + const schema = z.object({ + where: this.makeWhereSchema(model, true), + create: this.makeCreateDataSchema(model, false), + update: this.makeUpdateDataSchema(model), + select: this.makeSelectSchema(model).optional(), + include: this.makeIncludeSchema(model).optional(), + omit: this.makeOmitSchema(model).optional(), + }); return this.refineForSelectIncludeMutuallyExclusive(schema); } @@ -1074,9 +1090,9 @@ export class InputValidator { }); if (!hasRelation) { - return z.object(uncheckedVariantFields).strict(); + return z.strictObject(uncheckedVariantFields); } else { - return z.union([z.object(uncheckedVariantFields).strict(), z.object(checkedVariantFields).strict()]); + return z.union([z.strictObject(uncheckedVariantFields), z.strictObject(checkedVariantFields)]); } } @@ -1085,13 +1101,11 @@ export class InputValidator { // #region Delete private makeDeleteSchema(model: GetModels) { - const schema = z - .object({ - where: this.makeWhereSchema(model, true), - select: this.makeSelectSchema(model).optional(), - include: this.makeIncludeSchema(model).optional(), - }) - .strict(); + const schema = z.object({ + where: this.makeWhereSchema(model, true), + select: this.makeSelectSchema(model).optional(), + include: this.makeIncludeSchema(model).optional(), + }); return this.refineForSelectIncludeMutuallyExclusive(schema); } @@ -1101,7 +1115,7 @@ export class InputValidator { where: this.makeWhereSchema(model, false).optional(), limit: z.number().int().nonnegative().optional(), }) - .strict() + .optional(); } @@ -1113,12 +1127,12 @@ export class InputValidator { return z .object({ where: this.makeWhereSchema(model, false).optional(), - skip: z.number().int().nonnegative().optional(), - take: z.number().int().optional(), + skip: this.makeSkipSchema().optional(), + take: this.makeTakeSchema().optional(), orderBy: this.orArray(this.makeOrderBySchema(model, true, false), true).optional(), select: this.makeCountAggregateInputSchema(model).optional(), }) - .strict() + .optional(); } @@ -1126,18 +1140,16 @@ export class InputValidator { const modelDef = requireModel(this.schema, model); return z.union([ z.literal(true), - z - .object({ - _all: z.literal(true).optional(), - ...Object.keys(modelDef.fields).reduce( - (acc, field) => { - acc[field] = z.literal(true).optional(); - return acc; - }, - {} as Record, - ), - }) - .strict(), + z.object({ + _all: z.literal(true).optional(), + ...Object.keys(modelDef.fields).reduce( + (acc, field) => { + acc[field] = z.literal(true).optional(); + return acc; + }, + {} as Record, + ), + }), ]); } @@ -1149,8 +1161,8 @@ export class InputValidator { return z .object({ where: this.makeWhereSchema(model, false).optional(), - skip: z.number().int().nonnegative().optional(), - take: z.number().int().optional(), + skip: this.makeSkipSchema().optional(), + take: this.makeTakeSchema().optional(), orderBy: this.orArray(this.makeOrderBySchema(model, true, false), true).optional(), _count: this.makeCountAggregateInputSchema(model).optional(), _avg: this.makeSumAvgInputSchema(model).optional(), @@ -1158,13 +1170,13 @@ export class InputValidator { _min: this.makeMinMaxInputSchema(model).optional(), _max: this.makeMinMaxInputSchema(model).optional(), }) - .strict() + .optional(); } makeSumAvgInputSchema(model: GetModels) { const modelDef = requireModel(this.schema, model); - return z.object( + return z.strictObject( Object.keys(modelDef.fields).reduce( (acc, field) => { const fieldDef = requireField(this.schema, model, field); @@ -1180,7 +1192,7 @@ export class InputValidator { makeMinMaxInputSchema(model: GetModels) { const modelDef = requireModel(this.schema, model); - return z.object( + return z.strictObject( Object.keys(modelDef.fields).reduce( (acc, field) => { const fieldDef = requireField(this.schema, model, field); @@ -1198,22 +1210,19 @@ export class InputValidator { const modelDef = requireModel(this.schema, model); const nonRelationFields = Object.keys(modelDef.fields).filter((field) => !modelDef.fields[field]?.relation); - let schema = z - .object({ - where: this.makeWhereSchema(model, false).optional(), - orderBy: this.orArray(this.makeOrderBySchema(model, false, true), true).optional(), - by: this.orArray(z.enum(nonRelationFields), true), - having: this.makeWhereSchema(model, false, true).optional(), - skip: z.number().int().nonnegative().optional(), - take: z.number().int().optional(), - _count: this.makeCountAggregateInputSchema(model).optional(), - _avg: this.makeSumAvgInputSchema(model).optional(), - _sum: this.makeSumAvgInputSchema(model).optional(), - _min: this.makeMinMaxInputSchema(model).optional(), - _max: this.makeMinMaxInputSchema(model).optional(), - }) - .strict(); - + let schema = z.object({ + where: this.makeWhereSchema(model, false).optional(), + orderBy: this.orArray(this.makeOrderBySchema(model, false, true), true).optional(), + by: this.orArray(z.enum(nonRelationFields), true), + having: this.makeWhereSchema(model, false, true).optional(), + skip: this.makeSkipSchema().optional(), + take: this.makeTakeSchema().optional(), + _count: this.makeCountAggregateInputSchema(model).optional(), + _avg: this.makeSumAvgInputSchema(model).optional(), + _sum: this.makeSumAvgInputSchema(model).optional(), + _min: this.makeMinMaxInputSchema(model).optional(), + _max: this.makeMinMaxInputSchema(model).optional(), + }); schema = schema.refine((value) => { const bys = typeof value.by === 'string' ? [value.by] : value.by; if ( @@ -1249,6 +1258,14 @@ export class InputValidator { // #region Helpers + private makeSkipSchema() { + return z.number().int().nonnegative(); + } + + private makeTakeSchema() { + return z.number().int(); + } + private refineForSelectIncludeMutuallyExclusive(schema: ZodType) { return schema.refine( (value: any) => !(value['select'] && value['include']), @@ -1275,5 +1292,8 @@ export class InputValidator { return NUMERIC_FIELD_TYPES.includes(fieldDef.type) && !fieldDef.array; } + private get providerSupportsCaseSensitivity() { + return this.schema.provider.type === 'postgresql'; + } // #endregion } diff --git a/packages/runtime/test/client-api/filter.test.ts b/packages/runtime/test/client-api/filter.test.ts index e9f49ce1..b7ec82af 100644 --- a/packages/runtime/test/client-api/filter.test.ts +++ b/packages/runtime/test/client-api/filter.test.ts @@ -5,7 +5,7 @@ import { createClientSpecs } from './client-specs'; const PG_DB_NAME = 'client-api-filter-tests'; -describe.each(createClientSpecs(PG_DB_NAME))('Client filter tests for $provider', ({ createClient }) => { +describe.each(createClientSpecs(PG_DB_NAME))('Client filter tests for $provider', ({ createClient, provider }) => { let client: ClientContract; beforeEach(async () => { @@ -76,19 +76,117 @@ describe.each(createClientSpecs(PG_DB_NAME))('Client filter tests for $provider' }), ).toResolveTruthy(); - // case-insensitive - await expect( - client.user.findFirst({ - where: { email: { equals: 'u1@Test.com' } }, - }), - ).toResolveFalsy(); - await expect( - client.user.findFirst({ - where: { - email: { equals: 'u1@Test.com', mode: 'insensitive' }, - }, - }), - ).toResolveTruthy(); + if (provider === 'sqlite') { + // sqlite: equalities are case-sensitive, match is case-insensitive + await expect( + client.user.findFirst({ + where: { email: { equals: 'u1@Test.com' } }, + }), + ).toResolveFalsy(); + + await expect( + client.user.findFirst({ + where: { email: { equals: 'u1@test.com' } }, + }), + ).toResolveTruthy(); + + await expect( + client.user.findFirst({ + where: { email: { contains: 'test' } }, + }), + ).toResolveTruthy(); + await expect( + client.user.findFirst({ + where: { email: { contains: 'Test' } }, + }), + ).toResolveTruthy(); + + await expect( + client.user.findFirst({ + where: { email: { startsWith: 'u1' } }, + }), + ).toResolveTruthy(); + await expect( + client.user.findFirst({ + where: { email: { startsWith: 'U1' } }, + }), + ).toResolveTruthy(); + + await expect( + client.user.findFirst({ + where: { + email: { in: ['u1@Test.com'] }, + }, + }), + ).toResolveFalsy(); + await expect( + client.user.findFirst({ + where: { + email: { in: ['u1@test.com'] }, + }, + }), + ).toResolveTruthy(); + } else if (provider === 'postgresql') { + // postgresql: default is case-sensitive, but can be toggled with "mode" + + await expect( + client.user.findFirst({ + where: { email: { equals: 'u1@Test.com' } }, + }), + ).toResolveFalsy(); + await expect( + client.user.findFirst({ + where: { + email: { equals: 'u1@Test.com', mode: 'insensitive' } as any, + }, + }), + ).toResolveTruthy(); + + await expect( + client.user.findFirst({ + where: { + email: { contains: 'u1@Test.com' }, + }, + }), + ).toResolveFalsy(); + await expect( + client.user.findFirst({ + where: { + email: { contains: 'u1@Test.com', mode: 'insensitive' } as any, + }, + }), + ).toResolveTruthy(); + + await expect( + client.user.findFirst({ + where: { + email: { endsWith: 'Test.com' }, + }, + }), + ).toResolveFalsy(); + await expect( + client.user.findFirst({ + where: { + email: { endsWith: 'Test.com', mode: 'insensitive' } as any, + }, + }), + ).toResolveTruthy(); + + await expect( + client.user.findFirst({ + where: { + email: { in: ['u1@Test.com'] }, + }, + }), + ).toResolveFalsy(); + await expect( + client.user.findFirst({ + where: { + email: { in: ['u1@Test.com'], mode: 'insensitive' } as any, + }, + }), + ).toResolveTruthy(); + } // in await expect( @@ -225,7 +323,9 @@ describe.each(createClientSpecs(PG_DB_NAME))('Client filter tests for $provider' // equals await expect(client.profile.findFirst({ where: { age: 20 } })).resolves.toMatchObject({ id: '1' }); - await expect(client.profile.findFirst({ where: { age: { equals: 20 } } })).resolves.toMatchObject({ id: '1' }); + await expect(client.profile.findFirst({ where: { age: { equals: 20 } } })).resolves.toMatchObject({ + id: '1', + }); await expect(client.profile.findFirst({ where: { age: { equals: 10 } } })).toResolveFalsy(); await expect(client.profile.findFirst({ where: { age: null } })).resolves.toMatchObject({ id: '2' }); await expect(client.profile.findFirst({ where: { age: { equals: null } } })).resolves.toMatchObject({ From 0c493d11c2c57810b6c43cb8f6443c8d4bfd4713 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 6 Aug 2025 17:10:53 +0800 Subject: [PATCH 4/6] chore: bump version 3.0.0-alpha.19 (#148) Co-authored-by: ymc9 <104139426+ymc9@users.noreply.github.com> --- package.json | 2 +- packages/cli/package.json | 2 +- packages/common-helpers/package.json | 2 +- packages/create-zenstack/package.json | 2 +- packages/dialects/sql.js/package.json | 2 +- packages/eslint-config/package.json | 2 +- packages/ide/vscode/package.json | 2 +- packages/language/package.json | 2 +- packages/runtime/package.json | 2 +- packages/sdk/package.json | 2 +- packages/tanstack-query/package.json | 2 +- packages/testtools/package.json | 2 +- packages/typescript-config/package.json | 2 +- packages/vitest-config/package.json | 2 +- packages/zod/package.json | 2 +- samples/blog/package.json | 2 +- tests/e2e/package.json | 2 +- 17 files changed, 17 insertions(+), 17 deletions(-) diff --git a/package.json b/package.json index d68572fb..2d32c2a8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "zenstack-v3", - "version": "3.0.0-alpha.18", + "version": "3.0.0-alpha.19", "description": "ZenStack", "packageManager": "pnpm@10.12.1", "scripts": { diff --git a/packages/cli/package.json b/packages/cli/package.json index 848ef771..6f90d159 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -3,7 +3,7 @@ "publisher": "zenstack", "displayName": "ZenStack CLI", "description": "FullStack database toolkit with built-in access control and automatic API generation.", - "version": "3.0.0-alpha.18", + "version": "3.0.0-alpha.19", "type": "module", "author": { "name": "ZenStack Team" diff --git a/packages/common-helpers/package.json b/packages/common-helpers/package.json index fe134156..a3dd1a34 100644 --- a/packages/common-helpers/package.json +++ b/packages/common-helpers/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/common-helpers", - "version": "3.0.0-alpha.18", + "version": "3.0.0-alpha.19", "description": "ZenStack Common Helpers", "type": "module", "scripts": { diff --git a/packages/create-zenstack/package.json b/packages/create-zenstack/package.json index 5da9edb5..dcd787d1 100644 --- a/packages/create-zenstack/package.json +++ b/packages/create-zenstack/package.json @@ -1,6 +1,6 @@ { "name": "create-zenstack", - "version": "3.0.0-alpha.18", + "version": "3.0.0-alpha.19", "description": "Create a new ZenStack project", "type": "module", "scripts": { diff --git a/packages/dialects/sql.js/package.json b/packages/dialects/sql.js/package.json index 9cb54c4a..7f637c42 100644 --- a/packages/dialects/sql.js/package.json +++ b/packages/dialects/sql.js/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/kysely-sql-js", - "version": "3.0.0-alpha.18", + "version": "3.0.0-alpha.19", "description": "Kysely dialect for sql.js", "type": "module", "scripts": { diff --git a/packages/eslint-config/package.json b/packages/eslint-config/package.json index 43f8876c..050cc7de 100644 --- a/packages/eslint-config/package.json +++ b/packages/eslint-config/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/eslint-config", - "version": "3.0.0-alpha.18", + "version": "3.0.0-alpha.19", "type": "module", "private": true, "license": "MIT" diff --git a/packages/ide/vscode/package.json b/packages/ide/vscode/package.json index 2473a999..17e50f4e 100644 --- a/packages/ide/vscode/package.json +++ b/packages/ide/vscode/package.json @@ -1,7 +1,7 @@ { "name": "zenstack", "publisher": "zenstack", - "version": "3.0.0-alpha.18", + "version": "3.0.0-alpha.19", "displayName": "ZenStack Language Tools", "description": "VSCode extension for ZenStack ZModel language", "private": true, diff --git a/packages/language/package.json b/packages/language/package.json index 8633c8d1..e30222a5 100644 --- a/packages/language/package.json +++ b/packages/language/package.json @@ -1,7 +1,7 @@ { "name": "@zenstackhq/language", "description": "ZenStack ZModel language specification", - "version": "3.0.0-alpha.18", + "version": "3.0.0-alpha.19", "license": "MIT", "author": "ZenStack Team", "files": [ diff --git a/packages/runtime/package.json b/packages/runtime/package.json index 26c0215d..6a9df0e6 100644 --- a/packages/runtime/package.json +++ b/packages/runtime/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/runtime", - "version": "3.0.0-alpha.18", + "version": "3.0.0-alpha.19", "description": "ZenStack Runtime", "type": "module", "scripts": { diff --git a/packages/sdk/package.json b/packages/sdk/package.json index ed0e0f8e..62c376ed 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/sdk", - "version": "3.0.0-alpha.18", + "version": "3.0.0-alpha.19", "description": "ZenStack SDK", "type": "module", "scripts": { diff --git a/packages/tanstack-query/package.json b/packages/tanstack-query/package.json index eab7cace..93785acb 100644 --- a/packages/tanstack-query/package.json +++ b/packages/tanstack-query/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/tanstack-query", - "version": "3.0.0-alpha.18", + "version": "3.0.0-alpha.19", "description": "", "main": "index.js", "type": "module", diff --git a/packages/testtools/package.json b/packages/testtools/package.json index 64b13db3..354b08dc 100644 --- a/packages/testtools/package.json +++ b/packages/testtools/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/testtools", - "version": "3.0.0-alpha.18", + "version": "3.0.0-alpha.19", "description": "ZenStack Test Tools", "type": "module", "scripts": { diff --git a/packages/typescript-config/package.json b/packages/typescript-config/package.json index fb7a8373..f0d6d30d 100644 --- a/packages/typescript-config/package.json +++ b/packages/typescript-config/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/typescript-config", - "version": "3.0.0-alpha.18", + "version": "3.0.0-alpha.19", "private": true, "license": "MIT" } diff --git a/packages/vitest-config/package.json b/packages/vitest-config/package.json index 1fd305bf..89977323 100644 --- a/packages/vitest-config/package.json +++ b/packages/vitest-config/package.json @@ -1,7 +1,7 @@ { "name": "@zenstackhq/vitest-config", "type": "module", - "version": "3.0.0-alpha.18", + "version": "3.0.0-alpha.19", "private": true, "license": "MIT", "exports": { diff --git a/packages/zod/package.json b/packages/zod/package.json index 54af0591..cae1d515 100644 --- a/packages/zod/package.json +++ b/packages/zod/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/zod", - "version": "3.0.0-alpha.18", + "version": "3.0.0-alpha.19", "description": "", "type": "module", "main": "index.js", diff --git a/samples/blog/package.json b/samples/blog/package.json index fc63d199..75d07851 100644 --- a/samples/blog/package.json +++ b/samples/blog/package.json @@ -1,6 +1,6 @@ { "name": "sample-blog", - "version": "3.0.0-alpha.18", + "version": "3.0.0-alpha.19", "description": "", "main": "index.js", "scripts": { diff --git a/tests/e2e/package.json b/tests/e2e/package.json index a10729a3..76daf5b7 100644 --- a/tests/e2e/package.json +++ b/tests/e2e/package.json @@ -1,6 +1,6 @@ { "name": "e2e", - "version": "3.0.0-alpha.18", + "version": "3.0.0-alpha.19", "private": true, "type": "module", "scripts": { From 90dbc1b5f42845842ba9a5faed303d7ac21f05ae Mon Sep 17 00:00:00 2001 From: Eugen Istoc Date: Wed, 6 Aug 2025 05:20:41 -0400 Subject: [PATCH 5/6] Update `better-sqlite3` version range (#146) * Update `better-sqlite3` version range * update lockfile --------- Co-authored-by: Yiming Cao Co-authored-by: ymc9 <104139426+ymc9@users.noreply.github.com> --- packages/runtime/package.json | 2 +- pnpm-lock.yaml | 13 +++++++++++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/packages/runtime/package.json b/packages/runtime/package.json index 6a9df0e6..3511eca9 100644 --- a/packages/runtime/package.json +++ b/packages/runtime/package.json @@ -75,7 +75,7 @@ "uuid": "^11.0.5" }, "peerDependencies": { - "better-sqlite3": "^11.8.1", + "better-sqlite3": "^12.2.0", "kysely": "catalog:", "pg": "^8.13.1", "zod": "catalog:" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3098cc6d..ccb8d834 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -258,8 +258,8 @@ importers: specifier: workspace:* version: link:../common-helpers better-sqlite3: - specifier: ^11.8.1 - version: 11.8.1 + specifier: ^12.2.0 + version: 12.2.0 decimal.js: specifier: ^10.4.3 version: 10.4.3 @@ -1258,6 +1258,10 @@ packages: better-sqlite3@11.8.1: resolution: {integrity: sha512-9BxNaBkblMjhJW8sMRZxnxVTRgbRmssZW0Oxc1MPBTfiR+WW21e2Mk4qu8CzrcZb1LwPCnFsfDEzq+SNcBU8eg==} + better-sqlite3@12.2.0: + resolution: {integrity: sha512-eGbYq2CT+tos1fBwLQ/tkBt9J5M3JEHjku4hbvQUePCckkvVf14xWj+1m7dGoK81M/fOjFT7yM9UMeKT/+vFLQ==} + engines: {node: 20.x || 22.x || 23.x || 24.x} + bindings@1.5.0: resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==} @@ -3212,6 +3216,11 @@ snapshots: bindings: 1.5.0 prebuild-install: 7.1.3 + better-sqlite3@12.2.0: + dependencies: + bindings: 1.5.0 + prebuild-install: 7.1.3 + bindings@1.5.0: dependencies: file-uri-to-path: 1.0.0 From 12d0fa1a44c4c9f21d2ba7f0c8b6a291f215fdd7 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 6 Aug 2025 17:20:52 +0800 Subject: [PATCH 6/6] chore: bump version 3.0.0-alpha.19 (#145) Co-authored-by: ymc9 <104139426+ymc9@users.noreply.github.com>