Skip to content
Open
Show file tree
Hide file tree
Changes from 7 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
284 changes: 283 additions & 1 deletion packages/cli/test/ts-schema-gen.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -747,5 +747,287 @@ model Post {
authType: 'User',
plugins: {}
});
})
});

it('supports specifying 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(fields: [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: {
fields: [
'email'
]
},
attributes: [
{
name: '@updatedAt',
args: [
{
name: 'fields',
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: {}
});
});
});
8 changes: 6 additions & 2 deletions packages/language/res/stdlib.zmodel
Original file line number Diff line number Diff line change
Expand Up @@ -405,9 +405,13 @@ attribute @omit()
*
* @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.
* timestamp. Mutually exclusive with the `fields` parameter.

* @param fields: A list of field names that are considered when the ORM client is determining whether any
* updates have been made to a record. The timestamp will only change when any of the specified fields
* are updated. Mutually exclusive with the `ignore` parameter.
*/
attribute @updatedAt(ignore: FieldReference[]?) @@@targetField([DateTimeField]) @@@prisma
attribute @updatedAt(ignore: FieldReference[]?, fields: FieldReference[]?) @@@targetField([DateTimeField]) @@@prisma

/**
* Add full text index (MySQL only).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,15 @@ export default class AttributeApplicationValidator implements AstValidator<Attri
}
}

@check('@updatedAt')
private _checkUpdatedAt(attr: AttributeApplication, accept: ValidationAcceptor) {
const ignoreArg = attr.args.find(arg => arg.$resolvedParam.name === 'ignore');
const fieldsArg = attr.args.find(arg => arg.$resolvedParam.name === 'fields');
if (ignoreArg && fieldsArg) {
accept('error', `\`ignore\` and \`fields\` are mutually exclusive`, { node: attr.$container });
}
}

@check('@@validate')
private _checkValidate(attr: AttributeApplication, accept: ValidationAcceptor) {
const condition = attr.args[0]?.value;
Expand Down
31 changes: 23 additions & 8 deletions packages/orm/src/client/crud/operations/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1159,14 +1159,29 @@ export abstract class BaseOperationHandler<Schema extends SchemaDef> {
const autoUpdatedFields: string[] = [];
for (const [fieldName, fieldDef] of Object.entries(modelDef.fields)) {
if (fieldDef.updatedAt && finalData[fieldName] === undefined) {
const ignoredFields = new Set(typeof fieldDef.updatedAt === 'boolean' ? [] : fieldDef.updatedAt.ignore);
const hasNonIgnoredFields = Object.keys(data).some(
(field) =>
(isScalarField(this.schema, modelDef.name, field) ||
isForeignKeyField(this.schema, modelDef.name, field)) &&
!ignoredFields.has(field),
);
if (hasNonIgnoredFields) {
let hasUpdated = true;
if (typeof fieldDef.updatedAt === 'object') {
if (fieldDef.updatedAt.ignore) {
const ignoredFields = new Set(fieldDef.updatedAt.ignore);
const hasNonIgnoredFields = Object.keys(data).some(
(field) =>
(isScalarField(this.schema, modelDef.name, field) ||
isForeignKeyField(this.schema, modelDef.name, field)) &&
!ignoredFields.has(field),
);
hasUpdated = hasNonIgnoredFields;
} else if (fieldDef.updatedAt.fields) {
const targetFields = new Set(fieldDef.updatedAt.fields);
const hasAnyTargetFields = Object.keys(data).some(
(field) =>
(isScalarField(this.schema, modelDef.name, field) ||
isForeignKeyField(this.schema, modelDef.name, field)) &&
targetFields.has(field),
);
hasUpdated = hasAnyTargetFields;
}
}
if (hasUpdated) {
if (finalData === data) {
Comment thread
coderabbitai[bot] marked this conversation as resolved.
finalData = clone(data);
}
Expand Down
1 change: 1 addition & 0 deletions packages/schema/src/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ export type RelationInfo = {

export type UpdatedAtInfo = {
ignore?: readonly string[];
fields?: readonly string[];
};

export type FieldDef = {
Expand Down
Loading
Loading