diff --git a/packages/orm/src/client/crud-types.ts b/packages/orm/src/client/crud-types.ts index b92854807..d0e9ef940 100644 --- a/packages/orm/src/client/crud-types.ts +++ b/packages/orm/src/client/crud-types.ts @@ -2137,11 +2137,16 @@ type UpdateRelationFieldPayload< ? ToManyRelationUpdateInput : ToOneRelationUpdateInput; +// Variance-annotated for the same reason as `ToOneRelationUpdateInput` below: without the +// annotations, comparing two instantiations with different `Options` (e.g. a generated +// `XxxUpdateArgs`/`XxxUpsertArgs` type passed to a client method) makes TypeScript measure +// variance structurally through the recursive nested update/upsert types, which blows up into +// "Type instantiation is excessively deep and possibly infinite" (#2778). type ToManyRelationUpdateInput< - Schema extends SchemaDef, - Model extends GetModels, - Field extends RelationFields, - Options extends QueryOptions, + in out Schema extends SchemaDef, + in out Model extends GetModels, + in out Field extends RelationFields, + in out Options extends QueryOptions, > = Omit< { /** diff --git a/tests/regression/test/issue-2778/regression.test.ts b/tests/regression/test/issue-2778/regression.test.ts new file mode 100644 index 000000000..35043ed60 --- /dev/null +++ b/tests/regression/test/issue-2778/regression.test.ts @@ -0,0 +1,39 @@ +import { createTestClient } from '@zenstackhq/testtools'; +import type { UpdateArgs, UpsertArgs } from '@zenstackhq/orm'; +import path from 'node:path'; +import { describe, expect, it } from 'vitest'; +import { schema, type SchemaType } from './schema'; + +// https://github.com/zenstackhq/zenstack/issues/2778 + +// These mirror the types emitted by `zenstack generate` into `input.ts` +type TagUpsertArgs = UpsertArgs; +type TagUpdateArgs = UpdateArgs; + +describe('Regression for issue #2778', () => { + it('accepts generated UpsertArgs/UpdateArgs with many-to-many relations without deep instantiation', async () => { + const db = await createTestClient(schema, { + usePrismaPush: true, + schemaFile: path.join(__dirname, 'schema.zmodel'), + }); + + // passing the generated args type directly to the client method used to trigger + // "Type instantiation is excessively deep and possibly infinite" + const upsertTag = (args: TagUpsertArgs) => db.tag.upsert(args); + const updateTag = (args: TagUpdateArgs) => db.tag.update(args); + + const tag = await upsertTag({ + where: { name: 't1' }, + create: { name: 't1', products: { create: { name: 'p1' } } }, + update: { name: 't2' }, + }); + expect(tag.name).toBe('t1'); + + const updated = await updateTag({ + where: { id: tag.id }, + data: { name: 't3', products: { create: { name: 'p2' } } }, + }); + expect(updated.name).toBe('t3'); + await expect(db.product.count()).resolves.toBe(2); + }); +}); diff --git a/tests/regression/test/issue-2778/schema.ts b/tests/regression/test/issue-2778/schema.ts new file mode 100644 index 000000000..7b925b640 --- /dev/null +++ b/tests/regression/test/issue-2778/schema.ts @@ -0,0 +1,72 @@ +////////////////////////////////////////////////////////////////////////////////////////////// +// DO NOT MODIFY THIS FILE // +// This file is automatically generated by ZenStack CLI and should not be manually updated. // +////////////////////////////////////////////////////////////////////////////////////////////// + +/* eslint-disable */ + +import { type SchemaDef, type AttributeApplication, type FieldDefault, ExpressionUtils } from "@zenstackhq/schema"; +export class SchemaType implements SchemaDef { + provider = { + type: "sqlite" + } as const; + models = { + Tag: { + name: "Tag", + fields: { + id: { + name: "id", + type: "String", + id: true, + attributes: [{ name: "@id" }, { name: "@default", args: [{ name: "value", value: ExpressionUtils.call("uuid") }] }] as readonly AttributeApplication[], + default: ExpressionUtils.call("uuid") as FieldDefault + }, + name: { + name: "name", + type: "String", + unique: true, + attributes: [{ name: "@unique" }] as readonly AttributeApplication[] + }, + products: { + name: "products", + type: "Product", + array: true, + relation: { opposite: "tags" } + } + }, + idFields: ["id"], + uniqueFields: { + id: { type: "String" }, + name: { type: "String" } + } + }, + Product: { + name: "Product", + fields: { + id: { + name: "id", + type: "String", + id: true, + attributes: [{ name: "@id" }, { name: "@default", args: [{ name: "value", value: ExpressionUtils.call("uuid") }] }] as readonly AttributeApplication[], + default: ExpressionUtils.call("uuid") as FieldDefault + }, + name: { + name: "name", + type: "String" + }, + tags: { + name: "tags", + type: "Tag", + array: true, + relation: { opposite: "products" } + } + }, + idFields: ["id"], + uniqueFields: { + id: { type: "String" } + } + } + } as const; + plugins = {}; +} +export const schema = new SchemaType(); diff --git a/tests/regression/test/issue-2778/schema.zmodel b/tests/regression/test/issue-2778/schema.zmodel new file mode 100644 index 000000000..5b90d8f76 --- /dev/null +++ b/tests/regression/test/issue-2778/schema.zmodel @@ -0,0 +1,16 @@ +datasource db { + provider = 'sqlite' + url = 'file:./dev.db' +} + +model Tag { + id String @id @default(uuid()) + name String @unique + products Product[] +} + +model Product { + id String @id @default(uuid()) + name String + tags Tag[] +}