From 9a6205148b3707aee049b2df34babf9225265286 Mon Sep 17 00:00:00 2001 From: Mike Willbanks Date: Sat, 10 Jan 2026 18:28:23 -0600 Subject: [PATCH 1/4] fix(orm): use id-only filter for update read-back (#585) --- .../orm/src/client/crud/operations/update.ts | 3 +- tests/e2e/orm/policy/crud/update.test.ts | 2 +- tests/regression/test/issue-586.test.ts | 87 +++++++++++++++++++ 3 files changed, 90 insertions(+), 2 deletions(-) create mode 100644 tests/regression/test/issue-586.test.ts diff --git a/packages/orm/src/client/crud/operations/update.ts b/packages/orm/src/client/crud/operations/update.ts index 805eb2a8..477f03f5 100644 --- a/packages/orm/src/client/crud/operations/update.ts +++ b/packages/orm/src/client/crud/operations/update.ts @@ -43,7 +43,8 @@ export class UpdateOperationHandler extends BaseOperat if (needReadBack) { // updated can be undefined if there's nothing to update, in that case we'll use the original // filter to read back the entity - const readFilter = updateResult ?? args.where; + // note that we trim filter to id fields only, just in case underlying executor returns more fields + const readFilter = updateResult ? getIdValues(this.schema, this.model, updateResult) : args.where; let readBackResult: any = undefined; readBackResult = await this.readUnique(tx, this.model, { select: args.select, diff --git a/tests/e2e/orm/policy/crud/update.test.ts b/tests/e2e/orm/policy/crud/update.test.ts index 7f060c88..95b17306 100644 --- a/tests/e2e/orm/policy/crud/update.test.ts +++ b/tests/e2e/orm/policy/crud/update.test.ts @@ -1,5 +1,5 @@ -import { describe, expect, it } from 'vitest'; import { createPolicyTestClient } from '@zenstackhq/testtools'; +import { describe, expect, it } from 'vitest'; describe('Update policy tests', () => { describe('Scalar condition tests', () => { diff --git a/tests/regression/test/issue-586.test.ts b/tests/regression/test/issue-586.test.ts new file mode 100644 index 00000000..0c124472 --- /dev/null +++ b/tests/regression/test/issue-586.test.ts @@ -0,0 +1,87 @@ +import { createPolicyTestClient } from '@zenstackhq/testtools'; +import { describe, expect, it } from 'vitest'; + +describe('Regression for issue #586', () => { + it('does not throw cannot-read-back for json array update with extra mutation plugin', async () => { + const schema = ` + + type AuthInfo { + aProperty Boolean + + @@auth + } + + type Foo { + bar String + baz Int + + @@allow("all", auth().aProperty) + } + + model JsonArrayRoot { + id String @id @default(cuid()) + + fields JsonArrayField[] + + @@allow("all", auth().aProperty) + } + + model JsonArrayField { + id String @id @default(cuid()) + data Foo[] @json + rootId String + + root JsonArrayRoot @relation(fields: [rootId], references: [id]) + + @@allow("all", auth().aProperty) + } + `; + + const db = await createPolicyTestClient(schema, { + provider: 'postgresql', + usePrismaPush: true, + plugins: [ + { + id: 'foo', + name: 'foo', + description: 'foo', + onEntityMutation: { + afterEntityMutation: async () => Promise.resolve(), + beforeEntityMutation: async () => Promise.resolve(), + runAfterMutationWithinTransaction: true, + }, + }, + ], + }); + + try { + const authed = db.$setAuth({ aProperty: true }); + + const root = await authed.jsonArrayRoot.create({ data: {} }); + + const created = await authed.jsonArrayField.create({ + data: { + data: [], + rootId: root.id, + }, + }); + + const updateData = [ + { bar: 'hello', baz: 1 }, + { bar: 'world', baz: 2 }, + ]; + + await expect( + authed.jsonArrayField.update({ + where: { id: created.id }, + data: { + data: updateData, + rootId: root.id, + }, + }), + ).resolves.toMatchObject({ data: updateData }); + } finally { + await db.$disconnect?.(); + } + }); +}); From 6bf2b51c5d197c1b93d9276ee46dc076205dac8a Mon Sep 17 00:00:00 2001 From: Yiming Cao Date: Sun, 11 Jan 2026 16:44:00 +0800 Subject: [PATCH 2/4] fix(zmodel): improve attribute argument assignability check (#588) * fix(zmodel): improve attribute argument assignability check fixes #584 * fix JSON array check * update * update ts schema generator --- .../attribute-application-validator.ts | 119 ++++++++++++--- packages/sdk/src/ts-schema-generator.ts | 23 ++- tests/regression/test/issue-584.test.ts | 140 ++++++++++++++++++ 3 files changed, 255 insertions(+), 27 deletions(-) create mode 100644 tests/regression/test/issue-584.test.ts diff --git a/packages/language/src/validators/attribute-application-validator.ts b/packages/language/src/validators/attribute-application-validator.ts index 74ea33ec..79db61cc 100644 --- a/packages/language/src/validators/attribute-application-validator.ts +++ b/packages/language/src/validators/attribute-application-validator.ts @@ -22,6 +22,7 @@ import { isLiteralExpr, isModel, isReferenceExpr, + isStringLiteral, isTypeDef, } from '../generated/ast'; import { @@ -103,10 +104,9 @@ export default class AttributeApplicationValidator implements AstValidator isLiteralJsonString(item))) { + return success; + } else { + return { + result: false, + error: 'expected an array of JSON string literals', + }; + } + } else { + if (isLiteralJsonString(arg.value)) { + return success; + } else { + return { + result: false, + error: 'expected a JSON string literal', + }; + } + } } dstIsArray = attr.$container.type.array; } @@ -417,17 +448,24 @@ function assignableToAttributeParam(arg: AttributeArg, param: AttributeParam, at const dstRef = param.type.reference; if (dstType === 'Any' && !dstIsArray) { - return true; + return success; } if (argResolvedType.decl === 'Any') { // arg is any type if (!argResolvedType.array) { // if it's not an array, it's assignable to any type - return true; + return success; } else { // otherwise it's assignable to any array type - return argResolvedType.array === dstIsArray; + if (argResolvedType.array === dstIsArray) { + return success; + } else { + return { + result: false, + error: `expected ${dstIsArray ? 'array' : 'non-array'}`, + }; + } } } @@ -435,12 +473,20 @@ function assignableToAttributeParam(arg: AttributeArg, param: AttributeParam, at // argument is reference or array or reference if (dstType === 'FieldReference' || dstType === 'TransitiveFieldReference') { if (dstIsArray) { - return ( + if ( isArrayExpr(arg.value) && - !arg.value.items.find((item) => !isReferenceExpr(item) || !isDataField(item.target.ref)) - ); + !arg.value.items.some((item) => !isReferenceExpr(item) || !isDataField(item.target.ref)) + ) { + return success; + } else { + return { result: false, error: 'expected an array of field references' }; + } } else { - return isReferenceExpr(arg.value) && isDataField(arg.value.target.ref); + if (isReferenceExpr(arg.value) && isDataField(arg.value.target.ref)) { + return success; + } else { + return { result: false, error: 'expected a field reference' }; + } } } @@ -454,13 +500,22 @@ function assignableToAttributeParam(arg: AttributeArg, param: AttributeParam, at attrArgDeclType = resolved(attr.$container.type.reference); dstIsArray = attr.$container.type.array; } - return attrArgDeclType === argResolvedType.decl && dstIsArray === argResolvedType.array; + + if (attrArgDeclType !== argResolvedType.decl) { + return genericError; + } + + if (dstIsArray !== argResolvedType.array) { + return { result: false, error: `expected ${dstIsArray ? 'array' : 'non-array'}` }; + } + + return success; } else if (dstType) { // scalar type if (typeof argResolvedType?.decl !== 'string') { // destination type is not a reference, so argument type must be a plain expression - return false; + return genericError; } if (dstType === 'ContextType') { @@ -468,7 +523,7 @@ function assignableToAttributeParam(arg: AttributeArg, param: AttributeParam, at // the attribute's container if (isDataField(attr.$container)) { if (!attr.$container?.type?.type) { - return false; + return genericError; } dstType = mapBuiltinTypeToExpressionType(attr.$container.type.type); dstIsArray = attr.$container.type.array; @@ -477,10 +532,18 @@ function assignableToAttributeParam(arg: AttributeArg, param: AttributeParam, at } } - return typeAssignable(dstType, argResolvedType.decl, arg.value) && dstIsArray === argResolvedType.array; + if (typeAssignable(dstType, argResolvedType.decl, arg.value) && dstIsArray === argResolvedType.array) { + return success; + } else { + return genericError; + } } else { // reference type - return (dstRef?.ref === argResolvedType.decl || dstType === 'Any') && dstIsArray === argResolvedType.array; + if ((dstRef?.ref === argResolvedType.decl || dstType === 'Any') && dstIsArray === argResolvedType.array) { + return success; + } else { + return genericError; + } } } @@ -552,3 +615,15 @@ export function validateAttributeApplication( ) { new AttributeApplicationValidator().validate(attr, accept, contextDataModel); } + +function isLiteralJsonString(value: Expression) { + if (!isStringLiteral(value)) { + return false; + } + try { + JSON.parse(value.value); + return true; + } catch { + return false; + } +} diff --git a/packages/sdk/src/ts-schema-generator.ts b/packages/sdk/src/ts-schema-generator.ts index 83e97a27..6baeff42 100644 --- a/packages/sdk/src/ts-schema-generator.ts +++ b/packages/sdk/src/ts-schema-generator.ts @@ -607,12 +607,15 @@ export class TsSchemaGenerator { const defaultValue = this.getFieldMappedDefault(field); if (defaultValue !== undefined) { - if (typeof defaultValue === 'object' && !Array.isArray(defaultValue)) { + if (defaultValue === null) { + objectFields.push( + ts.factory.createPropertyAssignment('default', this.createExpressionUtilsCall('_null')), + ); + } else if (typeof defaultValue === 'object' && !Array.isArray(defaultValue)) { if ('call' in defaultValue) { objectFields.push( ts.factory.createPropertyAssignment( 'default', - this.createExpressionUtilsCall('call', [ ts.factory.createStringLiteral(defaultValue.call), ...(defaultValue.args.length > 0 @@ -725,7 +728,15 @@ export class TsSchemaGenerator { private getFieldMappedDefault( field: DataField, - ): string | number | boolean | unknown[] | { call: string; args: any[] } | { authMember: string[] } | undefined { + ): + | string + | number + | boolean + | unknown[] + | { call: string; args: any[] } + | { authMember: string[] } + | null + | undefined { const defaultAttr = getAttribute(field, '@default'); if (!defaultAttr) { return undefined; @@ -738,7 +749,7 @@ export class TsSchemaGenerator { private getMappedValue( expr: Expression, fieldType: DataFieldType, - ): string | number | boolean | unknown[] | { call: string; args: any[] } | { authMember: string[] } | undefined { + ): string | number | boolean | unknown[] | { call: string; args: any[] } | { authMember: string[] } | null { if (isLiteralExpr(expr)) { const lit = (expr as LiteralExpr).value; return fieldType.type === 'Boolean' @@ -759,8 +770,10 @@ export class TsSchemaGenerator { return { authMember: this.getMemberAccessChain(expr), }; + } else if (isNullExpr(expr)) { + return null; } else { - throw new Error(`Unsupported default value type for ${expr.$type}`); + throw new Error(`Unsupported expression type: ${expr.$type}`); } } diff --git a/tests/regression/test/issue-584.test.ts b/tests/regression/test/issue-584.test.ts new file mode 100644 index 00000000..56dea871 --- /dev/null +++ b/tests/regression/test/issue-584.test.ts @@ -0,0 +1,140 @@ +import { createTestClient } from '@zenstackhq/testtools'; +import { describe, expect, it } from 'vitest'; + +describe('Regression for issue #584', () => { + it('correctly validates JSON default values', async () => { + await expect( + createTestClient( + ` +model Foo { + id String @id @default(cuid()) + data Json? @default(null) +} +`, + { usePrismaPush: true }, + ), + ).rejects.toThrow('expected a JSON string literal'); + + await expect( + createTestClient( + ` +model Foo { + id String @id @default(cuid()) + data Json? @default('hello') +} +`, + { usePrismaPush: true }, + ), + ).rejects.toThrow('expected a JSON string literal'); + + await expect( + createTestClient( + ` +model Foo { + id String @id @default(cuid()) + data Json? @default('"hello"') +} +`, + { usePrismaPush: true }, + ), + ).toResolveTruthy(); + + await expect( + createTestClient( + ` +model Foo { + id String @id @default(cuid()) + data Json? @default('[{"hello":1}]') +} +`, + { usePrismaPush: true }, + ), + ).toResolveTruthy(); + + await expect( + createTestClient( + ` +model Foo { + id String @id @default(cuid()) + data Json? @default('null') +} +`, + { usePrismaPush: true }, + ), + ).toResolveTruthy(); + }); + + it('correctly validates JSON array default values', async () => { + await expect( + createTestClient( + ` +model Foo { + id String @id @default(cuid()) + data Json[] @default(null) +} +`, + { usePrismaPush: true, provider: 'postgresql' }, + ), + ).rejects.toThrow('expected an array of JSON string literals'); + + await expect( + createTestClient( + ` +model Foo { + id String @id @default(cuid()) + data Json[] @default([1, 2, 3]) +} +`, + { usePrismaPush: true, provider: 'postgresql' }, + ), + ).rejects.toThrow('expected an array of JSON string literals'); + + await expect( + createTestClient( + ` +model Foo { + id String @id @default(cuid()) + data Json[] @default('[]') +} +`, + { usePrismaPush: true, provider: 'postgresql' }, + ), + ).rejects.toThrow('expected an array of JSON string literals'); + + await expect( + createTestClient( + ` +model Foo { + id String @id @default(cuid()) + data Json[] @default([]) +} +`, + { usePrismaPush: true, provider: 'postgresql' }, + ), + ).toResolveTruthy(); + + await expect( + createTestClient( + ` +model Foo { + id String @id @default(cuid()) + data Json[] @default(['1', '2', '3']) +} +`, + { usePrismaPush: true, provider: 'postgresql' }, + ), + ).toResolveTruthy(); + + await expect( + createTestClient( + ` +model Foo { + id String @id @default(cuid()) + data Json[] @default(['"1"', '"2"', 'null']) +} +`, + { usePrismaPush: true, provider: 'postgresql' }, + ), + ).toResolveTruthy(); + }); +}); From f54093db51387526ade98db6f5dfdb0befa49972 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 11 Jan 2026 16:44:20 +0800 Subject: [PATCH 3/4] chore: bump version 3.2.1 (#587) Co-authored-by: ymc9 <104139426+ymc9@users.noreply.github.com> --- package.json | 2 +- packages/auth-adapters/better-auth/package.json | 2 +- packages/cli/package.json | 2 +- packages/clients/client-helpers/package.json | 2 +- packages/clients/tanstack-query/package.json | 2 +- packages/common-helpers/package.json | 2 +- packages/config/eslint-config/package.json | 2 +- packages/config/typescript-config/package.json | 2 +- packages/config/vitest-config/package.json | 2 +- packages/create-zenstack/package.json | 2 +- packages/ide/vscode/package.json | 2 +- packages/language/package.json | 2 +- packages/orm/package.json | 2 +- packages/plugins/policy/package.json | 2 +- packages/schema/package.json | 2 +- packages/sdk/package.json | 2 +- packages/server/package.json | 2 +- packages/testtools/package.json | 2 +- packages/zod/package.json | 2 +- samples/orm/package.json | 2 +- tests/e2e/package.json | 2 +- tests/regression/package.json | 2 +- tests/runtimes/bun/package.json | 2 +- tests/runtimes/edge-runtime/package.json | 2 +- 24 files changed, 24 insertions(+), 24 deletions(-) diff --git a/package.json b/package.json index 08796a1d..0954f352 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "zenstack-v3", - "version": "3.2.0", + "version": "3.2.1", "description": "ZenStack", "packageManager": "pnpm@10.23.0", "type": "module", diff --git a/packages/auth-adapters/better-auth/package.json b/packages/auth-adapters/better-auth/package.json index 7be7725e..9239f3b8 100644 --- a/packages/auth-adapters/better-auth/package.json +++ b/packages/auth-adapters/better-auth/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/better-auth", - "version": "3.2.0", + "version": "3.2.1", "description": "ZenStack Better Auth Adapter. This adapter is modified from better-auth's Prisma adapter.", "type": "module", "scripts": { diff --git a/packages/cli/package.json b/packages/cli/package.json index c70afd24..401c80e2 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.2.0", + "version": "3.2.1", "type": "module", "author": { "name": "ZenStack Team" diff --git a/packages/clients/client-helpers/package.json b/packages/clients/client-helpers/package.json index d7a4bd9b..f31fe537 100644 --- a/packages/clients/client-helpers/package.json +++ b/packages/clients/client-helpers/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/client-helpers", - "version": "3.2.0", + "version": "3.2.1", "description": "Helpers for implementing clients that consume ZenStack's CRUD service", "type": "module", "scripts": { diff --git a/packages/clients/tanstack-query/package.json b/packages/clients/tanstack-query/package.json index 35b80f97..e7a52768 100644 --- a/packages/clients/tanstack-query/package.json +++ b/packages/clients/tanstack-query/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/tanstack-query", - "version": "3.2.0", + "version": "3.2.1", "description": "TanStack Query Client for consuming ZenStack v3's CRUD service", "type": "module", "scripts": { diff --git a/packages/common-helpers/package.json b/packages/common-helpers/package.json index a12480c8..e4a57539 100644 --- a/packages/common-helpers/package.json +++ b/packages/common-helpers/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/common-helpers", - "version": "3.2.0", + "version": "3.2.1", "description": "ZenStack Common Helpers", "type": "module", "scripts": { diff --git a/packages/config/eslint-config/package.json b/packages/config/eslint-config/package.json index d10f4f05..79d9933e 100644 --- a/packages/config/eslint-config/package.json +++ b/packages/config/eslint-config/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/eslint-config", - "version": "3.2.0", + "version": "3.2.1", "type": "module", "private": true, "license": "MIT" diff --git a/packages/config/typescript-config/package.json b/packages/config/typescript-config/package.json index 9e05c230..ba55bbca 100644 --- a/packages/config/typescript-config/package.json +++ b/packages/config/typescript-config/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/typescript-config", - "version": "3.2.0", + "version": "3.2.1", "private": true, "license": "MIT" } diff --git a/packages/config/vitest-config/package.json b/packages/config/vitest-config/package.json index addc8dc9..96405ecd 100644 --- a/packages/config/vitest-config/package.json +++ b/packages/config/vitest-config/package.json @@ -1,7 +1,7 @@ { "name": "@zenstackhq/vitest-config", "type": "module", - "version": "3.2.0", + "version": "3.2.1", "private": true, "license": "MIT", "exports": { diff --git a/packages/create-zenstack/package.json b/packages/create-zenstack/package.json index e314b5d7..45b0def5 100644 --- a/packages/create-zenstack/package.json +++ b/packages/create-zenstack/package.json @@ -1,6 +1,6 @@ { "name": "create-zenstack", - "version": "3.2.0", + "version": "3.2.1", "description": "Create a new ZenStack project", "type": "module", "scripts": { diff --git a/packages/ide/vscode/package.json b/packages/ide/vscode/package.json index 6f99cde1..0976e969 100644 --- a/packages/ide/vscode/package.json +++ b/packages/ide/vscode/package.json @@ -1,7 +1,7 @@ { "name": "zenstack-v3", "publisher": "zenstack", - "version": "3.2.0", + "version": "3.2.1", "displayName": "ZenStack V3 Language Tools", "description": "VSCode extension for ZenStack (v3) ZModel language", "private": true, diff --git a/packages/language/package.json b/packages/language/package.json index 091920db..3f4cca8c 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.2.0", + "version": "3.2.1", "license": "MIT", "author": "ZenStack Team", "files": [ diff --git a/packages/orm/package.json b/packages/orm/package.json index 173c16fd..ccd94a9c 100644 --- a/packages/orm/package.json +++ b/packages/orm/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/orm", - "version": "3.2.0", + "version": "3.2.1", "description": "ZenStack ORM", "type": "module", "scripts": { diff --git a/packages/plugins/policy/package.json b/packages/plugins/policy/package.json index 0700ddda..e686d56c 100644 --- a/packages/plugins/policy/package.json +++ b/packages/plugins/policy/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/plugin-policy", - "version": "3.2.0", + "version": "3.2.1", "description": "ZenStack Policy Plugin", "type": "module", "scripts": { diff --git a/packages/schema/package.json b/packages/schema/package.json index 1d6e0146..bc25a5bf 100644 --- a/packages/schema/package.json +++ b/packages/schema/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/schema", - "version": "3.2.0", + "version": "3.2.1", "description": "ZenStack Runtime Schema", "type": "module", "scripts": { diff --git a/packages/sdk/package.json b/packages/sdk/package.json index fbdef344..a2fe1f0d 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/sdk", - "version": "3.2.0", + "version": "3.2.1", "description": "ZenStack SDK", "type": "module", "scripts": { diff --git a/packages/server/package.json b/packages/server/package.json index d39396d5..d1ca34d4 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/server", - "version": "3.2.0", + "version": "3.2.1", "description": "ZenStack automatic CRUD API handlers and server adapters", "type": "module", "scripts": { diff --git a/packages/testtools/package.json b/packages/testtools/package.json index 80f8221c..ed5f9bc9 100644 --- a/packages/testtools/package.json +++ b/packages/testtools/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/testtools", - "version": "3.2.0", + "version": "3.2.1", "description": "ZenStack Test Tools", "type": "module", "scripts": { diff --git a/packages/zod/package.json b/packages/zod/package.json index 736a3982..3176a7c0 100644 --- a/packages/zod/package.json +++ b/packages/zod/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/zod", - "version": "3.2.0", + "version": "3.2.1", "description": "", "type": "module", "main": "index.js", diff --git a/samples/orm/package.json b/samples/orm/package.json index 7017177e..fd43fa6c 100644 --- a/samples/orm/package.json +++ b/samples/orm/package.json @@ -1,6 +1,6 @@ { "name": "sample-blog", - "version": "3.2.0", + "version": "3.2.1", "description": "", "main": "index.js", "private": true, diff --git a/tests/e2e/package.json b/tests/e2e/package.json index 7d849e26..c24650ba 100644 --- a/tests/e2e/package.json +++ b/tests/e2e/package.json @@ -1,6 +1,6 @@ { "name": "e2e", - "version": "3.2.0", + "version": "3.2.1", "private": true, "type": "module", "scripts": { diff --git a/tests/regression/package.json b/tests/regression/package.json index 2c1bbdf5..1e66dafb 100644 --- a/tests/regression/package.json +++ b/tests/regression/package.json @@ -1,6 +1,6 @@ { "name": "regression", - "version": "3.2.0", + "version": "3.2.1", "private": true, "type": "module", "scripts": { diff --git a/tests/runtimes/bun/package.json b/tests/runtimes/bun/package.json index 4bf3f97c..9c0f9eef 100644 --- a/tests/runtimes/bun/package.json +++ b/tests/runtimes/bun/package.json @@ -1,6 +1,6 @@ { "name": "bun-e2e", - "version": "3.2.0", + "version": "3.2.1", "private": true, "type": "module", "scripts": { diff --git a/tests/runtimes/edge-runtime/package.json b/tests/runtimes/edge-runtime/package.json index 82fd042b..2036296e 100644 --- a/tests/runtimes/edge-runtime/package.json +++ b/tests/runtimes/edge-runtime/package.json @@ -1,6 +1,6 @@ { "name": "edge-runtime-e2e", - "version": "3.2.0", + "version": "3.2.1", "private": true, "type": "module", "scripts": { From 154ba296d242b41e80ad677c88f9f5ee2ce01ddb Mon Sep 17 00:00:00 2001 From: Yiming Cao Date: Sun, 11 Jan 2026 17:45:51 +0800 Subject: [PATCH 4/4] fix(orm): incorrect result type when "_count" is nested inside "include" (#589) --- packages/orm/src/client/crud-types.ts | 6 ++- tests/e2e/orm/client-api/find.test.ts | 46 +++++++++++++++++++++++ tests/e2e/orm/schemas/typing/typecheck.ts | 19 ++++++++++ 3 files changed, 70 insertions(+), 1 deletion(-) diff --git a/packages/orm/src/client/crud-types.ts b/packages/orm/src/client/crud-types.ts index 071e6674..1b6f3d3c 100644 --- a/packages/orm/src/client/crud-types.ts +++ b/packages/orm/src/client/crud-types.ts @@ -214,7 +214,11 @@ export type ModelResult< ModelFieldIsOptional, FieldIsArray >; - } + } & ('_count' extends keyof I + ? I['_count'] extends false | undefined + ? {} + : { _count: SelectCountResult } + : {}) : Args extends { omit: infer O } & Record ? DefaultModelResult : DefaultModelResult, diff --git a/tests/e2e/orm/client-api/find.test.ts b/tests/e2e/orm/client-api/find.test.ts index 765492cc..0a881288 100644 --- a/tests/e2e/orm/client-api/find.test.ts +++ b/tests/e2e/orm/client-api/find.test.ts @@ -1122,6 +1122,52 @@ describe('Client find tests ', () => { }); }); + it('supports _count inside include', async () => { + const user = await createUser(client, 'u1@test.com'); + await createPosts(client, user.id); + + // Test _count with select inside include + const result = await client.user.findFirst({ + include: { + posts: { select: { title: true } }, + _count: { select: { posts: true } }, + }, + }); + + expect(result).toBeDefined(); + expect(result?.posts).toHaveLength(2); + expect(result?.posts[0]).toHaveProperty('title'); + // TypeScript should recognize _count property exists + expect(result?._count).toBeDefined(); + expect(result?._count.posts).toBe(2); + + // Test _count with boolean true inside include + const result2 = await client.user.findFirst({ + include: { + posts: true, + _count: true, + }, + }); + + expect(result2).toBeDefined(); + expect(result2?.posts).toHaveLength(2); + expect(result2?._count).toBeDefined(); + expect(result2?._count.posts).toBe(2); + + // Test _count with filtered posts inside include + const result3 = await client.user.findFirst({ + include: { + posts: { where: { published: true } }, + _count: { select: { posts: { where: { published: true } } } }, + }, + }); + + expect(result3).toBeDefined(); + expect(result3?.posts).toHaveLength(1); + expect(result3?._count).toBeDefined(); + expect(result3?._count.posts).toBe(1); + }); + it('supports $expr', async () => { await createUser(client, 'yiming@gmail.com'); await createUser(client, 'yiming@zenstack.dev'); diff --git a/tests/e2e/orm/schemas/typing/typecheck.ts b/tests/e2e/orm/schemas/typing/typecheck.ts index 976c04c8..f73d4927 100644 --- a/tests/e2e/orm/schemas/typing/typecheck.ts +++ b/tests/e2e/orm/schemas/typing/typecheck.ts @@ -162,6 +162,25 @@ async function find() { }) ).profile?.region?.city; + // _count inside include should be properly typed + const includeWithCount = await client.user.findFirst({ + include: { + posts: { select: { title: true } }, + _count: { select: { posts: true } }, + }, + }); + console.log(includeWithCount?.posts[0]?.title); + console.log(includeWithCount?._count.posts); + + const includeWithCountTrue = await client.user.findFirst({ + include: { + posts: true, + _count: true, + }, + }); + console.log(includeWithCountTrue?.posts[0]?.title); + console.log(includeWithCountTrue?._count.posts); + ( await client.user.findFirstOrThrow({ select: {