Skip to content

Commit 298092b

Browse files
committed
Refactor PeriodicReport service/repo layers
1 parent 373df4e commit 298092b

File tree

3 files changed

+153
-131
lines changed

3 files changed

+153
-131
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, exception);
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 & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { Injectable } from '@nestjs/common';
22
import {
33
type CalendarDate,
4-
CreationFailed,
54
DateInterval,
65
type ID,
76
type ObjectView,
@@ -44,20 +43,15 @@ export class PeriodicReportService {
4443
if (input.intervals.length === 0) {
4544
return;
4645
}
47-
try {
48-
const result = await this.repo.merge(input);
49-
this.logger.info(`Merged ${input.type.toLowerCase()} reports`, {
50-
existing: input.intervals.length - result.length,
51-
new: result.length,
52-
parent: input.parent,
53-
newIntervals: result.map(({ interval }) =>
54-
DateInterval.fromObject(interval).toISO(),
55-
),
56-
});
57-
} catch (exception) {
58-
const Report = resolveReportType({ type: input.type });
59-
throw new CreationFailed(Report);
60-
}
46+
const result = await this.repo.merge(input);
47+
this.logger.info(`Merged ${input.type.toLowerCase()} reports`, {
48+
existing: input.intervals.length - result.length,
49+
new: result.length,
50+
parent: input.parent,
51+
newIntervals: result.map(({ interval }) =>
52+
DateInterval.fromObject(interval).toISO(),
53+
),
54+
});
6155
}
6256

6357
async update(input: UpdatePeriodicReportInput) {
@@ -70,11 +64,22 @@ export class PeriodicReportService {
7064

7165
const { reportFile, ...simpleChanges } = changes;
7266

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

7580
if (reportFile) {
7681
const file = await this.files.updateDefinedFile(
77-
current.reportFile,
82+
this.secure(current, session).reportFile,
7883
'file',
7984
reportFile,
8085
);
@@ -197,10 +202,7 @@ export class PeriodicReportService {
197202
// no change
198203
return;
199204
}
200-
await this.repo.update(report, {
201-
start: at,
202-
end: at,
203-
});
205+
await this.repo.update({ id: report.id, start: at, end: at }, session);
204206
} else {
205207
await this.merge({
206208
intervals: [{ start: at, end: at }],

0 commit comments

Comments
 (0)