Skip to content

feat: fields argument for @updatedAt#2379

Open
sanny-io wants to merge 8 commits intozenstackhq:devfrom
sanny-io:feat/updated-at-fields
Open

feat: fields argument for @updatedAt#2379
sanny-io wants to merge 8 commits intozenstackhq:devfrom
sanny-io:feat/updated-at-fields

Conversation

@sanny-io
Copy link
Contributor

@sanny-io sanny-io commented Feb 12, 2026

Sample Usage

model User {
  name String
  nameUpdatedAt DateTime @updatedAt(fields: [name])
}

Mutually exclusive with the ignore argument, with an error in ZModel if you try to use both.

Prisma issues: prisma/prisma#7602 prisma/prisma#12164
Closes #2354

Summary by CodeRabbit

  • New Features

    • @updatedAt now supports a fields parameter to specify which fields trigger timestamp updates.
  • Bug Fixes

    • Update logic only auto-updates timestamps when configured target fields actually change.
    • Validation enforces mutual exclusivity between fields and ignore.
  • Tests

    • Added comprehensive end-to-end tests covering fields and ignore behaviors; adjusted related tests.
  • Documentation

    • Attribute docs updated to clarify fields vs ignore semantics.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 12, 2026

📝 Walkthrough

Walkthrough

Adds a fields parameter to @updatedAt, enforces mutual exclusivity with ignore, exposes it in schema types and TS generation, updates ORM update logic to respect fields or ignore, and adds/updates unit and E2E tests and example schemas to cover the behaviors.

Changes

Cohort / File(s) Summary
Language Definition & Validation
packages/language/res/stdlib.zmodel, packages/language/src/validators/attribute-application-validator.ts
Added optional fields: FieldReference[]? argument to @updatedAt and validator that errors if both ignore and fields are present.
Schema Type
packages/schema/src/schema.ts
Extended UpdatedAtInfo with optional fields?: readonly string[].
TS Schema Generation & CLI Tests
packages/sdk/src/ts-schema-generator.ts, packages/cli/test/ts-schema-gen.test.ts
TS generator now emits an updatedAt object driven by either ignore or fields; unit test added to verify @updatedAt(fields: [...]) output.
ORM Runtime Update Logic
packages/orm/src/client/crud/operations/base.ts
Update flow now computes effective fields (including relation/FK mappings) and only auto-sets updatedAt when specified fields or non-ignored fields are present; introduces helper to derive effective fields and records auto-updated fields.
E2E Tests & Example Schemas
tests/e2e/orm/client-api/updated-at.test.ts, tests/e2e/orm/client-api/update.test.ts, tests/e2e/orm/schemas/basic/schema.ts, tests/e2e/orm/schemas/basic/schema.zmodel
Added comprehensive E2E tests for fields and ignore behaviors (many scenarios including relations); removed/rewrote overlapping update tests and simplified example schemas to use plain @updatedAt where applicable.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related issues

  • zenstackhq/zenstack-v3#593 — Adds fields parameter and associated runtime/validation/TSgen/test coverage; aligns directly with these changes.
  • #2354 — Feature request to allow targeting specific fields for @updatedAt; this PR implements the requested fields parameter.

Poem

