Skip to content

Commit

Permalink
Improve hooks - allow changing db properties in beforeCreate/beforeUp…
Browse files Browse the repository at this point in the history
…date hooks (closes #5)

Improve hooks - allow changing db properties in beforeCreate/beforeUpdate hooks (closes #5)
  • Loading branch information
B4nan committed Jan 17, 2019
1 parent c636852 commit 26008a0
Show file tree
Hide file tree
Showing 13 changed files with 106 additions and 61 deletions.
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,12 @@ export class Book extends BaseEntity {
@PrimaryKey()
_id: ObjectID;

@Property()
createdAt = new Date();

@Property({ onUpdate: () => new Date() })
updatedAt = new Date();

@Property()
title: string;

Expand All @@ -83,7 +89,7 @@ export class Book extends BaseEntity {
```

With your entities set up, you can start using entity manager and repositories as described
in following section. For more examples, take a look at `tests/EntityManager.test.ts`.
in following section. For more examples, take a look at `tests/EntityManager.mongo.test.ts`.

## Persisting and cascading

Expand Down
9 changes: 4 additions & 5 deletions lib/BaseEntity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,6 @@ import { IPrimaryKey } from './decorators/PrimaryKey';
export abstract class BaseEntity {

id: IPrimaryKey;
createdAt = new Date();
updatedAt = new Date();
[property: string]: any | BaseEntity | Collection<BaseEntity>;

private _initialized = false;
Expand Down Expand Up @@ -115,14 +113,14 @@ export abstract class BaseEntity {
}

toObject(parent: BaseEntity = this, collection: Collection<BaseEntity> = null): any {
const ret = { id: this.id, createdAt: this.createdAt, updatedAt: this.updatedAt } as any;
const ret = { id: this.id } as any;

if (!this.isInitialized()) {
return { id: this.id } as any;
return ret;
}

Object.keys(this).forEach(prop => {
if (['id', 'createdAt', 'updatedAt'].includes(prop) || prop.startsWith('_')) {
if (prop === 'id' || prop.startsWith('_')) {
return;
}

Expand Down Expand Up @@ -173,6 +171,7 @@ export interface EntityProperty {
reference: ReferenceType;
fieldName?: string;
attributes?: { [attribute: string]: any };
onUpdate?: () => any;
owner?: boolean;
inversedBy?: string;
mappedBy?: string;
Expand Down
8 changes: 2 additions & 6 deletions lib/EntityFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ export class EntityFactory {

private initEntity<T extends BaseEntity>(entity: T, properties: any, data: any, exclude: string[]): void {
// process base entity properties first
['_id', 'id', 'createdAt', 'updatedAt'].forEach(k => {
['_id', 'id'].forEach(k => {
if (data[k]) {
entity[k] = data[k];
}
Expand Down Expand Up @@ -186,12 +186,8 @@ export class EntityFactory {
this.metadata[name].collection = namingStrategy.classToTableName(this.metadata[name].name);
}

// add createdAt and updatedAt properties
const props = this.metadata[name].properties;
props.createdAt = { name: 'createdAt', type: 'Date', reference: ReferenceType.SCALAR } as EntityProperty;
props.updatedAt = { name: 'updatedAt', type: 'Date', reference: ReferenceType.SCALAR } as EntityProperty;

// init types and column names
const props = this.metadata[name].properties;
Object.keys(props).forEach(p => {
if (props[p].entity) {
const type = props[p].entity();
Expand Down
12 changes: 0 additions & 12 deletions lib/EntityManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,22 +156,10 @@ export class EntityManager {
}

async nativeInsert(entityName: string, data: any): Promise<IPrimaryKey> {
if (!data.createdAt) {
data.createdAt = new Date();
}

if (!data.updatedAt) {
data.updatedAt = new Date();
}

return this.driver.nativeInsert(entityName, data);
}

async nativeUpdate(entityName: string, where: FilterQuery<BaseEntity>, data: any): Promise<number> {
if (!data.updatedAt) {
data.updatedAt = new Date();
}

return this.driver.nativeUpdate(entityName, where, data);
}

Expand Down
22 changes: 14 additions & 8 deletions lib/UnitOfWork.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ export class UnitOfWork {
if (entity.id && this.identityMap[`${meta.name}-${entity.id}`]) {
ret.payload = Utils.diffEntities(this.identityMap[`${meta.name}-${entity.id}`], entity);
} else {
ret.payload = Object.assign({}, entity); // TODO maybe we need deep copy? or no copy at all?
ret.payload = Object.assign({}, entity); // TODO maybe we need deep copy?
}

delete ret.payload[this.foreignKey];
Expand All @@ -65,7 +65,7 @@ export class UnitOfWork {
this.removeUnknownProperties(ret, meta);
this.em.validator.validate(ret.entity, ret.payload, meta);

if (Object.keys(ret.payload).length === 0) {
if (entity.id && Object.keys(ret.payload).length === 0) {
return null;
}

Expand Down Expand Up @@ -128,15 +128,15 @@ export class UnitOfWork {
const properties = Object.keys(changeSet.payload);

for (const p of properties) {
if (!meta.properties[p] && !['_id', 'createdAt', 'updatedAt'].includes(p)) {
if (!meta.properties[p]) {
delete changeSet.payload[p];
}
}
}

private async immediateCommit(changeSet: ChangeSet, removeFromStack = true): Promise<void> {
const type = changeSet.entity[this.foreignKey] ? 'Update' : 'Create';
this.runHooks(`before${type}`, changeSet.entity);
this.runHooks(`before${type}`, changeSet.entity, changeSet.payload);

const metadata = this.em.entityFactory.getMetadata();
const meta = metadata[changeSet.entity.constructor.name];
Expand Down Expand Up @@ -165,17 +165,18 @@ export class UnitOfWork {

reference.dirty = false;
}

if (prop.onUpdate) {
changeSet.entity[prop.name] = changeSet.payload[prop.name] = prop.onUpdate();
}
}

// persist the entity itself
const entityName = changeSet.entity.constructor.name;

if (changeSet.entity[this.foreignKey]) {
changeSet.entity.updatedAt = changeSet.payload.updatedAt = new Date();
await this.em.getDriver().nativeUpdate(entityName, changeSet.entity[this.foreignKey], changeSet.payload);
} else {
changeSet.entity.createdAt = changeSet.payload.createdAt = new Date();
changeSet.entity.updatedAt = changeSet.payload.updatedAt = new Date();
changeSet.entity[this.foreignKey] = await this.em.getDriver().nativeInsert(entityName, changeSet.payload);
delete changeSet.entity['_initialized'];
this.em.merge(changeSet.name, changeSet.entity);
Expand All @@ -188,12 +189,17 @@ export class UnitOfWork {
}
}

private runHooks(type: string, entity: BaseEntity) {
private runHooks(type: string, entity: BaseEntity, payload: any = null) {
const metadata = this.em.entityFactory.getMetadata();
const hooks = metadata[entity.constructor.name].hooks;

if (hooks && hooks[type] && hooks[type].length > 0) {
const copy = Utils.copy(entity);
hooks[type].forEach(hook => entity[hook]());

if (payload) {
Object.assign(payload, Utils.diffEntities(copy, entity));
}
}
}

Expand Down
4 changes: 2 additions & 2 deletions lib/Utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { getMetadataStorage } from './MikroORM';

export class Utils {

private static readonly DIFF_IGNORED_KEYS = ['_id', '_initialized', 'createdAt', 'updatedAt'];
private static readonly DIFF_IGNORED_KEYS = ['_id', '_initialized'];

static isObject(o: any): boolean {
return typeof o === 'object' && o !== null;
Expand Down Expand Up @@ -66,7 +66,7 @@ export class Utils {
const meta = metadata[a.constructor.name];
Object.keys(diff).forEach((prop: string) => {
if ((meta.properties[prop]).reference === ReferenceType.MANY_TO_ONE) {
diff[prop] = new ObjectID(diff[prop]);
diff[prop] = new ObjectID(diff[prop]); // FIXME
}
});

Expand Down
1 change: 1 addition & 0 deletions lib/decorators/Property.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,6 @@ export function Property(options: PropertyOptions = {}): Function {
export type PropertyOptions = {
name?: string;
type?: any;
onUpdate?: () => any;
[prop: string]: any;
}
39 changes: 31 additions & 8 deletions tests/EntityManager.test.ts → tests/EntityManager.mongo.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ import { AuthorRepository } from './repositories/AuthorRepository';
import { initORM, wipeDatabase } from './bootstrap';

/**
* @class EntityManagerTest
* @class EntityManagerMongoTest
*/
describe('EntityManager', () => {
describe('EntityManagerMongo', () => {

let orm: MikroORM;

Expand Down Expand Up @@ -496,25 +496,48 @@ describe('EntityManager', () => {
await expect(ent.tests.getIdentifiers()).toEqual([t2.id, t1.id, t3.id]);
});

test('property onUpdate hook (updatedAt field)', async () => {
const repo = orm.em.getRepository<Author>(Author.name);
const author = new Author('name', 'email');
await expect(author.createdAt).not.toBeUndefined();
await expect(author.updatedAt).not.toBeUndefined();
await expect(author.updatedAt).toEqual(author.createdAt);
await repo.persist(author);

author.name = 'name1';
await repo.persist(author);
await expect(author.createdAt).not.toBeUndefined();
await expect(author.updatedAt).not.toBeUndefined();
await expect(author.updatedAt).not.toEqual(author.createdAt);
await expect(author.updatedAt > author.createdAt).toBe(true);

orm.em.clear();
const ent = await repo.findOne(author.id);
await expect(ent.createdAt).not.toBeUndefined();
await expect(ent.updatedAt).not.toBeUndefined();
await expect(ent.updatedAt).not.toEqual(ent.createdAt);
await expect(ent.updatedAt > ent.createdAt).toBe(true);
});

test('EM supports native insert/update/delete/aggregate', async () => {
orm.em.options.debug = false;
const res1 = await orm.em.nativeInsert(Publisher.name, { name: 'native name 1' });
const res1 = await orm.em.nativeInsert(Author.name, { name: 'native name 1' });
expect(res1).toBeInstanceOf(ObjectID);

const res2 = await orm.em.nativeUpdate(Publisher.name, { name: 'native name 1' }, { name: 'new native name' });
const res2 = await orm.em.nativeUpdate(Author.name, { name: 'native name 1' }, { name: 'new native name' });
expect(res2).toBe(1);

const res3 = await orm.em.aggregate(Publisher.name, [{ $match: { name: 'new native name' } }]);
const res3 = await orm.em.aggregate(Author.name, [{ $match: { name: 'new native name' } }]);
expect(res3.length).toBe(1);
expect(res3[0]).toMatchObject({ name: 'new native name' });

const res4 = await orm.em.nativeDelete(Publisher.name, { name: 'new native name' });
const res4 = await orm.em.nativeDelete(Author.name, { name: 'new native name' });
expect(res4).toBe(1);

const res5 = await orm.em.nativeInsert(Publisher.name, { createdAt: new Date('1989-11-17'), updatedAt: new Date('2018-10-28'), name: 'native name 2' });
const res5 = await orm.em.nativeInsert(Author.name, { createdAt: new Date('1989-11-17'), updatedAt: new Date('2018-10-28'), name: 'native name 2' });
expect(res5).toBeInstanceOf(ObjectID);

const res6 = await orm.em.nativeUpdate(Publisher.name, { name: 'native name 2' }, { name: 'new native name', updatedAt: new Date('2018-10-28') });
const res6 = await orm.em.nativeUpdate(Author.name, { name: 'native name 2' }, { name: 'new native name', updatedAt: new Date('2018-10-28') });
expect(res6).toBe(1);
});

Expand Down
35 changes: 29 additions & 6 deletions tests/EntityManager.mysql.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -495,24 +495,47 @@ describe('EntityManagerMySql', () => {
await expect(ent.tests.getIdentifiers()).toEqual([t2.id, t1.id, t3.id]);
});

test('property onUpdate hook (updatedAt field)', async () => {
const repo = orm.em.getRepository<Author2>(Author2.name);
const author = new Author2('name', 'email');
await expect(author.createdAt).not.toBeUndefined();
await expect(author.updatedAt).not.toBeUndefined();
await expect(author.updatedAt).toEqual(author.createdAt);
await repo.persist(author);

author.name = 'name1';
await repo.persist(author);
await expect(author.createdAt).not.toBeUndefined();
await expect(author.updatedAt).not.toBeUndefined();
await expect(author.updatedAt).not.toEqual(author.createdAt);
await expect(author.updatedAt > author.createdAt).toBe(true);

orm.em.clear();
const ent = await repo.findOne(author.id);
await expect(ent.createdAt).not.toBeUndefined();
await expect(ent.updatedAt).not.toBeUndefined();
await expect(ent.updatedAt).not.toEqual(ent.createdAt);
await expect(ent.updatedAt > ent.createdAt).toBe(true);
});

test('EM supports native insert/update/delete', async () => {
orm.em.options.debug = false;
const res1 = await orm.em.nativeInsert(Publisher2.name, { name: 'native name 1' });
const res1 = await orm.em.nativeInsert(Author2.name, { name: 'native name 1' });
expect(typeof res1).toBe('number');

const res2 = await orm.em.nativeUpdate(Publisher2.name, { name: 'native name 1' }, { name: 'new native name' });
const res2 = await orm.em.nativeUpdate(Author2.name, { name: 'native name 1' }, { name: 'new native name' });
expect(res2).toBe(1);

const res3 = await orm.em.aggregate(Publisher2.name, []);
const res3 = await orm.em.aggregate(Author2.name, []);
expect(res3).toBeUndefined();

const res4 = await orm.em.nativeDelete(Publisher2.name, { name: 'new native name' });
const res4 = await orm.em.nativeDelete(Author2.name, { name: 'new native name' });
expect(res4).toBe(1);

const res5 = await orm.em.nativeInsert(Publisher2.name, { createdAt: new Date('1989-11-17'), updatedAt: new Date('2018-10-28'), name: 'native name 2' });
const res5 = await orm.em.nativeInsert(Author2.name, { createdAt: new Date('1989-11-17'), updatedAt: new Date('2018-10-28'), name: 'native name 2' });
expect(typeof res5).toBe('number');

const res6 = await orm.em.nativeUpdate(Publisher2.name, { name: 'native name 2' }, { name: 'new native name', updatedAt: new Date('2018-10-28') });
const res6 = await orm.em.nativeUpdate(Author2.name, { name: 'native name 2' }, { name: 'new native name', updatedAt: new Date('2018-10-28') });
expect(res6).toBe(1);
});

Expand Down
5 changes: 2 additions & 3 deletions tests/Utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,6 @@ describe('Utils', () => {
expect(Utils.diff({a: 'a'}, {a: 'b', b: ['c']})).toEqual({a: 'b', b: ['c']});
expect(Utils.diff({a: 'a', b: ['c']}, {b: []})).toEqual({b: []});
expect(Utils.diff({a: 'a', b: ['c']}, {a: 'b'})).toEqual({a: 'b'});
expect(Utils.diff({_id: 'a', createdAt: 1, updatedAt: 1}, {_id: 'b', createdAt: 2, updatedAt: 2})).toEqual({}); // ignored fields
expect(Utils.diff({a: new Date()}, {a: new Date('2018-01-01')})).toEqual({a: new Date('2018-01-01')});
});

Expand All @@ -75,7 +74,7 @@ describe('Utils', () => {
author1.books = new Collection<Book>(author1, {} as EntityProperty);
const author2 = new Author('Name 2', 'e-mail');
author2.books = new Collection<Book>(author2, {} as EntityProperty);
expect(Utils.diffEntities(author1, author2)).toEqual({ name: 'Name 2' });
expect(Utils.diffEntities(author1, author2).books).toBeUndefined();
});

test('prepareEntity changes entity to string id', async () => {
Expand All @@ -85,7 +84,7 @@ describe('Utils', () => {
author2.favouriteBook = book;
author2.version = 123;
await orm.em.persist([author1, author2, book]);
expect(Utils.diffEntities(author1, author2)).toEqual({ name: 'Name 2', favouriteBook: book._id });
expect(Utils.diffEntities(author1, author2)).toMatchObject({ name: 'Name 2', favouriteBook: book._id });
});

test('copy', () => {
Expand Down
6 changes: 6 additions & 0 deletions tests/entities-mysql/Author2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@ export class Author2 extends BaseEntity {
@PrimaryKey()
id: number;

@Property()
createdAt = new Date();

@Property({ onUpdate: () => new Date() })
updatedAt = new Date();

@Property()
name: string;

Expand Down
6 changes: 6 additions & 0 deletions tests/entities/Author.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@ export class Author extends BaseEntity {
@PrimaryKey()
_id: ObjectID;

@Property()
createdAt = new Date();

@Property({ onUpdate: () => new Date() })
updatedAt = new Date();

@Property()
name: string;

Expand Down
Loading

0 comments on commit 26008a0

Please sign in to comment.