Skip to content
This repository was archived by the owner on Mar 1, 2026. It is now read-only.

Commit 2b3d7d6

Browse files
sanny-ioymc9
andauthored
feat: ignore argument for @updatedAt (#572)
* feat: `ignore` argument for `@updatedAt` * chore: add tests * Trigger Build * Check test. * Use `getTime` * Retry. * Retry. * Retry. * Retry. * Retry. * Retry. * Retry. * Clean up. * Document param. * Extract to function. * Relocate function. * Adjust formatting. * Use `getAttributeArg` * Use `$resolvedParam` * Null check. * fix: resolve a merge error * fix: delay data clone to right before changing it --------- Co-authored-by: ymc9 <104139426+ymc9@users.noreply.github.com>
1 parent a42efc1 commit 2b3d7d6

9 files changed

Lines changed: 397 additions & 21 deletions

File tree

packages/cli/test/ts-schema-gen.test.ts

Lines changed: 282 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -466,4 +466,286 @@ model User {
466466
expect(schemaLite!.models.User.fields.id.attributes).toBeUndefined();
467467
expect(schemaLite!.models.User.fields.email.attributes).toBeUndefined();
468468
});
469+
470+
it('supports ignorable fields for @updatedAt', async () => {
471+
const { schema } = await generateTsSchema(`
472+
model User {
473+
id String @id @default(uuid())
474+
name String
475+
email String @unique
476+
createdAt DateTime @default(now())
477+
updatedAt DateTime @updatedAt(ignore: [email])
478+
posts Post[]
479+
480+
@@map('users')
481+
}
482+
483+
model Post {
484+
id String @id @default(cuid())
485+
title String
486+
published Boolean @default(false)
487+
author User @relation(fields: [authorId], references: [id], onDelete: Cascade)
488+
authorId String
489+
}
490+
`);
491+
492+
expect(schema).toMatchObject({
493+
provider: {
494+
type: 'sqlite'
495+
},
496+
models: {
497+
User: {
498+
name: 'User',
499+
fields: {
500+
id: {
501+
name: 'id',
502+
type: 'String',
503+
id: true,
504+
attributes: [
505+
{
506+
name: '@id'
507+
},
508+
{
509+
name: '@default',
510+
args: [
511+
{
512+
name: 'value',
513+
value: {
514+
kind: 'call',
515+
function: 'uuid'
516+
}
517+
}
518+
]
519+
}
520+
],
521+
default: {
522+
kind: 'call',
523+
function: 'uuid'
524+
}
525+
},
526+
name: {
527+
name: 'name',
528+
type: 'String'
529+
},
530+
email: {
531+
name: 'email',
532+
type: 'String',
533+
unique: true,
534+
attributes: [
535+
{
536+
name: '@unique'
537+
}
538+
]
539+
},
540+
createdAt: {
541+
name: 'createdAt',
542+
type: 'DateTime',
543+
attributes: [
544+
{
545+
name: '@default',
546+
args: [
547+
{
548+
name: 'value',
549+
value: {
550+
kind: 'call',
551+
function: 'now'
552+
}
553+
}
554+
]
555+
}
556+
],
557+
default: {
558+
kind: 'call',
559+
function: 'now'
560+
}
561+
},
562+
updatedAt: {
563+
name: 'updatedAt',
564+
type: 'DateTime',
565+
updatedAt: {
566+
ignore: [
567+
'email'
568+
]
569+
},
570+
attributes: [
571+
{
572+
name: '@updatedAt',
573+
args: [
574+
{
575+
name: 'ignore',
576+
value: {
577+
kind: 'array',
578+
items: [
579+
{
580+
kind: 'field',
581+
field: 'email'
582+
}
583+
]
584+
}
585+
}
586+
]
587+
}
588+
]
589+
},
590+
posts: {
591+
name: 'posts',
592+
type: 'Post',
593+
array: true,
594+
relation: {
595+
opposite: 'author'
596+
}
597+
}
598+
},
599+
attributes: [
600+
{
601+
name: '@@map',
602+
args: [
603+
{
604+
name: 'name',
605+
value: {
606+
kind: 'literal',
607+
value: 'users'
608+
}
609+
}
610+
]
611+
}
612+
],
613+
idFields: [
614+
'id'
615+
],
616+
uniqueFields: {
617+
id: {
618+
type: 'String'
619+
},
620+
email: {
621+
type: 'String'
622+
}
623+
}
624+
},
625+
Post: {
626+
name: 'Post',
627+
fields: {
628+
id: {
629+
name: 'id',
630+
type: 'String',
631+
id: true,
632+
attributes: [
633+
{
634+
name: '@id'
635+
},
636+
{
637+
name: '@default',
638+
args: [
639+
{
640+
name: 'value',
641+
value: {
642+
kind: 'call',
643+
function: 'cuid'
644+
}
645+
}
646+
]
647+
}
648+
],
649+
default: {
650+
kind: 'call',
651+
function: 'cuid'
652+
}
653+
},
654+
title: {
655+
name: 'title',
656+
type: 'String'
657+
},
658+
published: {
659+
name: 'published',
660+
type: 'Boolean',
661+
attributes: [
662+
{
663+
name: '@default',
664+
args: [
665+
{
666+
name: 'value',
667+
value: {
668+
kind: 'literal',
669+
value: false
670+
}
671+
}
672+
]
673+
}
674+
],
675+
default: false
676+
},
677+
author: {
678+
name: 'author',
679+
type: 'User',
680+
attributes: [
681+
{
682+
name: '@relation',
683+
args: [
684+
{
685+
name: 'fields',
686+
value: {
687+
kind: 'array',
688+
items: [
689+
{
690+
kind: 'field',
691+
field: 'authorId'
692+
}
693+
]
694+
}
695+
},
696+
{
697+
name: 'references',
698+
value: {
699+
kind: 'array',
700+
items: [
701+
{
702+
kind: 'field',
703+
field: 'id'
704+
}
705+
]
706+
}
707+
},
708+
{
709+
name: 'onDelete',
710+
value: {
711+
kind: 'literal',
712+
value: 'Cascade'
713+
}
714+
}
715+
]
716+
}
717+
],
718+
relation: {
719+
opposite: 'posts',
720+
fields: [
721+
'authorId'
722+
],
723+
references: [
724+
'id'
725+
],
726+
onDelete: 'Cascade'
727+
}
728+
},
729+
authorId: {
730+
name: 'authorId',
731+
type: 'String',
732+
foreignKeyFor: [
733+
'author'
734+
]
735+
}
736+
},
737+
idFields: [
738+
'id'
739+
],
740+
uniqueFields: {
741+
id: {
742+
type: 'String'
743+
}
744+
}
745+
}
746+
},
747+
authType: 'User',
748+
plugins: {}
749+
});
750+
})
469751
});

