Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
24 changes: 24 additions & 0 deletions docs/3.table-modifiers.md
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,30 @@ localize(locale: string, fields?: Array<keyof this>): 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()`:
Expand Down
7 changes: 7 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -125,6 +129,9 @@
"table": [
"dist/table.d.ts"
],
"modifiers/autodate": [
"dist/modifiers/autodate.d.ts"
],
"modifiers/common": [
"dist/modifiers/common.d.ts"
],
Expand Down
54 changes: 54 additions & 0 deletions src/modifiers/autodate.ts
Original file line number Diff line number Diff line change
@@ -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<object, Options> {
Comment thread
Upd4ting marked this conversation as resolved.
public insert(object: Record<string, unknown>, field: string) {
object[field] = new Date();
}

public update(object: Record<string, unknown>, field: string) {
if (this.options.type === "updated") {
object[field] = new Date();
} else {
delete object[field];
}
Comment thread
Upd4ting marked this conversation as resolved.
}
}

/**
* 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",
});
});
1 change: 1 addition & 0 deletions src/modifiers/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from "./autodate";
export * from "./common";
export * from "./encryption";
export * from "./hash";
Expand Down
128 changes: 128 additions & 0 deletions src/tests/modifiers/autodate.test.ts
Original file line number Diff line number Diff line change
@@ -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<string, unknown> = {};
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<string, unknown> = {};
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<string, unknown> = { 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<string, unknown> = { 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);
}
Comment thread
Upd4ting marked this conversation as resolved.

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);
}
Loading