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

[WIP] feat(repository): hasManyThrough #4438

Closed
wants to merge 10 commits into from
Closed

Conversation

derdeka
Copy link
Contributor

@derdeka derdeka commented Jan 15, 2020

Just another try / draft / poc to implement hasManyThrough
as the original implementation has birthday in a few days.

Early feedback welcome.

This PR is a continuation of #2359.

Checklist

👉 Read and sign the CLA (Contributor License Agreement) 👈

  • npm test passes on your machine
  • New tests added or existing tests modified to cover all changes
  • Code conforms with the style guide
  • API Documentation in code was updated
  • Documentation in /docs/site was updated
  • Affected artifact templates in packages/cli were updated
  • Affected example projects in examples/* were updated

👉 Check out how to submit a PR 👈

@bajtos
Copy link
Member

bajtos commented Feb 7, 2020

Thank you @derdeka for the pull request.

@agnes512 & @hacksparrow can you please review?

Copy link
Contributor

@agnes512 agnes512 left a comment

Choose a reason for hiding this comment

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

Thank you for adding the skeleton of has many through!

Do we want to implement other details such as resolveHasManyThroughMetadata in this PR or separate them out?

const targetRepository = await this.getTargetRepository();
const throughRepository = await this.getThroughRepository();
const throughConstraint = this.getThroughConstraint();
// TODO(derdeka): How to delete throughInstances?
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we want to delete the through instances? I was expecting this method to delete the target instances. WDYT?

Copy link
Contributor

Choose a reason for hiding this comment

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

I think it should delete the through instance too. When the target is deleted, the FK in the through model will no longer reference a valid target model.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

On a relational database this can be easily done by a fk trigger on delete cascade. But what should happen on a non-relational database or if there is no trigger?

Copy link
Member

Choose a reason for hiding this comment

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

IMO, this should delete through instances first and then delete the target instances second. That way:

  1. We avoid violating referential integrity enforced by SQL databases by deleting target models that are still references by through models.
  2. We avoid leaving orphaned "through" model instances in the databases. By an orphaned instance I mean an instance that's referencing a target model that no longer exists.

Please make sure to include acceptance-level test for this feature!

Here are two scenario to consider & cover by tests, I'll use "Source", "Target" and "Through" as model names instead of Customer/Seller/Order.

  1. Given through links: {sourceId: 1, targetId: 1} and {sourceId: 2, targetId: 2}.
    When I delete all models belonging to source id 1,
    then the target model id 1 and the first link is deleted.

  2. Given trough links {sourceId: 1, targetId: 1}, {sourceId: 2, targetId: 1} and {sourceId: 2, targetId: 2}.
    When I delete all models belonging to source id 1,
    then the target model id 1 and the first two links are deleted. Only {sourceId: 2, targetId: 2} link is preserved.

@@ -98,3 +105,129 @@ export interface HasManyThroughRepository<
},
): Promise<void>;
}

export class DefaultHasManyThroughRepository<
Copy link
Contributor

Choose a reason for hiding this comment

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

Let's add API docs for it.

Copy link
Member

Choose a reason for hiding this comment

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

+1, also include "experimental feature" warning.

@derdeka derdeka force-pushed the has-many-through branch 2 times, most recently from 9bd8f60 to 05c1bfe Compare March 3, 2020 20:15
Comment on lines +73 to +78
const seller = await customerRepo
.sellers(existingCustomerId)
.create(sellerData, {
throughData: orderData,
});
expect(toJSON(seller)).containDeep(toJSON(sellerData));
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Creating a seller though order is not the best example / test usecase for hasManyThrough.
Can anyone give a hint, which models / usecase we could use?

Copy link
Member

Choose a reason for hiding this comment

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

How about Category-Product domain, where each category can have multiple products and each product can belong to multiple categories? The "through" model can be CategoryProduct (or CategoryProductLink?) and the operation to test is "create a new product in the given category".

Copy link
Member

Choose a reason for hiding this comment

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

Alternatively, we can reverse the relation to "Seller has many Customer through Order". I think in such case it's more reasonable to create a new customer via the seller?

} from '../fixtures/models';
import {givenBoundCrudRepositories} from '../helpers';

export function hasManyRelationAcceptance(
Copy link
Contributor

Choose a reason for hiding this comment

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

Why do need to export this function? Also, I don't see any place where it is called.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@@ -87,12 +87,12 @@ export interface HasManyThroughDefinition extends RelationDefinitionBase {
/**
* The foreign key in the source model, e.g. Customer#id.
*/
keyFrom: string;
keyFrom?: string;
Copy link
Contributor

Choose a reason for hiding this comment

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

Why make these keys optional? If justified, this change should be PR of its own to minimize the size of the PR.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'll happy to extract this into a PR if there is agreement in overall structure.

Copy link
Member

