From b5d386761aaec9fbb735a4f27600172c2be509b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ju=CC=88rg=20Lehni?= Date: Mon, 12 Oct 2026 23:01:18 +0200 Subject: [PATCH] Write the foreign keys of has-many and has-one relates in upsertGraph() with noUpdate (#355) --- doc/release-notes/changelog.md | 1 + lib/queryBuilder/graph/patch/GraphPatchAction.js | 14 +++++++++++++- tests/integration/upsertGraph.js | 16 ++++++++++++++++ 3 files changed, 30 insertions(+), 1 deletion(-) diff --git a/doc/release-notes/changelog.md b/doc/release-notes/changelog.md index 614a7233c..457308a08 100644 --- a/doc/release-notes/changelog.md +++ b/doc/release-notes/changelog.md @@ -6,6 +6,7 @@ - `insertGraph()` and `upsertGraph()` reject only after the queries they already started have finished, so a transaction is rolled back after them. Before, rows of these queries could be committed after the rollback on SQLite [#336](https://github.com/ditojs/objection/pull/336) - `upsertGraph()` sets the foreign key of a model that a `#ref` relates into a `HasManyRelation` or `HasOneRelation`. Before, the row was left without it, and the id of the owner could be cleared [#311](https://github.com/ditojs/objection/issues/311) +- `upsertGraph()` with `relate` and `noUpdate` writes the foreign keys of models that it relates through a `HasManyRelation` or `HasOneRelation`, like it does for `BelongsToOneRelation` and `ManyToManyRelation`, instead of leaving them unrelated without an error, as it did since 1.5.0 [#355](https://github.com/ditojs/objection/pull/355) ## 3.9.0 diff --git a/lib/queryBuilder/graph/patch/GraphPatchAction.js b/lib/queryBuilder/graph/patch/GraphPatchAction.js index 642ee3c40..98d52e7e2 100644 --- a/lib/queryBuilder/graph/patch/GraphPatchAction.js +++ b/lib/queryBuilder/graph/patch/GraphPatchAction.js @@ -31,6 +31,14 @@ class GraphPatchAction extends GraphAction { // properties. That's why we handle them here. const changePropsBecauseOfBelongsToOneDelete = this._handleBelongsToOneDeletes(node); + // HasManyRelation and HasOneRelation relates write the related object's + // foreign keys, also with `noUpdate`. + const changedPropsBecauseOfRelate = + this.graphOptions.shouldRelate(node, this.graphData) && + node.parentEdge.relation.isObjectionHasManyRelation + ? node.parentEdge.relation.relatedProp.props + : []; + const handleUpdate = () => { const { changedProps, unchangedProps } = this._findChanges(node); const allProps = [...changedProps, ...unchangedProps]; @@ -38,7 +46,11 @@ class GraphPatchAction extends GraphAction { const propsToUpdate = difference( shouldPatch || shouldUpdate ? changedProps - : [...changedPropsBecauseOfBelongsToOneInsert, ...changePropsBecauseOfBelongsToOneDelete], + : [ + ...changedPropsBecauseOfBelongsToOneInsert, + ...changePropsBecauseOfBelongsToOneDelete, + ...changedPropsBecauseOfRelate, + ], // Remove id properties from the props to update. With upsertGraph // it never makes sense to change the id. diff --git a/tests/integration/upsertGraph.js b/tests/integration/upsertGraph.js index ac655d89d..047e1d2be 100644 --- a/tests/integration/upsertGraph.js +++ b/tests/integration/upsertGraph.js @@ -2748,6 +2748,22 @@ module.exports = (session) => { }); }); + it('should only write the foreign key when hasManyRelation and hasOneRelation models are related with noUpdate: true', async () => { + await Model1.query(session.knex).upsertGraph( + { + id: 1, + model1Relation1Inverse: { id: 4, model1Prop1: 'this should not be written to db' }, + model1Relation2: [{ idCol: 1, model2Prop1: 'this should not be written to db' }], + }, + { fetchStrategy, relate: true, noUpdate: true, noDelete: true }, + ); + + const model1 = await Model1.query(session.knex).findById(4); + expect([model1.model1Id, model1.model1Prop1]).to.eql([1, 'manyToMany 1']); + const model2 = await Model2.query(session.knex).findById(1); + expect([model2.model1Id, model2.model2Prop1]).to.eql([1, 'hasMany 1']); + }); + it('should not update other than the relation properties when belongsToOneRelation is unrelated but the parent has noUpdate: true', () => { const upsert1 = { id: 2,