Skip to content

Commit

Permalink
feat(repository): implement belongsTo relation
Browse files Browse the repository at this point in the history
  • Loading branch information
bajtos committed Oct 4, 2018
1 parent 495ff49 commit df8c64c
Show file tree
Hide file tree
Showing 26 changed files with 1,030 additions and 298 deletions.
5 changes: 3 additions & 2 deletions examples/todo-list/src/models/todo.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT

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

@model()
export class Todo extends Entity {
Expand All @@ -29,7 +30,7 @@ export class Todo extends Entity {
})
isComplete: boolean;

@property()
@belongsTo(() => TodoList)
todoListId: number;

getId() {
Expand Down
25 changes: 21 additions & 4 deletions examples/todo-list/src/repositories/todo.repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,32 @@
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT

import {DefaultCrudRepository, juggler} from '@loopback/repository';
import {Todo} from '../models';
import {inject} from '@loopback/core';
import {Getter, inject} from '@loopback/core';
import {
BelongsToAccessor,
DefaultCrudRepository,
juggler,
repository,
} from '@loopback/repository';
import {Todo, TodoList} from '../models';
import {TodoListRepository} from './todo-list.repository';

export class TodoRepository extends DefaultCrudRepository<
Todo,
typeof Todo.prototype.id
> {
constructor(@inject('datasources.db') dataSource: juggler.DataSource) {
public todoList: BelongsToAccessor<TodoList, typeof Todo.prototype.id>;

constructor(
@inject('datasources.db') dataSource: juggler.DataSource,
@repository.getter('TodoListRepository')
protected todoListRepositoryGetter: Getter<TodoListRepository>,
) {
super(Todo, dataSource);

this.todoList = this._createBelongsToAccessorFor(
'todoListId',
todoListRepositoryGetter,
);
}
}
13 changes: 10 additions & 3 deletions examples/todo-list/test/acceptance/todo-list-todo.acceptance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
createRestAppClient,
expect,
givenHttpServerConfig,
toJSON,
} from '@loopback/testlab';
import {TodoListApplication} from '../../src/application';
import {Todo, TodoList} from '../../src/models/';
Expand Down Expand Up @@ -41,13 +42,17 @@ describe('TodoListApplication', () => {
});

it('creates todo for a todoList', async () => {
const todo = givenTodo();
const todo = givenTodo({todoListId: undefined});
const response = await client
.post(`/todo-lists/${persistedTodoList.id}/todos`)
.send(todo)
.expect(200);

expect(response.body).to.containDeep(todo);
const expected = {...todo, todoListId: persistedTodoList.id};
expect(response.body).to.containEql(expected);

const created = await todoRepo.findById(response.body.id);
expect(toJSON(created)).to.deepEqual({id: response.body.id, ...expected});
});

context('when dealing with multiple persisted Todos', () => {
Expand Down Expand Up @@ -213,7 +218,9 @@ describe('TodoListApplication', () => {
id: typeof Todo.prototype.id,
todo?: Partial<Todo>,
) {
return await todoListRepo.todos(id).create(givenTodo(todo));
const data = givenTodo(todo);
delete data.todoListId;
return await todoListRepo.todos(id).create(data);
}

async function givenTodoListInstance(todoList?: Partial<TodoList>) {
Expand Down
2 changes: 1 addition & 1 deletion examples/todo-list/test/acceptance/todo-list.acceptance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ describe('TodoListApplication', () => {

it('returns 404 when updating a todo-list that does not exist', () => {
return client
.patch('/todos/99999')
.patch('/todo-lists/99999')
.send(givenTodoList())
.expect(404);
});
Expand Down
1 change: 1 addition & 0 deletions examples/todo-list/test/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export function givenTodo(todo?: Partial<Todo>) {
title: 'do a thing',
desc: 'There are some things that need doing',
isComplete: false,
todoListId: 999,
},
todo,
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,22 @@
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT

import {model, property, Entity, hasMany} from '@loopback/repository';
import {MetadataInspector} from '@loopback/context';
import {
belongsTo,
Entity,
hasMany,
model,
property,
} from '@loopback/repository';
import {expect} from '@loopback/testlab';
import * as Ajv from 'ajv';
import {
modelToJsonSchema,
JSON_SCHEMA_KEY,
getJsonSchema,
JsonSchema,
JSON_SCHEMA_KEY,
modelToJsonSchema,
} from '../..';
import {expect} from '@loopback/testlab';
import {MetadataInspector} from '@loopback/context';
import * as Ajv from 'ajv';

describe('build-schema', () => {
describe('modelToJsonSchema', () => {
Expand Down Expand Up @@ -420,13 +426,13 @@ describe('build-schema', () => {
expectValidJsonSchema(jsonSchema);
});

it('properly converts models with hasMany properties', () => {
it('properly converts models with hasMany/belongsTo relation', () => {
@model()
class Order extends Entity {
@property({id: true})
id: number;

@property()
@belongsTo(() => Customer)
customerId: number;
}

Expand All @@ -439,10 +445,16 @@ describe('build-schema', () => {
orders: Order[];
}

const orderSchema = modelToJsonSchema(Order);
const customerSchema = modelToJsonSchema(Customer);

expectValidJsonSchema(customerSchema);
expectValidJsonSchema(orderSchema);

expect(orderSchema.properties).to.deepEqual({
id: {type: 'number'},
customerId: {type: 'number'},
});
expect(customerSchema.properties).to.deepEqual({
id: {type: 'number'},
orders: {
Expand Down
5 changes: 2 additions & 3 deletions packages/repository/examples/models/order.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,7 @@ class Order extends Entity {
// as simple types string, number, boolean can be inferred
@property({type: 'string', id: true, generated: true})
id: string;
customerId: string;

@belongsTo()
customer: Customer;
@belongsTo(() => Customer)
customerId: string;
}
108 changes: 96 additions & 12 deletions packages/repository/src/decorators/relation.decorator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,14 @@
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT

import {PropertyDecoratorFactory} from '@loopback/context';
import {Entity, EntityResolver, Model, RelationDefinitionMap} from '../model';
import {MetadataInspector, PropertyDecoratorFactory} from '@loopback/context';
import {
Entity,
EntityResolver,
Model,
PropertyDefinition,
RelationDefinitionMap,
} from '../model';
import {TypeResolver} from '../type-resolver';
import {property} from './model.decorator';

Expand All @@ -21,19 +27,65 @@ export enum RelationType {
export const RELATIONS_KEY = 'loopback:relations';

export interface RelationDefinitionBase {
/**
* The type of the relation, must be one of RelationType values.
*/
type: RelationType;

/**
* The relation name, typically matching the name of the accessor property
* defined on the source model. For example "orders" or "customer".
*/
name: string;

/**
* The source model of this relation.
*
* E.g. when a Customer has many Order instances, then Customer is the source.
*/
source: typeof Entity;

/**
* The target model of this relation.
*
* E.g. when a Customer has many Order instances, then Order is the target.
*/
target: TypeResolver<Entity, typeof Entity>;
}

export interface HasManyDefinition extends RelationDefinitionBase {
type: RelationType.hasMany;

/**
* The foreign key used by the target model.
*
* E.g. when a Customer has many Order instances, then keyTo is "customerId".
* Note that "customerId" is the default FK assumed by the framework, users
* can provide a custom FK name by setting "keyTo".
*/
keyTo?: string;
}

export interface BelongsToDefinition extends RelationDefinitionBase {
type: RelationType.belongsTo;

/*
* The foreign key in the source model, e.g. Order#customerId.
*/
keyFrom: string;

/*
* The primary key of the target model, e.g Customer#id.
*/
keyTo?: string;
}

// TODO(bajtos) add other relation types, e.g. BelongsToDefinition
export type RelationMetadata = HasManyDefinition | RelationDefinitionBase;
export type RelationMetadata =
| HasManyDefinition
| BelongsToDefinition
// TODO(bajtos) add other relation types and remove RelationDefinitionBase once
// all relation types are covered.
| RelationDefinitionBase;

/**
* Decorator for relations
Expand All @@ -48,12 +100,45 @@ export function relation(definition?: Object) {
/**
* Decorator for belongsTo
* @param definition
* @returns {(target:any, key:string)}
* @returns {(target: Object, key:string)}
*/
export function belongsTo(definition?: Object) {
// Apply model definition to the model class
const rel = Object.assign({type: RelationType.belongsTo}, definition);
return PropertyDecoratorFactory.createDecorator(RELATIONS_KEY, rel);
export function belongsTo<T extends Entity>(
targetResolver: EntityResolver<T>,
definition?: Partial<BelongsToDefinition>,
) {
return function(decoratedTarget: Entity, decoratedKey: string) {
const propMeta: PropertyDefinition = {
type: MetadataInspector.getDesignTypeForProperty(
decoratedTarget,
decoratedKey,
),
// TODO(bajtos) Make the foreign key required once our REST API layer
// allows controller methods to exclude required properties
// required: true,
};
property(propMeta)(decoratedTarget, decoratedKey);

// @belongsTo() is typically decorating the foreign key property,
// e.g. customerId. We need to strip the trailing "Id" suffix from the name.
const relationName = decoratedKey.replace(/Id$/, '');

const meta: BelongsToDefinition = Object.assign(
// default values, can be customized by the caller
{
keyFrom: decoratedKey,
},
// properties provided by the caller
definition,
// properties enforced by the decorator
{
type: RelationType.belongsTo,
name: relationName,
source: decoratedTarget.constructor,
target: targetResolver,
},
);
relation(meta)(decoratedTarget, decoratedKey);
};
}

/**
Expand All @@ -78,14 +163,13 @@ export function hasMany<T extends Entity>(
targetResolver: EntityResolver<T>,
definition?: Partial<HasManyDefinition>,
) {
// todo(shimks): extract out common logic (such as @property.array) to
// @relation
return function(decoratedTarget: Object, key: string) {
property.array(targetResolver)(decoratedTarget, key);

const meta: HasManyDefinition = Object.assign(
// default values, can be customized by the caller
{},
// properties customizable by users
// properties provided by the caller
definition,
// properties enforced by the decorator
{
Expand Down
19 changes: 18 additions & 1 deletion packages/repository/src/repositories/legacy-juggler-bridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,16 @@ import {
Options,
PositionalParameters,
} from '../common-types';
import {HasManyDefinition} from '../decorators/relation.decorator';
import {
BelongsToDefinition,
HasManyDefinition,
} from '../decorators/relation.decorator';
import {EntityNotFoundError} from '../errors';
import {Entity, ModelDefinition} from '../model';
import {Filter, Where} from '../query';
import {
BelongsToAccessor,
createBelongsToAccessor,
createHasManyRepositoryFactory,
HasManyRepositoryFactory,
} from './relation.factory';
Expand Down Expand Up @@ -179,6 +184,18 @@ export class DefaultCrudRepository<T extends Entity, ID>
);
}

protected _createBelongsToAccessorFor<Target extends Entity, TargetId>(
relationName: string,
targetRepoGetter: Getter<EntityCrudRepository<Target, TargetId>>,
): BelongsToAccessor<Target, ID> {
const meta = this.entityClass.definition.relations[relationName];
return createBelongsToAccessor<Target, TargetId, T, ID>(
meta as BelongsToDefinition,
targetRepoGetter,
this,
);
}

async create(entity: DataObject<T>, options?: Options): Promise<T> {
const model = await ensurePromise(this.modelClass.create(entity, options));
return this.toEntity(model);
Expand Down
Loading

0 comments on commit df8c64c

Please sign in to comment.