Skip to content

Commit 25440c6

Browse files
ymc9claude
andcommitted
fix(orm): disallow include on models without relation fields (#2488)
When a model has no relation fields, the `include` option in find args (findFirst, findMany, etc.) should not be available. Previously, `IncludeInput` resolved to `{}` for relation-less models, and TypeScript's structural typing allowed arbitrary keys to be passed. Use a mapped type in `SelectIncludeOmit` so the `include` key only exists when `RelationFields` is non-empty, avoiding the excessive stack depth that a conditional branch would cause. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent cb49667 commit 25440c6

8 files changed

Lines changed: 119 additions & 13 deletions

File tree

packages/orm/src/client/crud-types.ts

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -970,14 +970,16 @@ export type SelectIncludeOmit<
970970
* Explicitly omit fields from the query result.
971971
*/
972972
omit?: (OmitInput<Schema, Model> & ExtResultSelectOmitFields<ExtResult, Model & string>) | null;
973-
} & (AllowRelation extends true
974-
? {
975-
/**
976-
* Specifies relations to be included in the query result. All scalar fields are included.
977-
*/
978-
include?: IncludeInput<Schema, Model, Options, AllowCount, ExtResult> | null;
979-
}
980-
: {});
973+
} & {
974+
/**
975+
* Specifies relations to be included in the query result. All scalar fields are included.
976+
*/
977+
[K in AllowRelation extends true
978+
? RelationFields<Schema, Model> extends never
979+
? never
980+
: 'include'
981+
: never]?: IncludeInput<Schema, Model, Options, AllowCount, ExtResult> | null;
982+
};
981983

982984
export type SelectInput<
983985
Schema extends SchemaDef,

packages/orm/src/client/crud/dialects/lateral-join-dialect-base.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -245,12 +245,12 @@ export abstract class LateralJoinDialectBase<Schema extends SchemaDef> extends B
245245
);
246246
}
247247