🐰 I hopped through code to mark the hour,
I watch the fields that give me power,
Ignore or target, never both in play,
I tick when chosen fields change their way,
A tiny rabbit keeping timestamps gay. ✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately and concisely describes the main feature addition: introducing a 'fields' argument for the @updatedAt attribute.
Linked Issues check ✅ Passed The PR fully implements all coding requirements from issue #2354: adds fields parameter, enforces mutual exclusivity with ignore, and implements the targeting logic across schema, validation, and ORM layers.
Out of Scope Changes check ✅ Passed All changes are directly related to implementing the fields argument feature. Test modifications (additions and deletions) align with the new feature requirements and are appropriately scoped.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@packages/orm/src/client/crud/operations/base.ts`:
- Around line 1151-1175: The updatedAt logic (around fieldDef.updatedAt /
hasUpdated) currently only checks keys on the incoming data using
isScalarField/isForeignKeyField so nested relation operations that mutate owned
FKs later via parentUpdates (e.g., author: { connect/create/update } which apply
FK changes through parentUpdates) are ignored; update the hasUpdated
determination to also inspect parentUpdates for relation entries that will
produce FK changes (look at parentUpdates collection and treat relation ops that
result in owned FK writes as equivalent to scalar FK presence), keeping the
existing ignore/fields semantics (respect fieldDef.updatedAt.ignore and
fieldDef.updatedAt.fields) and using the same isScalarField/isForeignKeyField
logic when mapping relation->affected FK names so updatedAt is bumped correctly.
🧹 Nitpick comments (4)
tests/e2e/orm/client-api/updated-at.test.ts (4)

6-19: anyFieldUpdatedAt is declared in both schemas but never asserted on.

Both schemas define anyFieldUpdatedAt DateTime @updatedAt`` but no test case checks its behavior. This field should advance on every update, making it a useful baseline assertion — especially in the negative tests (lines 73–127 and 200–247) where it should still advance even when targeted/ignored updatedAt fields don't.

Consider adding assertions like:

+            expect(updatedUser1.anyFieldUpdatedAt.getTime()).toBeGreaterThan(user.anyFieldUpdatedAt.getTime());

in the negative-case tests to confirm that a plain @updatedAt still fires while the field-scoped ones correctly don't.

Also applies to: 131-144


22-71: Missing negative assertion: emailUpdatedAt should NOT advance when only name is updated.

The first update (line 32–40) changes only name, so emailUpdatedAt should remain unchanged. Adding this assertion would validate that fields: [email] correctly excludes unrelated changes — which is the core value proposition of this feature.

Proposed addition after line 49
             expect(updatedUser1.nameUpdatedAt.getTime()).toBeGreaterThan(nameUpdatedAt.getTime());
             expect(updatedUser1.majorFieldUpdatedAt.getTime()).toBeGreaterThan(majorFieldUpdatedAt.getTime());
+            expect(updatedUser1.emailUpdatedAt.getTime()).toEqual(user.emailUpdatedAt.getTime());

200-247: Missing complementary assertion: exceptEmailUpdatedAt should advance when only name is updated.

In the first update (lines 210–218), only name is changed. exceptEmailUpdatedAt (which ignores [email]) should still advance because name is not in its ignore list. This would strengthen the test by confirming that non-ignored updatedAt fields correctly fire when their counterpart stays suppressed.

Proposed addition
+            const exceptEmailUpdatedAt = user.exceptEmailUpdatedAt;
             ...
+            expect(updatedUser1.exceptEmailUpdatedAt.getTime()).toBeGreaterThan(exceptEmailUpdatedAt.getTime());

48-49: Potential flakiness from same-millisecond timestamps.

All assertions comparing timestamps use toBeGreaterThan(previousTimestamp.getTime()). If the DB operations complete within the same millisecond (unlikely but possible under fast CI), these will fail intermittently. Consider adding a small delay (e.g., await new Promise(r => setTimeout(r, 10))) before the update calls if flakiness is observed, or switching to toBeGreaterThanOrEqual with an additional not-equal check.

Also applies to: 68-70, 101-104, 174-176, 195-197

@zenstackhq zenstackhq deleted a comment from coderabbitai bot Feb 17, 2026
@zenstackhq zenstackhq deleted a comment from coderabbitai bot Feb 17, 2026
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

♻️ Duplicate comments (1)
packages/orm/src/client/crud/operations/base.ts (1)

1164-1180: ⚠️ Potential issue | 🟠 Major

Handle relation-driven FK writes when computing hasUpdated.

This branch only inspects direct scalar/FK keys on data. Nested relation ops like author: { connect: ... } can still update owned FK fields later via processRelationUpdates() / parentUpdates, so @updatedAt(ignore: ...) and @updatedAt(fields: ...) can miss real FK changes and skip the timestamp bump.


ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: aab2e7e9-9f87-4de9-96c8-73ad41cdee78

📥 Commits

Reviewing files that changed from the base of the PR and between 2bea83b and a3c2d22.

📒 Files selected for processing (5)
  • packages/orm/src/client/crud/operations/base.ts
  • packages/schema/src/schema.ts
  • packages/sdk/src/ts-schema-generator.ts
  • tests/e2e/orm/schemas/basic/schema.ts
  • tests/e2e/orm/schemas/basic/schema.zmodel

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/orm/src/client/crud/operations/base.ts`:
- Around line 1166-1179: The current updatedAt detection treats any owned
relation payload as if it updates the parent FK because
getUpdatedAtEffectiveFields is called for every key in data; change the logic in
the hasNonIgnoredFields/hasAnyTargetFields branches so you only expand relation
keys into effective fields when the relation operation will actually produce
parentUpdates/owned-FK writes (i.e., detect operation types like
connect/disconnect/set/upsert/create that mutate the FK or move this entire
updatedAt decision until after relation processing completes), update the code
paths around getUpdatedAtEffectiveFields, fieldDef.updatedAt.fields and the
hasUpdated assignment accordingly, and add a regression test for nested relation
update and the upsert path to ensure nested update payloads (e.g., data: { user:
{ update: { ... } } }) do not falsely trigger FK-based updatedAt logic.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: f053dcb5-888b-4f81-bd31-80a08a736bab