Choose a reason for hiding this comment

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

I am ok with these changes, I see they are consistent with what we already have in place for HasManyDefinition (see above in the source code file).

I agree a new PR would allow us to land these fixes sooner, on the other hand such PR would be a bit anemic - since no code is using HasManyThroughDefinition yet, there will be no way how to verify correctness of such small PR.

In that light, I think it may be better better to keep these changes inside this bigger PR.

Copy link
Member

Choose a reason for hiding this comment

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

Cross-posting from #4438 (comment):

I think it would be best to extract the changes in relation definitions, the implementation bits of resolving through relations and the accompanying unit tests into a new pull request, to get a smaller patch that's easier to review & iterate on.

I think this is the best way how to get a PR changing relation definition that's smaller but still meaningful.

@amitgiri0001
Copy link
Contributor

Do we have any ETA for this?

@dhmlau
Copy link
Member

dhmlau commented Mar 27, 2020

@derdeka, if your PR is ready for review, could you please make it a non-draft PR? Thanks!

@dhmlau dhmlau added this to the April 2020 milestone Mar 27, 2020
Copy link
Member

@bajtos bajtos left a comment

Choose a reason for hiding this comment

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

Great stuff, @derdeka! I like that your pull request includes a code coverage for the different bits involved in new relation type.

I found a possible design problem in the way how definition interfaces for hasManyThrough are designed, see my comments towards the bottom. Considering that this pull request is already quite large at 918 new lines, I think it would be best to to extract the changes in relation definitions, the implementation bits of resolving through relations and the accompanying unit tests into a new pull request, to get a smaller patch that's easier to review & iterate on.

Comment on lines +73 to +78
const seller = await customerRepo
.sellers(existingCustomerId)
.create(sellerData, {
throughData: orderData,
});
expect(toJSON(seller)).containDeep(toJSON(sellerData));
Copy link
Member

Choose a reason for hiding this comment

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

How about Category-Product domain, where each category can have multiple products and each product can belong to multiple categories? The "through" model can be CategoryProduct (or CategoryProductLink?) and the operation to test is "create a new product in the given category".

// customerRepo.inclusionResolvers.set(
// 'sellers',
// customerRepo.sellers.inclusionResolver,
// );
Copy link
Member

Choose a reason for hiding this comment

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

We don't keep code that's commented out. Please uncomment or remove this block before the PR is landed.

Personally, I am fine to leave inclusion resolvers out of scope of this pull request if it enables us to get the initial implementation landed sooner.

Comment on lines +109 to +110
async () => customerRepo,
async () => orderRepo,
Copy link
Member

Choose a reason for hiding this comment

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

Have you considered using Getter.fromValue?

Suggested change
async () => customerRepo,
async () => orderRepo,
Getter.fromValue(customerRepo),
Getter.fromValue(orderRepo),

ThroughEntity extends Entity,
ForeignKeyType
> = (
fkValue: ForeignKeyType,
Copy link
Member

Choose a reason for hiding this comment

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

Can you please clarify which model (source or target) is this foreign-key type referring to?

Let's say I have a Category model using number as the primary key and a Product model using string (ObjectID) as the primary key. What type should I use for ForeignKeyType?

Also isn't the foreign key type always the same as SourceID or TargetID? As I understand has-many-through, the through table contains rows of (sourceId, targetId) pairs. Are you perhaps trying to support a more advanced use case, where the link is established using a non-PK field, e.g. using Category.name instead of Category.id as the foreign key? I believe such use case is not supported in existing relations (hasMany, belongsTo) yet, I think we should leave it out of the initial implementation of hasManyThrough.

I think ForeignKeyType should be the same type as SourceID. If that's true then I think it's better to call this generic/template parameter as SourceID to make it immediately clear which type is expected.

Let's discuss.

HasManyThroughRepository,
} from './has-many-through.repository';

export type HasManyThroughRepositoryFactory<
Copy link
Member

Choose a reason for hiding this comment

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

Please add a tsdoc comment to all newly created public interfaces and include a warning about experimental status. See e.g. this place for inspiration:

https://github.com/strongloop/loopback-next/blob/2b94fd9cd4cdd3030c83023535c28c8376d67905/packages/repository/src/relations/relation.types.ts#L79-L82

Copy link
Member

Choose a reason for hiding this comment

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

@bajtos We might want to also utilize the tsdoc @beta modifier.

Copy link
Member

Choose a reason for hiding this comment

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

I think @alpha would be more appropriate, see https://api-extractor.com/pages/tsdoc/doc_comment_syntax/#release-tags

Indicates that an API item is eventually intended to be public, but currently is in an early stage of development. Third parties should not use “alpha” APIs.

AFAIK, we are not using these modifiers yet, I have no idea how are they rendered in our API docs (if they are rendered at all).

