-
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
feat(repository): add link and unlink methods for hasManyThrough repository #5719
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 . |
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
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:
|
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
link
andunlink
to 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', }
createTargetConstraint
creates ->{id: 1}
for the Target modelcreateFkValues
extracts fks ->1
or[1, 2]
createThroughConstraint
creates ->{sourceId: 5}
for the through modelcreateThroughFkConstraint
creates ->{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'})
returnsp
createThroughConstraint(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
createFkValues
togetTargetKeyFromThroughModel
. 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
createThroughConstraint
to make it clear the constraint is being applied on the "from"/"source" side of the link. For example,createSourceThroughConstraint
orcreateThroughConstraintOnSource
. - Similarly
createThroughFkConstraint
can be renamed tocreateTargetThroughConstraint
orcreateThroughConstraintOnTarget
.
(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!
|
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.
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
link
andunlink
methods.(continuation of #4438, #5674 and #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 👈