Skip to content

Commit

Permalink
feat(repository): add inclusionResolvers to DefaultCrudRepository
Browse files Browse the repository at this point in the history
- add registerInclusionResolver method to DefaultCrudRepository
- add includeRelatedModels method to DefaultCrudRepository
- modify find* methods to use includeRelatedModels helper

Co-authored-by: Nora <nora.abdelgadir@ibm.com>
Co-authored-by: Miroslav Bajtoš <mbajtoss@gmail.com>
  • Loading branch information
3 people committed Aug 23, 2019
1 parent d9c31c4 commit 642c4b6
Show file tree
Hide file tree
Showing 2 changed files with 283 additions and 9 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT

import {expect} from '@loopback/testlab';
import {expect, toJSON} from '@loopback/testlab';
import {
bindModel,
DefaultCrudRepository,
Expand All @@ -15,6 +15,21 @@ import {
ModelDefinition,
property,
} from '../../..';
import {
belongsTo,
BelongsToAccessor,
BelongsToDefinition,
createBelongsToAccessor,
createHasManyRepositoryFactory,
createHasOneRepositoryFactory,
hasMany,
HasManyDefinition,
HasManyRepositoryFactory,
hasOne,
HasOneDefinition,
HasOneRepositoryFactory,
InclusionResolver,
} from '../../../relations';
import {CrudConnectorStub} from '../crud-connector.stub';
const TransactionClass = require('loopback-datasource-juggler').Transaction;

Expand Down Expand Up @@ -274,6 +289,190 @@ describe('DefaultCrudRepository', () => {
});
});

context('find* methods including relations', () => {
@model()
class Author extends Entity {
@property({id: true})
id?: number;
@property()
name: string;
@belongsTo(() => Folder)
folderId: number;
}

@model()
class Folder extends Entity {
@property({id: true})
id?: number;
@property()
name: string;
@hasMany(() => File)
files: File[];
@hasOne(() => Author)
author: Author;
}

@model()
class File extends Entity {
@property({id: true})
id?: number;
@property()
title: string;
@belongsTo(() => Folder)
folderId: number;
}

let folderRepo: DefaultCrudRepository<Folder, unknown, {}>;
let fileRepo: DefaultCrudRepository<File, unknown, {}>;
let authorRepo: DefaultCrudRepository<Author, unknown, {}>;

let folderFiles: HasManyRepositoryFactory<File, typeof Folder.prototype.id>;
let fileFolder: BelongsToAccessor<Folder, typeof File.prototype.id>;
let folderAuthor: HasOneRepositoryFactory<
Author,
typeof Folder.prototype.id
>;

before(() => {
ds = new juggler.DataSource({
name: 'db',
connector: 'memory',
});
folderRepo = new DefaultCrudRepository(Folder, ds);
fileRepo = new DefaultCrudRepository(File, ds);
authorRepo = new DefaultCrudRepository(Author, ds);
});

before(() => {
// using a variable instead of a repository property
folderFiles = createHasManyRepositoryFactory(
Folder.definition.relations.files as HasManyDefinition,
async () => fileRepo,
);
folderAuthor = createHasOneRepositoryFactory(
Folder.definition.relations.author as HasOneDefinition,
async () => authorRepo,
);
fileFolder = createBelongsToAccessor(
File.definition.relations.folder as BelongsToDefinition,
async () => folderRepo,
fileRepo,
);
});

beforeEach(async () => {
await folderRepo.deleteAll();
await fileRepo.deleteAll();
await authorRepo.deleteAll();
});

it('implements Repository.find() with included models', async () => {
const createdFolders = await folderRepo.createAll([
{name: 'f1', id: 1},
{name: 'f2', id: 2},
]);
const files = await fileRepo.createAll([
{id: 1, title: 'file1', folderId: 1},
{id: 2, title: 'file2', folderId: 3},
]);

folderRepo.registerInclusionResolver('files', hasManyResolver);

const folders = await folderRepo.find({include: [{relation: 'files'}]});

expect(toJSON(folders)).to.deepEqual([
{...createdFolders[0].toJSON(), files: [toJSON(files[0])]},
{...createdFolders[1].toJSON(), files: []},
]);
});

it('implements Repository.findById() with included models', async () => {
const folders = await folderRepo.createAll([
{name: 'f1', id: 1},
{name: 'f2', id: 2},
]);
const createdFile = await fileRepo.create({
id: 1,
title: 'file1',
folderId: 1,
});

fileRepo.registerInclusionResolver('folder', belongsToResolver);

const file = await fileRepo.findById(1, {
include: [{relation: 'folder'}],
});

expect(file.toJSON()).to.deepEqual({
...createdFile.toJSON(),
folder: folders[0].toJSON(),
});
});

it('implements Repository.findOne() with included models', async () => {
const folders = await folderRepo.createAll([
{name: 'f1', id: 1},
{name: 'f2', id: 2},
]);
const createdAuthor = await authorRepo.create({
id: 1,
name: 'a1',
folderId: 1,
});

folderRepo.registerInclusionResolver('author', hasOneResolver);

const folder = await folderRepo.findOne({
include: [{relation: 'author'}],
});

expect(folder!.toJSON()).to.deepEqual({
...folders[0].toJSON(),
author: createdAuthor.toJSON(),
});
});

// stub resolvers

const hasManyResolver: InclusionResolver<Folder, File> = async entities => {
const files = [];
for (const entity of entities) {
const file = await folderFiles(entity.id).find();
files.push(file);
}

return files;
};

const belongsToResolver: InclusionResolver<
File,
Folder
> = async entities => {
const folders = [];

for (const file of entities) {
const folder = await fileFolder(file.folderId);
folders.push(folder);
}

return folders;
};

const hasOneResolver: InclusionResolver<
Folder,
Author
> = async entities => {
const authors = [];

for (const folder of entities) {
const author = await folderAuthor(folder.id).get();
authors.push(author);
}

return authors;
};
});

