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..de9b6af --- /dev/null +++ b/src/tests/modifiers/autodate.test.ts @@ -0,0 +1,128 @@ +import { + AutoDateModifier, + CreationTime, + UpdateTime, +} from "@antelopejs/interface-database-decorators/modifiers/autodate"; +import { + fromPlainData, + toDatabase, + 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()); + it("omits creation date from update database payloads", async () => + OmitCreationDateFromUpdatePayloadTest()); +}); + +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); +} + +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); +}