Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(repository): implement inclusionResolver for hasMany relation #3595

Merged
merged 1 commit into from
Sep 20, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 92 additions & 0 deletions docs/site/HasMany-relation.md
Original file line number Diff line number Diff line change
Expand Up @@ -314,3 +314,95 @@ certain properties from the JSON/OpenAPI spec schema built for the `requestBody`
payload. See its [GitHub
issue](https://github.com/strongloop/loopback-next/issues/1179) to follow the discussion.
" %}

## Querying related models

LoopBack 4 has the concept of an `inclusion resolver` in relations, which helps
to query data through an `include` filter. An inclusion resolver is a function
that can fetch target models for the given list of source model instances.
LoopBack 4 creates a different inclusion resolver for each relation type.

Use the relation between `Customer` and `Order` we show above, a `Customer` has
many `Order`s.

After setting up the relation in the repository class, the inclusion resolver
allows users to retrieve all customers along with their related orders through
the following code:

```ts
customerRepo.find({include: [{relation: 'orders'}]});
```

### Enable/disable the inclusion resolvers:

- Base repository classes have a public property `inclusionResolvers`, which
maintains a map containing inclusion resolvers for each relation.
- The `inclusionResolver` of a certain relation is built when the source
repository class calls the `createHasManyRepositoryFactoryFor` function in the
constructor with the relation name.
- Call `registerInclusionResolver` to add the resolver of that relation to the
`inclusionResolvers` map. (As we realized in LB3, not all relations are
allowed to be traversed. Users can decide to which resolvers can be added.)

The following code snippet shows how to register the inclusion resolver for the
has-many relation 'orders':

```ts
export class CustomerRepository extends DefaultCrudRepository {
products: HasManyRepositoryFactory<Order, typeof Customer.prototype.id>;

constructor(
dataSource: juggler.DataSource,
orderRepositoryGetter: Getter<OrderRepository>,
) {
super(Customer, dataSource);

// we already have this line to create a HasManyRepository factory
this.orders = this.createHasManyRepositoryFactoryFor(
'orders',
orderRepositoryGetter,
);

// add this line to register inclusion resolver
this.registerInclusion('orders', this.orders.inclusionResolver);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm wondering if we can make it even simpler as we can use orders to get the inclusionResolver (this['orders'].inclusionResolver).

this.registerInclusion('orders');

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point. I think it's doable.
Let me land this PR and open a new one for it.

}
}
```

- We can simply include the relation in queries via `find()`, `findOne()`, and
`findById()` methods. Example:

```ts
customerRepository.find({include: [{relation: 'orders'}]});
```

which returns:

```ts
[
{
id: 1,
name: 'Thor',
orders: [
{name: 'Mjolnir', customerId: 1},
{name: 'Rocket Raccoon', customerId: 1},
],
},
{
id: 2,
name: 'Captain',
orders: [{name: 'Shield', customerId: 2}],
},
];
```

- You can delete a relation from `inclusionResolvers` to disable the inclusion
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is this doing exactly? It is not clear to me.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just to show the way to disable a certain inclusion resolver. Because not all models are traversable.

for a certain relation. e.g
`customerRepository.inclusionResolvers.delete('orders')`

{% include note.html content="
Inclusion with custom scope:
Besides specifying the relation name to include, it's also possible to specify additional scope constraints.
However, this feature is not supported yet. Check our GitHub issue for more information:
[Include related models with a custom scope](https://github.com/strongloop/loopback-next/issues/3453).
" %}
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import {
} from '../../..';
import {
deleteAllModelsInDefaultDataSource,
MixedIdType,
withCrudCtx,
} from '../../../helpers.repository-tests';
import {
Expand All @@ -39,7 +38,6 @@ export function belongsToInclusionResolverAcceptance(
before(deleteAllModelsInDefaultDataSource);
let customerRepo: CustomerRepository;
let orderRepo: OrderRepository;
let existingCustomerId: MixedIdType;

before(
withCrudCtx(async function setupRepository(ctx: CrudTestContext) {
Expand All @@ -62,9 +60,10 @@ export function belongsToInclusionResolverAcceptance(
});

it('throws an error if it tries to query nonexists relation names', async () => {
const customer = await customerRepo.create({name: 'customer'});
await orderRepo.create({
description: 'shiba',
customerId: existingCustomerId,
description: 'an order',
customerId: customer.id,
});
await expect(
orderRepo.find({include: [{relation: 'shipment'}]}),
Expand Down Expand Up @@ -166,9 +165,10 @@ export function belongsToInclusionResolverAcceptance(
});
// scope for inclusion is not supported yet
it('throws error if the inclusion query contains a non-empty scope', async () => {
const customer = await customerRepo.create({name: 'customer'});
await orderRepo.create({
description: 'shiba',
customerId: existingCustomerId,
description: 'an order',
customerId: customer.id,
});
await expect(
orderRepo.find({
Expand All @@ -178,9 +178,10 @@ export function belongsToInclusionResolverAcceptance(
});

it('throws error if the target repository does not have the registered resolver', async () => {
const customer = await customerRepo.create({name: 'customer'});
await orderRepo.create({
description: 'shiba',
customerId: existingCustomerId,
description: 'an order',
customerId: customer.id,
});
// unregister the resolver
orderRepo.inclusionResolvers.delete('customer');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,11 +98,13 @@ export function belongsToRelationAcceptance(
name: 'Order McForder',
});
const order = await orderRepo.create({
customerId: deletedCustomer.id, // does not exist
description: 'Order of a fictional customer',
customerId: deletedCustomer.id,
description: 'custotmer will be deleted',
});
await customerRepo.deleteAll();

await orderRepo.deleteAll();

await expect(findCustomerOfOrder(order.id)).to.be.rejectedWith(
EntityNotFoundError,
);
Expand Down
Loading