Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(model): auto-managed createdAt and updatedAt fields #285

Closed
wants to merge 2 commits into from
Closed
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
22 changes: 22 additions & 0 deletions src/base-model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[];
Expand All @@ -44,6 +50,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 +173,9 @@ export default abstract class Model<T> {
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);
}
Expand Down Expand Up @@ -204,6 +217,9 @@ export default abstract class Model<T> {
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,
Expand Down Expand Up @@ -537,6 +553,12 @@ export default abstract class Model<T> {
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),
Expand Down
46 changes: 46 additions & 0 deletions test/create.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -25,6 +29,48 @@ 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 and autoUpdatedAt 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',
updatedAt: '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 {
Expand Down
15 changes: 15 additions & 0 deletions test/models/hashkey-up-to-date.ts
Original file line number Diff line number Diff line change
@@ -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<HashKeyEntity & UpToDateEntity> {
protected tableName = 'table_test_hashkey';

protected pk = 'hashkey';

protected documentClient = documentClient;

protected autoCreatedAt = true;

protected autoUpdatedAt = true;
}
21 changes: 21 additions & 0 deletions test/save.spec.ts
Original file line number Diff line number Diff line change
@@ -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 () => {
Expand All @@ -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 {
Expand Down
66 changes: 65 additions & 1 deletion test/update.spec.ts
Original file line number Diff line number Diff line change
@@ -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',
Expand All @@ -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', {
Expand All @@ -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');
});
Expand Down