From 5174c1ed0bfc7c7757487219e41caa7c0736c2ab Mon Sep 17 00:00:00 2001 From: ymc9 <104139426+ymc9@users.noreply.github.com> Date: Thu, 4 Dec 2025 18:04:22 +0800 Subject: [PATCH 1/5] feat(orm): implement JSON null values and equality filter --- README.md | 2 + packages/orm/src/client/crud-types.ts | 38 ++++- .../src/client/crud/dialects/base-dialect.ts | 63 +++++--- .../src/client/crud/dialects/postgresql.ts | 11 +- .../orm/src/client/crud/dialects/sqlite.ts | 10 ++ .../orm/src/client/crud/validator/index.ts | 79 ++++++--- packages/orm/src/client/index.ts | 1 + packages/orm/src/client/null-values.ts | 17 ++ packages/orm/src/utils/type-utils.ts | 9 +- tests/e2e/orm/client-api/json-filter.test.ts | 153 ++++++++++++++++++ tests/e2e/orm/schemas/json/input.ts | 30 ++++ tests/e2e/orm/schemas/json/models.ts | 10 ++ tests/e2e/orm/schemas/json/schema.ts | 46 ++++++ tests/e2e/orm/schemas/json/schema.zmodel | 9 ++ 14 files changed, 428 insertions(+), 50 deletions(-) create mode 100644 packages/orm/src/client/null-values.ts create mode 100644 tests/e2e/orm/client-api/json-filter.test.ts create mode 100644 tests/e2e/orm/schemas/json/input.ts create mode 100644 tests/e2e/orm/schemas/json/models.ts create mode 100644 tests/e2e/orm/schemas/json/schema.ts create mode 100644 tests/e2e/orm/schemas/json/schema.zmodel diff --git a/README.md b/README.md index 4da4a3f4..16f6309f 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,8 @@ # What's ZenStack +> Read full documentation at 👉🏻 https://zenstack.dev/v3. + ZenStack is a TypeScript database toolkit for developing full-stack or backend Node.js/Bun applications. It provides a unified data modeling and access solution with the following features: - A modern schema-first ORM that's compatible with [Prisma](https://github.com/prisma/prisma)'s schema and API diff --git a/packages/orm/src/client/crud-types.ts b/packages/orm/src/client/crud-types.ts index 55e3edfd..9b2a7bb2 100644 --- a/packages/orm/src/client/crud-types.ts +++ b/packages/orm/src/client/crud-types.ts @@ -33,6 +33,8 @@ import type { } from '../schema'; import type { AtLeast, + JsonNullValues, + JsonValue, MapBaseType, NonEmptyArray, NullableIf, @@ -44,6 +46,7 @@ import type { WrapType, XOR, } from '../utils/type-utils'; +import type { DbNull, JsonNull } from './null-values'; import type { ClientOptions } from './options'; import type { ToKyselySchema } from './query-builder'; @@ -359,7 +362,7 @@ type PrimitiveFilter : T extends 'Json' - ? 'Not implemented yet' // TODO: Json filter + ? JsonFilter : never; type CommonPrimitiveFilter< @@ -452,6 +455,11 @@ export type BooleanFilter; -// For unknown reason toplevel `Simplify` can't simplify this type, so we added an extra layer -// to make it work type ScalarCreatePayload< Schema extends SchemaDef, Model extends GetModels, Field extends ScalarFields, -> = Simplify< - | MapModelFieldType +> = + | ScalarFieldMutationPayload | (FieldIsArray extends true ? { set?: MapModelFieldType; } - : never) ->; + : never); + +type ScalarFieldMutationPayload< + Schema extends SchemaDef, + Model extends GetModels, + Field extends GetModelFields, +> = + IsJsonField extends true + ? ModelFieldIsOptional extends true + ? JsonValue | JsonNull | DbNull + : JsonValue | JsonNull + : MapModelFieldType; + +type IsJsonField< + Schema extends SchemaDef, + Model extends GetModels, + Field extends GetModelFields, +> = GetModelFieldType extends 'Json' ? true : false; type CreateFKPayload> = OptionalWrap< Schema, @@ -932,7 +954,7 @@ type ScalarUpdatePayload< Model extends GetModels, Field extends NonRelationFields, > = - | MapModelFieldType + | ScalarFieldMutationPayload | (Field extends NumericFields ? { set?: NullableIf>; diff --git a/packages/orm/src/client/crud/dialects/base-dialect.ts b/packages/orm/src/client/crud/dialects/base-dialect.ts index 42af09d2..c652e140 100644 --- a/packages/orm/src/client/crud/dialects/base-dialect.ts +++ b/packages/orm/src/client/crud/dialects/base-dialect.ts @@ -15,6 +15,7 @@ import type { StringFilter, } from '../../crud-types'; import { createConfigError, createInvalidInputError, createNotSupportedError } from '../../errors'; +import { AnyNullClass, DbNullClass, JsonNullClass } from '../../null-values'; import type { ClientOptions } from '../../options'; import { aggregate, @@ -499,24 +500,50 @@ export abstract class BaseCrudDialect { return this.buildEnumFilter(fieldRef, fieldDef, payload); } - return ( - match(fieldDef.type as BuiltinType) - .with('String', () => this.buildStringFilter(fieldRef, payload)) - .with(P.union('Int', 'Float', 'Decimal', 'BigInt'), (type) => - this.buildNumberFilter(fieldRef, type, payload), - ) - .with('Boolean', () => this.buildBooleanFilter(fieldRef, payload)) - .with('DateTime', () => this.buildDateTimeFilter(fieldRef, payload)) - .with('Bytes', () => this.buildBytesFilter(fieldRef, payload)) - // TODO: JSON filters - .with('Json', () => { - throw createNotSupportedError('JSON filters are not supported yet'); - }) - .with('Unsupported', () => { - throw createInvalidInputError(`Unsupported field cannot be used in filters`); - }) - .exhaustive() - ); + return match(fieldDef.type as BuiltinType) + .with('String', () => this.buildStringFilter(fieldRef, payload)) + .with(P.union('Int', 'Float', 'Decimal', 'BigInt'), (type) => + this.buildNumberFilter(fieldRef, type, payload), + ) + .with('Boolean', () => this.buildBooleanFilter(fieldRef, payload)) + .with('DateTime', () => this.buildDateTimeFilter(fieldRef, payload)) + .with('Bytes', () => this.buildBytesFilter(fieldRef, payload)) + .with('Json', () => this.buildJsonFilter(fieldRef, payload)) + .with('Unsupported', () => { + throw createInvalidInputError(`Unsupported field cannot be used in filters`); + }) + .exhaustive(); + } + + private buildJsonFilter(lhs: Expression, payload: any): any { + const clauses: Expression[] = []; + invariant(payload && typeof payload === 'object', 'Json filter payload must be an object'); + for (const [key, value] of Object.entries(payload)) { + switch (key) { + case 'equals': { + clauses.push(this.buildJsonValueFilterClause(lhs, value)); + break; + } + case 'not': { + clauses.push(this.eb.not(this.buildJsonValueFilterClause(lhs, value))); + break; + } + } + } + return this.and(...clauses); + } + + private buildJsonValueFilterClause(lhs: Expression, value: unknown) { + if (value instanceof DbNullClass) { + return this.eb(lhs, 'is', null); + } else if (value instanceof JsonNullClass) { + return this.eb.and([this.eb(lhs, '=', 'null'), this.eb(lhs, 'is not', null)]); + } else if (value instanceof AnyNullClass) { + // AnyNull matches both DB NULL and JSON null + return this.eb.or([this.eb(lhs, 'is', null), this.eb(lhs, '=', 'null')]); + } else { + return this.buildLiteralFilter(lhs, 'Json', value); + } } private buildLiteralFilter(lhs: Expression, type: BuiltinType, rhs: unknown) { diff --git a/packages/orm/src/client/crud/dialects/postgresql.ts b/packages/orm/src/client/crud/dialects/postgresql.ts index aa4cedd0..fe67b870 100644 --- a/packages/orm/src/client/crud/dialects/postgresql.ts +++ b/packages/orm/src/client/crud/dialects/postgresql.ts @@ -14,6 +14,7 @@ import type { BuiltinType, FieldDef, GetModels, SchemaDef } from '../../../schem import { DELEGATE_JOINED_FIELD_PREFIX } from '../../constants'; import type { FindArgs } from '../../crud-types'; import { createInternalError } from '../../errors'; +import { AnyNullClass, DbNullClass, JsonNullClass } from '../../null-values'; import type { ClientOptions } from '../../options'; import { buildJoinPairs, @@ -25,7 +26,6 @@ import { requireModel, } from '../../query-utils'; import { BaseCrudDialect } from './base-dialect'; - export class PostgresCrudDialect extends BaseCrudDialect { private isoDateSchema = z.iso.datetime({ local: true, offset: true }); @@ -42,6 +42,15 @@ export class PostgresCrudDialect extends BaseCrudDiale return value; } + // Handle special null classes for JSON fields + if (value instanceof JsonNullClass) { + return 'null'; + } else if (value instanceof DbNullClass) { + return null; + } else if (value instanceof AnyNullClass) { + invariant(false, 'should not reach here: AnyNull is not a valid input value'); + } + if (Array.isArray(value)) { if (type === 'Json' && !forArrayField) { // node-pg incorrectly handles array values passed to non-array JSON fields, diff --git a/packages/orm/src/client/crud/dialects/sqlite.ts b/packages/orm/src/client/crud/dialects/sqlite.ts index 04b22d03..286c215b 100644 --- a/packages/orm/src/client/crud/dialects/sqlite.ts +++ b/packages/orm/src/client/crud/dialects/sqlite.ts @@ -13,6 +13,7 @@ import type { BuiltinType, FieldDef, GetModels, SchemaDef } from '../../../schem import { DELEGATE_JOINED_FIELD_PREFIX } from '../../constants'; import type { FindArgs } from '../../crud-types'; import { createInternalError } from '../../errors'; +import { AnyNullClass, DbNullClass, JsonNullClass } from '../../null-values'; import { getDelegateDescendantModels, getManyToManyRelation, @@ -33,6 +34,15 @@ export class SqliteCrudDialect extends BaseCrudDialect return value; } + // Handle special null classes for JSON fields + if (value instanceof JsonNullClass) { + return 'null'; + } else if (value instanceof DbNullClass) { + return null; + } else if (value instanceof AnyNullClass) { + invariant(false, 'should not reach here: AnyNull is not a valid input value'); + } + if (Array.isArray(value)) { return value.map((v) => this.transformPrimitive(v, type, false)); } else { diff --git a/packages/orm/src/client/crud/validator/index.ts b/packages/orm/src/client/crud/validator/index.ts index 30032224..7c90937a 100644 --- a/packages/orm/src/client/crud/validator/index.ts +++ b/packages/orm/src/client/crud/validator/index.ts @@ -32,6 +32,7 @@ import { type UpsertArgs, } from '../../crud-types'; import { createInternalError, createInvalidInputError } from '../../errors'; +import { AnyNullClass, DbNullClass, JsonNullClass } from '../../null-values'; import { fieldHasDefaultValue, getDiscriminatorField, @@ -328,8 +329,9 @@ export class InputValidator { addDecimalValidation(z.string(), attributes, this.extraValidationsEnabled), ]); }) - .with('DateTime', () => z.union([z.date(), z.string().datetime()])) + .with('DateTime', () => z.union([z.date(), z.iso.datetime()])) .with('Bytes', () => z.instanceof(Uint8Array)) + .with('Json', () => this.makeJsonValueSchema(false, false)) .otherwise(() => z.unknown()); } } @@ -553,20 +555,47 @@ export class InputValidator { // typed JSON field return this.makeTypeDefFilterSchema(type, optional); } - return ( - match(type) - .with('String', () => this.makeStringFilterSchema(optional, withAggregations)) - .with(P.union('Int', 'Float', 'Decimal', 'BigInt'), (type) => - this.makeNumberFilterSchema(this.makePrimitiveSchema(type), optional, withAggregations), - ) - .with('Boolean', () => this.makeBooleanFilterSchema(optional, withAggregations)) - .with('DateTime', () => this.makeDateTimeFilterSchema(optional, withAggregations)) - .with('Bytes', () => this.makeBytesFilterSchema(optional, withAggregations)) - // TODO: JSON filters - .with('Json', () => z.any()) - .with('Unsupported', () => z.never()) - .exhaustive() - ); + return match(type) + .with('String', () => this.makeStringFilterSchema(optional, withAggregations)) + .with(P.union('Int', 'Float', 'Decimal', 'BigInt'), (type) => + this.makeNumberFilterSchema(this.makePrimitiveSchema(type), optional, withAggregations), + ) + .with('Boolean', () => this.makeBooleanFilterSchema(optional, withAggregations)) + .with('DateTime', () => this.makeDateTimeFilterSchema(optional, withAggregations)) + .with('Bytes', () => this.makeBytesFilterSchema(optional, withAggregations)) + .with('Json', () => this.makeJsonFilterSchema(optional)) + .with('Unsupported', () => z.never()) + .exhaustive(); + } + + private makeJsonValueSchema(nullable: boolean, forFilter: boolean): z.ZodType { + const options: z.ZodType[] = [z.string(), z.number(), z.boolean(), z.instanceof(JsonNullClass)]; + + if (nullable) { + options.push(z.instanceof(DbNullClass)); + } + + if (forFilter) { + options.push(z.instanceof(AnyNullClass)); + } + + const schema = z.union([ + ...options, + z.lazy(() => this.makeJsonValueSchema(false, false).array()), + z.record( + z.string(), + z.lazy(() => this.makeJsonValueSchema(false, false)), + ), + ]); + return this.nullableIf(schema, nullable); + } + + private makeJsonFilterSchema(optional: boolean) { + const valueSchema = this.makeJsonValueSchema(optional, true); + return z.object({ + equals: valueSchema.optional(), + not: valueSchema.optional(), + }); } private makeTypeDefFilterSchema(_type: string, _optional: boolean) { @@ -576,7 +605,7 @@ export class InputValidator { private makeDateTimeFilterSchema(optional: boolean, withAggregations: boolean): ZodType { return this.makeCommonPrimitiveFilterSchema( - z.union([z.string().datetime(), z.date()]), + z.union([z.iso.datetime(), z.date()]), optional, () => z.lazy(() => this.makeDateTimeFilterSchema(optional, withAggregations)), withAggregations ? ['_count', '_min', '_max'] : undefined, @@ -977,7 +1006,7 @@ export class InputValidator { uncheckedVariantFields[field] = fieldSchema; } } else { - let fieldSchema: ZodType = this.makePrimitiveSchema(fieldDef.type, fieldDef.attributes); + let fieldSchema = this.makePrimitiveSchema(fieldDef.type, fieldDef.attributes); if (fieldDef.array) { fieldSchema = addListValidation(fieldSchema.array(), fieldDef.attributes); @@ -996,7 +1025,12 @@ export class InputValidator { } if (fieldDef.optional) { - fieldSchema = fieldSchema.nullable(); + if (fieldDef.type === 'Json') { + // DbNull for Json fields + fieldSchema = z.union([fieldSchema, z.instanceof(DbNullClass)]); + } else { + fieldSchema = fieldSchema.nullable(); + } } uncheckedVariantFields[field] = fieldSchema; @@ -1242,7 +1276,7 @@ export class InputValidator { uncheckedVariantFields[field] = fieldSchema; } } else { - let fieldSchema: ZodType = this.makePrimitiveSchema(fieldDef.type, fieldDef.attributes); + let fieldSchema = this.makePrimitiveSchema(fieldDef.type, fieldDef.attributes); if (this.isNumericField(fieldDef)) { fieldSchema = z.union([ @@ -1276,7 +1310,12 @@ export class InputValidator { } if (fieldDef.optional) { - fieldSchema = fieldSchema.nullable(); + if (fieldDef.type === 'Json') { + // DbNull for Json fields + fieldSchema = z.union([fieldSchema, z.instanceof(DbNullClass)]); + } else { + fieldSchema = fieldSchema.nullable(); + } } // all fields are optional in update diff --git a/packages/orm/src/client/index.ts b/packages/orm/src/client/index.ts index 6a320300..291e288a 100644 --- a/packages/orm/src/client/index.ts +++ b/packages/orm/src/client/index.ts @@ -4,6 +4,7 @@ export type * from './crud-types'; export { getCrudDialect } from './crud/dialects'; export { BaseCrudDialect } from './crud/dialects/base-dialect'; export { ORMError, ORMErrorReason, RejectedByPolicyReason } from './errors'; +export { AnyNull, DbNull, JsonNull } from './null-values'; export * from './options'; export * from './plugin'; export type { ZenStackPromise } from './promise'; diff --git a/packages/orm/src/client/null-values.ts b/packages/orm/src/client/null-values.ts new file mode 100644 index 00000000..c277f55b --- /dev/null +++ b/packages/orm/src/client/null-values.ts @@ -0,0 +1,17 @@ +export class DbNullClass { + __brand = 'DbNull' as const; +} +export const DbNull = new DbNullClass(); +export type DbNull = typeof DbNull; + +export class JsonNullClass { + __brand = 'JsonNull' as const; +} +export const JsonNull = new JsonNullClass(); +export type JsonNull = typeof JsonNull; + +export class AnyNullClass { + __brand = 'AnyNull' as const; +} +export const AnyNull = new AnyNullClass(); +export type AnyNull = typeof AnyNull; diff --git a/packages/orm/src/utils/type-utils.ts b/packages/orm/src/utils/type-utils.ts index 26828cf6..6dd03e53 100644 --- a/packages/orm/src/utils/type-utils.ts +++ b/packages/orm/src/utils/type-utils.ts @@ -1,4 +1,5 @@ import type Decimal from 'decimal.js'; +import type { AnyNull, DbNull, JsonNull } from '../client/null-values'; export type Optional = Omit & Partial>; @@ -44,10 +45,12 @@ type TypeMap = { export type MapBaseType = T extends keyof TypeMap ? TypeMap[T] : unknown; -export type JsonValue = string | number | boolean | null | JsonObject | JsonArray; +export type JsonValue = string | number | boolean | JsonObject | JsonArray; -export type JsonObject = { [key: string]: JsonValue }; -export type JsonArray = Array; +export type JsonObject = { [key: string]: JsonValue | null }; +export type JsonArray = ReadonlyArray; + +export type JsonNullValues = DbNull | JsonNull | AnyNull; export function call(code: string) { return { code }; diff --git a/tests/e2e/orm/client-api/json-filter.test.ts b/tests/e2e/orm/client-api/json-filter.test.ts new file mode 100644 index 00000000..8304b6e5 --- /dev/null +++ b/tests/e2e/orm/client-api/json-filter.test.ts @@ -0,0 +1,153 @@ +import { createTestClient } from '@zenstackhq/testtools'; +import { describe, it, expect } from 'vitest'; +import { schema } from '../schemas/json/schema'; +import { JsonNull, DbNull, AnyNull } from '@zenstackhq/orm'; + +describe('Json filter tests', () => { + it('works with simple equality filter', async () => { + const db = await createTestClient(schema); + await db.plainJson.create({ data: { data: { hello: 'world' } } }); + + await expect( + db.plainJson.findFirst({ where: { data: { equals: { hello: 'world' } } } }), + ).resolves.toMatchObject({ + data: { hello: 'world' }, + }); + await expect(db.plainJson.findFirst({ where: { data: { not: { hello: 'foo' } } } })).resolves.toMatchObject({ + data: { hello: 'world' }, + }); + await expect(db.plainJson.findFirst({ where: { data: { not: { hello: 'world' } } } })).toResolveNull(); + }); + + it('distinguishes between JsonNull and DbNull', async () => { + const db = await createTestClient(schema); + + // Create records with different null types + // Record 1: data contains JSON null, data1 is DB NULL (unset) + const rec1 = await db.plainJson.create({ data: { data: JsonNull } }); + + // Record 2: data contains object, data1 explicitly set to JSON null + const rec2 = await db.plainJson.create({ data: { data: { foo: 'bar' }, data1: JsonNull } }); + + // Record 3: data contains object, data1 is DB NULL (unset) + const rec3 = await db.plainJson.create({ data: { data: { hello: 'world' }, data1: DbNull } }); + + // Record 4: data contains object, data1 explicitly set to an object + const rec4 = await db.plainJson.create({ data: { data: { test: 'value' }, data1: { key: 'value' } } }); + + // Test JsonNull - should match JSON null value in data field + const jsonNullResults = await db.plainJson.findMany({ + where: { data: { equals: JsonNull } }, + }); + expect(jsonNullResults).toHaveLength(1); + expect(jsonNullResults[0]?.data).toBe(null); // JSON null is returned as null + expect(jsonNullResults[0]?.id).toBe(rec1.id); + + // Test JsonNull in data1 field + const jsonNullData1Results = await db.plainJson.findMany({ + where: { data1: { equals: JsonNull } }, + }); + expect(jsonNullData1Results).toHaveLength(1); // Only record 2 has data1 as JSON null + expect(jsonNullData1Results[0]?.data1).toBe(null); + expect(jsonNullData1Results[0]?.id).toBe(rec2.id); + + // Test NOT JsonNull - should exclude JSON null records + const notJsonNull = await db.plainJson.findMany({ + where: { data: { not: JsonNull } }, + }); + expect(notJsonNull).toHaveLength(3); // Should exclude the JsonNull record + expect(notJsonNull.map((r) => r.id).sort()).toEqual([rec2.id, rec3.id, rec4.id].sort()); + + // Test data1 with actual value - "not JsonNull" should match DB NULL and actual objects + const data1NotJsonNull = await db.plainJson.findMany({ + where: { data1: { not: JsonNull } }, + }); + // Records 1, 3 have DB NULL, record 4 has an object - all should match "not JsonNull" + expect(data1NotJsonNull.length).toBe(3); + + // Test DbNull - should match database NULL values + const dbNullResults = await db.plainJson.findMany({ + where: { data1: { equals: DbNull } }, + }); + // Records 1 and 3 have data1 as DB NULL + expect(dbNullResults.length).toBe(2); + expect(dbNullResults.map((r) => r.id).sort()).toEqual([rec1.id, rec3.id].sort()); + + // Test AnyNull - should match both JSON null and DB NULL + const anyNullResults = await db.plainJson.findMany({ + where: { data1: { equals: AnyNull } }, + }); + // Records 1, 2, and 3: rec1 (DB NULL), rec2 (JSON null), rec3 (DB NULL) + expect(anyNullResults.length).toBe(3); + expect(anyNullResults.map((r) => r.id).sort()).toEqual([rec1.id, rec2.id, rec3.id].sort()); + + // invalid input + // @ts-expect-error + await expect(db.plainJson.create({ data: { data: null } })).toBeRejectedByValidation(); + // @ts-expect-error + await expect(db.plainJson.create({ data: { data: DbNull } })).toBeRejectedByValidation(); + // @ts-expect-error + await expect(db.plainJson.create({ data: { data1: null } })).toBeRejectedByValidation(); + // @ts-expect-error + await expect(db.plainJson.update({ where: { id: rec1.id }, data: { data: null } })).toBeRejectedByValidation(); + await expect( + // @ts-expect-error + db.plainJson.update({ where: { id: rec1.id }, data: { data: DbNull } }), + ).toBeRejectedByValidation(); + // @ts-expect-error + await expect(db.plainJson.update({ where: { id: rec1.id }, data: { data1: null } })).toBeRejectedByValidation(); + }); + + it('works with updates', async () => { + const db = await createTestClient(schema); + const rec = await db.plainJson.create({ data: { data: { hello: 'world' }, data1: 'data1' } }); + + // Update to JSON null + await db.plainJson.update({ + where: { id: rec.id }, + data: { data: JsonNull }, + }); + await expect(db.plainJson.findUnique({ where: { id: rec.id } })).resolves.toMatchObject({ + data: null, + }); + + // Update to DB null + await db.plainJson.update({ + where: { id: rec.id }, + data: { data1: DbNull }, + }); + await expect(db.plainJson.findUnique({ where: { id: rec.id } })).resolves.toMatchObject({ + data1: null, + }); + + // Update to actual object + await db.plainJson.update({ + where: { id: rec.id }, + data: { data: { updated: 'value' }, data1: { another: 'value' } }, + }); + await expect(db.plainJson.findUnique({ where: { id: rec.id } })).resolves.toMatchObject({ + data: { updated: 'value' }, + data1: { another: 'value' }, + }); + }); + + it('works with JSON arrays', async () => { + const db = await createTestClient( + ` +model PlainJson { + id Int @id @default(autoincrement()) + data Json[] +} +`, + { provider: 'postgresql' }, + ); + + await expect(db.plainJson.create({ data: { data: [{ a: 1 }, { b: 2 }] } })).resolves.toMatchObject({ + data: [{ a: 1 }, { b: 2 }], + }); + await expect(db.plainJson.create({ data: { data: { set: [{ a: 1 }, { b: 2 }] } } })).resolves.toMatchObject({ + data: [{ a: 1 }, { b: 2 }], + }); + await expect(db.plainJson.create({ data: { data: DbNull } })).toBeRejectedByValidation(); + }); +}); diff --git a/tests/e2e/orm/schemas/json/input.ts b/tests/e2e/orm/schemas/json/input.ts new file mode 100644 index 00000000..3ee54f67 --- /dev/null +++ b/tests/e2e/orm/schemas/json/input.ts @@ -0,0 +1,30 @@ +////////////////////////////////////////////////////////////////////////////////////////////// +// DO NOT MODIFY THIS FILE // +// This file is automatically generated by ZenStack CLI and should not be manually updated. // +////////////////////////////////////////////////////////////////////////////////////////////// + +/* eslint-disable */ + +import { type SchemaType as $Schema } from "./schema"; +import type { FindManyArgs as $FindManyArgs, FindUniqueArgs as $FindUniqueArgs, FindFirstArgs as $FindFirstArgs, CreateArgs as $CreateArgs, CreateManyArgs as $CreateManyArgs, CreateManyAndReturnArgs as $CreateManyAndReturnArgs, UpdateArgs as $UpdateArgs, UpdateManyArgs as $UpdateManyArgs, UpdateManyAndReturnArgs as $UpdateManyAndReturnArgs, UpsertArgs as $UpsertArgs, DeleteArgs as $DeleteArgs, DeleteManyArgs as $DeleteManyArgs, CountArgs as $CountArgs, AggregateArgs as $AggregateArgs, GroupByArgs as $GroupByArgs, WhereInput as $WhereInput, SelectInput as $SelectInput, IncludeInput as $IncludeInput, OmitInput as $OmitInput, ClientOptions as $ClientOptions } from "@zenstackhq/orm"; +import type { SimplifiedModelResult as $SimplifiedModelResult, SelectIncludeOmit as $SelectIncludeOmit } from "@zenstackhq/orm"; +export type PlainJsonFindManyArgs = $FindManyArgs<$Schema, "PlainJson">; +export type PlainJsonFindUniqueArgs = $FindUniqueArgs<$Schema, "PlainJson">; +export type PlainJsonFindFirstArgs = $FindFirstArgs<$Schema, "PlainJson">; +export type PlainJsonCreateArgs = $CreateArgs<$Schema, "PlainJson">; +export type PlainJsonCreateManyArgs = $CreateManyArgs<$Schema, "PlainJson">; +export type PlainJsonCreateManyAndReturnArgs = $CreateManyAndReturnArgs<$Schema, "PlainJson">; +export type PlainJsonUpdateArgs = $UpdateArgs<$Schema, "PlainJson">; +export type PlainJsonUpdateManyArgs = $UpdateManyArgs<$Schema, "PlainJson">; +export type PlainJsonUpdateManyAndReturnArgs = $UpdateManyAndReturnArgs<$Schema, "PlainJson">; +export type PlainJsonUpsertArgs = $UpsertArgs<$Schema, "PlainJson">; +export type PlainJsonDeleteArgs = $DeleteArgs<$Schema, "PlainJson">; +export type PlainJsonDeleteManyArgs = $DeleteManyArgs<$Schema, "PlainJson">; +export type PlainJsonCountArgs = $CountArgs<$Schema, "PlainJson">; +export type PlainJsonAggregateArgs = $AggregateArgs<$Schema, "PlainJson">; +export type PlainJsonGroupByArgs = $GroupByArgs<$Schema, "PlainJson">; +export type PlainJsonWhereInput = $WhereInput<$Schema, "PlainJson">; +export type PlainJsonSelect = $SelectInput<$Schema, "PlainJson">; +export type PlainJsonInclude = $IncludeInput<$Schema, "PlainJson">; +export type PlainJsonOmit = $OmitInput<$Schema, "PlainJson">; +export type PlainJsonGetPayload, Options extends $ClientOptions<$Schema> = $ClientOptions<$Schema>> = $SimplifiedModelResult<$Schema, "PlainJson", Options, Args>; diff --git a/tests/e2e/orm/schemas/json/models.ts b/tests/e2e/orm/schemas/json/models.ts new file mode 100644 index 00000000..de178734 --- /dev/null +++ b/tests/e2e/orm/schemas/json/models.ts @@ -0,0 +1,10 @@ +////////////////////////////////////////////////////////////////////////////////////////////// +// DO NOT MODIFY THIS FILE // +// This file is automatically generated by ZenStack CLI and should not be manually updated. // +////////////////////////////////////////////////////////////////////////////////////////////// + +/* eslint-disable */ + +import { type SchemaType as $Schema } from "./schema"; +import { type ModelResult as $ModelResult } from "@zenstackhq/orm"; +export type PlainJson = $ModelResult<$Schema, "PlainJson">; diff --git a/tests/e2e/orm/schemas/json/schema.ts b/tests/e2e/orm/schemas/json/schema.ts new file mode 100644 index 00000000..d8be5cd7 --- /dev/null +++ b/tests/e2e/orm/schemas/json/schema.ts @@ -0,0 +1,46 @@ +////////////////////////////////////////////////////////////////////////////////////////////// +// DO NOT MODIFY THIS FILE // +// This file is automatically generated by ZenStack CLI and should not be manually updated. // +////////////////////////////////////////////////////////////////////////////////////////////// + +/* eslint-disable */ + +import { type SchemaDef, ExpressionUtils } from "@zenstackhq/orm/schema"; +const _schema = { + provider: { + type: "sqlite" + }, + models: { + PlainJson: { + name: "PlainJson", + fields: { + id: { + name: "id", + type: "Int", + id: true, + attributes: [{ name: "@id" }, { name: "@default", args: [{ name: "value", value: ExpressionUtils.call("autoincrement") }] }], + default: ExpressionUtils.call("autoincrement") + }, + data: { + name: "data", + type: "Json" + }, + data1: { + name: "data1", + type: "Json", + optional: true + } + }, + idFields: ["id"], + uniqueFields: { + id: { type: "Int" } + } + } + }, + plugins: {} +} as const satisfies SchemaDef; +type Schema = typeof _schema & { + __brand?: "schema"; +}; +export const schema: Schema = _schema; +export type SchemaType = Schema; diff --git a/tests/e2e/orm/schemas/json/schema.zmodel b/tests/e2e/orm/schemas/json/schema.zmodel new file mode 100644 index 00000000..4a981b60 --- /dev/null +++ b/tests/e2e/orm/schemas/json/schema.zmodel @@ -0,0 +1,9 @@ +datasource db { + provider = "sqlite" +} + +model PlainJson { + id Int @id @default(autoincrement()) + data Json + data1 Json? +} From 46881a279a622e59d6136d27226184b53c6c4b21 Mon Sep 17 00:00:00 2001 From: Yiming Cao Date: Thu, 4 Dec 2025 18:41:51 +0800 Subject: [PATCH 2/5] Update packages/orm/src/client/crud/validator/index.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- packages/orm/src/client/crud/validator/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/orm/src/client/crud/validator/index.ts b/packages/orm/src/client/crud/validator/index.ts index 7c90937a..d71bd801 100644 --- a/packages/orm/src/client/crud/validator/index.ts +++ b/packages/orm/src/client/crud/validator/index.ts @@ -584,7 +584,7 @@ export class InputValidator { z.lazy(() => this.makeJsonValueSchema(false, false).array()), z.record( z.string(), - z.lazy(() => this.makeJsonValueSchema(false, false)), + z.lazy(() => z.union([this.makeJsonValueSchema(false, false), z.null()])), ), ]); return this.nullableIf(schema, nullable); From d084489a91614066859d3abe3fcc0c3f6080b32e Mon Sep 17 00:00:00 2001 From: Yiming Cao Date: Thu, 4 Dec 2025 18:42:37 +0800 Subject: [PATCH 3/5] Update packages/orm/src/utils/type-utils.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- packages/orm/src/utils/type-utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/orm/src/utils/type-utils.ts b/packages/orm/src/utils/type-utils.ts index 6dd03e53..8fde7daf 100644 --- a/packages/orm/src/utils/type-utils.ts +++ b/packages/orm/src/utils/type-utils.ts @@ -48,7 +48,7 @@ export type MapBaseType = T extends keyof TypeMap ? TypeMap[T] export type JsonValue = string | number | boolean | JsonObject | JsonArray; export type JsonObject = { [key: string]: JsonValue | null }; -export type JsonArray = ReadonlyArray; +export type JsonArray = ReadonlyArray; export type JsonNullValues = DbNull | JsonNull | AnyNull; From 5bb11e66bc5c9c420a6352d24804fb533e66ec9e Mon Sep 17 00:00:00 2001 From: ymc9 <104139426+ymc9@users.noreply.github.com> Date: Thu, 4 Dec 2025 20:06:26 +0800 Subject: [PATCH 4/5] address PR comments --- .../orm/src/client/crud/dialects/sqlite.ts | 35 ++++---- .../orm/src/client/crud/validator/index.ts | 2 +- tests/e2e/orm/client-api/json-filter.test.ts | 81 +++++++++++++++++++ 3 files changed, 99 insertions(+), 19 deletions(-) diff --git a/packages/orm/src/client/crud/dialects/sqlite.ts b/packages/orm/src/client/crud/dialects/sqlite.ts index 286c215b..7bc0489a 100644 --- a/packages/orm/src/client/crud/dialects/sqlite.ts +++ b/packages/orm/src/client/crud/dialects/sqlite.ts @@ -43,27 +43,26 @@ export class SqliteCrudDialect extends BaseCrudDialect invariant(false, 'should not reach here: AnyNull is not a valid input value'); } + if (type === 'Json' || (this.schema.typeDefs && type in this.schema.typeDefs)) { + // JSON data should be stringified + return JSON.stringify(value); + } + if (Array.isArray(value)) { return value.map((v) => this.transformPrimitive(v, type, false)); } else { - if (this.schema.typeDefs && type in this.schema.typeDefs) { - // typed JSON field - return JSON.stringify(value); - } else { - return match(type) - .with('Boolean', () => (value ? 1 : 0)) - .with('DateTime', () => - value instanceof Date - ? value.toISOString() - : typeof value === 'string' - ? new Date(value).toISOString() - : value, - ) - .with('Decimal', () => (value as Decimal).toString()) - .with('Bytes', () => Buffer.from(value as Uint8Array)) - .with('Json', () => JSON.stringify(value)) - .otherwise(() => value); - } + return match(type) + .with('Boolean', () => (value ? 1 : 0)) + .with('DateTime', () => + value instanceof Date + ? value.toISOString() + : typeof value === 'string' + ? new Date(value).toISOString() + : value, + ) + .with('Decimal', () => (value as Decimal).toString()) + .with('Bytes', () => Buffer.from(value as Uint8Array)) + .otherwise(() => value); } } diff --git a/packages/orm/src/client/crud/validator/index.ts b/packages/orm/src/client/crud/validator/index.ts index d71bd801..2a5ddb89 100644 --- a/packages/orm/src/client/crud/validator/index.ts +++ b/packages/orm/src/client/crud/validator/index.ts @@ -581,7 +581,7 @@ export class InputValidator { const schema = z.union([ ...options, - z.lazy(() => this.makeJsonValueSchema(false, false).array()), + z.lazy(() => z.union([this.makeJsonValueSchema(false, false), z.null()]).array()), z.record( z.string(), z.lazy(() => z.union([this.makeJsonValueSchema(false, false), z.null()])), diff --git a/tests/e2e/orm/client-api/json-filter.test.ts b/tests/e2e/orm/client-api/json-filter.test.ts index 8304b6e5..2161018b 100644 --- a/tests/e2e/orm/client-api/json-filter.test.ts +++ b/tests/e2e/orm/client-api/json-filter.test.ts @@ -150,4 +150,85 @@ model PlainJson { }); await expect(db.plainJson.create({ data: { data: DbNull } })).toBeRejectedByValidation(); }); + + it('works with JSON objects containing null values', async () => { + const db = await createTestClient(schema); + + // Create a record with an object containing a null property value + const rec1 = await db.plainJson.create({ data: { data: { key: null } } }); + expect(rec1.data).toEqual({ key: null }); + + // Create a record with nested object containing null values + const rec2 = await db.plainJson.create({ data: { data: { outer: { inner: null }, valid: 'value' } } }); + expect(rec2.data).toEqual({ outer: { inner: null }, valid: 'value' }); + + // Query with equality filter for object with null value + await expect(db.plainJson.findFirst({ where: { data: { equals: { key: null } } } })).resolves.toMatchObject({ + id: rec1.id, + data: { key: null }, + }); + + // Query with equality filter for nested object with null value + await expect( + db.plainJson.findFirst({ where: { data: { equals: { outer: { inner: null }, valid: 'value' } } } }), + ).resolves.toMatchObject({ + id: rec2.id, + data: { outer: { inner: null }, valid: 'value' }, + }); + + // Query with not filter for object with null value + const notResults = await db.plainJson.findMany({ + where: { data: { not: { key: null } } }, + }); + expect(notResults.find((r) => r.id === rec1.id)).toBeUndefined(); + expect(notResults.find((r) => r.id === rec2.id)).toBeDefined(); + }); + + it('works with JSON arrays containing null values', async () => { + const db = await createTestClient(schema); + + // Create a record with an array containing null values + const rec1 = await db.plainJson.create({ data: { data: [1, null, 3] } }); + expect(rec1.data).toEqual([1, null, 3]); + + // Create a record with an array of objects including null + const rec2 = await db.plainJson.create({ data: { data: [{ a: 1 }, null, { b: 2 }] } }); + expect(rec2.data).toEqual([{ a: 1 }, null, { b: 2 }]); + + // Create a record with nested arrays containing null + const rec3 = await db.plainJson.create({ + data: { + data: [ + [1, null], + [null, 2], + ], + }, + }); + expect(rec3.data).toEqual([ + [1, null], + [null, 2], + ]); + + // Query with equality filter for array with null value + await expect(db.plainJson.findFirst({ where: { data: { equals: [1, null, 3] } } })).resolves.toMatchObject({ + id: rec1.id, + data: [1, null, 3], + }); + + // Query with equality filter for array of objects with null + await expect( + db.plainJson.findFirst({ where: { data: { equals: [{ a: 1 }, null, { b: 2 }] } } }), + ).resolves.toMatchObject({ + id: rec2.id, + data: [{ a: 1 }, null, { b: 2 }], + }); + + // Query with not filter for array with null value + const notResults = await db.plainJson.findMany({ + where: { data: { not: [1, null, 3] } }, + }); + expect(notResults.find((r) => r.id === rec1.id)).toBeUndefined(); + expect(notResults.find((r) => r.id === rec2.id)).toBeDefined(); + expect(notResults.find((r) => r.id === rec3.id)).toBeDefined(); + }); }); From 8cacb0d7a67f4295fc17fe37a1edf8302543c8bd Mon Sep 17 00:00:00 2001 From: ymc9 <104139426+ymc9@users.noreply.github.com> Date: Thu, 4 Dec 2025 20:30:18 +0800 Subject: [PATCH 5/5] speed up test type-checking --- tests/e2e/apps/rally/rally.test.ts | 6 +++--- tests/e2e/orm/client-api/aggregate.test.ts | 2 +- tests/e2e/orm/client-api/count.test.ts | 2 +- .../e2e/orm/client-api/create-many-and-return.test.ts | 10 +++++----- tests/e2e/orm/client-api/create-many.test.ts | 2 +- tests/e2e/orm/client-api/create.test.ts | 2 +- tests/e2e/orm/client-api/delegate.test.ts | 4 ++-- tests/e2e/orm/client-api/delete-many.test.ts | 2 +- tests/e2e/orm/client-api/delete.test.ts | 2 +- tests/e2e/orm/client-api/filter.test.ts | 2 +- tests/e2e/orm/client-api/find.test.ts | 2 +- tests/e2e/orm/client-api/group-by.test.ts | 2 +- tests/e2e/orm/client-api/name-mapping.test.ts | 4 ++-- tests/e2e/orm/client-api/raw-query.test.ts | 2 +- tests/e2e/orm/client-api/transaction.test.ts | 2 +- tests/e2e/orm/client-api/undefined-values.test.ts | 2 +- tests/e2e/orm/client-api/update-many.test.ts | 2 +- tests/e2e/orm/client-api/update.test.ts | 2 +- tests/e2e/orm/client-api/upsert.test.ts | 2 +- .../e2e/orm/plugin-infra/entity-mutation-hooks.test.ts | 2 +- tests/e2e/orm/plugin-infra/on-kysely-query.test.ts | 2 +- tests/e2e/orm/plugin-infra/on-query-hooks.test.ts | 2 +- tests/e2e/orm/policy/basic-schema-read.test.ts | 2 +- tests/e2e/tsconfig.json | 2 +- tests/regression/tsconfig.json | 2 +- 25 files changed, 33 insertions(+), 33 deletions(-) diff --git a/tests/e2e/apps/rally/rally.test.ts b/tests/e2e/apps/rally/rally.test.ts index 5b204275..51084594 100644 --- a/tests/e2e/apps/rally/rally.test.ts +++ b/tests/e2e/apps/rally/rally.test.ts @@ -5,10 +5,10 @@ import { beforeEach, describe, expect, it } from 'vitest'; import { schema, type SchemaType } from './zenstack/schema'; describe('Rally app tests', () => { - let db: ClientContract; + let db: ClientContract; beforeEach(async () => { - db = await createTestClient(schema, { + db = (await createTestClient(schema, { provider: 'postgresql', schemaFile: path.join(__dirname, 'zenstack/schema.zmodel'), copyFiles: [ @@ -19,7 +19,7 @@ describe('Rally app tests', () => { ], dataSourceExtensions: ['citext'], usePrismaPush: true, - }); + })) as any; }); it('works with queries', async () => { diff --git a/tests/e2e/orm/client-api/aggregate.test.ts b/tests/e2e/orm/client-api/aggregate.test.ts index 520e1218..848e498e 100644 --- a/tests/e2e/orm/client-api/aggregate.test.ts +++ b/tests/e2e/orm/client-api/aggregate.test.ts @@ -8,7 +8,7 @@ describe('Client aggregate tests', () => { let client: ClientContract; beforeEach(async () => { - client = await createTestClient(schema); + client = (await createTestClient(schema)) as any; }); afterEach(async () => { diff --git a/tests/e2e/orm/client-api/count.test.ts b/tests/e2e/orm/client-api/count.test.ts index 1168f9d6..3e57bf9a 100644 --- a/tests/e2e/orm/client-api/count.test.ts +++ b/tests/e2e/orm/client-api/count.test.ts @@ -7,7 +7,7 @@ describe('Client count tests', () => { let client: ClientContract; beforeEach(async () => { - client = await createTestClient(schema); + client = (await createTestClient(schema)) as any; }); afterEach(async () => { diff --git a/tests/e2e/orm/client-api/create-many-and-return.test.ts b/tests/e2e/orm/client-api/create-many-and-return.test.ts index aa9b0b90..f6049d5b 100644 --- a/tests/e2e/orm/client-api/create-many-and-return.test.ts +++ b/tests/e2e/orm/client-api/create-many-and-return.test.ts @@ -7,7 +7,7 @@ describe('Client createManyAndReturn tests', () => { let client: ClientContract; beforeEach(async () => { - client = await createTestClient(schema); + client = (await createTestClient(schema)) as any; }); afterEach(async () => { @@ -59,7 +59,7 @@ describe('Client createManyAndReturn tests', () => { }); it('works with select and omit', async () => { - let r = await client.user.createManyAndReturn({ + const r = await client.user.createManyAndReturn({ data: [{ email: 'u1@test.com', name: 'name' }], select: { email: true }, }); @@ -67,12 +67,12 @@ describe('Client createManyAndReturn tests', () => { // @ts-expect-error expect(r[0]!.name).toBeUndefined(); - r = await client.user.createManyAndReturn({ + const r1 = await client.user.createManyAndReturn({ data: [{ email: 'u2@test.com', name: 'name' }], omit: { name: true }, }); - expect(r[0]!.email).toBe('u2@test.com'); + expect(r1[0]!.email).toBe('u2@test.com'); // @ts-expect-error - expect(r[0]!.name).toBeUndefined(); + expect(r1[0]!.name).toBeUndefined(); }); }); diff --git a/tests/e2e/orm/client-api/create-many.test.ts b/tests/e2e/orm/client-api/create-many.test.ts index c55f05ac..75d93a29 100644 --- a/tests/e2e/orm/client-api/create-many.test.ts +++ b/tests/e2e/orm/client-api/create-many.test.ts @@ -7,7 +7,7 @@ describe('Client createMany tests', () => { let client: ClientContract; beforeEach(async () => { - client = await createTestClient(schema); + client = (await createTestClient(schema)) as any; }); afterEach(async () => { diff --git a/tests/e2e/orm/client-api/create.test.ts b/tests/e2e/orm/client-api/create.test.ts index dcf45eda..c47a413f 100644 --- a/tests/e2e/orm/client-api/create.test.ts +++ b/tests/e2e/orm/client-api/create.test.ts @@ -7,7 +7,7 @@ describe('Client create tests', () => { let client: ClientContract; beforeEach(async () => { - client = await createTestClient(schema); + client = (await createTestClient(schema)) as any; }); afterEach(async () => { diff --git a/tests/e2e/orm/client-api/delegate.test.ts b/tests/e2e/orm/client-api/delegate.test.ts index 9076c60d..c5f59c70 100644 --- a/tests/e2e/orm/client-api/delegate.test.ts +++ b/tests/e2e/orm/client-api/delegate.test.ts @@ -8,10 +8,10 @@ describe('Delegate model tests ', () => { let client: ClientContract; beforeEach(async () => { - client = await createTestClient(schema, { + client = (await createTestClient(schema, { usePrismaPush: true, schemaFile: path.join(__dirname, '../schemas/delegate/schema.zmodel'), - }); + })) as any; }); afterEach(async () => { diff --git a/tests/e2e/orm/client-api/delete-many.test.ts b/tests/e2e/orm/client-api/delete-many.test.ts index c7ea5530..c5fc84e1 100644 --- a/tests/e2e/orm/client-api/delete-many.test.ts +++ b/tests/e2e/orm/client-api/delete-many.test.ts @@ -7,7 +7,7 @@ describe('Client deleteMany tests', () => { let client: ClientContract; beforeEach(async () => { - client = await createTestClient(schema); + client = (await createTestClient(schema)) as any; }); afterEach(async () => { diff --git a/tests/e2e/orm/client-api/delete.test.ts b/tests/e2e/orm/client-api/delete.test.ts index e3839532..5050faad 100644 --- a/tests/e2e/orm/client-api/delete.test.ts +++ b/tests/e2e/orm/client-api/delete.test.ts @@ -7,7 +7,7 @@ describe('Client delete tests', () => { let client: ClientContract; beforeEach(async () => { - client = await createTestClient(schema); + client = (await createTestClient(schema)) as any; }); afterEach(async () => { diff --git a/tests/e2e/orm/client-api/filter.test.ts b/tests/e2e/orm/client-api/filter.test.ts index 290977bc..a8cd56a9 100644 --- a/tests/e2e/orm/client-api/filter.test.ts +++ b/tests/e2e/orm/client-api/filter.test.ts @@ -7,7 +7,7 @@ describe('Client filter tests ', () => { let client: ClientContract; beforeEach(async () => { - client = await createTestClient(schema); + client = (await createTestClient(schema)) as any; }); afterEach(async () => { diff --git a/tests/e2e/orm/client-api/find.test.ts b/tests/e2e/orm/client-api/find.test.ts index 765492cc..7f9df68f 100644 --- a/tests/e2e/orm/client-api/find.test.ts +++ b/tests/e2e/orm/client-api/find.test.ts @@ -8,7 +8,7 @@ describe('Client find tests ', () => { let client: ClientContract; beforeEach(async () => { - client = await createTestClient(schema); + client = (await createTestClient(schema)) as any; }); afterEach(async () => { diff --git a/tests/e2e/orm/client-api/group-by.test.ts b/tests/e2e/orm/client-api/group-by.test.ts index fd31131c..250bd905 100644 --- a/tests/e2e/orm/client-api/group-by.test.ts +++ b/tests/e2e/orm/client-api/group-by.test.ts @@ -8,7 +8,7 @@ describe('Client groupBy tests', () => { let client: ClientContract; beforeEach(async () => { - client = await createTestClient(schema); + client = (await createTestClient(schema)) as any; }); afterEach(async () => { diff --git a/tests/e2e/orm/client-api/name-mapping.test.ts b/tests/e2e/orm/client-api/name-mapping.test.ts index e1ba5430..5fd699a9 100644 --- a/tests/e2e/orm/client-api/name-mapping.test.ts +++ b/tests/e2e/orm/client-api/name-mapping.test.ts @@ -8,10 +8,10 @@ describe('Name mapping tests', () => { let db: ClientContract; beforeEach(async () => { - db = await createTestClient(schema, { + db = (await createTestClient(schema, { usePrismaPush: true, schemaFile: path.join(__dirname, '../schemas/name-mapping/schema.zmodel'), - }); + })) as any; }); afterEach(async () => { diff --git a/tests/e2e/orm/client-api/raw-query.test.ts b/tests/e2e/orm/client-api/raw-query.test.ts index 7fe0ecbc..b8de6047 100644 --- a/tests/e2e/orm/client-api/raw-query.test.ts +++ b/tests/e2e/orm/client-api/raw-query.test.ts @@ -7,7 +7,7 @@ describe('Client raw query tests', () => { let client: ClientContract; beforeEach(async () => { - client = await createTestClient(schema); + client = (await createTestClient(schema)) as any; }); afterEach(async () => { diff --git a/tests/e2e/orm/client-api/transaction.test.ts b/tests/e2e/orm/client-api/transaction.test.ts index e4f2192e..e8ae7f94 100644 --- a/tests/e2e/orm/client-api/transaction.test.ts +++ b/tests/e2e/orm/client-api/transaction.test.ts @@ -7,7 +7,7 @@ describe('Client raw query tests', () => { let client: ClientContract; beforeEach(async () => { - client = await createTestClient(schema); + client = (await createTestClient(schema)) as any; }); afterEach(async () => { diff --git a/tests/e2e/orm/client-api/undefined-values.test.ts b/tests/e2e/orm/client-api/undefined-values.test.ts index d2ed52f0..b1ac3962 100644 --- a/tests/e2e/orm/client-api/undefined-values.test.ts +++ b/tests/e2e/orm/client-api/undefined-values.test.ts @@ -8,7 +8,7 @@ describe('Client undefined values tests ', () => { let client: ClientContract; beforeEach(async () => { - client = await createTestClient(schema); + client = (await createTestClient(schema)) as any; }); afterEach(async () => { diff --git a/tests/e2e/orm/client-api/update-many.test.ts b/tests/e2e/orm/client-api/update-many.test.ts index 61776e3e..a5bc2b48 100644 --- a/tests/e2e/orm/client-api/update-many.test.ts +++ b/tests/e2e/orm/client-api/update-many.test.ts @@ -7,7 +7,7 @@ describe('Client updateMany tests', () => { let client: ClientContract; beforeEach(async () => { - client = await createTestClient(schema); + client = (await createTestClient(schema)) as any; }); afterEach(async () => { diff --git a/tests/e2e/orm/client-api/update.test.ts b/tests/e2e/orm/client-api/update.test.ts index c79396d7..46c71288 100644 --- a/tests/e2e/orm/client-api/update.test.ts +++ b/tests/e2e/orm/client-api/update.test.ts @@ -8,7 +8,7 @@ describe('Client update tests', () => { let client: ClientContract; beforeEach(async () => { - client = await createTestClient(schema); + client = (await createTestClient(schema)) as any; }); afterEach(async () => { diff --git a/tests/e2e/orm/client-api/upsert.test.ts b/tests/e2e/orm/client-api/upsert.test.ts index 3b3cb090..eba4df78 100644 --- a/tests/e2e/orm/client-api/upsert.test.ts +++ b/tests/e2e/orm/client-api/upsert.test.ts @@ -7,7 +7,7 @@ describe('Client upsert tests', () => { let client: ClientContract; beforeEach(async () => { - client = await createTestClient(schema); + client = (await createTestClient(schema)) as any; }); afterEach(async () => { diff --git a/tests/e2e/orm/plugin-infra/entity-mutation-hooks.test.ts b/tests/e2e/orm/plugin-infra/entity-mutation-hooks.test.ts index 1b0eea2f..4c97d1a9 100644 --- a/tests/e2e/orm/plugin-infra/entity-mutation-hooks.test.ts +++ b/tests/e2e/orm/plugin-infra/entity-mutation-hooks.test.ts @@ -8,7 +8,7 @@ describe('Entity mutation hooks tests', () => { let _client: ClientContract; beforeEach(async () => { - _client = await createTestClient(schema, {}); + _client = (await createTestClient(schema, {})) as any; }); afterEach(async () => { diff --git a/tests/e2e/orm/plugin-infra/on-kysely-query.test.ts b/tests/e2e/orm/plugin-infra/on-kysely-query.test.ts index 68602613..a4a45b32 100644 --- a/tests/e2e/orm/plugin-infra/on-kysely-query.test.ts +++ b/tests/e2e/orm/plugin-infra/on-kysely-query.test.ts @@ -8,7 +8,7 @@ describe('On kysely query tests', () => { let _client: ClientContract; beforeEach(async () => { - _client = await createTestClient(schema); + _client = (await createTestClient(schema)) as any; }); afterEach(async () => { diff --git a/tests/e2e/orm/plugin-infra/on-query-hooks.test.ts b/tests/e2e/orm/plugin-infra/on-query-hooks.test.ts index 0b74e76f..88126c98 100644 --- a/tests/e2e/orm/plugin-infra/on-query-hooks.test.ts +++ b/tests/e2e/orm/plugin-infra/on-query-hooks.test.ts @@ -7,7 +7,7 @@ describe('On query hooks tests', () => { let _client: ClientContract; beforeEach(async () => { - _client = await createTestClient(schema); + _client = (await createTestClient(schema)) as any; }); afterEach(async () => { diff --git a/tests/e2e/orm/policy/basic-schema-read.test.ts b/tests/e2e/orm/policy/basic-schema-read.test.ts index 7464a38c..3b212221 100644 --- a/tests/e2e/orm/policy/basic-schema-read.test.ts +++ b/tests/e2e/orm/policy/basic-schema-read.test.ts @@ -8,7 +8,7 @@ describe('Read policy tests', () => { let client: ClientContract; beforeEach(async () => { - client = await createTestClient(schema); + client = (await createTestClient(schema)) as any; }); afterEach(async () => { diff --git a/tests/e2e/tsconfig.json b/tests/e2e/tsconfig.json index 6d8efbee..22b23e0c 100644 --- a/tests/e2e/tsconfig.json +++ b/tests/e2e/tsconfig.json @@ -3,6 +3,6 @@ "compilerOptions": { "noEmit": true, "noImplicitAny": false, - "types": ["@zenstackhq/testtools/types"] + "types": ["@zenstackhq/testtools/types", "node"] } } diff --git a/tests/regression/tsconfig.json b/tests/regression/tsconfig.json index 1dd80744..98939568 100644 --- a/tests/regression/tsconfig.json +++ b/tests/regression/tsconfig.json @@ -4,5 +4,5 @@ "noEmit": true, "types": ["@zenstackhq/testtools/types", "node"] }, - "include": ["src/**/*.ts", "test/**/*.ts"] + "include": ["test/**/*.ts"] }