From 6404c0346c8f4691053b7cac140054b8938f8a57 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 17 Mar 2026 05:14:08 +0000 Subject: [PATCH 1/7] Initial plan From 417829388c607e7f663aa1c83b55095b1b9169a7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 17 Mar 2026 05:25:01 +0000 Subject: [PATCH 2/7] Fix: Disallow include for models without relation fields (SelectIncludeOmit) Co-authored-by: jiashengguo <16688722+jiashengguo@users.noreply.github.com> --- packages/orm/src/client/crud-types.ts | 20 ++++++++++++----- tests/e2e/orm/schemas/typing/typecheck.ts | 27 ++++++++++++++++++++++- 2 files changed, 40 insertions(+), 7 deletions(-) diff --git a/packages/orm/src/client/crud-types.ts b/packages/orm/src/client/crud-types.ts index 230b162ff..f0d4470e7 100644 --- a/packages/orm/src/client/crud-types.ts +++ b/packages/orm/src/client/crud-types.ts @@ -968,12 +968,14 @@ export type SelectIncludeOmit< */ omit?: (OmitInput & ExtResultSelectOmitFields) | null; } & (AllowRelation extends true - ? { - /** - * Specifies relations to be included in the query result. All scalar fields are included. - */ - include?: IncludeInput | null; - } + ? HasRelations extends true + ? { + /** + * Specifies relations to be included in the query result. All scalar fields are included. + */ + include?: IncludeInput | null; + } + : {} : {}); export type SelectInput< @@ -2390,6 +2392,12 @@ type HasToManyRelations> = keyof { + [Key in RelationFields]: true; +} extends never + ? false + : true; + type EnumValue> = GetEnum[keyof GetEnum< Schema, Enum diff --git a/tests/e2e/orm/schemas/typing/typecheck.ts b/tests/e2e/orm/schemas/typing/typecheck.ts index f73d4927f..aba6de1ab 100644 --- a/tests/e2e/orm/schemas/typing/typecheck.ts +++ b/tests/e2e/orm/schemas/typing/typecheck.ts @@ -1,4 +1,4 @@ -import { ZenStackClient } from '@zenstackhq/orm'; +import { ZenStackClient, type FindManyArgs } from '@zenstackhq/orm'; import SQLite from 'better-sqlite3'; import { SqliteDialect } from 'kysely'; import { Role, Status, type Identity, type IdentityProvider } from './models'; @@ -679,3 +679,28 @@ function typeDefs() { } main(); + +// Type test: `include` should not be allowed for models without relations +{ + type NoRelationsSchema = { + provider: { type: 'sqlite' }; + models: { + Dummy: { + name: 'Dummy'; + fields: { + id: { name: 'id'; type: 'Int'; id: true }; + name: { name: 'name'; type: 'String' }; + }; + idFields: ['id']; + uniqueFields: { id: { type: 'Int' } }; + }; + }; + enums: {}; + typeDefs: {}; + plugins: {}; + }; + type DummyFindManyArgs = FindManyArgs; + // @ts-expect-error include should not be allowed for models without relations + const _testIncludeNotAllowed: DummyFindManyArgs = { include: { abcdefg: true } }; + void _testIncludeNotAllowed; +} From b9d581eab0dc67e6122b5f92b4a03f97cd5a68a6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 17 Mar 2026 06:13:46 +0000 Subject: [PATCH 3/7] Fix build errors caused by SelectIncludeOmit type change Co-authored-by: jiashengguo <16688722+jiashengguo@users.noreply.github.com> --- .../src/client/crud/dialects/lateral-join-dialect-base.ts | 6 +++--- packages/orm/src/client/crud/dialects/sqlite.ts | 4 ++-- packages/orm/src/client/executor/zenstack-query-executor.ts | 5 +++-- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/packages/orm/src/client/crud/dialects/lateral-join-dialect-base.ts b/packages/orm/src/client/crud/dialects/lateral-join-dialect-base.ts index 94f29b20a..cbcdbee30 100644 --- a/packages/orm/src/client/crud/dialects/lateral-join-dialect-base.ts +++ b/packages/orm/src/client/crud/dialects/lateral-join-dialect-base.ts @@ -245,12 +245,12 @@ export abstract class LateralJoinDialectBase extends B ); } - if (typeof payload === 'object' && payload.include && typeof payload.include === 'object') { + if (typeof payload === 'object' && (payload as any).include && typeof (payload as any).include === 'object') { // include relation fields Object.assign( objArgs, - ...Object.entries(payload.include) + ...Object.entries((payload as any).include) .filter(([, value]) => value) .map(([field]) => ({ [field]: eb.ref(`${parentResultName}$${field}.$data`), @@ -270,7 +270,7 @@ export abstract class LateralJoinDialectBase extends B ) { let result = query; if (typeof payload === 'object') { - const selectInclude = payload.include ?? payload.select; + const selectInclude = (payload as any).include ?? payload.select; if (selectInclude && typeof selectInclude === 'object') { Object.entries(selectInclude) .filter(([, value]) => value) diff --git a/packages/orm/src/client/crud/dialects/sqlite.ts b/packages/orm/src/client/crud/dialects/sqlite.ts index c302cc716..28935c7c7 100644 --- a/packages/orm/src/client/crud/dialects/sqlite.ts +++ b/packages/orm/src/client/crud/dialects/sqlite.ts @@ -307,10 +307,10 @@ export class SqliteCrudDialect extends BaseCrudDialect ); } - if (typeof payload === 'object' && payload.include && typeof payload.include === 'object') { + if (typeof payload === 'object' && (payload as any).include && typeof (payload as any).include === 'object') { // include relation fields objArgs.push( - ...Object.entries(payload.include) + ...Object.entries((payload as any).include) .filter(([, value]) => value) .map(([field, value]) => { const subJson = this.buildRelationJSON( diff --git a/packages/orm/src/client/executor/zenstack-query-executor.ts b/packages/orm/src/client/executor/zenstack-query-executor.ts index 186af02c3..55070fcfb 100644 --- a/packages/orm/src/client/executor/zenstack-query-executor.ts +++ b/packages/orm/src/client/executor/zenstack-query-executor.ts @@ -36,7 +36,7 @@ import { TransactionIsolationLevel, type ClientContract } from '../contract'; import { getCrudDialect } from '../crud/dialects'; import type { BaseCrudDialect } from '../crud/dialects/base-dialect'; import { createDBQueryError, createInternalError, ORMError } from '../errors'; -import type { AfterEntityMutationCallback, OnKyselyQueryCallback } from '../plugin'; +import type { AfterEntityMutationCallback, BeforeEntityMutationCallback, OnKyselyQueryCallback } from '../plugin'; import { requireIdFields, stripAlias } from '../query-utils'; import { QueryNameMapper } from './name-mapper'; import { TempAliasTransformer } from './temp-alias-transformer'; @@ -355,7 +355,8 @@ export class ZenStackQueryExecutor extends DefaultQueryExecutor { continue; } - await onEntityMutation.beforeEntityMutation({ + // tsc perf + await (onEntityMutation.beforeEntityMutation as BeforeEntityMutationCallback)({ model: mutationInfo.model, action: mutationInfo.action, queryNode, From 46ea0e2e987b29541790fcb10d86577e1b729c47 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 17 Mar 2026 07:07:32 +0000 Subject: [PATCH 4/7] Replace (payload as any).include casts with 'include' in payload narrowing checks Co-authored-by: jiashengguo <16688722+jiashengguo@users.noreply.github.com> --- .../src/client/crud/dialects/lateral-join-dialect-base.ts | 6 +++--- packages/orm/src/client/crud/dialects/sqlite.ts | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/orm/src/client/crud/dialects/lateral-join-dialect-base.ts b/packages/orm/src/client/crud/dialects/lateral-join-dialect-base.ts index cbcdbee30..6a12e9e6b 100644 --- a/packages/orm/src/client/crud/dialects/lateral-join-dialect-base.ts +++ b/packages/orm/src/client/crud/dialects/lateral-join-dialect-base.ts @@ -245,12 +245,12 @@ export abstract class LateralJoinDialectBase extends B ); } - if (typeof payload === 'object' && (payload as any).include && typeof (payload as any).include === 'object') { + if (typeof payload === 'object' && 'include' in payload && payload.include && typeof payload.include === 'object') { // include relation fields Object.assign( objArgs, - ...Object.entries((payload as any).include) + ...Object.entries(payload.include) .filter(([, value]) => value) .map(([field]) => ({ [field]: eb.ref(`${parentResultName}$${field}.$data`), @@ -270,7 +270,7 @@ export abstract class LateralJoinDialectBase extends B ) { let result = query; if (typeof payload === 'object') { - const selectInclude = (payload as any).include ?? payload.select; + const selectInclude = ('include' in payload ? payload.include : undefined) ?? payload.select; if (selectInclude && typeof selectInclude === 'object') { Object.entries(selectInclude) .filter(([, value]) => value) diff --git a/packages/orm/src/client/crud/dialects/sqlite.ts b/packages/orm/src/client/crud/dialects/sqlite.ts index 28935c7c7..1cd4a164d 100644 --- a/packages/orm/src/client/crud/dialects/sqlite.ts +++ b/packages/orm/src/client/crud/dialects/sqlite.ts @@ -307,10 +307,10 @@ export class SqliteCrudDialect extends BaseCrudDialect ); } - if (typeof payload === 'object' && (payload as any).include && typeof (payload as any).include === 'object') { + if (typeof payload === 'object' && 'include' in payload && payload.include && typeof payload.include === 'object') { // include relation fields objArgs.push( - ...Object.entries((payload as any).include) + ...Object.entries(payload.include) .filter(([, value]) => value) .map(([field, value]) => { const subJson = this.buildRelationJSON( From 1a5c4373cfb78e7a45658c73806509faef1e30f3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 17 Mar 2026 21:15:39 +0000 Subject: [PATCH 5/7] Simplify HasRelations using RelationFields extends never form Co-authored-by: ymc9 <104139426+ymc9@users.noreply.github.com> --- packages/orm/src/client/crud-types.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/orm/src/client/crud-types.ts b/packages/orm/src/client/crud-types.ts index f0d4470e7..225785e1e 100644 --- a/packages/orm/src/client/crud-types.ts +++ b/packages/orm/src/client/crud-types.ts @@ -2392,9 +2392,10 @@ type HasToManyRelations> = keyof { - [Key in RelationFields]: true; -} extends never +type HasRelations> = RelationFields< + Schema, + Model +> extends never ? false : true; From e077a4d0c33e64d969f515456e146845050fe46b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 17 Mar 2026 21:18:11 +0000 Subject: [PATCH 6/7] Remove unnecessary BeforeEntityMutationCallback cast after HasRelations simplification Co-authored-by: ymc9 <104139426+ymc9@users.noreply.github.com> --- packages/orm/src/client/executor/zenstack-query-executor.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/orm/src/client/executor/zenstack-query-executor.ts b/packages/orm/src/client/executor/zenstack-query-executor.ts index 55070fcfb..186af02c3 100644 --- a/packages/orm/src/client/executor/zenstack-query-executor.ts +++ b/packages/orm/src/client/executor/zenstack-query-executor.ts @@ -36,7 +36,7 @@ import { TransactionIsolationLevel, type ClientContract } from '../contract'; import { getCrudDialect } from '../crud/dialects'; import type { BaseCrudDialect } from '../crud/dialects/base-dialect'; import { createDBQueryError, createInternalError, ORMError } from '../errors'; -import type { AfterEntityMutationCallback, BeforeEntityMutationCallback, OnKyselyQueryCallback } from '../plugin'; +import type { AfterEntityMutationCallback, OnKyselyQueryCallback } from '../plugin'; import { requireIdFields, stripAlias } from '../query-utils'; import { QueryNameMapper } from './name-mapper'; import { TempAliasTransformer } from './temp-alias-transformer'; @@ -355,8 +355,7 @@ export class ZenStackQueryExecutor extends DefaultQueryExecutor { continue; } - // tsc perf - await (onEntityMutation.beforeEntityMutation as BeforeEntityMutationCallback)({ + await onEntityMutation.beforeEntityMutation({ model: mutationInfo.model, action: mutationInfo.action, queryNode, From 5292a9f927b6b75c484033e78f928d53087525e5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 18 Mar 2026 02:26:46 +0000 Subject: [PATCH 7/7] Restore BeforeEntityMutationCallback cast to fix TS2321 excessive stack depth in CI build Co-authored-by: ymc9 <104139426+ymc9@users.noreply.github.com> --- packages/orm/src/client/executor/zenstack-query-executor.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/orm/src/client/executor/zenstack-query-executor.ts b/packages/orm/src/client/executor/zenstack-query-executor.ts index 186af02c3..55070fcfb 100644 --- a/packages/orm/src/client/executor/zenstack-query-executor.ts +++ b/packages/orm/src/client/executor/zenstack-query-executor.ts @@ -36,7 +36,7 @@ import { TransactionIsolationLevel, type ClientContract } from '../contract'; import { getCrudDialect } from '../crud/dialects'; import type { BaseCrudDialect } from '../crud/dialects/base-dialect'; import { createDBQueryError, createInternalError, ORMError } from '../errors'; -import type { AfterEntityMutationCallback, OnKyselyQueryCallback } from '../plugin'; +import type { AfterEntityMutationCallback, BeforeEntityMutationCallback, OnKyselyQueryCallback } from '../plugin'; import { requireIdFields, stripAlias } from '../query-utils'; import { QueryNameMapper } from './name-mapper'; import { TempAliasTransformer } from './temp-alias-transformer'; @@ -355,7 +355,8 @@ export class ZenStackQueryExecutor extends DefaultQueryExecutor { continue; } - await onEntityMutation.beforeEntityMutation({ + // tsc perf + await (onEntityMutation.beforeEntityMutation as BeforeEntityMutationCallback)({ model: mutationInfo.model, action: mutationInfo.action, queryNode,