-
Notifications
You must be signed in to change notification settings - Fork 1.1k
feat(repository): add link and unlink methods for hasManyThrough repository #5719
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
Conversation
@jannyHou commented in #5674 (comment). The current design is to take a whole instance ( not just the ID) and link it without checking the existence. I don't have strong opinion on either of them. Would like to also hear from @hacksparrow and @bajtos . |
bajtos
left a comment
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.
My take is we should check target instance exists, if not throw error properly.
Not necessarily in the first implementation but make sure we create a story for it.@jannyHou commented in #5674 (comment). The current design is to take a whole instance ( not just the ID) and link it without checking the existence. I don't have strong opinion on either of them. Would like to also hear from @hacksparrow and @bajtos .
I am glad you are aware of the edge case and started the discussion about enforcing referential integrity! 👍
So far, LoopBack implements what we call "weak relations", where it's up to the database to enforce any referential integrity. Typically, when using SQL, there is a foreign key constraint configured to ensure the "link" rows cannot point to a source/target model that does not exist, and also that it's not possible to delete a source or a target row before all their "link" rows are removed first. (It's also possible to configure cascading delete, but let's not get distracted.) We have been discussing "strong relations" in the past, but they weren't a priority so far. You can learn more in #2331.
IMO, we should keep the current ("weak") design, if only for consistency with other existing relational APIs, and mention in the documentation that the referential integrity must be configured at database level.
See also #1718
packages/repository/src/relations/has-many/has-many-through.repository.ts
Outdated
Show resolved
Hide resolved
packages/repository/src/__tests__/integration/repositories/relation.factory.integration.ts
Outdated
Show resolved
Hide resolved
packages/repository/src/relations/has-many/has-many-through.helpers.ts
Outdated
Show resolved
Hide resolved
bajtos
left a comment
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.
Good progress.
I am a bit confused about the API of helpers for building constraints. It's possible I don't fully understand how they fit into the bigger picture, but this may be also a sign of subtle bugs in the design.
Can you please take a look at my comments and help me better understand the proposed implementation?
packages/repository/src/relations/has-many/has-many-through.repository.ts
Show resolved
Hide resolved
packages/repository/src/relations/has-many/has-many-through.helpers.ts
Outdated
Show resolved
Hide resolved
packages/repository/src/relations/has-many/has-many-through.helpers.ts
Outdated
Show resolved
Hide resolved
packages/repository/src/relations/has-many/has-many-through-repository.factory.ts
Outdated
Show resolved
Hide resolved
| type: 'number', | ||
| id: true, | ||
| required: true, | ||
| }) |
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 actually an interesting discussion point. Should "through" models have their own primary key (id), or should we use a composite primary key composed from source & target keys (e.g. itemId + customerId)?
@raymondfeng What's your opinion?
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.
Imagine we have Appointment as the through model to connect Doctor and Patient, a patient may have multiple appointments with the same doctor. So doctorId + patientId won't be unique in Appointment model, which should have its PK.
For the relation, we need to use two FKs (doctorId and patientId) instead of PK for Appointment.
packages/repository/src/__tests__/integration/repositories/relation.factory.integration.ts
Show resolved
Hide resolved
|
Thank @bajtos for the detailed review 🙇♀️ ( I accidentally closed the issue) Since we'd like To allow these CRUD functions, we need helpers to generate different kind of constraints to meet our requirements. For relationMetadata:
|
bajtos
left a comment
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 @bajtos for the detailed review 🙇♀️ ( I accidentally closed the issue)
Since we'd like
linkandunlinkto just take in id values, I modified/added some helpers. I'd like to summarize them there to help review the PR:
Thank you for the summary, it's helpful! 👍
To allow these CRUD functions, we need helpers to generate different kind of constraints to meet our requirements. For relationMetadata:
keyFrom: 'id', // Source model keyTo: 'id', // Target model through: { model: () => Link, // through model keyFrom: 'sourceId', keyTo: 'targetId', }
createTargetConstraintcreates ->{id: 1}for the Target modelcreateFkValuesextracts fks ->1or[1, 2]createThroughConstraintcreates ->{sourceId: 5}for the through modelcreateThroughFkConstraintcreates ->{targetId: 9}for the through model based on the passed in fks
I am little bit confused about the different id values you are showing (1, 2, 5, 9). Here is my understanding, could you please confirm I am getting it right?
Let's say we have "Doctor" has many "Patient" through "Appointment" relation, a doctor with id d, a patient with id p and an appointment with id a that's linking doctor d with patient p. Then:
createTargetConstraint(relationMeta, {id: 'a', doctorId: 'd', patientId: 'p'})returns{id: 'p'}createFkValues(relationMeta, {id: 'a', doctorId: 'd', patientId: 'p'})returnspcreateThroughConstraint(relationMeta, 'd')returns{doctorId: 'd'}createThroughFkConstraint(relationMeta, ['p'])returns{patientId: 'p'}
I find the function names a bit difficult to map to the actual behavior. It could be caused by my lack of deeper knowledge of other existing relation helpers, but perhaps we can improve the names a bit? In particular, I find the "fk" part ambiguous because there are two foreign keys involved in a has-many-through relation (through.keyFrom and through.keyTo).
An idea to consider:
- Rename
createFkValuestogetTargetKeyFromThroughModel. We are not creating anything here, just getting values from an object, right? Also the function accepts a single "through" model instance and extracts a single key (id) value, so I think we should not use plural form ("values") in the name. - Rename
createThroughConstraintto make it clear the constraint is being applied on the "from"/"source" side of the link. For example,createSourceThroughConstraintorcreateThroughConstraintOnSource. - Similarly
createThroughFkConstraintcan be renamed tocreateTargetThroughConstraintorcreateThroughConstraintOnTarget.
(On the second though, I like createThroughConstraintOnSource and createThroughConstraintOnTarget more, but please pick the names that you prefer and feel free to come up with your own.)
.../src/__tests__/unit/repositories/relations-helpers/resolve-has-many-through-metadata.unit.ts
Outdated
Show resolved
Hide resolved
packages/repository/src/relations/has-many/has-many-through.helpers.ts
Outdated
Show resolved
Hide resolved
| */ | ||
| export function createFkValues<Through extends Entity, TargetID>( | ||
| relationMeta: HasManyThroughResolvedDefinition, | ||
| throughInstances: Through | 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.
Is it necessary to support both Trough and Through[] variants? I noticed this design is affecting multiple helpers. Wouldn't it be simpler to support a single value only (Through) and let the caller of this function to deal with the array case by calling instances.map(i => createFkValues(relationMeta, i))?
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.
Edit: simplified to through[] (I found type conversions are annoying if I use Through), also simplified the returned type to TargetID.
That's correct 👍 Thank @bajtos . It makes more sense with the new names!
|
jannyHou
left a comment
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.
@agnes512 I like the new names of functions 👍 left a question for the test case.
packages/repository/src/__tests__/integration/repositories/relation.factory.integration.ts
Show resolved
Hide resolved
| function getThroughFkConstraint( | ||
| targetInstance: Target, | ||
| function getThroughConstraintOnTarget( | ||
| fkValues: TargetID, |
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.
Is the fkValues an array? if so I think the type should be TargetID[] :p
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 the any type escaped from the type checking. Converted it to TargetID[] in 975f25e.
bajtos
left a comment
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.
The new versions looks so much better now, I find it much easier to read & understand. Thank you, @agnes512!
I found few more places where we could improve the names for more clarity, but I am not able to tell where it's important and where it's just "icing on the cake". I'll leave it up to you to decide which comments to address and which to ignore.
packages/repository/src/relations/has-many/has-many-through.repository.ts
Show resolved
Hide resolved
packages/repository/src/relations/has-many/has-many-through-repository.factory.ts
Outdated
Show resolved
Hide resolved
packages/repository/src/relations/has-many/has-many-through.helpers.ts
Outdated
Show resolved
Hide resolved
.../src/__tests__/unit/repositories/relations-helpers/resolve-has-many-through-metadata.unit.ts
Show resolved
Hide resolved
packages/repository/src/relations/has-many/has-many-through.helpers.ts
Outdated
Show resolved
Hide resolved
packages/repository/src/relations/has-many/has-many-through.helpers.ts
Outdated
Show resolved
Hide resolved
packages/repository/src/relations/has-many/has-many-through-repository.factory.ts
Outdated
Show resolved
Hide resolved
packages/repository/src/relations/has-many/has-many-through-repository.factory.ts
Outdated
Show resolved
Hide resolved
packages/repository/src/relations/has-many/has-many-through.repository.ts
Outdated
Show resolved
Hide resolved
| const throughRepository = await this.getThroughRepository(); | ||
| const throughConstraint = this.getThroughConstraintOnSource(); | ||
| const targetConstraint = this.getThroughConstraintOnTarget([targetId]); | ||
| const constraints = {...targetConstraint, ...throughConstraint}; |
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.
Is it safe to combine constraints this way? Should we use WhereBuilder instead to ensure complex syntax like or is handled correctly?
| const constraints = {...targetConstraint, ...throughConstraint}; | |
| const constraints = new WhereBuilder(targetConstraint).and(throughConstraint).build(); |
@raymondfeng you may have better insight into what's safe and what's not, could you PTAL?
|
Thank you for the links to individual commits. I started to post review comments there and then realized they don't show as a regular PR review 😱 I re-posted my suggestions via regular review and deleted the comments attached to individual commits. You may get few weird notifications pointing to that deleted content, sorry for that! 🙈 |
packages/repository/src/__tests__/integration/repositories/relation.factory.integration.ts
Outdated
Show resolved
Hide resolved
.../src/__tests__/unit/repositories/relations-helpers/resolve-has-many-through-metadata.unit.ts
Outdated
Show resolved
Hide resolved
f847e2c to
a2bcb62
Compare
Implemented
linkandunlinkmethods.(continuation of #4438, #5674 and #2359)
Checklist
👉 Read and sign the CLA (Contributor License Agreement) 👈
npm testpasses on your machinepackages/cliwere updatedexamples/*were updated👉 Check out how to submit a PR 👈