Comment on lines +215 to +232
async link(
targetModelId: TargetID,
options?: Options & {
throughData?: DataObject<ThroughEntity>;
throughOptions?: Options;
},
): Promise<TargetEntity> {
throw new Error('Method not implemented.');
}

async unlink(
targetModelId: TargetID,
options?: Options & {
throughOptions?: Options;
},
): Promise<void> {
throw new Error('Method not implemented.');
}
Copy link
Member

Choose a reason for hiding this comment

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

What are your plans for these two not-implemented methods?

I slightly prefer to include their implementation in the initial pull request to verify that the current design of DefaultHasManyThroughRepository has all necessary pieces required to implement link/unlink functionality.

However, if you prefer to leave this out, then I am fine with that too, as long as this limitation is clearly documented somewhere and we have a follow-up task (or a new issue) tracked on GitHub.

@@ -87,12 +87,12 @@ export interface HasManyThroughDefinition extends RelationDefinitionBase {
/**
* The foreign key in the source model, e.g. Customer#id.
*/
keyFrom: string;
keyFrom?: string;
Copy link
Member

Choose a reason for hiding this comment

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

I am ok with these changes, I see they are consistent with what we already have in place for HasManyDefinition (see above in the source code file).

I agree a new PR would allow us to land these fixes sooner, on the other hand such PR would be a bit anemic - since no code is using HasManyThroughDefinition yet, there will be no way how to verify correctness of such small PR.

In that light, I think it may be better better to keep these changes inside this bigger PR.


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

through: {
Copy link
Member

Choose a reason for hiding this comment

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

A side note: in the future, I would like to add support for convenience shortcuts, e.g.

@hasMany(() => Seller, {through: () => Order})

See #2359 (comment)

This is out of scope of this pull request though.

@@ -17,7 +21,7 @@ import {HasManyDefinition, RelationType} from '../relation.types';
*/
export function hasMany<T extends Entity>(
targetResolver: EntityResolver<T>,
definition?: Partial<HasManyDefinition>,
definition?: Partial<HasManyDefinition | HasManyThroughDefinition>,
) {
return function(decoratedTarget: object, key: string) {
const meta: HasManyDefinition = Object.assign(
Copy link
Member

Choose a reason for hiding this comment

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

This is not correct for "has-many-through" case, is it?

See the discussion in for #2359 (comment) for more context.


/**
* The primary key of the target model, e.g Seller#id.
*/
keyTo: string;
keyTo?: string;
Copy link
Member

Choose a reason for hiding this comment

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

While we are discussing HasManyThroughDefinition. I am not sure if it's actually a good idea to have such interface, see the discussion in #2359 (comment) for more context.

Having two different interfaces for hasMany and hasManyThrough relations is already creating extra complexity as can be seen in my comment above related to @hasMany() decorator.

Cross-posting from #2359 (comment):

So far, we are using type to discriminate between different interfaces inheriting from RelationDefinitionBase. See https://www.typescriptlang.org/docs/handbook/advanced-types.html#discriminated-unions

If we want to keep using hasMany value for has-many-through relations, then I am fine with that, but let's capture that in the type system too.

We can either get rid of HasManyThroughDefinition completely and move all members to HasManyDefinition, or else modify HasManyThroughDefinition to inherit from HasManyDefinition.

Personally, I like most the option where there is a single HasManyDefinition interface with an optional through field that's used to distinguish between hasMany and hasManyThrough cases. Similarly, HasManyResolvedDefinition can contain an optional through field that's used to distinguish hasManyThrough case, this field can use a type like ThroughResolvedDefinition to tell the compiler that optional properties were resolved and will be always present.

I think it would be best to extract the changes in relation definitions, the implementation bits of resolving through relations and the accompanying unit tests into a new pull request, to get a smaller patch that's easier to review & iterate on.

@bajtos bajtos added feature feature parity Repository Issues related to @loopback/repository package labels Apr 2, 2020
@bajtos
Copy link
Member

bajtos commented Apr 2, 2020

@codejamninja Ping 👋 Since you are the author of #2359, which kick-started the implementation of hasManyThrough, would you like to join the discussion and/or review the proposed implementation too?

@MacGyver26
Copy link

Hello guys !

Do you know when this feature is going to be available in the framework ? thanks in advance.

@agnes512
Copy link
Contributor

hasManyThrough helpers and definitions have been modified/implemented in #5354 . Will work on factory next.

@agnes512
Copy link
Contributor

The code this PR covered are being landed, though the HasManyThrough relation is still in an experimental stage (a bug found in #5852). The documentation will be added in the near future.

@agnes512 agnes512 closed this Jun 29, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature parity feature Relations Model relations (has many, etc.) Repository Issues related to @loopback/repository package
Projects
None yet
Development

Successfully merging this pull request may close these issues.

9 participants