Skip to content

Commit

Permalink
feat(core): add findOneOrFail method to entity manager and repository
Browse files Browse the repository at this point in the history
Closes #133
  • Loading branch information
B4nan committed Sep 20, 2019
1 parent 0c45ac9 commit 59fd220
Show file tree
Hide file tree
Showing 9 changed files with 110 additions and 5 deletions.
38 changes: 38 additions & 0 deletions docs/entity-manager.md
Expand Up @@ -109,6 +109,36 @@ console.log(author.name); // Jon Snow
console.log(author.email); // undefined
```

### Handling not found entities

When you call `em.findOne()` and no entity is found based on your criteria, `null` will be
returned. If you rather have an `Error` instance thrown, you can use `em.findOneOrFail()`:

```typescript
const author = await orm.em.findOne(Author, { name: 'does-not-exist' });
console.log(author === null); // true

try {
const author = await orm.em.findOneOrFail(Author, { name: 'does-not-exist' });
// author will be always found here
} catch (e) {
console.error('Not found', e);
}
```

You can customize the error either globally via `findOneOrFailHandler` option, or locally via
`failHandler` option in `findOneOrFail` call.

```typescript
try {
const author = await orm.em.findOneOrFail(Author, { name: 'does-not-exist' }, {
failHandler: (entityName: string, where: Record<string, any> | IPrimaryKey) => new Error(`Failed: ${entityName} in ${util.inspect(where)}`)
});
} catch (e) {
console.error(e); // your custom error
}
```

## Type of fetched entities

Both `EntityManager.find` and `EntityManager.findOne()` methods have generic return types.
Expand Down Expand Up @@ -170,6 +200,14 @@ if the entity is already managed, no database call will be made.

---

#### `findOneOrFail<T extends IEntity>(entityName: string | EntityClass<T>, where: FilterQuery<T> | string, populate?: string[]): Promise<T>`

Just like `findOne`, but throws when entity not found, so it always resolves to given entity.
You can customize the error either globally via `findOneOrFailHandler` option, or locally via
`failHandler` option in `findOneOrFail` call.

---

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

Adds given entity to current Identity Map. After merging, entity becomes managed.
Expand Down
8 changes: 8 additions & 0 deletions docs/repositories.md
Expand Up @@ -106,6 +106,14 @@ if the entity is already managed, no database call will be made.

---

#### `findOneOrFail(where: FilterQuery<T> | string, populate?: string[]): Promise<T>`

Just like `findOne`, but throws when entity not found, so it always resolves to given entity.
You can customize the error either globally via `findOneOrFailHandler` option, or locally via
`failHandler` option in `findOneOrFail` call.

---

#### `merge(data: EntityData<T>): T`

Adds given entity to current Identity Map. After merging, entity becomes managed.
Expand Down
19 changes: 19 additions & 0 deletions lib/EntityManager.ts
Expand Up @@ -111,6 +111,21 @@ export class EntityManager {
return entity;
}

async findOneOrFail<T extends IEntityType<T>>(entityName: EntityName<T>, where: FilterQuery<T> | IPrimaryKey, options?: FindOneOrFailOptions): Promise<T | null>;
async findOneOrFail<T extends IEntityType<T>>(entityName: EntityName<T>, where: FilterQuery<T> | IPrimaryKey, populate?: string[], orderBy?: QueryOrderMap): Promise<T | null>;
async findOneOrFail<T extends IEntityType<T>>(entityName: EntityName<T>, where: FilterQuery<T> | IPrimaryKey, populate?: string[] | FindOneOrFailOptions, orderBy?: QueryOrderMap): Promise<T | null> {
const entity = await this.findOne(entityName, where, populate as string[], orderBy);

if (!entity) {
const options = Utils.isObject<FindOneOrFailOptions>(populate) ? populate : {};
options.failHandler = options.failHandler || this.config.get('findOneOrFailHandler');
entityName = Utils.className(entityName);
throw options.failHandler!(entityName, where);
}

return entity;
}

async transactional(cb: (em: EntityManager) => Promise<any>, ctx = this.transactionContext): Promise<any> {
const em = this.fork(false);
await em.getConnection().transactional(async trx => {
Expand Down Expand Up @@ -392,3 +407,7 @@ export interface FindOneOptions {
refresh?: boolean;
fields?: string[];
}

export interface FindOneOrFailOptions extends FindOneOptions {
failHandler?: (entityName: string, where: Record<string, any> | IPrimaryKey) => Error;
}
8 changes: 7 additions & 1 deletion lib/entity/EntityRepository.ts
@@ -1,4 +1,4 @@
import { EntityManager, FindOneOptions, FindOptions } from '../EntityManager';
import { EntityManager, FindOneOptions, FindOneOrFailOptions, FindOptions } from '../EntityManager';
import { EntityData, EntityName, IEntity, IEntityType, IPrimaryKey } from '../decorators';
import { QueryBuilder, QueryOrderMap } from '../query';
import { FilterQuery, IdentifiedReference, Reference } from '..';
Expand Down Expand Up @@ -30,6 +30,12 @@ export class EntityRepository<T extends IEntityType<T>> {
return this.em.findOne<T>(this.entityName, where, populate as string[], orderBy);
}

async findOneOrFail(where: FilterQuery<T> | IPrimaryKey, populate?: string[], orderBy?: QueryOrderMap): Promise<T | null>; // tslint:disable-next-line:lines-between-class-members
async findOneOrFail(where: FilterQuery<T> | IPrimaryKey, populate?: FindOneOrFailOptions, orderBy?: QueryOrderMap): Promise<T | null>; // tslint:disable-next-line:lines-between-class-members
async findOneOrFail(where: FilterQuery<T> | IPrimaryKey, populate: string[] | FindOneOrFailOptions = [], orderBy?: QueryOrderMap): Promise<T | null> {
return this.em.findOneOrFail<T>(this.entityName, where, populate as string[], orderBy);
}

async find(where: FilterQuery<T> | IPrimaryKey, options?: FindOptions): Promise<T[]>; // tslint:disable-next-line:lines-between-class-members
async find(where: FilterQuery<T> | IPrimaryKey, populate?: string[], orderBy?: QueryOrderMap, limit?: number, offset?: number): Promise<T[]>; // tslint:disable-next-line:lines-between-class-members
async find(where: FilterQuery<T> | IPrimaryKey, populate: string[] | FindOptions = [], orderBy: QueryOrderMap = {}, limit?: number, offset?: number): Promise<T[]> {
Expand Down
6 changes: 4 additions & 2 deletions lib/utils/Configuration.ts
Expand Up @@ -5,9 +5,9 @@ import { NamingStrategy } from '../naming-strategy';
import { CacheAdapter, FileCacheAdapter, NullCacheAdapter } from '../cache';
import { MetadataProvider, TypeScriptMetadataProvider } from '../metadata';
import { EntityFactory, EntityRepository } from '../entity';
import { EntityClass, EntityClassGroup, EntityName, EntityOptions, IEntity } from '../decorators';
import { EntityClass, EntityClassGroup, EntityName, EntityOptions, IEntity, IPrimaryKey } from '../decorators';
import { Hydrator, ObjectHydrator } from '../hydration';
import { Logger, LoggerNamespace, Utils } from '../utils';
import { Logger, LoggerNamespace, Utils, ValidationError } from '../utils';
import { EntityManager } from '../EntityManager';
import { IDatabaseDriver } from '..';
import { Platform } from '../platforms';
Expand All @@ -26,6 +26,7 @@ export class Configuration {
strict: false,
// tslint:disable-next-line:no-console
logger: console.log.bind(console),
findOneOrFailHandler: (entityName: string, where: Record<string, any> | IPrimaryKey) => ValidationError.findOneFailed(entityName, where),
baseDir: process.cwd(),
entityRepository: EntityRepository,
hydrator: ObjectHydrator,
Expand Down Expand Up @@ -201,6 +202,7 @@ export interface MikroORMOptions extends ConnectionOptions {
replicas?: Partial<ConnectionOptions>[];
strict: boolean;
logger: (message: string) => void;
findOneOrFailHandler: (entityName: string, where: Record<string, any> | IPrimaryKey) => Error;
debug: boolean | LoggerNamespace[];
highlight: boolean;
highlightTheme?: Record<string, string | string[]>;
Expand Down
7 changes: 6 additions & 1 deletion lib/utils/ValidationError.ts
@@ -1,4 +1,5 @@
import { EntityMetadata, EntityProperty, IEntity } from '../decorators';
import { inspect } from 'util';
import { EntityMetadata, EntityProperty, IEntity, IPrimaryKey } from '../decorators';
import { Utils } from './Utils';

export class ValidationError extends Error {
Expand Down Expand Up @@ -115,6 +116,10 @@ export class ValidationError extends Error {
return new ValidationError(`Entity '${name}' not found in ${path}`);
}

static findOneFailed(name: string, where: Record<string, any> | IPrimaryKey): ValidationError {
return new ValidationError(`${name} not found (${inspect(where)})`);
}

private static fromMessage(meta: EntityMetadata, prop: EntityProperty, message: string): ValidationError {
return new ValidationError(`${meta.name}.${prop.name} ${message}`);
}
Expand Down
13 changes: 13 additions & 0 deletions tests/EntityManager.mongo.test.ts
Expand Up @@ -1443,6 +1443,19 @@ describe('EntityManagerMongo', () => {
}
});

test('findOneOrFail', async () => {
const author = new Author('Jon Snow', 'snow@wall.st');
await orm.em.persistAndFlush(author);
orm.em.clear();

const a1 = await orm.em.findOneOrFail(Author, author.id);
expect(a1).not.toBeNull();
await expect(orm.em.findOneOrFail(Author, 123)).rejects.toThrowError('Author not found (123)');
await expect(orm.em.findOneOrFail(Author, { name: '123' })).rejects.toThrowError('Author not found ({ name: \'123\' })');
await expect(orm.em.findOneOrFail(Author, 123, { failHandler: () => new Error('Test') })).rejects.toThrowError('Test');
await expect(orm.em.findOneOrFail(Author, 123, { failHandler: (entityName: string) => new Error(`Failed: ${entityName}`) })).rejects.toThrowError('Failed: Author');
});

afterAll(async () => orm.close(true));

});
14 changes: 14 additions & 0 deletions tests/EntityRepository.test.ts
Expand Up @@ -8,6 +8,7 @@ const methods = {
persistLater: jest.fn(),
createQueryBuilder: jest.fn(),
findOne: jest.fn(),
findOneOrFail: jest.fn(),
find: jest.fn(),
remove: jest.fn(),
removeAndFlush: jest.fn(),
Expand Down Expand Up @@ -42,6 +43,8 @@ describe('EntityRepository', () => {
expect(methods.find.mock.calls[0]).toEqual([Publisher, { foo: 'bar' }, [], {}, undefined, undefined]);
await repo.findOne('bar');
expect(methods.findOne.mock.calls[0]).toEqual([Publisher, 'bar', [], undefined]);
await repo.findOneOrFail('bar');
expect(methods.findOneOrFail.mock.calls[0]).toEqual([Publisher, 'bar', [], undefined]);
await repo.createQueryBuilder();
expect(methods.createQueryBuilder.mock.calls[0]).toEqual([Publisher, undefined]);
await repo.remove('bar');
Expand Down Expand Up @@ -86,4 +89,15 @@ describe('EntityRepository', () => {
expect(methods.findOne.mock.calls[0]).toEqual([Publisher, { foo: 'bar' }, options, undefined]);
});

test('findOneOrFail() supports calling with config object', async () => {
const options = {
populate: ['test'],
orderBy: { test: QueryOrder.DESC },
handler: () => new Error('Test'),
};
methods.findOneOrFail.mock.calls = [];
await repo.findOneOrFail({ foo: 'bar' }, options);
expect(methods.findOneOrFail.mock.calls[0]).toEqual([Publisher, { foo: 'bar' }, options, undefined]);
});

});
2 changes: 1 addition & 1 deletion tests/entities/Publisher.ts
Expand Up @@ -33,7 +33,7 @@ export class Publisher {

}

export interface Publisher extends IEntity { }
export interface Publisher extends IEntity<string> { }

export enum PublisherType {
LOCAL = 'local',
Expand Down

0 comments on commit 59fd220

Please sign in to comment.