From da4f6c07118721442c0c6cdbfaada440a6e1fad5 Mon Sep 17 00:00:00 2001 From: sanny-io Date: Thu, 8 Jan 2026 03:03:59 +0000 Subject: [PATCH 01/22] feat: `ignore` argument for `@updatedAt` --- packages/language/res/stdlib.zmodel | 2 +- packages/orm/src/client/crud-types.ts | 3 ++- packages/orm/src/client/crud/operations/base.ts | 6 +++++- packages/schema/src/schema.ts | 8 ++++++-- packages/sdk/src/ts-schema-generator.ts | 14 ++++++++++++-- 5 files changed, 26 insertions(+), 7 deletions(-) diff --git a/packages/language/res/stdlib.zmodel b/packages/language/res/stdlib.zmodel index 81d52dc95..2394b55b0 100644 --- a/packages/language/res/stdlib.zmodel +++ b/packages/language/res/stdlib.zmodel @@ -403,7 +403,7 @@ attribute @omit() /** * Automatically stores the time when a record was last updated. */ -attribute @updatedAt() @@@targetField([DateTimeField]) @@@prisma +attribute @updatedAt(_ ignore: FieldReference[]?) @@@targetField([DateTimeField]) @@@prisma /** * Add full text index (MySQL only). diff --git a/packages/orm/src/client/crud-types.ts b/packages/orm/src/client/crud-types.ts index 471c43619..8c0074b36 100644 --- a/packages/orm/src/client/crud-types.ts +++ b/packages/orm/src/client/crud-types.ts @@ -30,6 +30,7 @@ import type { SchemaDef, TypeDefFieldIsArray, TypeDefFieldIsOptional, + UpdatedAtInfo, } from '../schema'; import type { AtLeast, @@ -980,7 +981,7 @@ type OptionalFieldsForCreate extends true ? Key - : GetModelField['updatedAt'] extends true + : GetModelField['updatedAt'] extends (true | UpdatedAtInfo) ? Key : never]: GetModelField; }; diff --git a/packages/orm/src/client/crud/operations/base.ts b/packages/orm/src/client/crud/operations/base.ts index fd32aa723..45757d220 100644 --- a/packages/orm/src/client/crud/operations/base.ts +++ b/packages/orm/src/client/crud/operations/base.ts @@ -994,11 +994,15 @@ export abstract class BaseOperationHandler { const autoUpdatedFields: string[] = []; for (const [fieldName, fieldDef] of Object.entries(modelDef.fields)) { if (fieldDef.updatedAt) { + const updatedScalarFieldsArr = Object.keys(data).filter((field) => isScalarField(this.schema, modelDef.name, field)); + const ignoredFieldsSet = new Set(typeof fieldDef.updatedAt === 'boolean' ? [] : fieldDef.updatedAt.ignore); if (finalData === data) { finalData = clone(data); } - finalData[fieldName] = this.dialect.transformPrimitive(new Date(), 'DateTime', false); + if (updatedScalarFieldsArr.some((field) => !ignoredFieldsSet.has(field))) { + finalData[fieldName] = this.dialect.transformPrimitive(new Date(), 'DateTime', false); autoUpdatedFields.push(fieldName); + } } } diff --git a/packages/schema/src/schema.ts b/packages/schema/src/schema.ts index 83640c357..214b29b08 100644 --- a/packages/schema/src/schema.ts +++ b/packages/schema/src/schema.ts @@ -59,6 +59,10 @@ export type RelationInfo = { onUpdate?: CascadeAction; }; +export type UpdatedAtInfo = { + ignore?: readonly string[], +}; + export type FieldDef = { name: string; type: string; @@ -66,7 +70,7 @@ export type FieldDef = { array?: boolean; optional?: boolean; unique?: boolean; - updatedAt?: boolean; + updatedAt?: boolean | UpdatedAtInfo; attributes?: readonly AttributeApplication[]; default?: MappedBuiltinType | Expression | readonly unknown[]; omit?: boolean; @@ -281,7 +285,7 @@ export type FieldHasDefault< Field extends GetModelFields, > = GetModelField['default'] extends object | number | string | boolean ? true - : GetModelField['updatedAt'] extends true + : GetModelField['updatedAt'] extends (true | UpdatedAtInfo) ? true : GetModelField['relation'] extends { hasDefault: true } ? true diff --git a/packages/sdk/src/ts-schema-generator.ts b/packages/sdk/src/ts-schema-generator.ts index c24436952..68ce170e9 100644 --- a/packages/sdk/src/ts-schema-generator.ts +++ b/packages/sdk/src/ts-schema-generator.ts @@ -563,8 +563,18 @@ export class TsSchemaGenerator { objectFields.push(ts.factory.createPropertyAssignment('array', ts.factory.createTrue())); } - if (hasAttribute(field, '@updatedAt')) { - objectFields.push(ts.factory.createPropertyAssignment('updatedAt', ts.factory.createTrue())); + const updatedAtAttrib = getAttribute(field, '@updatedAt') as DataFieldAttribute | undefined; + + if (updatedAtAttrib) { + const ignoreArg = updatedAtAttrib.args.find(arg => arg.name === 'ignore'); + objectFields.push(ts.factory.createPropertyAssignment('updatedAt', ignoreArg + ? ts.factory.createObjectLiteralExpression([ + ts.factory.createPropertyAssignment('ignore', ts.factory.createArrayLiteralExpression( + (ignoreArg.value as ArrayExpr).items.map((item) => ts.factory.createStringLiteral((item as ReferenceExpr).target.$refText)) + )) + ]) + : ts.factory.createTrue() + )); } if (hasAttribute(field, '@omit')) { From 1ed65652a08e7c3450fe3cbf872ff8af8d72ca1b Mon Sep 17 00:00:00 2001 From: sanny-io Date: Thu, 8 Jan 2026 03:04:15 +0000 Subject: [PATCH 02/22] chore: add tests --- packages/cli/test/ts-schema-gen.test.ts | 282 ++++++++++++++++++++++++ tests/e2e/orm/client-api/update.test.ts | 52 +++++ tests/e2e/orm/schemas/basic/schema.ts | 7 +- 3 files changed, 340 insertions(+), 1 deletion(-) diff --git a/packages/cli/test/ts-schema-gen.test.ts b/packages/cli/test/ts-schema-gen.test.ts index 2673567d0..53cf11172 100644 --- a/packages/cli/test/ts-schema-gen.test.ts +++ b/packages/cli/test/ts-schema-gen.test.ts @@ -442,4 +442,286 @@ model User { expect(schemaLite!.models.User.fields.id.attributes).toBeUndefined(); expect(schemaLite!.models.User.fields.email.attributes).toBeUndefined(); }); + + it('supports ignorable fields for @updatedAt', async () => { + const { schema } = await generateTsSchema(` +model User { + id String @id @default(uuid()) + name String + email String @unique + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt(ignore: [email]) + posts Post[] + + @@map('users') +} + +model Post { + id String @id @default(cuid()) + title String + published Boolean @default(false) + author User @relation(fields: [authorId], references: [id], onDelete: Cascade) + authorId String +} + `); + + expect(schema).toMatchObject({ + provider: { + type: 'sqlite' + }, + models: { + User: { + name: 'User', + fields: { + id: { + name: 'id', + type: 'String', + id: true, + attributes: [ + { + name: '@id' + }, + { + name: '@default', + args: [ + { + name: 'value', + value: { + kind: 'call', + function: 'uuid' + } + } + ] + } + ], + default: { + kind: 'call', + function: 'uuid' + } + }, + name: { + name: 'name', + type: 'String' + }, + email: { + name: 'email', + type: 'String', + unique: true, + attributes: [ + { + name: '@unique' + } + ] + }, + createdAt: { + name: 'createdAt', + type: 'DateTime', + attributes: [ + { + name: '@default', + args: [ + { + name: 'value', + value: { + kind: 'call', + function: 'now' + } + } + ] + } + ], + default: { + kind: 'call', + function: 'now' + } + }, + updatedAt: { + name: 'updatedAt', + type: 'DateTime', + updatedAt: { + ignore: [ + 'email' + ] + }, + attributes: [ + { + name: '@updatedAt', + args: [ + { + name: 'ignore', + value: { + kind: 'array', + items: [ + { + kind: 'field', + field: 'email' + } + ] + } + } + ] + } + ] + }, + posts: { + name: 'posts', + type: 'Post', + array: true, + relation: { + opposite: 'author' + } + } + }, + attributes: [ + { + name: '@@map', + args: [ + { + name: 'name', + value: { + kind: 'literal', + value: 'users' + } + } + ] + } + ], + idFields: [ + 'id' + ], + uniqueFields: { + id: { + type: 'String' + }, + email: { + type: 'String' + } + } + }, + Post: { + name: 'Post', + fields: { + id: { + name: 'id', + type: 'String', + id: true, + attributes: [ + { + name: '@id' + }, + { + name: '@default', + args: [ + { + name: 'value', + value: { + kind: 'call', + function: 'cuid' + } + } + ] + } + ], + default: { + kind: 'call', + function: 'cuid' + } + }, + title: { + name: 'title', + type: 'String' + }, + published: { + name: 'published', + type: 'Boolean', + attributes: [ + { + name: '@default', + args: [ + { + name: 'value', + value: { + kind: 'literal', + value: false + } + } + ] + } + ], + default: false + }, + author: { + name: 'author', + type: 'User', + attributes: [ + { + name: '@relation', + args: [ + { + name: 'fields', + value: { + kind: 'array', + items: [ + { + kind: 'field', + field: 'authorId' + } + ] + } + }, + { + name: 'references', + value: { + kind: 'array', + items: [ + { + kind: 'field', + field: 'id' + } + ] + } + }, + { + name: 'onDelete', + value: { + kind: 'literal', + value: 'Cascade' + } + } + ] + } + ], + relation: { + opposite: 'posts', + fields: [ + 'authorId' + ], + references: [ + 'id' + ], + onDelete: 'Cascade' + } + }, + authorId: { + name: 'authorId', + type: 'String', + foreignKeyFor: [ + 'author' + ] + } + }, + idFields: [ + 'id' + ], + uniqueFields: { + id: { + type: 'String' + } + } + } + }, + authType: 'User', + plugins: {} + }); + }) }); diff --git a/tests/e2e/orm/client-api/update.test.ts b/tests/e2e/orm/client-api/update.test.ts index c79396d7f..f5fa9b108 100644 --- a/tests/e2e/orm/client-api/update.test.ts +++ b/tests/e2e/orm/client-api/update.test.ts @@ -130,6 +130,58 @@ describe('Client update tests', () => { expect(updatedUser?.updatedAt).toEqual(originalUpdatedAt); }); + it('does not update updatedAt if only ignored fields are present', async () => { + const user = await createUser(client, 'u1@test.com'); + const originalUpdatedAt = user.updatedAt; + + await client.user.update({ + where: { id: user.id }, + data: { + id: 'User2', + }, + }); + + let updatedUser = await client.user.findUnique({ where: { id: 'User2' } }); + expect(updatedUser?.updatedAt).toEqual(originalUpdatedAt); + + await client.user.update({ + where: { id: 'User2' }, + data: { + createdAt: new Date(), + }, + }); + + updatedUser = await client.user.findUnique({ where: { id: 'User2' } }); + expect(updatedUser?.updatedAt).toEqual(originalUpdatedAt); + + await client.user.update({ + where: { id: 'User2' }, + data: { + id: 'User1', + createdAt: new Date(), + }, + }); + + updatedUser = await client.user.findUnique({ where: { id: 'User1' } }); + expect(updatedUser?.updatedAt).toEqual(originalUpdatedAt); + }); + + it('updates updatedAt if any non-ignored fields are present', async () => { + const user = await createUser(client, 'u1@test.com'); + const originalUpdatedAt = user.updatedAt; + + await client.user.update({ + where: { id: user.id }, + data: { + id: 'User2', + name: 'User2', + }, + }); + + const updatedUser = await client.user.findUnique({ where: { id: 'User2' } }); + expect(updatedUser?.updatedAt.getTime()).toBeGreaterThan(originalUpdatedAt.getTime()); + }); + it('works with numeric incremental update', async () => { await createUser(client, 'u1@test.com', { profile: { create: { id: '1', bio: 'bio' } }, diff --git a/tests/e2e/orm/schemas/basic/schema.ts b/tests/e2e/orm/schemas/basic/schema.ts index 5f067685e..98cfebda1 100644 --- a/tests/e2e/orm/schemas/basic/schema.ts +++ b/tests/e2e/orm/schemas/basic/schema.ts @@ -30,7 +30,12 @@ export class SchemaType implements SchemaDef { updatedAt: { name: "updatedAt", type: "DateTime", - updatedAt: true, + updatedAt: { + ignore: [ + 'id', + 'createdAt', + ], + }, attributes: [{ name: "@updatedAt" }] }, email: { From eceac5415daec99bfa8ee9515a286dd36c8a8fdb Mon Sep 17 00:00:00 2001 From: sanny-io Date: Thu, 8 Jan 2026 05:34:58 +0000 Subject: [PATCH 03/22] Trigger Build From 5c6afcbf11c4ff84ace166c38c32e30c7c619eea Mon Sep 17 00:00:00 2001 From: sanny-io Date: Thu, 8 Jan 2026 05:46:41 +0000 Subject: [PATCH 04/22] Check test. --- tests/e2e/orm/client-api/update.test.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/tests/e2e/orm/client-api/update.test.ts b/tests/e2e/orm/client-api/update.test.ts index f5fa9b108..924dfc37f 100644 --- a/tests/e2e/orm/client-api/update.test.ts +++ b/tests/e2e/orm/client-api/update.test.ts @@ -131,8 +131,12 @@ describe('Client update tests', () => { }); it('does not update updatedAt if only ignored fields are present', async () => { - const user = await createUser(client, 'u1@test.com'); - const originalUpdatedAt = user.updatedAt; + const originalUpdatedAt = new Date(); + const user = await createUser(client, 'u1@test.com', { + name: 'User1', + role: 'ADMIN', + updatedAt: originalUpdatedAt, + }); await client.user.update({ where: { id: user.id }, From 19512d573a830046033284cc3705b73a630c962c Mon Sep 17 00:00:00 2001 From: sanny-io Date: Thu, 8 Jan 2026 05:50:26 +0000 Subject: [PATCH 05/22] Use `getTime` --- tests/e2e/orm/client-api/update.test.ts | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/tests/e2e/orm/client-api/update.test.ts b/tests/e2e/orm/client-api/update.test.ts index 924dfc37f..92fac614a 100644 --- a/tests/e2e/orm/client-api/update.test.ts +++ b/tests/e2e/orm/client-api/update.test.ts @@ -131,12 +131,8 @@ describe('Client update tests', () => { }); it('does not update updatedAt if only ignored fields are present', async () => { - const originalUpdatedAt = new Date(); - const user = await createUser(client, 'u1@test.com', { - name: 'User1', - role: 'ADMIN', - updatedAt: originalUpdatedAt, - }); + const user = await createUser(client, 'u1@test.com'); + const originalUpdatedAt = user.updatedAt; await client.user.update({ where: { id: user.id }, @@ -146,7 +142,7 @@ describe('Client update tests', () => { }); let updatedUser = await client.user.findUnique({ where: { id: 'User2' } }); - expect(updatedUser?.updatedAt).toEqual(originalUpdatedAt); + expect(updatedUser?.updatedAt.getTime()).toEqual(originalUpdatedAt.getTime()); await client.user.update({ where: { id: 'User2' }, @@ -156,7 +152,7 @@ describe('Client update tests', () => { }); updatedUser = await client.user.findUnique({ where: { id: 'User2' } }); - expect(updatedUser?.updatedAt).toEqual(originalUpdatedAt); + expect(updatedUser?.updatedAt.getTime()).toEqual(originalUpdatedAt.getTime()); await client.user.update({ where: { id: 'User2' }, @@ -167,7 +163,7 @@ describe('Client update tests', () => { }); updatedUser = await client.user.findUnique({ where: { id: 'User1' } }); - expect(updatedUser?.updatedAt).toEqual(originalUpdatedAt); + expect(updatedUser?.updatedAt.getTime()).toEqual(originalUpdatedAt.getTime()); }); it('updates updatedAt if any non-ignored fields are present', async () => { From 2043cb9b5f65e5657ada98013e2067b122f13f41 Mon Sep 17 00:00:00 2001 From: sanny-io Date: Thu, 8 Jan 2026 06:02:16 +0000 Subject: [PATCH 06/22] Retry. --- tests/e2e/orm/client-api/update.test.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/tests/e2e/orm/client-api/update.test.ts b/tests/e2e/orm/client-api/update.test.ts index 92fac614a..a36070208 100644 --- a/tests/e2e/orm/client-api/update.test.ts +++ b/tests/e2e/orm/client-api/update.test.ts @@ -131,8 +131,12 @@ describe('Client update tests', () => { }); it('does not update updatedAt if only ignored fields are present', async () => { - const user = await createUser(client, 'u1@test.com'); - const originalUpdatedAt = user.updatedAt; + const originalUpdatedAt = new Date(); + const user = await createUser(client, 'u1@test.com', { + name: 'User1', + role: 'ADMIN', + updatedAt: originalUpdatedAt, + }); await client.user.update({ where: { id: user.id }, From 1e1de8a6c2836de4331be232bfc29c94be8160d5 Mon Sep 17 00:00:00 2001 From: sanny-io Date: Thu, 8 Jan 2026 06:26:30 +0000 Subject: [PATCH 07/22] Retry. --- tests/e2e/orm/client-api/update.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/e2e/orm/client-api/update.test.ts b/tests/e2e/orm/client-api/update.test.ts index a36070208..4c79e6076 100644 --- a/tests/e2e/orm/client-api/update.test.ts +++ b/tests/e2e/orm/client-api/update.test.ts @@ -141,7 +141,7 @@ describe('Client update tests', () => { await client.user.update({ where: { id: user.id }, data: { - id: 'User2', + // id: 'User2', }, }); From 30cf3ee4de4926ac6fbbc559f1369c95bef76467 Mon Sep 17 00:00:00 2001 From: sanny-io Date: Fri, 9 Jan 2026 02:45:16 +0000 Subject: [PATCH 08/22] Retry. --- tests/e2e/orm/client-api/update.test.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/e2e/orm/client-api/update.test.ts b/tests/e2e/orm/client-api/update.test.ts index 4c79e6076..e8c17b193 100644 --- a/tests/e2e/orm/client-api/update.test.ts +++ b/tests/e2e/orm/client-api/update.test.ts @@ -145,21 +145,21 @@ describe('Client update tests', () => { }, }); - let updatedUser = await client.user.findUnique({ where: { id: 'User2' } }); + let updatedUser = await client.user.findUnique({ where: { id: user.id } }); expect(updatedUser?.updatedAt.getTime()).toEqual(originalUpdatedAt.getTime()); await client.user.update({ - where: { id: 'User2' }, + where: { id: user.id }, data: { createdAt: new Date(), }, }); - updatedUser = await client.user.findUnique({ where: { id: 'User2' } }); + updatedUser = await client.user.findUnique({ where: { id: user.id } }); expect(updatedUser?.updatedAt.getTime()).toEqual(originalUpdatedAt.getTime()); await client.user.update({ - where: { id: 'User2' }, + where: { id: user.id }, data: { id: 'User1', createdAt: new Date(), From cb5709cbaf6bf0b1205d50eae169341e1ede4069 Mon Sep 17 00:00:00 2001 From: sanny-io Date: Fri, 9 Jan 2026 04:23:36 +0000 Subject: [PATCH 09/22] Retry. --- .../orm/src/client/crud/operations/base.ts | 6 +- tests/e2e/orm/client-api/update.test.ts | 63 +++++++++++-------- 2 files changed, 40 insertions(+), 29 deletions(-) diff --git a/packages/orm/src/client/crud/operations/base.ts b/packages/orm/src/client/crud/operations/base.ts index 6b4add541..63b8bd108 100644 --- a/packages/orm/src/client/crud/operations/base.ts +++ b/packages/orm/src/client/crud/operations/base.ts @@ -994,12 +994,12 @@ export abstract class BaseOperationHandler { const autoUpdatedFields: string[] = []; for (const [fieldName, fieldDef] of Object.entries(modelDef.fields)) { if (fieldDef.updatedAt) { - const updatedScalarFieldsArr = Object.keys(data).filter((field) => isScalarField(this.schema, modelDef.name, field)); - const ignoredFieldsSet = new Set(typeof fieldDef.updatedAt === 'boolean' ? [] : fieldDef.updatedAt.ignore); + const updatedScalarFields = Object.keys(data).filter((field) => isScalarField(this.schema, modelDef.name, field)); + const ignoredFields = new Set(typeof fieldDef.updatedAt === 'boolean' ? [] : fieldDef.updatedAt.ignore); if (finalData === data) { finalData = clone(data); } - if (updatedScalarFieldsArr.some((field) => !ignoredFieldsSet.has(field))) { + if (updatedScalarFields.some((field) => !ignoredFields.has(field))) { finalData[fieldName] = this.dialect.transformPrimitive(new Date(), 'DateTime', false); autoUpdatedFields.push(fieldName); } diff --git a/tests/e2e/orm/client-api/update.test.ts b/tests/e2e/orm/client-api/update.test.ts index e8c17b193..5b770d204 100644 --- a/tests/e2e/orm/client-api/update.test.ts +++ b/tests/e2e/orm/client-api/update.test.ts @@ -131,43 +131,54 @@ describe('Client update tests', () => { }); it('does not update updatedAt if only ignored fields are present', async () => { - const originalUpdatedAt = new Date(); - const user = await createUser(client, 'u1@test.com', { - name: 'User1', - role: 'ADMIN', - updatedAt: originalUpdatedAt, - }); + const user = await createUser(client, 'u1@test.com'); + const originalUpdatedAt = user.updatedAt; await client.user.update({ where: { id: user.id }, - data: { - // id: 'User2', - }, + data: {}, }); let updatedUser = await client.user.findUnique({ where: { id: user.id } }); expect(updatedUser?.updatedAt.getTime()).toEqual(originalUpdatedAt.getTime()); - await client.user.update({ - where: { id: user.id }, - data: { - createdAt: new Date(), - }, - }); + // const originalUpdatedAt = new Date(); + // const user = await createUser(client, 'u1@test.com', { + // name: 'User1', + // role: 'ADMIN', + // updatedAt: originalUpdatedAt, + // }); - updatedUser = await client.user.findUnique({ where: { id: user.id } }); - expect(updatedUser?.updatedAt.getTime()).toEqual(originalUpdatedAt.getTime()); + // await client.user.update({ + // where: { id: user.id }, + // data: { + // // id: 'User2', + // }, + // }); - await client.user.update({ - where: { id: user.id }, - data: { - id: 'User1', - createdAt: new Date(), - }, - }); + // let updatedUser = await client.user.findUnique({ where: { id: user.id } }); + // expect(updatedUser?.updatedAt.getTime()).toEqual(originalUpdatedAt.getTime()); - updatedUser = await client.user.findUnique({ where: { id: 'User1' } }); - expect(updatedUser?.updatedAt.getTime()).toEqual(originalUpdatedAt.getTime()); + // await client.user.update({ + // where: { id: user.id }, + // data: { + // createdAt: new Date(), + // }, + // }); + + // updatedUser = await client.user.findUnique({ where: { id: user.id } }); + // expect(updatedUser?.updatedAt.getTime()).toEqual(originalUpdatedAt.getTime()); + + // await client.user.update({ + // where: { id: user.id }, + // data: { + // id: 'User1', + // createdAt: new Date(), + // }, + // }); + + // updatedUser = await client.user.findUnique({ where: { id: 'User1' } }); + // expect(updatedUser?.updatedAt.getTime()).toEqual(originalUpdatedAt.getTime()); }); it('updates updatedAt if any non-ignored fields are present', async () => { From d35d8f4a0e26c1917b561b823df6bb069f52c4db Mon Sep 17 00:00:00 2001 From: sanny-io Date: Fri, 9 Jan 2026 04:34:14 +0000 Subject: [PATCH 10/22] Retry. --- tests/e2e/orm/client-api/update.test.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/e2e/orm/client-api/update.test.ts b/tests/e2e/orm/client-api/update.test.ts index 5b770d204..f2789f7df 100644 --- a/tests/e2e/orm/client-api/update.test.ts +++ b/tests/e2e/orm/client-api/update.test.ts @@ -136,7 +136,9 @@ describe('Client update tests', () => { await client.user.update({ where: { id: user.id }, - data: {}, + data: { + createdAt: new Date(originalUpdatedAt.getTime() + 5000), + }, }); let updatedUser = await client.user.findUnique({ where: { id: user.id } }); From 4bb121d00462ac0a7a01e028d77118e4e608faa0 Mon Sep 17 00:00:00 2001 From: sanny-io Date: Fri, 9 Jan 2026 04:59:20 +0000 Subject: [PATCH 11/22] Retry. --- packages/orm/src/client/crud/operations/base.ts | 13 ++++++++++--- tests/e2e/orm/client-api/update.test.ts | 4 ++++ 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/packages/orm/src/client/crud/operations/base.ts b/packages/orm/src/client/crud/operations/base.ts index 63b8bd108..a4ff69a46 100644 --- a/packages/orm/src/client/crud/operations/base.ts +++ b/packages/orm/src/client/crud/operations/base.ts @@ -994,14 +994,21 @@ export abstract class BaseOperationHandler { const autoUpdatedFields: string[] = []; for (const [fieldName, fieldDef] of Object.entries(modelDef.fields)) { if (fieldDef.updatedAt) { - const updatedScalarFields = Object.keys(data).filter((field) => isScalarField(this.schema, modelDef.name, field)); const ignoredFields = new Set(typeof fieldDef.updatedAt === 'boolean' ? [] : fieldDef.updatedAt.ignore); + const hasUpdatedScalarFields = Object.keys(data).some((field) => ( + isScalarField(this.schema, modelDef.name, field) && + !ignoredFields.has(field) + )); if (finalData === data) { finalData = clone(data); } - if (updatedScalarFields.some((field) => !ignoredFields.has(field))) { + // @ts-expect-error + if (globalThis.updatedattest) { + console.log({ hasUpdatedScalarFields, data, ignoredFields }) + } + if (hasUpdatedScalarFields) { finalData[fieldName] = this.dialect.transformPrimitive(new Date(), 'DateTime', false); - autoUpdatedFields.push(fieldName); + autoUpdatedFields.push(fieldName); } } } diff --git a/tests/e2e/orm/client-api/update.test.ts b/tests/e2e/orm/client-api/update.test.ts index f2789f7df..eb92dc441 100644 --- a/tests/e2e/orm/client-api/update.test.ts +++ b/tests/e2e/orm/client-api/update.test.ts @@ -134,6 +134,8 @@ describe('Client update tests', () => { const user = await createUser(client, 'u1@test.com'); const originalUpdatedAt = user.updatedAt; + globalThis.updatedattest = true; + await client.user.update({ where: { id: user.id }, data: { @@ -144,6 +146,8 @@ describe('Client update tests', () => { let updatedUser = await client.user.findUnique({ where: { id: user.id } }); expect(updatedUser?.updatedAt.getTime()).toEqual(originalUpdatedAt.getTime()); + globalThis.updatedattest = false; + // const originalUpdatedAt = new Date(); // const user = await createUser(client, 'u1@test.com', { // name: 'User1', From 11c08308f21c59cbef6b12d68716527013dc712e Mon Sep 17 00:00:00 2001 From: sanny-io Date: Fri, 9 Jan 2026 05:13:50 +0000 Subject: [PATCH 12/22] Retry. --- packages/orm/src/client/crud/operations/base.ts | 2 +- tests/e2e/orm/schemas/basic/schema.zmodel | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/orm/src/client/crud/operations/base.ts b/packages/orm/src/client/crud/operations/base.ts index a4ff69a46..ed719594b 100644 --- a/packages/orm/src/client/crud/operations/base.ts +++ b/packages/orm/src/client/crud/operations/base.ts @@ -1004,7 +1004,7 @@ export abstract class BaseOperationHandler { } // @ts-expect-error if (globalThis.updatedattest) { - console.log({ hasUpdatedScalarFields, data, ignoredFields }) + console.log({ hasUpdatedScalarFields, data, ignoredFields, updatedat: fieldDef.updatedAt }) } if (hasUpdatedScalarFields) { finalData[fieldName] = this.dialect.transformPrimitive(new Date(), 'DateTime', false); diff --git a/tests/e2e/orm/schemas/basic/schema.zmodel b/tests/e2e/orm/schemas/basic/schema.zmodel index a831b827a..9a8914fd1 100644 --- a/tests/e2e/orm/schemas/basic/schema.zmodel +++ b/tests/e2e/orm/schemas/basic/schema.zmodel @@ -15,7 +15,7 @@ enum Role { type CommonFields { id String @id @default(cuid()) createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + updatedAt DateTime @updatedAt(ignore: [id, createdAt]) } model User with CommonFields { From 46db3e24a113f07c5b3a5e8407fc51ad335a62eb Mon Sep 17 00:00:00 2001 From: sanny-io Date: Fri, 9 Jan 2026 05:45:01 +0000 Subject: [PATCH 13/22] Clean up. --- .../orm/src/client/crud/operations/base.ts | 14 ++-- tests/e2e/orm/client-api/update.test.ts | 67 ++++++++----------- 2 files changed, 34 insertions(+), 47 deletions(-) diff --git a/packages/orm/src/client/crud/operations/base.ts b/packages/orm/src/client/crud/operations/base.ts index ed719594b..ded2010bc 100644 --- a/packages/orm/src/client/crud/operations/base.ts +++ b/packages/orm/src/client/crud/operations/base.ts @@ -995,18 +995,16 @@ export abstract class BaseOperationHandler { for (const [fieldName, fieldDef] of Object.entries(modelDef.fields)) { if (fieldDef.updatedAt) { const ignoredFields = new Set(typeof fieldDef.updatedAt === 'boolean' ? [] : fieldDef.updatedAt.ignore); - const hasUpdatedScalarFields = Object.keys(data).some((field) => ( - isScalarField(this.schema, modelDef.name, field) && - !ignoredFields.has(field) + const hasNonIgnoredFields = Object.keys(data).some((field) => ( + ( + isScalarField(this.schema, modelDef.name, field) || + isForeignKeyField(this.schema, modelDef.name, field) + ) && !ignoredFields.has(field) )); if (finalData === data) { finalData = clone(data); } - // @ts-expect-error - if (globalThis.updatedattest) { - console.log({ hasUpdatedScalarFields, data, ignoredFields, updatedat: fieldDef.updatedAt }) - } - if (hasUpdatedScalarFields) { + if (hasNonIgnoredFields) { finalData[fieldName] = this.dialect.transformPrimitive(new Date(), 'DateTime', false); autoUpdatedFields.push(fieldName); } diff --git a/tests/e2e/orm/client-api/update.test.ts b/tests/e2e/orm/client-api/update.test.ts index eb92dc441..cb13ee55f 100644 --- a/tests/e2e/orm/client-api/update.test.ts +++ b/tests/e2e/orm/client-api/update.test.ts @@ -134,57 +134,46 @@ describe('Client update tests', () => { const user = await createUser(client, 'u1@test.com'); const originalUpdatedAt = user.updatedAt; - globalThis.updatedattest = true; - await client.user.update({ - where: { id: user.id }, + where: { + id: user.id, + }, + data: { - createdAt: new Date(originalUpdatedAt.getTime() + 5000), + createdAt: new Date(), }, - }); + }) let updatedUser = await client.user.findUnique({ where: { id: user.id } }); expect(updatedUser?.updatedAt.getTime()).toEqual(originalUpdatedAt.getTime()); - globalThis.updatedattest = false; - - // const originalUpdatedAt = new Date(); - // const user = await createUser(client, 'u1@test.com', { - // name: 'User1', - // role: 'ADMIN', - // updatedAt: originalUpdatedAt, - // }); - - // await client.user.update({ - // where: { id: user.id }, - // data: { - // // id: 'User2', - // }, - // }); + await client.user.update({ + where: { + id: user.id, + }, - // let updatedUser = await client.user.findUnique({ where: { id: user.id } }); - // expect(updatedUser?.updatedAt.getTime()).toEqual(originalUpdatedAt.getTime()); + data: { + id: 'User2', + }, + }) - // await client.user.update({ - // where: { id: user.id }, - // data: { - // createdAt: new Date(), - // }, - // }); + updatedUser = await client.user.findUnique({ where: { id: 'User2' } }); + expect(updatedUser?.updatedAt.getTime()).toEqual(originalUpdatedAt.getTime()); - // updatedUser = await client.user.findUnique({ where: { id: user.id } }); - // expect(updatedUser?.updatedAt.getTime()).toEqual(originalUpdatedAt.getTime()); + // multiple ignored fields + await client.user.update({ + where: { + id: 'User2', + }, - // await client.user.update({ - // where: { id: user.id }, - // data: { - // id: 'User1', - // createdAt: new Date(), - // }, - // }); + data: { + id: 'User3', + createdAt: new Date(), + }, + }) - // updatedUser = await client.user.findUnique({ where: { id: 'User1' } }); - // expect(updatedUser?.updatedAt.getTime()).toEqual(originalUpdatedAt.getTime()); + updatedUser = await client.user.findUnique({ where: { id: 'User3' } }); + expect(updatedUser?.updatedAt.getTime()).toEqual(originalUpdatedAt.getTime()); }); it('updates updatedAt if any non-ignored fields are present', async () => { From 8754a7481fd2d01e2e6401d4e51e45b2b3052c2e Mon Sep 17 00:00:00 2001 From: sanny-io Date: Fri, 9 Jan 2026 06:57:49 +0000 Subject: [PATCH 14/22] Document param. --- packages/language/res/stdlib.zmodel | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/language/res/stdlib.zmodel b/packages/language/res/stdlib.zmodel index 2394b55b0..f3f568820 100644 --- a/packages/language/res/stdlib.zmodel +++ b/packages/language/res/stdlib.zmodel @@ -402,6 +402,10 @@ attribute @omit() /** * Automatically stores the time when a record was last updated. + * + * @param ignore: A list of field names that are not considered when the ORM client is determining whether any + * updates have been made to a record. An update that only contains ignored fields does not change the + * timestamp. */ attribute @updatedAt(_ ignore: FieldReference[]?) @@@targetField([DateTimeField]) @@@prisma From 2f2c3a7f52bd2d29871be80a05e8833f79474497 Mon Sep 17 00:00:00 2001 From: sanny-io Date: Sat, 10 Jan 2026 10:52:29 +0000 Subject: [PATCH 15/22] Extract to function. --- packages/sdk/src/ts-schema-generator.ts | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/packages/sdk/src/ts-schema-generator.ts b/packages/sdk/src/ts-schema-generator.ts index 1a4c26500..fdfd4073a 100644 --- a/packages/sdk/src/ts-schema-generator.ts +++ b/packages/sdk/src/ts-schema-generator.ts @@ -319,6 +319,14 @@ export class TsSchemaGenerator { ); } + private createUpdatedAtObject(ignoreArg: AttributeArg) { + return ts.factory.createObjectLiteralExpression([ + ts.factory.createPropertyAssignment('ignore', ts.factory.createArrayLiteralExpression( + (ignoreArg.value as ArrayExpr).items.map((item) => ts.factory.createStringLiteral((item as ReferenceExpr).target.$refText)) + )) + ]); + } + private getAllDataModels(model: Model) { return model.declarations.filter((d): d is DataModel => isDataModel(d) && !hasAttribute(d, '@@ignore')); } @@ -568,11 +576,7 @@ export class TsSchemaGenerator { if (updatedAtAttrib) { const ignoreArg = updatedAtAttrib.args.find(arg => arg.name === 'ignore'); objectFields.push(ts.factory.createPropertyAssignment('updatedAt', ignoreArg - ? ts.factory.createObjectLiteralExpression([ - ts.factory.createPropertyAssignment('ignore', ts.factory.createArrayLiteralExpression( - (ignoreArg.value as ArrayExpr).items.map((item) => ts.factory.createStringLiteral((item as ReferenceExpr).target.$refText)) - )) - ]) + ? this.createUpdatedAtObject(ignoreArg) : ts.factory.createTrue() )); } From 9db401df247188561de63297403c80a543bfbe3e Mon Sep 17 00:00:00 2001 From: sanny-io Date: Sat, 10 Jan 2026 10:53:40 +0000 Subject: [PATCH 16/22] Relocate function. --- packages/sdk/src/ts-schema-generator.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/sdk/src/ts-schema-generator.ts b/packages/sdk/src/ts-schema-generator.ts index fdfd4073a..c60903117 100644 --- a/packages/sdk/src/ts-schema-generator.ts +++ b/packages/sdk/src/ts-schema-generator.ts @@ -319,14 +319,6 @@ export class TsSchemaGenerator { ); } - private createUpdatedAtObject(ignoreArg: AttributeArg) { - return ts.factory.createObjectLiteralExpression([ - ts.factory.createPropertyAssignment('ignore', ts.factory.createArrayLiteralExpression( - (ignoreArg.value as ArrayExpr).items.map((item) => ts.factory.createStringLiteral((item as ReferenceExpr).target.$refText)) - )) - ]); - } - private getAllDataModels(model: Model) { return model.declarations.filter((d): d is DataModel => isDataModel(d) && !hasAttribute(d, '@@ignore')); } @@ -529,6 +521,14 @@ export class TsSchemaGenerator { ); } + private createUpdatedAtObject(ignoreArg: AttributeArg) { + return ts.factory.createObjectLiteralExpression([ + ts.factory.createPropertyAssignment('ignore', ts.factory.createArrayLiteralExpression( + (ignoreArg.value as ArrayExpr).items.map((item) => ts.factory.createStringLiteral((item as ReferenceExpr).target.$refText)) + )) + ]); + } + private mapFieldTypeToTSType(type: DataFieldType) { let result = match(type.type) .with('String', () => 'string') From 004c864e59fb722d9099430a3361eeba6af06eba Mon Sep 17 00:00:00 2001 From: sanny-io Date: Sat, 10 Jan 2026 11:02:54 +0000 Subject: [PATCH 17/22] Adjust formatting. --- packages/schema/src/schema.ts | 2 +- packages/sdk/src/ts-schema-generator.ts | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/schema/src/schema.ts b/packages/schema/src/schema.ts index fc25a3b2c..ba057addd 100644 --- a/packages/schema/src/schema.ts +++ b/packages/schema/src/schema.ts @@ -60,7 +60,7 @@ export type RelationInfo = { }; export type UpdatedAtInfo = { - ignore?: readonly string[], + ignore?: readonly string[]; }; export type FieldDef = { diff --git a/packages/sdk/src/ts-schema-generator.ts b/packages/sdk/src/ts-schema-generator.ts index c60903117..b33534f7e 100644 --- a/packages/sdk/src/ts-schema-generator.ts +++ b/packages/sdk/src/ts-schema-generator.ts @@ -572,12 +572,12 @@ export class TsSchemaGenerator { } const updatedAtAttrib = getAttribute(field, '@updatedAt') as DataFieldAttribute | undefined; - if (updatedAtAttrib) { const ignoreArg = updatedAtAttrib.args.find(arg => arg.name === 'ignore'); - objectFields.push(ts.factory.createPropertyAssignment('updatedAt', ignoreArg - ? this.createUpdatedAtObject(ignoreArg) - : ts.factory.createTrue() + objectFields.push(ts.factory.createPropertyAssignment('updatedAt', + ignoreArg + ? this.createUpdatedAtObject(ignoreArg) + : ts.factory.createTrue() )); } From 610c77a835574ef232fcc3b3b3ca4a7c68473b17 Mon Sep 17 00:00:00 2001 From: sanny-io Date: Sat, 10 Jan 2026 11:13:49 +0000 Subject: [PATCH 18/22] Use `getAttributeArg` --- packages/sdk/src/ts-schema-generator.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/sdk/src/ts-schema-generator.ts b/packages/sdk/src/ts-schema-generator.ts index b33534f7e..c49108d32 100644 --- a/packages/sdk/src/ts-schema-generator.ts +++ b/packages/sdk/src/ts-schema-generator.ts @@ -573,7 +573,7 @@ export class TsSchemaGenerator { const updatedAtAttrib = getAttribute(field, '@updatedAt') as DataFieldAttribute | undefined; if (updatedAtAttrib) { - const ignoreArg = updatedAtAttrib.args.find(arg => arg.name === 'ignore'); + const ignoreArg = getAttributeArg(updatedAtAttrib, 'ignore') as AttributeArg | undefined; objectFields.push(ts.factory.createPropertyAssignment('updatedAt', ignoreArg ? this.createUpdatedAtObject(ignoreArg) From 2fca1fa022dbfaa49dbc2e9b4fe032558907caf9 Mon Sep 17 00:00:00 2001 From: sanny-io Date: Sat, 10 Jan 2026 11:30:02 +0000 Subject: [PATCH 19/22] Use `$resolvedParam` --- packages/sdk/src/ts-schema-generator.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/sdk/src/ts-schema-generator.ts b/packages/sdk/src/ts-schema-generator.ts index c49108d32..52715c865 100644 --- a/packages/sdk/src/ts-schema-generator.ts +++ b/packages/sdk/src/ts-schema-generator.ts @@ -573,7 +573,7 @@ export class TsSchemaGenerator { const updatedAtAttrib = getAttribute(field, '@updatedAt') as DataFieldAttribute | undefined; if (updatedAtAttrib) { - const ignoreArg = getAttributeArg(updatedAtAttrib, 'ignore') as AttributeArg | undefined; + const ignoreArg = updatedAtAttrib.args.find(arg => arg.$resolvedParam.name === 'ignore'); objectFields.push(ts.factory.createPropertyAssignment('updatedAt', ignoreArg ? this.createUpdatedAtObject(ignoreArg) From 315cd200dcd09c91c8aad3e65c239b6eca98d257 Mon Sep 17 00:00:00 2001 From: sanny-io Date: Sat, 10 Jan 2026 11:35:54 +0000 Subject: [PATCH 20/22] Null check. --- packages/sdk/src/ts-schema-generator.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/sdk/src/ts-schema-generator.ts b/packages/sdk/src/ts-schema-generator.ts index 52715c865..96e73ffdf 100644 --- a/packages/sdk/src/ts-schema-generator.ts +++ b/packages/sdk/src/ts-schema-generator.ts @@ -573,7 +573,7 @@ export class TsSchemaGenerator { const updatedAtAttrib = getAttribute(field, '@updatedAt') as DataFieldAttribute | undefined; if (updatedAtAttrib) { - const ignoreArg = updatedAtAttrib.args.find(arg => arg.$resolvedParam.name === 'ignore'); + const ignoreArg = updatedAtAttrib.args.find(arg => arg.$resolvedParam?.name === 'ignore'); objectFields.push(ts.factory.createPropertyAssignment('updatedAt', ignoreArg ? this.createUpdatedAtObject(ignoreArg) From defcb6da90bba4177e242a9232a52b903178f751 Mon Sep 17 00:00:00 2001 From: ymc9 <104139426+ymc9@users.noreply.github.com> Date: Fri, 30 Jan 2026 16:02:31 +0800 Subject: [PATCH 21/22] fix: resolve a merge error --- packages/orm/src/client/crud/operations/base.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/orm/src/client/crud/operations/base.ts b/packages/orm/src/client/crud/operations/base.ts index 77e400c8d..8136b66ea 100644 --- a/packages/orm/src/client/crud/operations/base.ts +++ b/packages/orm/src/client/crud/operations/base.ts @@ -1148,7 +1148,7 @@ export abstract class BaseOperationHandler { // fill in automatically updated fields const autoUpdatedFields: string[] = []; for (const [fieldName, fieldDef] of Object.entries(modelDef.fields)) { - if (fieldDef.updatedAt) { + if (fieldDef.updatedAt && finalData[fieldName] === undefined) { const ignoredFields = new Set(typeof fieldDef.updatedAt === 'boolean' ? [] : fieldDef.updatedAt.ignore); const hasNonIgnoredFields = Object.keys(data).some( (field) => From 2f73c93bfbdabe96382775e3805efb8df394ac86 Mon Sep 17 00:00:00 2001 From: ymc9 <104139426+ymc9@users.noreply.github.com> Date: Fri, 30 Jan 2026 16:04:21 +0800 Subject: [PATCH 22/22] fix: delay data clone to right before changing it --- packages/orm/src/client/crud/operations/base.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/orm/src/client/crud/operations/base.ts b/packages/orm/src/client/crud/operations/base.ts index 8136b66ea..fc75cac9d 100644 --- a/packages/orm/src/client/crud/operations/base.ts +++ b/packages/orm/src/client/crud/operations/base.ts @@ -1156,10 +1156,10 @@ export abstract class BaseOperationHandler { isForeignKeyField(this.schema, modelDef.name, field)) && !ignoredFields.has(field), ); - if (finalData === data) { - finalData = clone(data); - } if (hasNonIgnoredFields) { + if (finalData === data) { + finalData = clone(data); + } finalData[fieldName] = this.dialect.transformInput(new Date(), 'DateTime', false); autoUpdatedFields.push(fieldName); }