Skip to content

Commit 4aeaf58

Browse files
committed
Refactor PeriodicReport service/repo layers
1 parent 10f0c8c commit 4aeaf58

File tree

3 files changed

+155
-145
lines changed

3 files changed

+155
-145
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
@@ -49,7 +49,7 @@ class PeriodicReport extends Resource {
4949
@Field()
5050
readonly skippedReason: SecuredStringNullable;
5151

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

5454
@SensitivityField({
5555
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
@@ -13,12 +13,13 @@ import {
1313
CalendarDate,
1414
generateId,
1515
ID,
16+
NotFoundException,
1617
Range,
18+
ServerException,
1719
Session,
1820
UnsecuredDto,
1921
} from '~/common';
2022
import { DtoRepository } from '~/core/database';
21-
import { ChangesOf } from '~/core/database/changes';
2223
import {
2324
ACTIVE,
2425
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: ['User', input.session.userId] },
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: ['User', input.session.userId] },
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+
throw new ServerException('Could not create periodic reports', exception);
178+
}
171179
}
172180

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'>>,
181+
async update(
182+
changes: Omit<UpdatePeriodicReportInput, 'reportFile'> &
183+
Pick<PeriodicReport, 'start' | 'end'>,
184+
session: Session,
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, session);
191+
}
192+
193+
async readOne(id: ID, session: Session) {
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, session);
182202
}
183203

184204
async list(input: PeriodicReportListInput, session: Session) {

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

Lines changed: 25 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,8 @@ import {
33
CalendarDate,
44
DateInterval,
55
ID,
6-
NotFoundException,
76
ObjectView,
87
Range,
9-
ServerException,
108
Session,
119
UnsecuredDto,
1210
} from '~/common';
@@ -44,36 +42,42 @@ export class PeriodicReportService {
4442
if (input.intervals.length === 0) {
4543
return;
4644
}
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-
throw new ServerException('Could not create periodic reports', exception);
59-
}
45+
const result = await this.repo.merge(input);
46+
this.logger.info(`Merged ${input.type.toLowerCase()} reports`, {
47+
existing: input.intervals.length - result.length,
48+
new: result.length,
49+
parent: input.parent,
50+
newIntervals: result.map(({ interval }) =>
51+
DateInterval.fromObject(interval).toISO(),
52+
),
53+
});
6054
}
6155

6256
async update(input: UpdatePeriodicReportInput, session: Session) {
63-
const currentRaw = await this.repo.readOne(input.id, session);
64-
const current = this.secure(currentRaw, session);
57+
const current = await this.repo.readOne(input.id, session);
6558
const changes = this.repo.getActualChanges(current, input);
6659
this.privileges
67-
.for(session, resolveReportType(current), currentRaw)
60+
.for(session, resolveReportType(current), current)
6861
.verifyChanges(changes);
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
session,
@@ -96,17 +100,6 @@ export class PeriodicReportService {
96100
session: Session,
97101
_view?: ObjectView,
98102
): Promise<PeriodicReport> {
99-
this.logger.debug(`read one`, {
100-
id,
101-
userId: session.userId,
102-
});
103-
if (!id) {
104-
throw new NotFoundException(
105-
'No periodic report id to search for',
106-
'periodicReport.id',
107-
);
108-
}
109-
110103
const result = await this.repo.readOne(id, session);
111104
return this.secure(result, session);
112105
}
@@ -232,10 +225,7 @@ export class PeriodicReportService {
232225
// no change
233226
return;
234227
}
235-
await this.repo.update(report, {
236-
start: at,
237-
end: at,
238-
});
228+
await this.repo.update({ id: report.id, start: at, end: at }, session);
239229
} else {
240230
await this.merge({
241231
intervals: [{ start: at, end: at }],

0 commit comments

Comments
 (0)