Skip to content

Commit

Permalink
feat: typescript API and example impl for inclusion of related models
Browse files Browse the repository at this point in the history
  • Loading branch information
bajtos committed Mar 21, 2019
1 parent 781cd1d commit 43baf2f
Show file tree
Hide file tree
Showing 10 changed files with 149 additions and 27 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ import {
toJSON,
} from '@loopback/testlab';
import {TodoListApplication} from '../../application';
import {TodoList} from '../../models/';
import {TodoListRepository} from '../../repositories/';
import {givenTodoList} from '../helpers';
import {Todo, TodoList} from '../../models';
import {TodoListRepository, TodoRepository} from '../../repositories';
import {givenTodo, givenTodoList} from '../helpers';

describe('TodoListApplication', () => {
let app: TodoListApplication;
Expand Down Expand Up @@ -178,6 +178,20 @@ describe('TodoListApplication', () => {
.expect(200, [toJSON(listInBlack)]);
});

it('includes Todos in query result', async () => {
const list = await givenTodoListInstance();
const todo = await givenTodoInstance({todoListId: list.id});

const response = await client.get('/todo-lists').query({
filter: JSON.stringify({include: [{relation: 'todos'}]}),
});
expect(response.body).to.have.length(1);
expect(response.body[0]).to.deepEqual({
...toJSON(list),
todos: [toJSON(todo)],
});
});

/*
============================================================================
TEST HELPERS
Expand Down Expand Up @@ -218,6 +232,11 @@ describe('TodoListApplication', () => {
return await todoListRepo.create(givenTodoList(todoList));
}

async function givenTodoInstance(todo?: Partial<Todo>) {
const repo = await app.getRepository(TodoRepository);
return await repo.create(givenTodo(todo));
}

function givenMutlipleTodoListInstances() {
return Promise.all([
givenTodoListInstance(),
Expand Down
14 changes: 14 additions & 0 deletions examples/todo-list/src/__tests__/acceptance/todo.acceptance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,20 @@ describe('TodoListApplication', () => {
.expect(200, [toJSON(todoInProgress)]);
});

it('includes TodoList in query result', async () => {
const list = await givenTodoListInstance();
const todo = await givenTodoInstance({todoListId: list.id});

const response = await client.get('/todos').query({
filter: JSON.stringify({include: [{relation: 'todoList'}]}),
});
expect(response.body).to.have.length(1);
expect(response.body[0]).to.deepEqual({
...toJSON(todo),
todoList: toJSON(list),
});
});

/*
============================================================================
TEST HELPERS
Expand Down
8 changes: 6 additions & 2 deletions examples/todo-list/src/models/todo-list-image.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT

import {Entity, model, property, belongsTo} from '@loopback/repository';
import {TodoList} from './todo-list.model';
import {belongsTo, Entity, model, property} from '@loopback/repository';
import {TodoList, TodoListLinks} from './todo-list.model';

@model()
export class TodoListImage extends Entity {
Expand All @@ -29,3 +29,7 @@ export class TodoListImage extends Entity {
super(data);
}
}

export interface TodoListImageLinks {
todoList?: TodoList & TodoListLinks;
}
9 changes: 7 additions & 2 deletions examples/todo-list/src/models/todo-list.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT

import {Entity, model, property, hasMany, hasOne} from '@loopback/repository';
import {Entity, hasMany, hasOne, model, property} from '@loopback/repository';
import {TodoListImage, TodoListImageLinks} from './todo-list-image.model';
import {Todo} from './todo.model';
import {TodoListImage} from './todo-list-image.model';

@model()
export class TodoList extends Entity {
Expand Down Expand Up @@ -36,3 +36,8 @@ export class TodoList extends Entity {
super(data);
}
}

export interface TodoListLinks {
todos?: (Todo & TodoListLinks)[];
image?: TodoListImage & TodoListImageLinks;
}
6 changes: 5 additions & 1 deletion examples/todo-list/src/models/todo.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
// License text available at https://opensource.org/licenses/MIT

import {Entity, property, model, belongsTo} from '@loopback/repository';
import {TodoList} from './todo-list.model';
import {TodoList, TodoListLinks} from './todo-list.model';

@model()
export class Todo extends Entity {
Expand Down Expand Up @@ -41,3 +41,7 @@ export class Todo extends Entity {
super(data);
}
}

export interface TodoLinks {
todoList?: TodoList & TodoListLinks;
}
30 changes: 28 additions & 2 deletions examples/todo-list/src/repositories/todo-list.repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,17 @@ import {
juggler,
repository,
HasOneRepositoryFactory,
Filter,
Options,
} from '@loopback/repository';
import {Todo, TodoList, TodoListImage} from '../models';
import {Todo, TodoList, TodoListImage, TodoListLinks} from '../models';
import {TodoRepository} from './todo.repository';
import {TodoListImageRepository} from './todo-list-image.repository';

export class TodoListRepository extends DefaultCrudRepository<
TodoList,
typeof TodoList.prototype.id
typeof TodoList.prototype.id,
TodoListLinks
> {
public readonly todos: HasManyRepositoryFactory<
Todo,
Expand Down Expand Up @@ -49,4 +52,27 @@ export class TodoListRepository extends DefaultCrudRepository<
public findByTitle(title: string) {
return this.findOne({where: {title}});
}

async find(
filter?: Filter<TodoList>,
options?: Options,
): Promise<(TodoList & Partial<TodoListLinks>)[]> {
// Prevent juggler for applying "include" filter
// Juggler is not aware of LB4 relations
const include = filter && filter.include;
filter = filter && Object.assign(filter, {include: undefined});

const result = await super.find(filter, options);

// poor-mans inclusion resolver, this should be handled by DefaultCrudRepo
// and use `inq` operator to fetch related todos in fewer DB queries
if (include && include.length && include[0].relation === 'todos') {
await Promise.all(
result.map(async r => {
r.todos = await this.todos(r.id).find();
}),
);
}
return result;
}
}
30 changes: 28 additions & 2 deletions examples/todo-list/src/repositories/todo.repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,16 @@ import {
DefaultCrudRepository,
juggler,
repository,
Options,
Filter,
} from '@loopback/repository';
import {Todo, TodoList} from '../models';
import {Todo, TodoList, TodoLinks} from '../models';
import {TodoListRepository} from './todo-list.repository';

export class TodoRepository extends DefaultCrudRepository<
Todo,
typeof Todo.prototype.id
typeof Todo.prototype.id,
TodoLinks
> {
public readonly todoList: BelongsToAccessor<
TodoList,
Expand All @@ -34,4 +37,27 @@ export class TodoRepository extends DefaultCrudRepository<
todoListRepositoryGetter,
);
}

async find(
filter?: Filter<Todo>,
options?: Options,
): Promise<(Todo & Partial<TodoLinks>)[]> {
// Prevent juggler for applying "include" filter
// Juggler is not aware of LB4 relations
const include = filter && filter.include;
filter = filter && Object.assign(filter, {include: undefined});

const result = await super.find(filter, options);

// poor-mans inclusion resolver, this should be handled by DefaultCrudRepo
// and use `inq` operator to fetch related todos in fewer DB queries
if (include && include.length && include[0].relation === 'todoList') {
await Promise.all(
result.map(async r => {
r.todoList = await this.todoList(r.id);
}),
);
}
return result;
}
}
6 changes: 6 additions & 0 deletions packages/repository/src/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,12 @@ export abstract class Model {
json[p] = asJSON((this as AnyObject)[p]);
}
}
// Include relational links in the JSON representation
for (const r in def.relations) {
if (r in this) {
json[r] = asJSON((this as AnyObject)[r]);
}
}
return json;
}

Expand Down
26 changes: 18 additions & 8 deletions packages/repository/src/repositories/legacy-juggler-bridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,8 +88,11 @@ export function ensurePromise<T>(p: legacy.PromiseOrVoid<T>): Promise<T> {
* Default implementation of CRUD repository using legacy juggler model
* and data source
*/
export class DefaultCrudRepository<T extends Entity, ID>
implements EntityCrudRepository<T, ID> {
export class DefaultCrudRepository<
T extends Entity,
ID,
Links extends object = {}
> implements EntityCrudRepository<T, ID, Links> {
modelClass: juggler.PersistedModelClass;

/**
Expand Down Expand Up @@ -334,7 +337,10 @@ export class DefaultCrudRepository<T extends Entity, ID>
}
}

async find(filter?: Filter<T>, options?: Options): Promise<T[]> {
async find(
filter?: Filter<T>,
options?: Options,
): Promise<(T & Partial<Links>)[]> {
const models = await ensurePromise(
this.modelClass.find(filter as legacy.Filter, options),
);
Expand All @@ -349,14 +355,18 @@ export class DefaultCrudRepository<T extends Entity, ID>
return this.toEntity(model);
}

async findById(id: ID, filter?: Filter<T>, options?: Options): Promise<T> {
async findById(
id: ID,
filter?: Filter<T>,
options?: Options,
): Promise<T & Partial<Links>> {
const model = await ensurePromise(
this.modelClass.findById(id, filter as legacy.Filter, options),
);
if (!model) {
throw new EntityNotFoundError(this.entityClass, id);
}
return this.toEntity(model);
return this.toEntity<T & Partial<Links>>(model);
}

update(entity: T, options?: Options): Promise<void> {
Expand Down Expand Up @@ -441,11 +451,11 @@ export class DefaultCrudRepository<T extends Entity, ID>
throw new Error('Not implemented');
}

protected toEntity(model: juggler.PersistedModel): T {
return new this.entityClass(model.toObject()) as T;
protected toEntity<R extends T>(model: juggler.PersistedModel): R {
return new this.entityClass(model.toObject()) as R;
}

protected toEntities(models: juggler.PersistedModel[]): T[] {
protected toEntities<R extends T>(models: juggler.PersistedModel[]): R[] {
return models.map(m => this.toEntity(m));
}
}
22 changes: 15 additions & 7 deletions packages/repository/src/repositories/repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,10 @@ export interface ExecutableRepository<T extends Model> extends Repository<T> {
/**
* Basic CRUD operations for ValueObject and Entity. No ID is required.
*/
export interface CrudRepository<T extends ValueObject | Entity>
extends Repository<T> {
export interface CrudRepository<
T extends ValueObject | Entity,
Links extends object = {}
> extends Repository<T> {
/**
* Create a new record
* @param dataObject The data to be created
Expand All @@ -64,7 +66,7 @@ export interface CrudRepository<T extends ValueObject | Entity>
* @param options Options for the operations
* @returns A promise of an array of records found
*/
find(filter?: Filter<T>, options?: Options): Promise<T[]>;
find(filter?: Filter<T>, options?: Options): Promise<(T & Partial<Links>)[]>;

/**
* Updating matching records with attributes from the data object
Expand Down Expand Up @@ -105,9 +107,11 @@ export interface EntityRepository<T extends Entity, ID>
/**
* CRUD operations for a repository of entities
*/
export interface EntityCrudRepository<T extends Entity, ID>
extends EntityRepository<T, ID>,
CrudRepository<T> {
export interface EntityCrudRepository<
T extends Entity,
ID,
Links extends object = {}
> extends EntityRepository<T, ID>, CrudRepository<T, Links> {
// entityClass should have type "typeof T", but that's not supported by TSC
entityClass: typeof Entity & {prototype: T};

Expand Down Expand Up @@ -146,7 +150,11 @@ export interface EntityCrudRepository<T extends Entity, ID>
* @param options Options for the operations
* @returns A promise of an entity found for the id
*/
findById(id: ID, filter?: Filter<T>, options?: Options): Promise<T>;
findById(
id: ID,
filter?: Filter<T>,
options?: Options,
): Promise<T & Partial<Links>>;

/**
* Update an entity by id with property/value pairs in the data object
Expand Down

0 comments on commit 43baf2f

Please sign in to comment.