-
Notifications
You must be signed in to change notification settings - Fork 1.1k
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
Conversation
e48642d
to
cc8d66e
Compare
packages/repository/src/relations/has-many/has-many-through.repository.ts
Outdated
Show resolved
Hide resolved
cc8d66e
to
cf18e08
Compare
Thank you @derdeka for the pull request. @agnes512 & @hacksparrow can you please review? |
There was a problem hiding this 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? |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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:
- We avoid violating referential integrity enforced by SQL databases by deleting target models that are still references by through models.
- 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.
-
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. -
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< |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
9bd8f60
to
05c1bfe
Compare
const seller = await customerRepo | ||
.sellers(existingCustomerId) | ||
.create(sellerData, { | ||
throughData: orderData, | ||
}); | ||
expect(toJSON(seller)).containDeep(toJSON(sellerData)); |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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".
There was a problem hiding this comment.
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( |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same as other relation tests, which are picked up by:
https://github.com/strongloop/loopback-next/blob/098dc971f1fcb00321ca9226a2475c62aa5d00d0/packages/repository-tests/src/crud-test-suite.ts#L57-L58
packages/repository-tests/src/crud/relations/acceptance/has-many-through.relation.acceptance.ts
Show resolved
Hide resolved
@@ -87,12 +87,12 @@ export interface HasManyThroughDefinition extends RelationDefinitionBase { | |||
/** | |||
* The foreign key in the source model, e.g. Customer#id. | |||
*/ | |||
keyFrom: string; | |||
keyFrom?: string; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
69193a7
to
2b94fd9
Compare
Do we have any ETA for this? |
@derdeka, if your PR is ready for review, could you please make it a non-draft PR? Thanks! |
There was a problem hiding this 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.
const seller = await customerRepo | ||
.sellers(existingCustomerId) | ||
.create(sellerData, { | ||
throughData: orderData, | ||
}); | ||
expect(toJSON(seller)).containDeep(toJSON(sellerData)); |
There was a problem hiding this comment.
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, | ||
// ); |
There was a problem hiding this comment.
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.
async () => customerRepo, | ||
async () => orderRepo, |
There was a problem hiding this comment.
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
?
async () => customerRepo, | |
async () => orderRepo, | |
Getter.fromValue(customerRepo), | |
Getter.fromValue(orderRepo), |
ThroughEntity extends Entity, | ||
ForeignKeyType | ||
> = ( | ||
fkValue: ForeignKeyType, |
There was a problem hiding this comment.
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< |
There was a problem hiding this comment.
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:
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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).
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.'); | ||
} |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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: { |
There was a problem hiding this comment.
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( |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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 fromRelationDefinitionBase
. See https://www.typescriptlang.org/docs/handbook/advanced-types.html#discriminated-unionsIf 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 toHasManyDefinition
, or else modifyHasManyThroughDefinition
to inherit fromHasManyDefinition
.
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.
@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? |
Hello guys ! Do you know when this feature is going to be available in the framework ? thanks in advance. |
hasManyThrough helpers and definitions have been modified/implemented in #5354 . Will work on factory next. |
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. |
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 machinepackages/cli
were updatedexamples/*
were updated👉 Check out how to submit a PR 👈