Skip to content

Commit 42e54ce

Browse files
authored
Merge pull request #14 from ovos/upsertgraph-unrelate-delete-props-on-individual-models
upsertGraph: allow marking individual models in graph to be unrelated or deleted
2 parents b05d92f + 26733a8 commit 42e54ce

File tree

7 files changed

+262
-3
lines changed

7 files changed

+262
-3
lines changed

lib/model/Model.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -729,6 +729,8 @@ Model.uidProp = '#id';
729729
Model.uidRefProp = '#ref';
730730
Model.dbRefProp = '#dbRef';
731731
Model.propRefRegex = /#ref{([^\.]+)\.([^}]+)}/g;
732+
Model.graphUnrelateProp = '#unrelate';
733+
Model.graphDeleteProp = '#delete';
732734
Model.jsonAttributes = null;
733735
Model.cloneObjectAttributes = true;
734736
Model.virtualAttributes = null;

lib/model/graph/ModelGraphNode.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,14 @@ class ModelGraphNode {
4646
return this.obj[this.modelClass.dbRefProp];
4747
}
4848

49+
get toBeUnrelated() {
50+
return this.obj[this.modelClass.graphUnrelateProp] === true;
51+
}
52+
53+
get toBeDeleted() {
54+
return this.obj[this.modelClass.graphDeleteProp] === true;
55+
}
56+
4957
get parentNode() {
5058
if (this.parentEdge) {
5159
return this.parentEdge.ownerNode;

lib/queryBuilder/graph/GraphFetcher.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,9 @@ function shouldSelectColumn(column, selects, node) {
141141
!selects.has(column) &&
142142
column !== modelClass.propertyNameToColumnName(modelClass.dbRefProp) &&
143143
column !== modelClass.propertyNameToColumnName(modelClass.uidRefProp) &&
144-
column !== modelClass.propertyNameToColumnName(modelClass.uidProp)
144+
column !== modelClass.propertyNameToColumnName(modelClass.uidProp) &&
145+
column !== modelClass.propertyNameToColumnName(modelClass.graphUnrelateProp) &&
146+
column !== modelClass.propertyNameToColumnName(modelClass.graphDeleteProp)
145147
);
146148
}
147149

lib/queryBuilder/graph/GraphOptions.js

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,10 @@ class GraphOptions {
5151

5252
// Like `shouldRelate` but ignores settings that explicitly disable relate operations.
5353
shouldRelateIgnoreDisable(node, graphData) {
54+
if (node.toBeUnrelated || node.toBeDeleted) {
55+
return false;
56+
}
57+
5458
if (node.isReference || node.isDbReference) {
5559
return true;
5660
}
@@ -71,6 +75,10 @@ class GraphOptions {
7175

7276
// Like `shouldInsert` but ignores settings that explicitly disable insert operations.
7377
shouldInsertIgnoreDisable(node, graphData) {
78+
if (node.toBeUnrelated || node.toBeDeleted) {
79+
return false;
80+
}
81+
7482
return (
7583
!getCurrentNode(node, graphData) &&
7684
!this.shouldRelateIgnoreDisable(node, graphData) &&
@@ -89,6 +97,10 @@ class GraphOptions {
8997
// Like `shouldPatch() || shouldUpdate()` but ignores settings that explicitly disable
9098
// update or patch operations.
9199
shouldPatchOrUpdateIgnoreDisable(node, graphData) {
100+
if (node.toBeUnrelated || node.toBeDeleted) {
101+
return false;
102+
}
103+
92104
if (this.shouldRelate(node, graphData)) {
93105
// We should update all nodes that are going to be related. Note that
94106
// we don't actually update anything unless there is something to update
@@ -121,16 +133,22 @@ class GraphOptions {
121133
}
122134

123135
shouldUnrelate(currentNode, graphData) {
136+
const node = getNode(currentNode, graphData.graph);
137+
if (node && node.toBeUnrelated) return true;
138+
124139
return (
125-
!getNode(currentNode, graphData.graph) &&
140+
!node &&
126141
!this._hasOption(currentNode, NO_UNRELATE) &&
127142
this.shouldUnrelateIgnoreDisable(currentNode)
128143
);
129144
}
130145

131146
shouldDelete(currentNode, graphData) {
147+
const node = getNode(currentNode, graphData.graph);
148+
if (node && node.toBeDeleted) return true;
149+
132150
return (
133-
!getNode(currentNode, graphData.graph) &&
151+
!node &&
134152
!this._hasOption(currentNode, NO_DELETE) &&
135153
!this.shouldUnrelateIgnoreDisable(currentNode)
136154
);

lib/queryBuilder/graph/GraphUpsert.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,8 @@ function checkForNotFoundErrors(graphData, builder) {
207207
for (const node of graph.nodes) {
208208
if (
209209
node.obj.$hasId() &&
210+
!node.toBeUnrelated &&
211+
!node.toBeDeleted &&
210212
!graphOptions.shouldInsertIgnoreDisable(node, graphData) &&
211213
!graphOptions.shouldRelateIgnoreDisable(node, graphData) &&
212214
!currentGraph.nodeForNode(node)

tests/integration/upsertGraph.js

Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1281,6 +1281,229 @@ module.exports = (session) => {
12811281
});
12821282
});
12831283

1284+
it('should respect noDelete flag and special #unrelate and #delete model props', () => {
1285+
const upsert = {
1286+
// the root gets updated because it has an id
1287+
id: 2,
1288+
model1Prop1: 'updated root 2',
1289+
1290+
// unrelate
1291+
model1Relation1: null,
1292+
1293+
// update idCol=1
1294+
// delete idCol=2 with `#delete: true` special prop
1295+
// and insert one new
1296+
model1Relation2: [
1297+
{
1298+
idCol: 1,
1299+
model2Prop1: 'updated hasMany 1',
1300+
1301+
// unrelate id=4 with `#unrelate: true` special prop
1302+
// don't unrelate id=5 because of `noUnrelate`
1303+
// relate id=6
1304+
// and insert one new
1305+
model2Relation1: [
1306+
{
1307+
id: 4,
1308+
'#unrelate': true,
1309+
},
1310+
{
1311+
// This is the new row.
1312+
model1Prop1: 'inserted manyToMany',
1313+
},
1314+
{
1315+
id: 6,
1316+
},
1317+
],
1318+
},
1319+
{
1320+
idCol: 2,
1321+
'#delete': true,
1322+
},
1323+
{
1324+
// This is the new row.
1325+
model2Prop1: 'inserted hasMany',
1326+
},
1327+
],
1328+
};
1329+
1330+
return transaction(session.knex, (trx) => {
1331+
return Model1.query(trx)
1332+
.upsertGraph(upsert, {
1333+
fetchStrategy,
1334+
relate: true,
1335+
noDelete: true,
1336+
unrelate: ['model1Relation1'],
1337+
})
1338+
.then((result) => {
1339+
// Fetch the graph from the database.
1340+
return Model1.query(trx)
1341+
.findById(2)
1342+
.withGraphFetched(
1343+
'[model1Relation1, model1Relation2(orderById).model2Relation1(orderById)]'
1344+
);
1345+
})
1346+
.then(omitIrrelevantProps)
1347+
.then((result) => {
1348+
expect(result).to.eql({
1349+
id: 2,
1350+
model1Id: null,
1351+
model1Prop1: 'updated root 2',
1352+
1353+
model1Relation1: null,
1354+
1355+
model1Relation2: [
1356+
{
1357+
idCol: 1,
1358+
model1Id: 2,
1359+
model2Prop1: 'updated hasMany 1',
1360+
1361+
model2Relation1: [
1362+
{
1363+
id: 5,
1364+
model1Id: null,
1365+
model1Prop1: 'manyToMany 2',
1366+
},
1367+
{
1368+
id: 6,
1369+
model1Id: null,
1370+
model1Prop1: 'manyToMany 3',
1371+
},
1372+
{
1373+
id: 8,
1374+
model1Id: null,
1375+
model1Prop1: 'inserted manyToMany',
1376+
},
1377+
],
1378+
},
1379+
{
1380+
idCol: 3,
1381+
model1Id: 2,
1382+
model2Prop1: 'inserted hasMany',
1383+
model2Relation1: [],
1384+
},
1385+
],
1386+
});
1387+
1388+
return Promise.all([trx('Model1'), trx('model2')]).then(
1389+
([model1Rows, model2Rows]) => {
1390+
// Row 3 should NOT be deleted.
1391+
expect(model1Rows.find((it) => it.id == 3)).to.eql({
1392+
id: 3,
1393+
model1Id: null,
1394+
model1Prop1: 'belongsToOne',
1395+
model1Prop2: null,
1396+
});
1397+
1398+
// Row 4 should NOT be deleted.
1399+
expect(model1Rows.find((it) => it.id == 4)).to.eql({
1400+
id: 4,
1401+
model1Id: null,
1402+
model1Prop1: 'manyToMany 1',
1403+
model1Prop2: null,
1404+
});
1405+
1406+
// Row 2 should be deleted.
1407+
expect(model2Rows.find((it) => it.id_col == 2)).to.equal(undefined);
1408+
}
1409+
);
1410+
});
1411+
});
1412+
});
1413+
1414+
it('should not fail when trying to #unrelate or #delete a model which either does not exist or is linked elsewhere', () => {
1415+
const upsert = {
1416+
id: 2,
1417+
model1Relation2: [
1418+
{
1419+
idCol: 1,
1420+
model2Relation1: [
1421+
// ignore unrelating/deleting models which are linked to a different base model
1422+
// id 6&7 are linked to model1Relation2.idCol=2
1423+
{ id: 6, '#unrelate': true },
1424+
{ id: 7, '#delete': true },
1425+
// ignore unrelating/deleting non-existing models
1426+
{ id: 900, '#unrelate': true },
1427+
{ id: 901, '#delete': true },
1428+
// add new model
1429+
{ model1Prop1: 'inserted manyToMany' },
1430+
],
1431+
},
1432+
// ignore delete (non-existing) idCol=3 with `#delete: true` special prop
1433+
// ignore unrelate (non-existing) idCol=4 with `#unrelate: true` special prop
1434+
{ idCol: 3, '#delete': true },
1435+
{ idCol: 4, '#unrelate': true },
1436+
],
1437+
};
1438+
1439+
return transaction(session.knex, (trx) => {
1440+
return Model1.query(trx)
1441+
.upsertGraph(upsert, {
1442+
fetchStrategy,
1443+
relate: true,
1444+
noDelete: true,
1445+
})
1446+
.then((result) => {
1447+
// Fetch the graph from the database.
1448+
return Model1.query(trx)
1449+
.findById(2)
1450+
.withGraphFetched('[model1Relation2(orderById).model2Relation1(orderById)]');
1451+
})
1452+
.then(omitIrrelevantProps)
1453+
.then((result) => {
1454+
expect(result).to.eql({
1455+
id: 2,
1456+
model1Id: 3,
1457+
model1Prop1: 'root 2',
1458+
1459+
model1Relation2: [
1460+
{
1461+
idCol: 1,
1462+
model1Id: 2,
1463+
model2Prop1: 'hasMany 1',
1464+
1465+
model2Relation1: [
1466+
{
1467+
id: 4,
1468+
model1Id: null,
1469+
model1Prop1: 'manyToMany 1',
1470+
},
1471+
{
1472+
id: 5,
1473+
model1Id: null,
1474+
model1Prop1: 'manyToMany 2',
1475+
},
1476+
{
1477+
id: 8,
1478+
model1Id: null,
1479+
model1Prop1: 'inserted manyToMany',
1480+
},
1481+
],
1482+
},
1483+
{
1484+
idCol: 2,
1485+
model1Id: 2,
1486+
model2Prop1: 'hasMany 2',
1487+
1488+
model2Relation1: [
1489+
{
1490+
id: 6,
1491+
model1Id: null,
1492+
model1Prop1: 'manyToMany 3',
1493+
},
1494+
{
1495+
id: 7,
1496+
model1Id: null,
1497+
model1Prop1: 'manyToMany 4',
1498+
},
1499+
],
1500+
},
1501+
],
1502+
});
1503+
});
1504+
});
1505+
});
1506+
12841507
it('should relate and unrelate some models if `unrelate` and `relate` are arrays of relation paths', () => {
12851508
const upsert = {
12861509
// the root gets updated because it has an id

typings/objection/index.d.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1434,6 +1434,8 @@ declare namespace Objection {
14341434
uidRefProp: string;
14351435
dbRefProp: string;
14361436
propRefRegex: RegExp;
1437+
graphUnrelateProp: string;
1438+
graphDeleteProp: string;
14371439
pickJsonSchemaProperties: boolean;
14381440
relatedFindQueryMutates: boolean;
14391441
relatedInsertQueryMutates: boolean;
@@ -1540,6 +1542,8 @@ declare namespace Objection {
15401542
static uidRefProp: string;
15411543
static dbRefProp: string;
15421544
static propRefRegex: RegExp;
1545+
static graphUnrelateProp: string;
1546+
static graphDeleteProp: string;
15431547
static pickJsonSchemaProperties: boolean;
15441548
static relatedFindQueryMutates: boolean;
15451549
static relatedInsertQueryMutates: boolean;

0 commit comments

Comments
 (0)