From 477e32b4138a8e0f106e0ffc7255942b22ea330f Mon Sep 17 00:00:00 2001 From: Antony Rizzitelli Date: Wed, 15 Jul 2026 15:20:10 +0200 Subject: [PATCH 1/2] feat(modifiers): add AutoDateModifier with CreationTime and UpdateTime decorators Move the automatic timestamp modifier from the CMS cms-base interface into the shared modifiers so any table definition can use it. Fields marked with CreationTime are set on insert and preserved on update, while UpdateTime fields are refreshed on every write. --- docs/3.table-modifiers.md | 24 +++++++ package.json | 7 ++ src/modifiers/autodate.ts | 54 ++++++++++++++++ src/modifiers/index.ts | 1 + src/tests/modifiers/autodate.test.ts | 96 ++++++++++++++++++++++++++++ 5 files changed, 182 insertions(+) create mode 100644 src/modifiers/autodate.ts create mode 100644 src/tests/modifiers/autodate.test.ts diff --git a/docs/3.table-modifiers.md b/docs/3.table-modifiers.md index b27009c..56d0e82 100644 --- a/docs/3.table-modifiers.md +++ b/docs/3.table-modifiers.md @@ -157,6 +157,30 @@ localize(locale: string, fields?: Array): this Sets the active locale for accessing localized fields. If `fields` is provided, only those fields are unlocked for the specified locale. +### AutoDateModifier + +The `AutoDateModifier` automatically populates timestamp fields during insert and update operations. Use the `CreationTime` and `UpdateTime` decorators to mark fields. + +```typescript +import { Table, CreationTime, UpdateTime } from "@antelopejs/interface-database-decorators"; + +class Article extends Table { + declare title: string; + + @CreationTime() + declare creationDate: Date; + + @UpdateTime() + declare updateDate: Date; +} +``` + +#### How It Works + +- On insert, both `CreationTime` and `UpdateTime` fields are set to the current date. +- On update, `UpdateTime` fields are refreshed while `CreationTime` fields are removed from the payload so the original creation date is preserved. +- This is an event-only modifier: it does not transform stored values and does not require adding a mixin through `Table.with()`. + ## Combine Multiple Modifiers Pass multiple modifier classes to `Table.with()`: diff --git a/package.json b/package.json index 146ea86..84925f9 100644 --- a/package.json +++ b/package.json @@ -49,6 +49,10 @@ "types": "./dist/table.d.ts", "default": "./dist/table.js" }, + "./modifiers/autodate": { + "types": "./dist/modifiers/autodate.d.ts", + "default": "./dist/modifiers/autodate.js" + }, "./modifiers/common": { "types": "./dist/modifiers/common.d.ts", "default": "./dist/modifiers/common.js" @@ -125,6 +129,9 @@ "table": [ "dist/table.d.ts" ], + "modifiers/autodate": [ + "dist/modifiers/autodate.d.ts" + ], "modifiers/common": [ "dist/modifiers/common.d.ts" ], diff --git a/src/modifiers/autodate.ts b/src/modifiers/autodate.ts new file mode 100644 index 0000000..86a0dbd --- /dev/null +++ b/src/modifiers/autodate.ts @@ -0,0 +1,54 @@ +import { MakePropertyDecorator } from "@antelopejs/interface-core/decorators"; +import { attachModifier, Modifier } from "./common"; + +type AutoDateType = "created" | "updated"; + +type Options = { + /** Timestamp behavior: `created` is only set on insert, `updated` is refreshed on every update */ + type: AutoDateType; +}; + +/** + * Auto-date modifier. Enables the use of {@link CreationTime} and + * {@link UpdateTime} on table fields. + * + * This is an event-only modifier: it does not transform stored values and + * does not require the Table class to incorporate a mixin. + */ +export class AutoDateModifier extends Modifier { + public insert(object: Record, field: string) { + object[field] = new Date(); + } + + public update(object: Record, field: string) { + if (this.options.type === "updated") { + object[field] = new Date(); + } else { + delete object[field]; + } + } +} + +/** + * Mark a Database Table class field as an automatic creation timestamp. + * + * The field is set to the current date on insert and removed from update + * payloads so the original creation date is preserved. + */ +export const CreationTime = MakePropertyDecorator((target, propertyKey) => { + attachModifier(target.constructor, AutoDateModifier, propertyKey, { + type: "created", + }); +}); + +/** + * Mark a Database Table class field as an automatic update timestamp. + * + * The field is set to the current date on insert and refreshed on every + * update. + */ +export const UpdateTime = MakePropertyDecorator((target, propertyKey) => { + attachModifier(target.constructor, AutoDateModifier, propertyKey, { + type: "updated", + }); +}); diff --git a/src/modifiers/index.ts b/src/modifiers/index.ts index 865e095..e44b836 100644 --- a/src/modifiers/index.ts +++ b/src/modifiers/index.ts @@ -1,3 +1,4 @@ +export * from "./autodate"; export * from "./common"; export * from "./encryption"; export * from "./hash"; diff --git a/src/tests/modifiers/autodate.test.ts b/src/tests/modifiers/autodate.test.ts new file mode 100644 index 0000000..d82e499 --- /dev/null +++ b/src/tests/modifiers/autodate.test.ts @@ -0,0 +1,96 @@ +import { + AutoDateModifier, + CreationTime, + UpdateTime, +} from "@antelopejs/interface-database-decorators/modifiers/autodate"; +import { triggerEvent } from "@antelopejs/interface-database-decorators/modifiers/common"; +import { Field, Table } from "@antelopejs/interface-database-decorators/table"; +import { expect } from "chai"; + +describe("Modifiers - autodate", () => { + it("sets creation date on insert", async () => SetCreationDateOnInsertTest()); + it("sets update date on insert", async () => SetUpdateDateOnInsertTest()); + it("refreshes update date on update", async () => + RefreshUpdateDateOnUpdateTest()); + it("preserves creation date on update", async () => + PreserveCreationDateOnUpdateTest()); + it("handles decorators through events", async () => + HandleDecoratorsThroughEventsTest()); +}); + +interface AutoDateOptions { + type: "created" | "updated"; +} + +class TestableAutoDateModifier extends AutoDateModifier { + public setOptions(options: AutoDateOptions) { + this.options = options; + } +} + +async function SetCreationDateOnInsertTest() { + const modifier = new TestableAutoDateModifier(); + modifier.setOptions({ type: "created" }); + + const object: Record = {}; + modifier.insert(object, "createdAt"); + + expect(object.createdAt).to.be.an.instanceOf(Date); +} + +async function SetUpdateDateOnInsertTest() { + const modifier = new TestableAutoDateModifier(); + modifier.setOptions({ type: "updated" }); + + const object: Record = {}; + modifier.insert(object, "updatedAt"); + + expect(object.updatedAt).to.be.an.instanceOf(Date); +} + +async function RefreshUpdateDateOnUpdateTest() { + const modifier = new TestableAutoDateModifier(); + modifier.setOptions({ type: "updated" }); + + const previousDate = new Date(0); + const object: Record = { updatedAt: previousDate }; + modifier.update(object, "updatedAt"); + + expect(object.updatedAt).to.be.an.instanceOf(Date); + expect(object.updatedAt).to.not.equal(previousDate); +} + +async function PreserveCreationDateOnUpdateTest() { + const modifier = new TestableAutoDateModifier(); + modifier.setOptions({ type: "created" }); + + const object: Record = { createdAt: new Date(0) }; + modifier.update(object, "createdAt"); + + expect(object).to.not.have.property("createdAt"); +} + +async function HandleDecoratorsThroughEventsTest() { + class TestTable extends Table { + @CreationTime() + @Field("date") + declare createdAt: Date; + + @UpdateTime() + @Field("date") + declare updatedAt: Date; + } + + const instance = new TestTable(); + triggerEvent(instance, "insert"); + + expect(instance.createdAt).to.be.an.instanceOf(Date); + expect(instance.updatedAt).to.be.an.instanceOf(Date); + + const insertedUpdateDate = instance.updatedAt; + triggerEvent(instance, "update"); + + expect(instance.createdAt).to.equal(undefined); + expect(instance.updatedAt).to.be.an.instanceOf(Date); + expect(instance.updatedAt).to.not.equal(insertedUpdateDate); +} From 6880baa948dc48b071910bca896dfb4a1113e3f3 Mon Sep 17 00:00:00 2001 From: Antony Rizzitelli Date: Wed, 15 Jul 2026 15:49:00 +0200 Subject: [PATCH 2/2] test(modifiers): assert autodate behavior on database payloads Address greptile review feedback: verify through toDatabase that update payloads omit CreationTime fields and refresh UpdateTime fields. --- src/tests/modifiers/autodate.test.ts | 34 +++++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/src/tests/modifiers/autodate.test.ts b/src/tests/modifiers/autodate.test.ts index d82e499..de9b6af 100644 --- a/src/tests/modifiers/autodate.test.ts +++ b/src/tests/modifiers/autodate.test.ts @@ -3,7 +3,11 @@ import { CreationTime, UpdateTime, } from "@antelopejs/interface-database-decorators/modifiers/autodate"; -import { triggerEvent } from "@antelopejs/interface-database-decorators/modifiers/common"; +import { + fromPlainData, + toDatabase, + triggerEvent, +} from "@antelopejs/interface-database-decorators/modifiers/common"; import { Field, Table } from "@antelopejs/interface-database-decorators/table"; import { expect } from "chai"; @@ -16,6 +20,8 @@ describe("Modifiers - autodate", () => { PreserveCreationDateOnUpdateTest()); it("handles decorators through events", async () => HandleDecoratorsThroughEventsTest()); + it("omits creation date from update database payloads", async () => + OmitCreationDateFromUpdatePayloadTest()); }); interface AutoDateOptions { @@ -94,3 +100,29 @@ async function HandleDecoratorsThroughEventsTest() { expect(instance.updatedAt).to.be.an.instanceOf(Date); expect(instance.updatedAt).to.not.equal(insertedUpdateDate); } + +async function OmitCreationDateFromUpdatePayloadTest() { + class TestTable extends Table { + @CreationTime() + @Field("date") + declare createdAt: Date; + + @UpdateTime() + @Field("date") + declare updatedAt: Date; + } + + const inserted = fromPlainData({}, TestTable); + triggerEvent(inserted, "insert"); + const insertPayload = toDatabase(inserted); + + expect(insertPayload.createdAt).to.be.an.instanceOf(Date); + expect(insertPayload.updatedAt).to.be.an.instanceOf(Date); + + const updated = fromPlainData({ createdAt: new Date(0) }, TestTable); + triggerEvent(updated, "update"); + const updatePayload = toDatabase(updated); + + expect(updatePayload).to.not.have.property("createdAt"); + expect(updatePayload.updatedAt).to.be.an.instanceOf(Date); +}