Skip to content

Commit a010d59

Browse files
authored
Add abstraction to update one-to-many relations (#2892)
1 parent 93aafd0 commit a010d59

File tree

4 files changed

+120
-4
lines changed

4 files changed

+120
-4
lines changed

src/core/database/common.repository.ts

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,19 @@ import { Inject, Injectable, Optional } from '@nestjs/common';
22
import { node, relation } from 'cypher-query-builder';
33
import { DateTime } from 'luxon';
44
import {
5+
EnhancedResource,
56
getDbClassLabels,
67
getDbPropertyUnique,
78
ID,
89
isIdLike,
10+
NotFoundException,
911
ResourceShape,
1012
ServerException,
1113
Session,
1214
} from '../../common';
1315
import { DatabaseService } from './database.service';
1416
import { createUniqueConstraint } from './indexer';
15-
import { ACTIVE } from './query';
17+
import { ACTIVE, updateRelationList } from './query';
1618
import { BaseNode } from './results';
1719

1820
/**
@@ -90,6 +92,33 @@ export class CommonRepository {
9092
.run();
9193
}
9294

95+
async updateRelationList({
96+
id,
97+
label,
98+
relation,
99+
newList,
100+
}: {
101+
id: ID;
102+
label?: string | EnhancedResource<any> | ResourceShape<any>;
103+
relation: string;
104+
newList: readonly ID[];
105+
}) {
106+
const resolvedLabel = !label
107+
? 'BaseNode'
108+
: typeof label === 'string'
109+
? label
110+
: EnhancedResource.of(label).dbLabel;
111+
const node = await this.db
112+
.query()
113+
.matchNode('node', resolvedLabel, { id })
114+
.apply(updateRelationList({ relation, newList }))
115+
.return('node')
116+
.first();
117+
if (!node) {
118+
throw new NotFoundException();
119+
}
120+
}
121+
93122
async checkDeletePermission(id: ID, session: Session | ID) {
94123
return await this.db.checkDeletePermission(id, session);
95124
}

src/core/database/dto.repository.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import { LazyGetter as Once } from 'lazy-get-decorator';
44
import { lowerCase } from 'lodash';
55
import {
66
EnhancedResource,
7-
getDbClassLabels,
87
getDbPropertyUnique,
98
ID,
109
NotFoundException,
@@ -93,7 +92,7 @@ export const DtoRepository = <
9392
async readMany(ids: readonly ID[], ...args: HydrateArgs) {
9493
return await this.db
9594
.query()
96-
.matchNode('node', getDbClassLabels(resource)[0])
95+
.matchNode('node', this.resource.dbLabel)
9796
.where({ 'node.id': inArray(ids) })
9897
.apply(this.hydrate(...args))
9998
.map('dto')
@@ -124,10 +123,19 @@ export const DtoRepository = <
124123
otherLabel,
125124
id,
126125
otherId,
127-
getDbClassLabels(resource)[0],
126+
this.resource.dbLabel,
128127
);
129128
}
130129

130+
async updateRelationList(
131+
options: Parameters<CommonRepository['updateRelationList']>[0],
132+
) {
133+
await super.updateRelationList({
134+
label: resource,
135+
...options,
136+
});
137+
}
138+
131139
/**
132140
* Given a `(node:TBaseNode)` output `dto` as `UnsecuredDto<TResource>`
133141
*

src/core/database/query/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ export {
77
defaultPermanentAfter,
88
} from './properties/update-property';
99
export * from './properties/update-properties';
10+
export * from './properties/update-relation-list';
1011
export * from './create-relationships';
1112
export * from './cypher-expression';
1213
export * from './cypher-functions';
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import { node, Query, relation } from 'cypher-query-builder';
2+
import { DateTime } from 'luxon';
3+
import { ID } from '~/common';
4+
import { exp, variable, Variable } from '../index';
5+
6+
export interface UpdateRelationListOptions {
7+
node?: Variable;
8+
relation: string | Variable;
9+
newList: readonly ID[] | Variable;
10+
}
11+
12+
export const updateRelationList =
13+
(options: UpdateRelationListOptions) =>
14+
<R>(query: Query<R>) => {
15+
const nodeName = options.node ?? variable('node');
16+
const now = query.params.addParam(DateTime.now(), 'now');
17+
const relName =
18+
options.relation instanceof Variable
19+
? options.relation
20+
: variable(
21+
query.params.addParam(options.relation, 'relationName').toString(),
22+
);
23+
const newList =
24+
options.newList instanceof Variable
25+
? options.newList
26+
: variable(
27+
query.params.addParam(options.newList, 'newList').toString(),
28+
);
29+
30+
query.comment(
31+
`updateListProperty(${String(nodeName)}, ${String(relName)})`,
32+
);
33+
return query.subQuery([nodeName, relName, newList], (sub) =>
34+
sub
35+
.with([
36+
`${String(nodeName)} as parent`,
37+
`${String(relName)} as relName`,
38+
`${String(newList)} as childIds`,
39+
])
40+
.subQuery(['parent', 'relName', 'childIds'], (sub) =>
41+
sub
42+
.match([
43+
node('parent'),
44+
relation('out', 'r'), // ACTIVE
45+
node('child', 'BaseNode'),
46+
])
47+
.raw(`where type(r) = relName and not child.id in childIds`)
48+
.delete('r')
49+
// soft delete, cant usage without "permanentAfter" logic
50+
// .setVariables({
51+
// 'r.active': 'false',
52+
// 'r.deletedAt': String(now),
53+
// })
54+
.return('count(child) as deletedCount'),
55+
)
56+
.subQuery(['parent', 'relName', 'childIds'], (q) =>
57+
q
58+
.raw('unwind childIds as childId')
59+
.matchNode('child', 'BaseNode', { id: variable('childId') })
60+
.raw(
61+
`call apoc.merge.relationship(
62+
parent,
63+
relName,
64+
{}, // { active: true },
65+
{ createdAt: ${String(now)} },
66+
child
67+
) yield rel`,
68+
)
69+
.return('count(child) as totalCount'),
70+
)
71+
.return(
72+
exp({
73+
deletedCount: 'deletedCount',
74+
totalCount: 'totalCount',
75+
}).as('stats'),
76+
),
77+
);
78+
};

0 commit comments

Comments
 (0)