Skip to content

Commit eefda36

Browse files
committed
Refactor PeriodicReport service/repo layers
1 parent d37dd5e commit eefda36

File tree

3 files changed

+153
-132
lines changed

3 files changed

+153
-132
lines changed

src/components/periodic-report/dto/periodic-report.dto.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ class PeriodicReport extends Resource {
4444
@Field()
4545
readonly skippedReason: SecuredStringNullable;
4646

47-
readonly reportFile: DefinedFile;
47+
readonly reportFile: DefinedFile; //TODO? - Secured<LinkTo<'File'> | null>
4848

4949
@SensitivityField({
5050
description: "Based on the project's sensitivity",

src/components/periodic-report/periodic-report.repository.ts

Lines changed: 129 additions & 109 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,14 @@ import {
1111
} from 'cypher-query-builder';
1212
import {
1313
type CalendarDate,
14+
CreationFailed,
1415
generateId,
1516
type ID,
17+
NotFoundException,
1618
type Range,
1719
type UnsecuredDto,
1820
} from '~/common';
1921
import { DtoRepository } from '~/core/database';
20-
import { type ChangesOf } from '~/core/database/changes';
2122
import {
2223
ACTIVE,
2324
createNode,
@@ -65,120 +66,139 @@ export class PeriodicReportRepository extends DtoRepository<
6566
}
6667

6768
async merge(input: MergePeriodicReports) {
68-
const Report = resolveReportType(input);
69+
try {
70+
const Report = resolveReportType(input);
6971

70-
// Create IDs here that will feed into the reports that are new.
71-
// If only neo4j had a nanoid generator natively.
72-
const intervals = await Promise.all(
73-
input.intervals.map(async (interval) => ({
74-
tempId: await generateId(),
75-
start: interval.start,
76-
end: interval.end,
77-
tempFileId: await generateId(),
78-
})),
79-
);
72+
// Create IDs here that will feed into the reports that are new.
73+
// If only neo4j had a nanoid generator natively.
74+
const intervals = await Promise.all(
75+
input.intervals.map(async (interval) => ({
76+
tempId: await generateId(),
77+
start: interval.start,
78+
end: interval.end,
79+
tempFileId: await generateId(),
80+
})),
81+
);
8082

81-
const isProgress = input.type === ReportType.Progress;
82-
const extraCreateOptions = isProgress
83-
? this.progressRepo.getCreateOptions(input)
84-
: {};
83+
const isProgress = input.type === ReportType.Progress;
84+
const extraCreateOptions = isProgress
85+
? this.progressRepo.getCreateOptions(input)
86+
: {};
8587

86-
const query = this.db
87-
.query()
88-
// before interval list, so it's the same time across reports
89-
.with('datetime() as now')
90-
.matchNode('parent', 'BaseNode', { id: input.parent })
91-
.unwind(intervals, 'interval')
92-
.comment('Stop processing this row if the report already exists')
93-
.subQuery('parent, interval', (sub) =>
94-
sub
95-
.match([
96-
[
97-
node('parent'),
98-
relation('out', '', 'report', ACTIVE),
99-
node('node', `${input.type}Report`),
100-
],
101-
[
102-
node('node'),
103-
relation('out', '', 'start', ACTIVE),
104-
node('', 'Property', { value: variable('interval.start') }),
105-
],
106-
[
107-
node('node'),
108-
relation('out', '', 'end', ACTIVE),
109-
node('', 'Property', { value: variable('interval.end') }),
110-
],
111-
])
112-
// Invert zero rows into one row
113-
// We want to continue out of this sub-query having 1 row when
114-
// the report doesn't exist.
115-
// However, the match above gives us zero rows in this case.
116-
// Use count() to get us back to 1 row, and to create a temp list
117-
// of how many rows we want (0 if report exists, 1 if it doesn't).
118-
// Then use UNWIND to convert this list into rows.
119-
.with('CASE WHEN count(node) = 0 THEN [true] ELSE [] END as rows')
120-
.raw('UNWIND rows as row')
121-
// nonsense value, the 1 row returned is what is important, not this column
122-
.return('true as itIsNew'),
123-
)
124-
.apply(
125-
await createNode(Report as typeof IPeriodicReport, {
126-
baseNodeProps: {
127-
id: variable('interval.tempId'),
128-
createdAt: variable('now'),
129-
...extraCreateOptions.baseNodeProps,
130-
},
131-
initialProps: {
132-
type: input.type,
133-
start: variable('interval.start'),
134-
end: variable('interval.end'),
135-
skippedReason: null,
136-
receivedDate: null,
137-
reportFile: variable('interval.tempFileId'),
138-
...extraCreateOptions.initialProps,
139-
},
140-
}),
141-
)
142-
.apply(
143-
createRelationships(Report, 'in', {
144-
report: variable('parent'),
145-
}),
146-
)
147-
.apply(isProgress ? this.progressRepo.amendAfterCreateNode() : undefined)
148-
// rename node to report, so we can call create node again for the file
149-
.with('now, interval, node as report')
150-
.apply(
151-
await createNode(File, {
152-
initialProps: {
153-
name: variable('apoc.temporal.format(interval.end, "date")'),
154-
},
155-
baseNodeProps: {
156-
id: variable('interval.tempFileId'),
157-
createdAt: variable('now'),
158-
},
159-
}),
160-
)
161-
.apply(
162-
createRelationships(File, {
163-
in: { reportFileNode: variable('report') },
164-
out: { createdBy: currentUser },
165-
}),
166-
)
167-
.return<{ id: ID; interval: Range<CalendarDate> }>(
168-
'report.id as id, interval',
169-
);
170-
return await query.run();
88+
const query = this.db
89+
.query()
90+
// before interval list, so it's the same time across reports
91+
.with('datetime() as now')
92+
.matchNode('parent', 'BaseNode', { id: input.parent })
93+
.unwind(intervals, 'interval')
94+
.comment('Stop processing this row if the report already exists')
95+
.subQuery('parent, interval', (sub) =>
96+
sub
97+
.match([
98+
[
99+
node('parent'),
100+
relation('out', '', 'report', ACTIVE),
101+
node('node', `${input.type}Report`),
102+
],
103+
[
104+
node('node'),
105+
relation('out', '', 'start', ACTIVE),
106+
node('', 'Property', { value: variable('interval.start') }),
107+
],
108+
[
109+
node('node'),
110+
relation('out', '', 'end', ACTIVE),
111+
node('', 'Property', { value: variable('interval.end') }),
112+
],
113+
])
114+
// Invert zero rows into one row
115+
// We want to continue out of this sub-query having 1 row when
116+
// the report doesn't exist.
117+
// However, the match above gives us zero rows in this case.
118+
// Use count() to get us back to 1 row, and to create a temp list
119+
// of how many rows we want (0 if report exists, 1 if it doesn't).
120+
// Then use UNWIND to convert this list into rows.
121+
.with('CASE WHEN count(node) = 0 THEN [true] ELSE [] END as rows')
122+
.raw('UNWIND rows as row')
123+
// nonsense value, the 1 row returned is what is important, not this column
124+
.return('true as itIsNew'),
125+
)
126+
.apply(
127+
await createNode(Report as typeof IPeriodicReport, {
128+
baseNodeProps: {
129+
id: variable('interval.tempId'),
130+
createdAt: variable('now'),
131+
...extraCreateOptions.baseNodeProps,
132+
},
133+
initialProps: {
134+
type: input.type,
135+
start: variable('interval.start'),
136+
end: variable('interval.end'),
137+
skippedReason: null,
138+
receivedDate: null,
139+
reportFile: variable('interval.tempFileId'),
140+
...extraCreateOptions.initialProps,
141+
},
142+
}),
143+
)
144+
.apply(
145+
createRelationships(Report, 'in', {
146+
report: variable('parent'),
147+
}),
148+
)
149+
.apply(
150+
isProgress ? this.progressRepo.amendAfterCreateNode() : undefined,
151+
)
152+
// rename node to report, so we can call create node again for the file
153+
.with('now, interval, node as report')
154+
.apply(
155+
await createNode(File, {
156+
initialProps: {
157+
name: variable('apoc.temporal.format(interval.end, "date")'),
158+
},
159+
baseNodeProps: {
160+
id: variable('interval.tempFileId'),
161+
createdAt: variable('now'),
162+
},
163+
}),
164+
)
165+
.apply(
166+
createRelationships(File, {
167+
in: { reportFileNode: variable('report') },
168+
out: { createdBy: currentUser },
169+
}),
170+
)
171+
.return<{ id: ID; interval: Range<CalendarDate> }>(
172+
'report.id as id, interval',
173+
);
174+
175+
return await query.run();
176+
} catch (exception) {
177+
const Report = resolveReportType({ type: input.type });
178+
throw new CreationFailed(Report);
179+
}
171180
}
172181

173-
async update<T extends PeriodicReport | UnsecuredDto<PeriodicReport>>(
174-
existing: T,
175-
simpleChanges: Omit<
176-
ChangesOf<PeriodicReport, UpdatePeriodicReportInput>,
177-
'reportFile'
178-
> &
179-
Partial<Pick<PeriodicReport, 'start' | 'end'>>,
182+
async update(
183+
changes: Omit<UpdatePeriodicReportInput, 'reportFile'> &
184+
Pick<PeriodicReport, 'start' | 'end'>,
180185
) {
181-
return await this.updateProperties(existing, simpleChanges);
186+
const { id, ...simpleChanges } = changes;
187+
188+
await this.updateProperties({ id }, simpleChanges);
189+
190+
return await this.readOne(id);
191+
}
192+
193+
async readOne(id: ID) {
194+
if (!id) {
195+
throw new NotFoundException(
196+
'No periodic report id to search for',
197+
'periodicReport.id',
198+
);
199+
}
200+
201+
return await super.readOne(id);
182202
}
183203

184204
async list(input: PeriodicReportListInput) {

src/components/periodic-report/periodic-report.service.ts

Lines changed: 23 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
11
import { Injectable } from '@nestjs/common';
22
import {
33
type CalendarDate,
4-
CreationFailed,
54
DateInterval,
65
type ID,
7-
NotFoundException,
86
type ObjectView,
97
type Range,
108
type UnsecuredDto,
@@ -43,20 +41,15 @@ export class PeriodicReportService {
4341
if (input.intervals.length === 0) {
4442
return;
4543
}
46-
try {
47-
const result = await this.repo.merge(input);
48-
this.logger.info(`Merged ${input.type.toLowerCase()} reports`, {
49-
existing: input.intervals.length - result.length,
50-
new: result.length,
51-
parent: input.parent,
52-
newIntervals: result.map(({ interval }) =>
53-
DateInterval.fromObject(interval).toISO(),
54-
),
55-
});
56-
} catch (exception) {
57-
const Report = resolveReportType({ type: input.type });
58-
throw new CreationFailed(Report);
59-
}
44+
const result = await this.repo.merge(input);
45+
this.logger.info(`Merged ${input.type.toLowerCase()} reports`, {
46+
existing: input.intervals.length - result.length,
47+
new: result.length,
48+
parent: input.parent,
49+
newIntervals: result.map(({ interval }) =>
50+
DateInterval.fromObject(interval).toISO(),
51+
),
52+
});
6053
}
6154

6255
async update(input: UpdatePeriodicReportInput) {
@@ -69,11 +62,22 @@ export class PeriodicReportService {
6962

7063
const { reportFile, ...simpleChanges } = changes;
7164

72-
const updated = await this.repo.update(current, simpleChanges);
65+
const updated = this.secure(
66+
await this.repo.update(
67+
{
68+
id: current.id,
69+
start: current.start,
70+
end: current.end,
71+
...simpleChanges,
72+
},
73+
session,
74+
),
75+
session,
76+
);
7377

7478
if (reportFile) {
7579
const file = await this.files.updateDefinedFile(
76-
current.reportFile,
80+
this.secure(current, session).reportFile,
7781
'file',
7882
reportFile,
7983
);
@@ -203,10 +207,7 @@ export class PeriodicReportService {
203207
// no change
204208
return;
205209
}
206-
await this.repo.update(report, {
207-
start: at,
208-
end: at,
209-
});
210+
await this.repo.update({ id: report.id, start: at, end: at }, session);
210211
} else {
211212
await this.merge({
212213
intervals: [{ start: at, end: at }],

0 commit comments

Comments
 (0)