From 100f5d42c8b915fb1102e0b0d88b125b1c634896 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?William=20Lin=C2=A0?= Date: Fri, 10 Nov 2023 16:05:08 +0100 Subject: [PATCH 1/2] feat(model): autoCreatedAt field added to model --- src/base-model.ts | 13 +++++++++ test/create.spec.ts | 45 +++++++++++++++++++++++++++++++ test/models/hashkey-up-to-date.ts | 15 +++++++++++ 3 files changed, 73 insertions(+) create mode 100644 test/models/hashkey-up-to-date.ts diff --git a/src/base-model.ts b/src/base-model.ts index e4ee75e..d3dbb61 100644 --- a/src/base-model.ts +++ b/src/base-model.ts @@ -24,6 +24,12 @@ import ValidationError from './validation-error'; import { PutCommandInput } from '@aws-sdk/lib-dynamodb/dist-types/commands/PutCommand'; export type KeyValue = string | number | Buffer | boolean | null; + +export interface UpToDateEntity { + createdAt?: string; + updatedAt?: string; +} + type SimpleKey = KeyValue; type CompositeKey = { pk: KeyValue; sk: KeyValue }; type Keys = SimpleKey[] | CompositeKey[]; @@ -44,6 +50,10 @@ export default abstract class Model { 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 }); @@ -163,6 +173,9 @@ export default abstract class Model { error.name = 'E_ALREADY_EXISTS'; throw error; } + if (this.autoCreatedAt) { + (toCreate as T & UpToDateEntity).createdAt = new Date().toISOString(); + } // Save item return this.save(toCreate, putOptions); } diff --git a/test/create.spec.ts b/test/create.spec.ts index f3774d7..cb9efe3 100644 --- a/test/create.spec.ts +++ b/test/create.spec.ts @@ -2,11 +2,15 @@ import { clearTables } from './hooks/create-tables'; import HashKeyModel from './models/hashkey'; import HashKeyJoiModel from './models/hashkey-joi'; import CompositeKeyModel from './models/composite-keys'; +import HashKeyUpToDateModel from './models/hashkey-up-to-date'; describe('The create method', () => { beforeEach(async () => { await clearTables(); }); + afterEach(async () => { + jest.resetAllMocks(); + }); test('should save the item held by the class', async () => { const item = { hashkey: 'bar', @@ -25,6 +29,47 @@ describe('The create method', () => { const saved = await foo.get('bar'); expect(saved).toEqual(item); }); + test('should not override date when autoCreatedAt is false', async () => { + const item = { + hashkey: 'bar', + string: 'whatever', + stringmap: { foo: 'bar' }, + stringset: ['bar, bar'], + number: 43, + bool: true, + list: ['foo', 42], + createdAt: '2023-08-09T10:11:12.131Z', + }; + const foo = new HashKeyModel(item); + expect(foo.getItem()).toBe(item); + await foo.create({ + ReturnConsumedCapacity: 'NONE', + }); + const saved = await foo.get('bar'); + expect(saved).toEqual(item); + }); + test('should save the item with autoCreatedAt field', async () => { + const item = { + hashkey: 'bar', + string: 'whatever', + stringmap: { foo: 'bar' }, + stringset: ['bar, bar'], + number: 43, + bool: true, + list: ['foo', 42], + }; + const expectedItem = { + ...item, + createdAt: '2023-11-10T14:36:39.297Z', + } + const foo = new HashKeyUpToDateModel(item); + jest.spyOn(Date.prototype, 'toISOString').mockImplementation(() => '2023-11-10T14:36:39.297Z'); + await foo.create({ + ReturnConsumedCapacity: 'NONE', + }); + const saved = await foo.get('bar'); + expect(saved).toEqual(expectedItem); + }); test('should throw an error if not item is held by the class', async () => { const foo = new HashKeyModel(); try { diff --git a/test/models/hashkey-up-to-date.ts b/test/models/hashkey-up-to-date.ts new file mode 100644 index 0000000..4368389 --- /dev/null +++ b/test/models/hashkey-up-to-date.ts @@ -0,0 +1,15 @@ +import Model, { UpToDateEntity } from '../../src/base-model'; +import documentClient from './common'; +import { HashKeyEntity } from './hashkey'; + +export default class HashKeyUpToDateModel extends Model { + protected tableName = 'table_test_hashkey'; + + protected pk = 'hashkey'; + + protected documentClient = documentClient; + + protected autoCreatedAt = true; + + protected autoUpdatedAt = true; +} From 644013e5ce1a5e14d0e637da7ca5ba163555da38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?William=20Lin=C2=A0?= Date: Fri, 10 Nov 2023 17:04:32 +0100 Subject: [PATCH 2/2] feat(Model): auto updatedAt in save and update --- src/base-model.ts | 9 +++++++ test/create.spec.ts | 3 ++- test/save.spec.ts | 21 +++++++++++++++ test/update.spec.ts | 66 ++++++++++++++++++++++++++++++++++++++++++++- 4 files changed, 97 insertions(+), 2 deletions(-) diff --git a/src/base-model.ts b/src/base-model.ts index d3dbb61..11a1064 100644 --- a/src/base-model.ts +++ b/src/base-model.ts @@ -217,6 +217,9 @@ export default abstract class Model { throw new ValidationError('Validation error', error); } } + if (this.autoUpdatedAt) { + (toSave as T & UpToDateEntity).updatedAt = new Date().toISOString(); + } // Prepare putItem operation const params: PutCommandInput = { TableName: this.tableName, @@ -550,6 +553,12 @@ export default abstract class Model { nativeOptions = options; } this.testKeys(pk, sk); + if (this.autoUpdatedAt) { + updateActions['updatedAt'] = { + action: "PUT", + value: new Date().toISOString(), + } + } const params: UpdateCommandInput = { TableName: this.tableName, Key: this.buildKeys(pk, sk), diff --git a/test/create.spec.ts b/test/create.spec.ts index cb9efe3..51a51b9 100644 --- a/test/create.spec.ts +++ b/test/create.spec.ts @@ -48,7 +48,7 @@ describe('The create method', () => { const saved = await foo.get('bar'); expect(saved).toEqual(item); }); - test('should save the item with autoCreatedAt field', async () => { + test('should save the item with autoCreatedAt field and autoUpdatedAt field', async () => { const item = { hashkey: 'bar', string: 'whatever', @@ -61,6 +61,7 @@ describe('The create method', () => { const expectedItem = { ...item, createdAt: '2023-11-10T14:36:39.297Z', + updatedAt: '2023-11-10T14:36:39.297Z', } const foo = new HashKeyUpToDateModel(item); jest.spyOn(Date.prototype, 'toISOString').mockImplementation(() => '2023-11-10T14:36:39.297Z'); diff --git a/test/save.spec.ts b/test/save.spec.ts index de47ee7..80c0cb2 100644 --- a/test/save.spec.ts +++ b/test/save.spec.ts @@ -1,6 +1,7 @@ import { clearTables } from './hooks/create-tables'; import HashKeyModel from './models/hashkey'; import HashKeyJoiModel from './models/hashkey-joi'; +import HashKeyUpToDateModel from './models/hashkey-up-to-date'; describe('The save method', () => { beforeEach(async () => { @@ -21,6 +22,26 @@ describe('The save method', () => { const saved = await foo.get('bar'); expect(saved).toEqual(item); }); + test('should save the item with updatedAt field', async () => { + jest.spyOn(Date.prototype, 'toISOString').mockImplementation(() => '2023-11-10T14:36:39.297Z'); + const item = { + hashkey: 'bar', + string: 'whatever', + stringmap: { foo: 'bar' }, + stringset: ['bar, bar'], + number: 43, + bool: true, + list: ['foo', 42], + }; + const savedItem = { + ...item, + updatedAt: '2023-11-10T14:36:39.297Z' + } + const foo = new HashKeyUpToDateModel(item); + await foo.save(); + const saved = await foo.get('bar'); + expect(saved).toEqual(savedItem); + }); test('should throw an error if not item is held by the class', async () => { const foo = new HashKeyModel(); try { diff --git a/test/update.spec.ts b/test/update.spec.ts index 5a6cb4e..b32a02f 100644 --- a/test/update.spec.ts +++ b/test/update.spec.ts @@ -1,10 +1,12 @@ import HashKeyModel from './models/hashkey'; import { clearTables } from './hooks/create-tables'; import { put, remove } from '../src'; +import HashKeyUpToDateModel from './models/hashkey-up-to-date'; describe('The update method', () => { const model = new HashKeyModel(); - beforeAll(async () => { + const modelUpToDate = new HashKeyUpToDateModel(); + beforeEach(async () => { await clearTables(); await model.save({ hashkey: 'hashkey', @@ -19,6 +21,19 @@ describe('The update method', () => { optionalList: [42, 'foo'], optionalStringmap: { bar: 'baz' }, }); + await modelUpToDate.save({ + hashkey: 'hashkeyUpToDate', + 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 update the item with the correct actions', async () => { await model.update('hashkey', { @@ -42,6 +57,55 @@ describe('The update method', () => { optionalStringmap: { bar: 'baz' }, }); }); + test('should update the item with updatedAt field', async () => { + jest.spyOn(Date.prototype, 'toISOString').mockImplementation(() => '2023-11-10T14:36:39.297Z'); + await modelUpToDate.update('hashkeyUpToDate', { + number: put(43), + bool: put(null), + optionalNumber: remove(), + }); + const updated = await modelUpToDate.get('hashkeyUpToDate'); + expect(updated).toEqual({ + hashkey: 'hashkeyUpToDate', + number: 43, + bool: null, + string: 'string', + stringset: ['string', 'string'], + list: [42, 'foo'], + stringmap: { bar: 'baz' }, + optionalStringset: ['string', 'string'], + optionalList: [42, 'foo'], + optionalStringmap: { bar: 'baz' }, + updatedAt: '2023-11-10T14:36:39.297Z', + }); + }); + test('should update the item with updatedAt field a second time', async () => { + await modelUpToDate.update('hashkeyUpToDate', { + number: put(43), + bool: put(null), + optionalNumber: remove(), + }); + jest.spyOn(Date.prototype, 'toISOString').mockImplementation(() => '2023-12-10T14:36:39.297Z'); + await modelUpToDate.update('hashkeyUpToDate', { + number: put(43), + bool: put(null), + optionalNumber: remove(), + }); + const updated = await modelUpToDate.get('hashkeyUpToDate'); + expect(updated).toEqual({ + hashkey: 'hashkeyUpToDate', + number: 43, + bool: null, + string: 'string', + stringset: ['string', 'string'], + list: [42, 'foo'], + stringmap: { bar: 'baz' }, + optionalStringset: ['string', 'string'], + optionalList: [42, 'foo'], + optionalStringmap: { bar: 'baz' }, + updatedAt: '2023-12-10T14:36:39.297Z', + }); + }); test.todo('should throw if item doest not exist'); test.todo('should throw is hash key is not given'); });