Skip to content

Commit

Permalink
feat(core): add support for mapping raw DB results via EM.map() (#61)
Browse files Browse the repository at this point in the history
Closes #56
  • Loading branch information
B4nan committed Jun 10, 2019
1 parent 4c421fb commit 5d227ae
Show file tree
Hide file tree
Showing 8 changed files with 69 additions and 6 deletions.
7 changes: 7 additions & 0 deletions docs/entity-manager.md
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,13 @@ This is useful when you want to work with cached entities.

---

#### `map<T extends IEntity>(entityName: string | EntityClass<T>, data: EntityData<T>): T`

Maps raw DB result to entity, adding it to current Identity Map. Equivalent to
`IDatabaseDriver.mapResult()` followed by `EntityManager.merge()`.

---

#### `getReference<T extends IEntity>(entityName: string | EntityClass<T>, id: string): T`

Gets a reference to the entity identified by the given type and identifier without actually
Expand Down
19 changes: 19 additions & 0 deletions docs/query-builder.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,25 @@ const entities = res6.map(data => orm.em.merge(Book, data));
console.log(entities); // array of Book entities
```

## Mapping raw results to entities

Another way to create entity from raw results (that are not necessarily mapped to entity properties)
is to use `map()` method of `EntityManager`, that is basically a shortcut for mapping results
via `IDatabaseDriver.mapResult()` (which converts field names to property names - e.g. `created_at`
to `createdAt`) and `merge()` which converts the data to entity instance and makes it managed.

This method comes handy when you want to use 3rd party query builder like [Knex.js](https://knexjs.org/),
where the result is not mapped to entity properties automatically:

```typescript
const results = await knex.select('*').from('users').where(knex.raw('id = ?', [id]));
const users = results.map(user => orm.em.map(User, user));

// or use EntityRepository.map()
const repo = orm.em.getRepository(User);
const users = results.map(user => repo.map(user));
```

## Implicit joining

`QueryBuilder` supports automatic joining based on entity metadata:
Expand Down
8 changes: 8 additions & 0 deletions lib/EntityManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,14 @@ export class EntityManager {
return res.affectedRows;
}

map<T extends IEntityType<T>>(entityName: EntityName<T>, result: EntityData<T>): T {
entityName = Utils.className(entityName);
const meta = this.metadata[entityName];
const data = this.driver.mapResult(result, meta)!;

return this.merge<T>(entityName, data, true);
}

/**
* Shortcut to driver's aggregate method. Available in MongoDriver only.
*/
Expand Down
6 changes: 3 additions & 3 deletions lib/drivers/DatabaseDriver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,9 @@ export abstract class DatabaseDriver<C extends Connection> implements IDatabaseD
return map;
}

mapResult<T extends IEntityType<T>>(result: T, meta: EntityMetadata): T {
mapResult<T extends IEntityType<T>>(result: EntityData<T>, meta: EntityMetadata): T | null {
if (!result || !meta) {
return result || null;
return null;
}

const ret = Object.assign({}, result);
Expand All @@ -73,7 +73,7 @@ export abstract class DatabaseDriver<C extends Connection> implements IDatabaseD
}
});

return ret;
return ret as T;
}

getConnection(): C {
Expand Down
2 changes: 1 addition & 1 deletion lib/drivers/IDatabaseDriver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export interface IDatabaseDriver<C extends Connection = Connection> {

aggregate(entityName: string, pipeline: any[]): Promise<any[]>;

mapResult<T extends IEntityType<T>>(result: T, meta: EntityMetadata): T;
mapResult<T extends IEntityType<T>>(result: EntityData<T>, meta: EntityMetadata): T | null;

/**
* When driver uses pivot tables for M:N, this method will load identifiers for given collections from them
Expand Down
4 changes: 2 additions & 2 deletions lib/drivers/MongoDriver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export class MongoDriver extends DatabaseDriver<MongoConnection> {
where = this.renameFields(entityName, where);
const res = await this.connection.find<T>(entityName, where, orderBy, limit, offset);

return res.map((r: T) => this.mapResult(r, this.metadata[entityName]));
return res.map((r: T) => this.mapResult<T>(r, this.metadata[entityName])!);
}

async findOne<T extends IEntityType<T>>(entityName: string, where: FilterQuery<T> | IPrimaryKey, populate: string[] = [], orderBy: Record<string, QueryOrder> = {}, fields?: string[], lockMode?: LockMode): Promise<T | null> {
Expand All @@ -28,7 +28,7 @@ export class MongoDriver extends DatabaseDriver<MongoConnection> {
where = this.renameFields(entityName, where) as FilterQuery<T>;
const res = await this.connection.find<T>(entityName, where, orderBy, 1, undefined, fields);

return this.mapResult(res[0], this.metadata[entityName]);
return this.mapResult<T>(res[0], this.metadata[entityName]);
}

async count<T extends IEntityType<T>>(entityName: string, where: FilterQuery<T>): Promise<number> {
Expand Down
4 changes: 4 additions & 0 deletions lib/entity/EntityRepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,10 @@ export class EntityRepository<T extends IEntityType<T>> {
return this.em.nativeDelete(this.entityName, where)
}

map(result: EntityData<T>): T {
return this.em.map(this.entityName, result);
}

async aggregate(pipeline: any[]): Promise<any[]> {
return this.em.aggregate(this.entityName, pipeline)
}
Expand Down
25 changes: 25 additions & 0 deletions tests/EntityManager.mysql.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,31 @@ describe('EntityManagerMySql', () => {
expect(book.uuid).toBeDefined();
});

test('manual mapping of raw DB results to entities vie EM.map()', async () => {
const repo = orm.em.getRepository(Book2);
const book = repo.map({
uuid_pk: '123-dsa',
title: 'name',
created_at: '2019-06-09T07:50:25.722Z',
author_id: 123,
publisher_id: 321,
tags: [1, 2, 3],
})!;
expect(book.uuid).toBe('123-dsa');
expect(book.title).toBe('name');
expect(book.createdAt).toBeInstanceOf(Date);
expect(book.author).toBeInstanceOf(Author2);
expect(book.author.id).toBe(123);
expect(book.publisher).toBeInstanceOf(Publisher2);
expect(book.publisher.id).toBe(321);
expect(book.tags.length).toBe(3);
expect(book.tags[0]).toBeInstanceOf(BookTag2);
expect(book.tags[0].id).toBe(1);
expect(book.tags[1].id).toBe(2);
expect(book.tags[2].id).toBe(3);
expect(repo.getReference(book.uuid)).toBe(book);
});

test('should work with boolean values', async () => {
const repo = orm.em.getRepository(Author2);
const author = new Author2('name', 'email');
Expand Down

0 comments on commit 5d227ae

Please sign in to comment.