Skip to content

Commit

Permalink
feat(Model): add autoCreatedAt & updatedAt
Browse files Browse the repository at this point in the history
  • Loading branch information
“Hamza authored and MarioArnt committed Jan 15, 2024
1 parent 27f8c47 commit 4ce8d2b
Show file tree
Hide file tree
Showing 7 changed files with 169 additions and 3 deletions.
19 changes: 16 additions & 3 deletions src/base-model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import {
} from '@aws-sdk/lib-dynamodb';
import Query from './query';
import Scan from './scan';
import { IUpdateActions, buildUpdateActions } from './update-operators';
import { IUpdateActions, buildUpdateActions, put } from './update-operators';
import ValidationError from './validation-error';
import { PutCommandInput } from '@aws-sdk/lib-dynamodb/dist-types/commands/PutCommand';

Expand All @@ -44,6 +44,10 @@ export default abstract class Model<T> {

protected schema: ObjectSchema | undefined;

protected autoCreatedAt = false;

protected autoUpdatedAt = false;

constructor(item?: T, options?: DynamoDBClientConfig, translateConfig?: TranslateConfig) {
this.item = item;
const client = new DynamoDBClient(options ?? { region: process.env.AWS_REGION });
Expand Down Expand Up @@ -163,6 +167,9 @@ export default abstract class Model<T> {
error.name = 'E_ALREADY_EXISTS';
throw error;
}
if (this.autoCreatedAt) {
Object.assign(toCreate, { createdAt: new Date().toISOString() });
}
// Save item
return this.save(toCreate, putOptions);
}
Expand Down Expand Up @@ -204,6 +211,9 @@ export default abstract class Model<T> {
throw new ValidationError('Validation error', error);
}
}
if (this.autoUpdatedAt) {
Object.assign(toSave, { updatedAt: new Date().toISOString() });
}
// Prepare putItem operation
const params: PutCommandInput = {
TableName: this.tableName,
Expand Down Expand Up @@ -337,8 +347,8 @@ export default abstract class Model<T> {
const _pk: KeyValue | undefined = Model.isKey(pk_item)
? pk_item
: pk_item
? this.pkValue(pk_item)
: undefined;
? this.pkValue(pk_item)
: undefined;
const _sk: KeyValue = sk_options != null && Model.isKey(sk_options) ? sk_options : null;
const deleteOptions = this.getOptions(sk_options, options);
// Build delete item params
Expand Down Expand Up @@ -537,6 +547,9 @@ export default abstract class Model<T> {
nativeOptions = options;
}
this.testKeys(pk, sk);
if (this.autoUpdatedAt) {
updateActions['updatedAt'] = put(new Date().toISOString());
}
const params: UpdateCommandInput = {
TableName: this.tableName,
Key: this.buildKeys(pk, sk),
Expand Down
37 changes: 37 additions & 0 deletions test/create.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { clearTables } from './hooks/create-tables';
import HashKeyModel from './models/hashkey';
import TimeTrackedModel from './models/autoCreatedAt-autoUpdatedAt';
import HashKeyJoiModel from './models/hashkey-joi';
import CompositeKeyModel from './models/composite-keys';

Expand All @@ -25,6 +26,42 @@ describe('The create method', () => {
const saved = await foo.get('bar');
expect(saved).toEqual(item);
});
test('should not save with the createdAt field if autoCreatedAt is set to false', async () => {
const item = {
hashkey: 'bar',
string: 'whatever',
stringmap: { foo: 'bar' },
stringset: ['bar, bar'],
number: 43,
bool: true,
list: ['foo', 42],
};
const foo = new HashKeyModel(item);
expect(foo.getItem()).toBe(item);
await foo.create({
ReturnConsumedCapacity: 'NONE',
});
const saved = await foo.get('bar');
expect(saved).not.toHaveProperty('createdAt')
});
test('should save the item with createdAt field when autoCreatedAt is enabled', async () => {
const item = {
hashkey: 'bar',
string: 'whatever',
stringmap: { foo: 'bar' },
stringset: ['bar, bar'],
number: 43,
bool: true,
list: ['foo', 42],
};
const foo = new TimeTrackedModel(item);
expect(foo.getItem()).toBe(item);
await foo.create({
ReturnConsumedCapacity: 'NONE',
});
const saved = await foo.get('bar');
expect(saved).toHaveProperty('createdAt')
});
test('should throw an error if not item is held by the class', async () => {
const foo = new HashKeyModel();
try {
Expand Down
2 changes: 2 additions & 0 deletions test/hooks/create-tables.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import compositeTable from '../tables/composite-key';
import hashTable from '../tables/hash-key';
import numericalTable from '../tables/numerical-keys';
import timeTrackedTable from '../tables/autoCreatedAt-autoUpdatedAt';
import {
CreateTableCommand,
CreateTableCommandInput,
Expand All @@ -12,6 +13,7 @@ const tables: Record<string, CreateTableCommandInput> = {
hashTable,
compositeTable,
numericalTable,
timeTrackedTable
};

const dynamodb = new DynamoDBClient({
Expand Down
20 changes: 20 additions & 0 deletions test/models/autoCreatedAt-autoUpdatedAt.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import Model from "../../src";
import { HashKeyEntity } from "./hashkey";
import documentClient from './common';

interface TimeTrackedEntity {
createdAt?: string;
updatedAt?: string;
}

export default class TimeTrackedModel extends Model<HashKeyEntity & TimeTrackedEntity> {
protected tableName = 'table_test_autoCreatedAt_autoUpdatedAt';

protected pk = 'hashkey';

protected documentClient = documentClient;

protected autoCreatedAt = true;

protected autoUpdatedAt = true;
}
34 changes: 34 additions & 0 deletions test/save.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { clearTables } from './hooks/create-tables';
import HashKeyModel from './models/hashkey';
import TimeTrackedModel from './models/autoCreatedAt-autoUpdatedAt';
import HashKeyJoiModel from './models/hashkey-joi';

describe('The save method', () => {
Expand All @@ -21,6 +22,39 @@ describe('The save method', () => {
const saved = await foo.get('bar');
expect(saved).toEqual(item);
});
test('should save the item with the updatedAt field when autoUpdatedAt is enabled', async () => {
const item = {
hashkey: 'bar',
string: 'whatever',
stringmap: { foo: 'bar' },
stringset: ['bar, bar'],
number: 43,
bool: true,
list: ['foo', 42],
};
const foo = new TimeTrackedModel();
await foo.save(item);
const saved = await foo.get('bar');
expect(saved).toHaveProperty('updatedAt');
});
test('should save the item and update the updatedAt field when calling the save method subsequently', async () => {
const item = {
hashkey: 'bar',
string: 'whatever',
stringmap: { foo: 'bar' },
stringset: ['bar, bar'],
number: 43,
bool: true,
list: ['foo', 42],
};
const foo = new TimeTrackedModel();
await foo.save(item);
const firstCallResult = await foo.get('bar');
await foo.save(item);
const secondCallResult = await foo.get('bar');
expect(firstCallResult.updatedAt).not.toEqual(secondCallResult.updatedAt);

});
test('should throw an error if not item is held by the class', async () => {
const foo = new HashKeyModel();
try {
Expand Down
21 changes: 21 additions & 0 deletions test/tables/autoCreatedAt-autoUpdatedAt.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { CreateTableCommandInput } from "@aws-sdk/client-dynamodb";

export default {
AttributeDefinitions: [
{
AttributeName: 'hashkey',
AttributeType: 'S',
},
],
KeySchema: [
{
AttributeName: 'hashkey',
KeyType: 'HASH',
},
],
ProvisionedThroughput: {
ReadCapacityUnits: 5,
WriteCapacityUnits: 5,
},
TableName: 'table_test_autoCreatedAt_autoUpdatedAt',
} as CreateTableCommandInput;
39 changes: 39 additions & 0 deletions test/update.spec.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import HashKeyModel from './models/hashkey';
import TimeTrackedModel from './models/autoCreatedAt-autoUpdatedAt';
import { clearTables } from './hooks/create-tables';
import { put, remove } from '../src';

describe('The update method', () => {
const model = new HashKeyModel();
const timeTrackedModel = new TimeTrackedModel();
beforeAll(async () => {
await clearTables();
await model.save({
Expand All @@ -19,6 +21,43 @@ describe('The update method', () => {
optionalList: [42, 'foo'],
optionalStringmap: { bar: 'baz' },
});
await timeTrackedModel.save({
hashkey: 'hashkey',
number: 42,
bool: true,
string: 'string',
stringset: ['string', 'string'],
list: [42, 'foo'],
stringmap: { bar: 'baz' },
optionalNumber: 42,
optionalStringset: ['string', 'string'],
optionalList: [42, 'foo'],
optionalStringmap: { bar: 'baz' },
});
});
test('should save the item with updatedAt field when updating an item for the first time and autoUpdatedAt is enabled', async () => {
await timeTrackedModel.update('hashkey', {
number: put(43),
bool: put(null),
optionalNumber: remove(),
});
const updatedItem = await timeTrackedModel.get('hashkey');
expect(updatedItem).toHaveProperty('updatedAt');

});
test('should save the item and update the updatedAt field when calling the update method subsequently', async () => {
await timeTrackedModel.update('hashkey', {
number: put(43),
bool: put(null),
optionalNumber: remove(),
});
const firstCallResult = await timeTrackedModel.get('hashkey');
await timeTrackedModel.update('hashkey', {
number: put(13),
});
const secondCallResult = await timeTrackedModel.get('hashkey');
expect(firstCallResult.updatedAt).not.toEqual(secondCallResult.updatedAt);

});
test('should update the item with the correct actions', async () => {
await model.update('hashkey', {
Expand Down

0 comments on commit 4ce8d2b

Please sign in to comment.