248-
if (typeof payload === 'object' && payload.include && typeof payload.include === 'object') {
248+
if (typeof payload === 'object' && (payload as any).include && typeof (payload as any).include === 'object') {
249249
// include relation fields
250250

251251
Object.assign(
252252
objArgs,
253-
...Object.entries<any>(payload.include)
253+
...Object.entries<any>((payload as any).include)
254254
.filter(([, value]) => value)
255255
.map(([field]) => ({
256256
[field]: eb.ref(`${parentResultName}$${field}.$data`),
@@ -270,7 +270,7 @@ export abstract class LateralJoinDialectBase<Schema extends SchemaDef> extends B
270270
) {
271271
let result = query;
272272
if (typeof payload === 'object') {
273-
const selectInclude = payload.include ?? payload.select;
273+
const selectInclude = (payload as any).include ?? payload.select;
274274
if (selectInclude && typeof selectInclude === 'object') {
275275
Object.entries<any>(selectInclude)
276276
.filter(([, value]) => value)

packages/orm/src/client/crud/dialects/sqlite.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -307,10 +307,10 @@ export class SqliteCrudDialect<Schema extends SchemaDef> extends BaseCrudDialect
307307
);
308308
}
309309

310-
if (typeof payload === 'object' && payload.include && typeof payload.include === 'object') {
310+
if (typeof payload === 'object' && (payload as any).include && typeof (payload as any).include === 'object') {
311311
// include relation fields
312312
objArgs.push(
313-
...Object.entries<any>(payload.include)
313+
...Object.entries<any>((payload as any).include)
314314
.filter(([, value]) => value)
315315
.map(([field, value]) => {
316316
const subJson = this.buildRelationJSON(
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
//////////////////////////////////////////////////////////////////////////////////////////////
2+
// DO NOT MODIFY THIS FILE //
3+
// This file is automatically generated by ZenStack CLI and should not be manually updated. //
4+
//////////////////////////////////////////////////////////////////////////////////////////////
5+
6+
/* eslint-disable */
7+
8+
import { type SchemaType as $Schema } from "./schema";
9+
import type { FindManyArgs as $FindManyArgs, FindUniqueArgs as $FindUniqueArgs, FindFirstArgs as $FindFirstArgs, ExistsArgs as $ExistsArgs, 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, QueryOptions as $QueryOptions } from "@zenstackhq/orm";
10+
import type { SimplifiedPlainResult as $Result, SelectIncludeOmit as $SelectIncludeOmit } from "@zenstackhq/orm";
11+
export type DummyFindManyArgs = $FindManyArgs<$Schema, "Dummy">;
12+
export type DummyFindUniqueArgs = $FindUniqueArgs<$Schema, "Dummy">;
13+
export type DummyFindFirstArgs = $FindFirstArgs<$Schema, "Dummy">;
14+
export type DummyExistsArgs = $ExistsArgs<$Schema, "Dummy">;
15+
export type DummyCreateArgs = $CreateArgs<$Schema, "Dummy">;
16+
export type DummyCreateManyArgs = $CreateManyArgs<$Schema, "Dummy">;
17+
export type DummyCreateManyAndReturnArgs = $CreateManyAndReturnArgs<$Schema, "Dummy">;
18+
export type DummyUpdateArgs = $UpdateArgs<$Schema, "Dummy">;
19+
export type DummyUpdateManyArgs = $UpdateManyArgs<$Schema, "Dummy">;
20+
export type DummyUpdateManyAndReturnArgs = $UpdateManyAndReturnArgs<$Schema, "Dummy">;
21+
export type DummyUpsertArgs = $UpsertArgs<$Schema, "Dummy">;
22+
export type DummyDeleteArgs = $DeleteArgs<$Schema, "Dummy">;
23+
export type DummyDeleteManyArgs = $DeleteManyArgs<$Schema, "Dummy">;
24+
export type DummyCountArgs = $CountArgs<$Schema, "Dummy">;
25+
export type DummyAggregateArgs = $AggregateArgs<$Schema, "Dummy">;
26+
export type DummyGroupByArgs = $GroupByArgs<$Schema, "Dummy">;
27+
export type DummyWhereInput = $WhereInput<$Schema, "Dummy">;
28+
export type DummySelect = $SelectInput<$Schema, "Dummy">;
29+
export type DummyInclude = $IncludeInput<$Schema, "Dummy">;
30+
export type DummyOmit = $OmitInput<$Schema, "Dummy">;
31+
export type DummyGetPayload<Args extends $SelectIncludeOmit<$Schema, "Dummy", true>, Options extends $QueryOptions<$Schema> = $QueryOptions<$Schema>> = $Result<$Schema, "Dummy", Args, Options>;
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
//////////////////////////////////////////////////////////////////////////////////////////////
2+
// DO NOT MODIFY THIS FILE //
3+
// This file is automatically generated by ZenStack CLI and should not be manually updated. //
4+
//////////////////////////////////////////////////////////////////////////////////////////////
5+
6+
/* eslint-disable */
7+
8+
import { type SchemaType as $Schema } from "./schema";
9+
import type { ModelResult as $ModelResult } from "@zenstackhq/orm";
10+
export type Dummy = $ModelResult<$Schema, "Dummy">;
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { createTestClient } from '@zenstackhq/testtools';
2+
import { it, expect } from 'vitest';
3+
import { schema } from './schema';
4+
5+
// https://github.com/zenstackhq/zenstack/issues/2488
6+
// `include` should not be allowed on models without relation fields.
7+
8+
it('should not allow include on model with no relations', async () => {
9+
const db = await createTestClient(schema);
10+
11+
// @ts-expect-error - `include` should not be allowed on models without relations
12+
void db.dummy.findFirst({ include: { foo: true } });
13+
14+
// verify findFirst still works without include
15+
const result = await db.dummy.findFirst();
16+
expect(result).toBeNull();
17+
});
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
//////////////////////////////////////////////////////////////////////////////////////////////
2+
// DO NOT MODIFY THIS FILE //
3+
// This file is automatically generated by ZenStack CLI and should not be manually updated. //
4+
//////////////////////////////////////////////////////////////////////////////////////////////
5+
6+
/* eslint-disable */
7+
8+
import { type SchemaDef, type AttributeApplication, type FieldDefault, ExpressionUtils } from "@zenstackhq/schema";
9+
export class SchemaType implements SchemaDef {
10+
provider = {
11+
type: "sqlite"
12+
} as const;
13+
models = {
14+
Dummy: {
15+
name: "Dummy",
16+
fields: {
17+
id: {
18+
name: "id",
19+
type: "Int",
20+
id: true,
21+
attributes: [{ name: "@id" }, { name: "@default", args: [{ name: "value", value: ExpressionUtils.call("autoincrement") }] }] as readonly AttributeApplication[],
22+
default: ExpressionUtils.call("autoincrement") as FieldDefault
23+
},
24+
name: {
25+
name: "name",
26+
type: "String"
27+
}
28+
},
29+
idFields: ["id"],
30+
uniqueFields: {
31+
id: { type: "Int" }
32+
}
33+
}
34+
} as const;
35+
plugins = {};
36+
}
37+
export const schema = new SchemaType();
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
datasource db {
2+
provider = "sqlite"
3+
url = "file:./dev.db"
4+
}
5+
6+
model Dummy {
7+
id Int @id @default(autoincrement())
8+
name String
9+
}

0 commit comments

Comments
 (0)