Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 27 additions & 22 deletions packages/orm/src/client/contract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,15 +28,20 @@ import type {
FindUniqueArgs,
GroupByArgs,
GroupByResult,
OmitWhere,
ProcedureFunc,
SelectSubset,
SelectSubsetWithWhere,
SimplifiedPlainResult,
Subset,
SubsetWithWhere,
TypeDefResult,
UpdateArgs,
UpdateManyAndReturnArgs,
UpdateManyArgs,
UpsertArgs,
WhereInput,
WhereUniqueInput,
} from './crud-types';
import type { Diagnostics } from './diagnostics';
import type { ClientOptions, QueryOptions } from './options';
Expand Down Expand Up @@ -405,8 +410,8 @@ export type AllModelOperations<
* });
* ```
*/
updateManyAndReturn<T extends UpdateManyAndReturnArgs<Schema, Model, Options, ExtQueryArgs, ExtResult>>(
args: Subset<T, UpdateManyAndReturnArgs<Schema, Model, Options, ExtQueryArgs, ExtResult>>,
updateManyAndReturn<T extends OmitWhere<UpdateManyAndReturnArgs<Schema, Model, Options, ExtQueryArgs, ExtResult>>>(
args: { where?: WhereInput<Schema, Model, Options> } & SubsetWithWhere<T, OmitWhere<UpdateManyAndReturnArgs<Schema, Model, Options, ExtQueryArgs, ExtResult>>>,
): ZenStackPromise<Schema, SimplifiedPlainResult<Schema, Model, T, Options, ExtResult>[]>;
});

Expand Down Expand Up @@ -498,8 +503,8 @@ type CommonModelOperations<
* }); // result: `{ _count: { posts: number } }`
* ```
*/
findMany<T extends FindManyArgs<Schema, Model, Options, ExtQueryArgs, ExtResult>>(
args?: SelectSubset<T, FindManyArgs<Schema, Model, Options, ExtQueryArgs, ExtResult>>,
findMany<T extends OmitWhere<FindManyArgs<Schema, Model, Options, ExtQueryArgs, ExtResult>>>(
args?: { where?: WhereInput<Schema, Model, Options> } & SelectSubset<T, FindManyArgs<Schema, Model, Options, ExtQueryArgs, ExtResult>>,
): ZenStackPromise<Schema, SimplifiedPlainResult<Schema, Model, T, Options, ExtResult>[]>;

/**
Expand All @@ -508,8 +513,8 @@ type CommonModelOperations<
* @returns a single entity or null if not found
* @see {@link findMany}
*/
findUnique<T extends FindUniqueArgs<Schema, Model, Options, ExtQueryArgs, ExtResult>>(
args: SelectSubset<T, FindUniqueArgs<Schema, Model, Options, ExtQueryArgs, ExtResult>>,
findUnique<T extends OmitWhere<FindUniqueArgs<Schema, Model, Options, ExtQueryArgs, ExtResult>>>(
args: { where: WhereUniqueInput<Schema, Model, Options> } & SelectSubset<T, FindUniqueArgs<Schema, Model, Options, ExtQueryArgs, ExtResult>>,
): ZenStackPromise<Schema, SimplifiedPlainResult<Schema, Model, T, Options, ExtResult> | null>;

/**
Expand All @@ -518,8 +523,8 @@ type CommonModelOperations<
* @returns a single entity
* @see {@link findMany}
*/
findUniqueOrThrow<T extends FindUniqueArgs<Schema, Model, Options, ExtQueryArgs, ExtResult>>(
args: SelectSubset<T, FindUniqueArgs<Schema, Model, Options, ExtQueryArgs, ExtResult>>,
findUniqueOrThrow<T extends OmitWhere<FindUniqueArgs<Schema, Model, Options, ExtQueryArgs, ExtResult>>>(
args: { where: WhereUniqueInput<Schema, Model, Options> } & SelectSubset<T, FindUniqueArgs<Schema, Model, Options, ExtQueryArgs, ExtResult>>,
): ZenStackPromise<Schema, SimplifiedPlainResult<Schema, Model, T, Options, ExtResult>>;

/**
Expand All @@ -528,8 +533,8 @@ type CommonModelOperations<
* @returns a single entity or null if not found
* @see {@link findMany}
*/
findFirst<T extends FindFirstArgs<Schema, Model, Options, ExtQueryArgs, ExtResult>>(
args?: SelectSubset<T, FindFirstArgs<Schema, Model, Options, ExtQueryArgs, ExtResult>>,
findFirst<T extends OmitWhere<FindFirstArgs<Schema, Model, Options, ExtQueryArgs, ExtResult>>>(
args?: { where?: WhereInput<Schema, Model, Options> } & SelectSubset<T, FindFirstArgs<Schema, Model, Options, ExtQueryArgs, ExtResult>>,
): ZenStackPromise<Schema, SimplifiedPlainResult<Schema, Model, T, Options, ExtResult> | null>;

/**
Expand All @@ -538,8 +543,8 @@ type CommonModelOperations<
* @returns a single entity
* @see {@link findMany}
*/
findFirstOrThrow<T extends FindFirstArgs<Schema, Model, Options, ExtQueryArgs, ExtResult>>(
args?: SelectSubset<T, FindFirstArgs<Schema, Model, Options, ExtQueryArgs, ExtResult>>,
findFirstOrThrow<T extends OmitWhere<FindFirstArgs<Schema, Model, Options, ExtQueryArgs, ExtResult>>>(
args?: { where?: WhereInput<Schema, Model, Options> } & SelectSubset<T, FindFirstArgs<Schema, Model, Options, ExtQueryArgs, ExtResult>>,
): ZenStackPromise<Schema, SimplifiedPlainResult<Schema, Model, T, Options, ExtResult>>;

/**
Expand Down Expand Up @@ -744,8 +749,8 @@ type CommonModelOperations<
* });
* ```
*/
update<T extends UpdateArgs<Schema, Model, Options, ExtQueryArgs, ExtResult>>(
args: SelectSubset<T, UpdateArgs<Schema, Model, Options, ExtQueryArgs, ExtResult>>,
update<T extends OmitWhere<UpdateArgs<Schema, Model, Options, ExtQueryArgs, ExtResult>>>(
args: { where: WhereUniqueInput<Schema, Model, Options> } & SelectSubsetWithWhere<T, OmitWhere<UpdateArgs<Schema, Model, Options, ExtQueryArgs, ExtResult>>>,
): ZenStackPromise<Schema, SimplifiedPlainResult<Schema, Model, T, Options, ExtResult>>;

/**
Expand All @@ -768,8 +773,8 @@ type CommonModelOperations<
* limit: 10
* });
*/
updateMany<T extends UpdateManyArgs<Schema, Model, Options, ExtQueryArgs>>(
args: Subset<T, UpdateManyArgs<Schema, Model, Options, ExtQueryArgs>>,
updateMany<T extends OmitWhere<UpdateManyArgs<Schema, Model, Options, ExtQueryArgs>>>(
args: { where?: WhereInput<Schema, Model, Options> } & SubsetWithWhere<T, OmitWhere<UpdateManyArgs<Schema, Model, Options, ExtQueryArgs>>>,
): ZenStackPromise<Schema, BatchResult>;

/**
Expand All @@ -792,8 +797,8 @@ type CommonModelOperations<
* });
* ```
*/
upsert<T extends UpsertArgs<Schema, Model, Options, ExtQueryArgs, ExtResult>>(
args: SelectSubset<T, UpsertArgs<Schema, Model, Options, ExtQueryArgs, ExtResult>>,
upsert<T extends OmitWhere<UpsertArgs<Schema, Model, Options, ExtQueryArgs, ExtResult>>>(
args: { where: WhereUniqueInput<Schema, Model, Options> } & SelectSubsetWithWhere<T, OmitWhere<UpsertArgs<Schema, Model, Options, ExtQueryArgs, ExtResult>>>,
): ZenStackPromise<Schema, SimplifiedPlainResult<Schema, Model, T, Options, ExtResult>>;

/**
Expand All @@ -815,8 +820,8 @@ type CommonModelOperations<
* }); // result: `{ id: string; email: string }`
* ```
*/
delete<T extends DeleteArgs<Schema, Model, Options, ExtQueryArgs, ExtResult>>(
args: SelectSubset<T, DeleteArgs<Schema, Model, Options, ExtQueryArgs, ExtResult>>,
delete<T extends OmitWhere<DeleteArgs<Schema, Model, Options, ExtQueryArgs, ExtResult>>>(
args: { where: WhereUniqueInput<Schema, Model, Options> } & SelectSubset<T, DeleteArgs<Schema, Model, Options, ExtQueryArgs, ExtResult>>,
): ZenStackPromise<Schema, SimplifiedPlainResult<Schema, Model, T, Options, ExtResult>>;

/**
Expand All @@ -838,8 +843,8 @@ type CommonModelOperations<
* });
* ```
*/
deleteMany<T extends DeleteManyArgs<Schema, Model, Options, ExtQueryArgs>>(
args?: Subset<T, DeleteManyArgs<Schema, Model, Options, ExtQueryArgs>>,
deleteMany<T extends OmitWhere<DeleteManyArgs<Schema, Model, Options, ExtQueryArgs>>>(
args?: { where?: WhereInput<Schema, Model, Options> } & Subset<T, DeleteManyArgs<Schema, Model, Options, ExtQueryArgs>>,
): ZenStackPromise<Schema, BatchResult>;

/**
Expand Down
52 changes: 52 additions & 0 deletions packages/orm/src/client/crud-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -347,6 +347,22 @@ export type WhereInput<
AND?: OrArray<WhereInput<Schema, Model, Options, ScalarOnly>>;
OR?: WhereInput<Schema, Model, Options, ScalarOnly>[];
NOT?: OrArray<WhereInput<Schema, Model, Options, ScalarOnly>>;
} & (IsDelegateModel<Schema, Model> extends true
? { $is?: SubModelWhereInput<Schema, Model, Options, ScalarOnly> }
: object);

/**
* Where filter that targets a specific sub-model of a delegate (polymorphic) base model.
* Keys are direct sub-model names; values are `WhereInput` for that sub-model.
* Multiple sub-model entries are combined with OR semantics.
*/
export type SubModelWhereInput<
Schema extends SchemaDef,
Model extends GetModels<Schema>,
Options extends QueryOptions<Schema> = QueryOptions<Schema>,
ScalarOnly extends boolean = false,
> = {
[SubModel in GetSubModels<Schema, Model>]?: WhereInput<Schema, SubModel, Options, ScalarOnly> | null;
};

type FieldFilter<
Expand Down Expand Up @@ -1088,6 +1104,42 @@ export type SelectSubset<T, U> = {
? 'Please either choose `select` or `omit`.'
: {});

/**
* Strips the `where` field from an args type so the remaining fields can be used as
* the generic type parameter `T` in CRUD methods, allowing `where` to be typed directly
* and benefit from TypeScript's excess property checking.
* @internal
*/
export type OmitWhere<T> = Omit<T, 'where'>;

/**
* Like {@link Subset} but maps the `where` key to `unknown` (instead of `never`) when
* `where` is not present in `U`. This is used in CRUD method signatures where `where`
* is separately typed as `{ where: WhereXxxInput }`: because TypeScript infers T from
* the full argument object (including the `where` field), a naive `Subset<T, OmitWhere<U>>`
* would produce `where: never` in the mapped result, collapsing the `where` type in the
* intersection to `never`. Mapping to `unknown` instead gives
* `{ where: W } & { where: unknown }` = `{ where: W }`, preserving both the correct type
* and TypeScript's excess-property checking on `where`.
* @internal
*/
export type SubsetWithWhere<T, U> = {
[key in keyof T]: key extends keyof U ? T[key] : key extends 'where' ? unknown : never;
};

/**
* Like {@link SelectSubset} but maps the `where` key to `unknown` (instead of `never`) when
* `where` is not present in `U`. See {@link SubsetWithWhere} for the rationale.
* @internal
*/
export type SelectSubsetWithWhere<T, U> = {
[key in keyof T]: key extends keyof U ? T[key] : key extends 'where' ? unknown : never;
} & (T extends { select: any; include: any }
? 'Please either choose `select` or `include`.'
: T extends { select: any; omit: any }
? 'Please either choose `select` or `omit`.'
: {});

type ToManyRelationFilter<
Schema extends SchemaDef,
Model extends GetModels<Schema>,
Expand Down
59 changes: 59 additions & 0 deletions packages/orm/src/client/crud/dialects/base-dialect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
ensureArray,
flattenCompoundUniqueFilters,
getDelegateDescendantModels,
getDiscriminatorField,
getManyToManyRelation,
getModelFields,
getRelationForeignKeyFieldPairs,
Expand Down Expand Up @@ -224,6 +225,11 @@ export abstract class BaseCrudDialect<Schema extends SchemaDef> {
result = this.and(result, _where['$expr'](this.eb));
}

// handle $is filter for delegate (polymorphic) base models
if ('$is' in _where && _where['$is'] != null && typeof _where['$is'] === 'object') {
result = this.and(result, this.buildIsFilter(model, modelAlias, _where['$is'] as Record<string, any>));
}

return result;
}

Expand Down Expand Up @@ -319,6 +325,59 @@ export abstract class BaseCrudDialect<Schema extends SchemaDef> {
.exhaustive();
}

/**
* Builds a filter expression for the `$is` operator on a delegate (polymorphic) base model.
* Each key in `payload` is a direct sub-model name; the value is an optional `WhereInput` for
* that sub-model. Multiple sub-model entries are combined with OR semantics.
*/
private buildIsFilter(model: string, modelAlias: string, payload: Record<string, any>): Expression<SqlBool> {
const discriminatorField = getDiscriminatorField(this.schema, model);
if (!discriminatorField) {
throw createInvalidInputError(
`"$is" filter is only supported on delegate models; "${model}" is not a delegate model. ` +
`Only models with a @@delegate attribute support the "$is" filter.`,
);
}

const discriminatorFieldDef = requireField(this.schema, model, discriminatorField);
const discriminatorTableAlias = discriminatorFieldDef.originModel ?? modelAlias;
const discriminatorRef = this.eb.ref(`${discriminatorTableAlias}.${discriminatorField}`);

const conditions: Expression<SqlBool>[] = [];

for (const [subModelName, subWhere] of Object.entries(payload)) {
// discriminator must equal the sub-model name
const discriminatorCheck = this.eb(discriminatorRef, '=', subModelName);

if (subWhere == null || (typeof subWhere === 'object' && Object.keys(subWhere).length === 0)) {
// no sub-model field filter — just check the discriminator
conditions.push(discriminatorCheck);
} else {
// build a correlated EXISTS subquery for sub-model-specific field filters
const subAlias = tmpAlias(`${modelAlias}__is__${subModelName}`);
const idFields = requireIdFields(this.schema, model);

// correlate sub-model rows to the outer model rows via primary key
const joinConditions = idFields.map((idField) =>
this.eb(this.eb.ref(`${subAlias}.${idField}`), '=', this.eb.ref(`${modelAlias}.${idField}`)),
);

const subWhereFilter = this.buildFilter(subModelName, subAlias, subWhere);

const existsSubquery = this.eb
.selectFrom(`${subModelName} as ${subAlias}`)
.select(this.eb.lit(1).as('__exists'))
.where(this.and(...joinConditions, subWhereFilter));

conditions.push(this.and(discriminatorCheck, this.eb.exists(existsSubquery)));
Comment on lines +365 to +372
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Build the $is EXISTS subquery from buildSelectModel() and buildExistsExpression().

subWhere is the full WhereInput of the sub-model, so it can reference inherited base fields and relations. With selectFrom(\${subModelName} as ${subAlias}`), aliases like Asset/Videoare never joined, so a valid filter such as$is: { Video: { viewCount: { gt: 0 } } }will generate broken refs. Also,this.eb.exists(...)` bypasses the dialect hook that handles same-table EXISTS rewrites.

🛠 Proposed fix
-                const existsSubquery = this.eb
-                    .selectFrom(`${subModelName} as ${subAlias}`)
+                const existsSubquery = this.buildSelectModel(subModelName, subAlias)
                     .select(this.eb.lit(1).as('__exists'))
                     .where(this.and(...joinConditions, subWhereFilter));
 
-                conditions.push(this.and(discriminatorCheck, this.eb.exists(existsSubquery)));
+                conditions.push(this.and(discriminatorCheck, this.buildExistsExpression(existsSubquery)));
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const subWhereFilter = this.buildFilter(subModelName, subAlias, subWhere);
const existsSubquery = this.eb
.selectFrom(`${subModelName} as ${subAlias}`)
.select(this.eb.lit(1).as('__exists'))
.where(this.and(...joinConditions, subWhereFilter));
conditions.push(this.and(discriminatorCheck, this.eb.exists(existsSubquery)));
const subWhereFilter = this.buildFilter(subModelName, subAlias, subWhere);
const existsSubquery = this.buildSelectModel(subModelName, subAlias)
.select(this.eb.lit(1).as('__exists'))
.where(this.and(...joinConditions, subWhereFilter));
conditions.push(this.and(discriminatorCheck, this.buildExistsExpression(existsSubquery)));
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/orm/src/client/crud/dialects/base-dialect.ts` around lines 344 -
351, The current $is EXISTS subquery builds a manual selectFrom and uses
this.eb.exists, which prevents proper alias joins for inherited fields and skips
dialect EXISTS rewrites; instead, call buildSelectModel(subModelName, subAlias,
subWhere) to produce the subquery (so inherited fields and relation aliases are
resolved) and then wrap that subquery with buildExistsExpression(subquery)
rather than this.eb.exists; replace the selectFrom/select(this.eb.lit(1)) block
and this.eb.exists call with these two helpers (referencing buildFilter only if
needed for composing subWhere) so the discriminatorCheck +
buildExistsExpression(subquery) is pushed into conditions.

}
}

if (conditions.length === 0) return this.true();
if (conditions.length === 1) return conditions[0]!;
return this.or(...conditions);
}

private buildRelationFilter(model: string, modelAlias: string, field: string, fieldDef: FieldDef, payload: any) {
if (!fieldDef.array) {
return this.buildToOneRelationFilter(model, modelAlias, field, fieldDef, payload);
Expand Down
17 changes: 17 additions & 0 deletions packages/orm/src/client/zod/factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -484,6 +484,23 @@ export class ZodSchemaFactory<
// expression builder
fields['$expr'] = z.custom((v) => typeof v === 'function', { error: '"$expr" must be a function' }).optional();

// $is sub-model filter for delegate (polymorphic) base models
const modelDef = requireModel(this.schema, model);
if (modelDef.isDelegate && modelDef.subModels && modelDef.subModels.length > 0) {
const subModelSchema = z.object(
Object.fromEntries(
modelDef.subModels.map((subModel) => [
subModel,
z
.lazy(() => this.makeWhereSchema(subModel, false, false, false, options))
.nullish()
.optional(),
]),
),
);
fields['$is'] = subModelSchema.optional();
Comment on lines +490 to +501
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cat -n packages/orm/src/client/zod/factory.ts | sed -n '485,505p'

Repository: zenstackhq/zenstack

Length of output: 1123


🏁 Script executed:

rg -n "subModelSchema\|strictObject" packages/orm/src/client/zod/factory.ts -A 2 -B 2

Repository: zenstackhq/zenstack

Length of output: 45


🏁 Script executed:

rg -n "\$is" packages/orm/src/client/crud/dialects/base-dialect.ts -A 5 -B 2

Repository: zenstackhq/zenstack

Length of output: 45


🏁 Script executed:

rg -n "z\.object|z\.strictObject" packages/orm/src/client/zod/factory.ts -B 1 -A 1 | head -100

Repository: zenstackhq/zenstack

Length of output: 3776


🏁 Script executed:

fd -type f -name "*.ts" -path "*dialects*" | head -10

Repository: zenstackhq/zenstack

Length of output: 233


🏁 Script executed:

rg -n "\\\$is" packages/orm/src/client/crud/ -A 3 -B 1 | head -80

Repository: zenstackhq/zenstack

Length of output: 1984


🏁 Script executed:

rg -n "z\.object\(" packages/orm/src/client/zod/factory.ts

Repository: zenstackhq/zenstack

Length of output: 111


🏁 Script executed:

rg -n "buildIsFilter" packages/orm/src/client/crud/dialects/base-dialect.ts -A 30 | head -80

Repository: zenstackhq/zenstack

Length of output: 3365


🏁 Script executed:

rg -n "createInvalidInputError" packages/orm/src/client/crud/dialects/base-dialect.ts -B 2

Repository: zenstackhq/zenstack

Length of output: 1781


🏁 Script executed:

rg -n "buildIsFilter" packages/orm/src/client/crud/dialects/base-dialect.ts -A 50 | tail -40

Repository: zenstackhq/zenstack

Length of output: 2244


🏁 Script executed:

cat -n packages/orm/src/client/crud/dialects/base-dialect.ts | sed -n '312,360p'

Repository: zenstackhq/zenstack

Length of output: 2975


Use z.strictObject(...) to reject typos in sub-model names instead of silently widening the query.

z.object(...) strips unknown keys. With a typo like $is: { Vidoe: {} } instead of $is: { Video: {} }, the unknown key is stripped, the payload becomes empty, and buildIsFilter returns this.true() at line 355 of base-dialect.ts—turning the typo into a match-all filter instead of a validation error. All other schemas in this file use z.strictObject(...).

🔒 Proposed fix
-            const subModelSchema = z.object(
+            const subModelSchema = z.strictObject(
                 Object.fromEntries(
                     modelDef.subModels.map((subModel) => [
                         subModel,
                         z
                             .lazy(() => this.makeWhereSchema(subModel, false, false, false, options))
                             .nullish()
                             .optional(),
                     ]),
                 ),
             );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/orm/src/client/zod/factory.ts` around lines 490 - 501, Replace the
permissive z.object used to build the subModelSchema with z.strictObject so
unknown sub-model keys are rejected (preventing typos like Vidoe from being
stripped); specifically change the creation of subModelSchema (where you call
z.object(Object.fromEntries(modelDef.subModels.map(...)) ) ) to use
z.strictObject and keep the existing z.lazy(() => this.makeWhereSchema(subModel,
false, false, false, options)).nullish().optional() entries, and ensure
fields['$is'] is still set to subModelSchema.optional() so buildIsFilter no
longer receives a silently-widened payload.

}

// logical operators
fields['AND'] = this.orArray(
z.lazy(() => this.makeWhereSchema(model, false, withoutRelationFields, false, options)),
Expand Down
63 changes: 63 additions & 0 deletions tests/e2e/orm/client-api/delegate.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -557,6 +557,69 @@ describe('Delegate model tests ', () => {
}),
).toResolveFalsy();
});

it('works with $is sub-model filter on base model', async () => {
// add an Image so we can test OR semantics of $is
await client.image.create({
data: { format: 'png', viewCount: 2 },
});

// $is: { Video: {} } — all assets that are Videos (2 RatedVideos)
await expect(
client.asset.findMany({
where: { $is: { Video: {} } },
}),
).toResolveWithLength(2);

// $is: { Video: null } — null value also means "is a Video"
await expect(
client.asset.findMany({
where: { $is: { Video: null } },
}),
).toResolveWithLength(2);

// $is: { Video: { duration: { gt: 100 } } } — only v2
await expect(
client.asset.findMany({
where: { $is: { Video: { duration: { gt: 100 } } } },
}),
).toResolveWithLength(1);

// $is combined with base-model field filter
await expect(
client.asset.findMany({
where: { viewCount: { gt: 0 }, $is: { Video: { duration: { gt: 100 } } } },
}),
).toResolveWithLength(1);

// $is: { Video: { duration: { gte: 100 } } } — both videos
await expect(
client.asset.findMany({
where: { $is: { Video: { duration: { gte: 100 } } } },
}),
).toResolveWithLength(2);

// $is with multiple sub-models → OR semantics (1 Video with viewCount>0 OR the Image)
await expect(
client.asset.findMany({
where: { $is: { Video: { duration: { gt: 100 } }, Image: { format: 'png' } } },
}),
).toResolveWithLength(2);

// $is on Video (which is itself a delegate) — filter on its sub-model RatedVideo
await expect(
client.video.findMany({
where: { $is: { RatedVideo: { rating: 5 } } },
}),
).toResolveWithLength(1);

// nested $is: Asset.$is.Video.$is.RatedVideo
await expect(
client.asset.findMany({
where: { $is: { Video: { $is: { RatedVideo: { rating: 5 } } } } },
}),
).toResolveWithLength(1);
});
});

describe('Delegate update tests', () => {
Expand Down
Loading
Loading