📥 Commits

Reviewing files that changed from the base of the PR and between a3c2d22 and d3b5032.

📒 Files selected for processing (2)
  • packages/orm/src/client/crud/operations/base.ts
  • tests/e2e/orm/client-api/updated-at.test.ts

Comment on lines +1166 to +1179
const hasNonIgnoredFields = Object.keys(data).some((field) => {
const effectiveFields = this.getUpdatedAtEffectiveFields(modelDef.name, field);
return (
effectiveFields.length > 0 &&
!effectiveFields.some((f) => ignoredFields.has(f))
);
});
hasUpdated = hasNonIgnoredFields;
} else if (fieldDef.updatedAt.fields) {
const targetFields = new Set(fieldDef.updatedAt.fields);
const hasAnyTargetFields = Object.keys(data).some((field) => {
const effectiveFields = this.getUpdatedAtEffectiveFields(modelDef.name, field);
return effectiveFields.some((f) => targetFields.has(f));
});
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Don't treat every owned relation payload as an FK change.

Line 1540 expands any owned relation key to [relation, ...relation.fields], so Line 1176 will also match @updatedAt(fields: [userId]) for a payload like data: { title: 'x', user: { update: { name: 'y' } } }. That only mutates the related row, but the title write gives the parent row a real update so the timestamp bump gets persisted anyway. Please derive the effective fields from operations that actually produce parentUpdates/owned-FK writes, or move this decision after relation processing. A regression test for nested relation update and the matched-upsert path would lock this down.

Also applies to: 1540-1554

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/orm/src/client/crud/operations/base.ts` around lines 1166 - 1179,
The current updatedAt detection treats any owned relation payload as if it
updates the parent FK because getUpdatedAtEffectiveFields is called for every
key in data; change the logic in the hasNonIgnoredFields/hasAnyTargetFields
branches so you only expand relation keys into effective fields when the
relation operation will actually produce parentUpdates/owned-FK writes (i.e.,
detect operation types like connect/disconnect/set/upsert/create that mutate the
FK or move this entire updatedAt decision until after relation processing
completes), update the code paths around getUpdatedAtEffectiveFields,
fieldDef.updatedAt.fields and the hasUpdated assignment accordingly, and add a
regression test for nested relation update and the upsert path to ensure nested
update payloads (e.g., data: { user: { update: { ... } } }) do not falsely
trigger FK-based updatedAt logic.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Allow targeting specific fields for @updatedAt

2 participants