it('implements Repository.delete()', async () => {
const repo = new DefaultCrudRepository(Note, ds);
const note = await repo.create({title: 't3', content: 'c3'});
Expand Down Expand Up @@ -419,6 +618,21 @@ describe('DefaultCrudRepository', () => {
'execute() must be implemented by the connector',
);
});

it('has the property inclusionResolvers', () => {
const repo = new DefaultCrudRepository(Note, ds);
expect(repo.inclusionResolvers).to.be.instanceof(Map);
});

it('implements Repository.registerInclusionResolver()', () => {
const repo = new DefaultCrudRepository(Note, ds);
const resolver: InclusionResolver<Note, Entity> = async entities => {
return entities;
};
repo.registerInclusionResolver('notes', resolver);
const setResolver = repo.inclusionResolvers.get('notes');
expect(setResolver).to.eql(resolver);
});
});

describe('DefaultTransactionalRepository', () => {
Expand Down
76 changes: 68 additions & 8 deletions packages/repository/src/repositories/legacy-juggler-bridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import {
} from '../common-types';
import {EntityNotFoundError} from '../errors';
import {Entity, Model, PropertyType} from '../model';
import {Filter, Where} from '../query';
import {Filter, Inclusion, Where} from '../query';
import {
BelongsToAccessor,
BelongsToDefinition,
Expand All @@ -28,6 +28,7 @@ import {
HasManyRepositoryFactory,
HasOneDefinition,
HasOneRepositoryFactory,
includeRelatedModels,
InclusionResolver,
} from '../relations';
import {IsolationLevel, Transaction} from '../transaction';
Expand Down Expand Up @@ -359,32 +360,51 @@ export class DefaultCrudRepository<
filter?: Filter<T>,
options?: Options,
): Promise<(T & Relations)[]> {
const include = filter && filter.include;
const models = await ensurePromise(
this.modelClass.find(filter as legacy.Filter, options),
this.modelClass.find(this.normalizeFilter(filter), options),
);
return this.toEntities(models);
const entities = this.toEntities(models);
return this.includeRelatedModels(entities, include, options);
}

async findOne(filter?: Filter<T>, options?: Options): Promise<T | null> {
async findOne(
filter?: Filter<T>,
options?: Options,
): Promise<(T & Relations) | null> {
const model = await ensurePromise(
this.modelClass.findOne(filter as legacy.Filter, options),
this.modelClass.findOne(this.normalizeFilter(filter), options),
);
if (!model) return null;
return this.toEntity(model);
const entity = this.toEntity(model);
const include = filter && filter.include;
const resolved = await this.includeRelatedModels(
[entity],
include,
options,
);
return resolved[0];
}

async findById(
id: ID,
filter?: Filter<T>,
options?: Options,
): Promise<T & Relations> {
const include = filter && filter.include;
const model = await ensurePromise(
this.modelClass.findById(id, filter as legacy.Filter, options),
this.modelClass.findById(id, this.normalizeFilter(filter), options),
);
if (!model) {
throw new EntityNotFoundError(this.entityClass, id);
}
return this.toEntity<T & Relations>(model);
const entity = this.toEntity(model);
const resolved = await this.includeRelatedModels(
[entity],
include,
options,
);
return resolved[0];
}

update(entity: T, options?: Options): Promise<void> {
Expand Down Expand Up @@ -474,6 +494,46 @@ export class DefaultCrudRepository<
protected toEntities<R extends T>(models: juggler.PersistedModel[]): R[] {
return models.map(m => this.toEntity<R>(m));
}

/**
* Register an inclusion resolver for the related model name.
*
* @param relationName - Name of the relation defined on the source model
* @param resolver - Resolver function for getting related model entities
*/
registerInclusionResolver(
relationName: string,
resolver: InclusionResolver<T, Entity>,
) {
this.inclusionResolvers.set(relationName, resolver);
}

/**
* Returns model instances that include related models of this repository
* that have a registered resolver.
*
* @param entities - An array of entity instances or data
* @param include -Inclusion filter
* @param options - Options for the operations
*/
protected async includeRelatedModels(
entities: T[],
include?: Inclusion<T>[],
options?: Options,
): Promise<(T & Relations)[]> {
return includeRelatedModels<T, Relations>(this, entities, include, options);
}

/**
* Removes juggler's "include" filter as it does not apply to LoopBack 4
* relations.
*
* @param filter - Query filter
*/
protected normalizeFilter(filter?: Filter<T>): legacy.Filter | undefined {
if (!filter) return undefined;
return {...filter, include: undefined} as legacy.Filter;
}
}

/**
Expand Down

0 comments on commit 642c4b6

Please sign in to comment.