packages/language/res/stdlib.zmodel

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -402,8 +402,12 @@ attribute @omit()
402402

403403
/**
404404
* Automatically stores the time when a record was last updated.
405+
*
406+
* @param ignore: A list of field names that are not considered when the ORM client is determining whether any
407+
* updates have been made to a record. An update that only contains ignored fields does not change the
408+
* timestamp.
405409
*/
406-
attribute @updatedAt() @@@targetField([DateTimeField]) @@@prisma
410+
attribute @updatedAt(_ ignore: FieldReference[]?) @@@targetField([DateTimeField]) @@@prisma
407411

408412
/**
409413
* Add full text index (MySQL only).

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import type {
3131
SchemaDef,
3232
TypeDefFieldIsArray,
3333
TypeDefFieldIsOptional,
34+
UpdatedAtInfo,
3435
} from '../schema';
3536
import type {
3637
AtLeast,
@@ -994,7 +995,7 @@ type OptionalFieldsForCreate<Schema extends SchemaDef, Model extends GetModels<S
994995
? Key
995996
: FieldIsArray<Schema, Model, Key> extends true
996997
? Key
997-
: GetModelField<Schema, Model, Key>['updatedAt'] extends true
998+
: GetModelField<Schema, Model, Key>['updatedAt'] extends (true | UpdatedAtInfo)
998999
? Key
9991000
: never]: GetModelField<Schema, Model, Key>;
10001001
};

packages/orm/src/client/crud/operations/base.ts

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1149,11 +1149,20 @@ export abstract class BaseOperationHandler<Schema extends SchemaDef> {
11491149
const autoUpdatedFields: string[] = [];
11501150
for (const [fieldName, fieldDef] of Object.entries(modelDef.fields)) {
11511151
if (fieldDef.updatedAt && finalData[fieldName] === undefined) {
1152-
if (finalData === data) {
1153-
finalData = clone(data);
1152+
const ignoredFields = new Set(typeof fieldDef.updatedAt === 'boolean' ? [] : fieldDef.updatedAt.ignore);
1153+
const hasNonIgnoredFields = Object.keys(data).some(
1154+
(field) =>
1155+
(isScalarField(this.schema, modelDef.name, field) ||
1156+
isForeignKeyField(this.schema, modelDef.name, field)) &&
1157+
!ignoredFields.has(field),
1158+
);
1159+
if (hasNonIgnoredFields) {
1160+
if (finalData === data) {
1161+
finalData = clone(data);
1162+
}
1163+
finalData[fieldName] = this.dialect.transformInput(new Date(), 'DateTime', false);
1164+
autoUpdatedFields.push(fieldName);
11541165
}
1155-
finalData[fieldName] = this.dialect.transformInput(new Date(), 'DateTime', false);
1156-
autoUpdatedFields.push(fieldName);
11571166
}
11581167
}
11591168

packages/schema/src/schema.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,14 +59,18 @@ export type RelationInfo = {
5959
onUpdate?: CascadeAction;
6060
};
6161

62+
export type UpdatedAtInfo = {
63+
ignore?: readonly string[];
64+
};
65+
6266
export type FieldDef = {
6367
name: string;
6468
type: string;
6569
id?: boolean;
6670
array?: boolean;
6771
optional?: boolean;
6872
unique?: boolean;
69-
updatedAt?: boolean;
73+
updatedAt?: boolean | UpdatedAtInfo;
7074
attributes?: readonly AttributeApplication[];
7175
default?: MappedBuiltinType | Expression | readonly unknown[];
7276
omit?: boolean;
@@ -283,7 +287,7 @@ export type FieldHasDefault<
283287
Field extends GetModelFields<Schema, Model>,
284288
> = GetModelField<Schema, Model, Field>['default'] extends object | number | string | boolean
285289
? true
286-
: GetModelField<Schema, Model, Field>['updatedAt'] extends true
290+
: GetModelField<Schema, Model, Field>['updatedAt'] extends (true | UpdatedAtInfo)
287291
? true
288292
: GetModelField<Schema, Model, Field>['relation'] extends { hasDefault: true }
289293
? true

0 commit comments

Comments
 (0)