Skip to content

Commit

Permalink
Refactor PeriodicReport service/repo layers
Browse files Browse the repository at this point in the history
  • Loading branch information
bryanjnelson committed Jul 2, 2024
1 parent 10f0c8c commit 4aeaf58
Show file tree
Hide file tree
Showing 3 changed files with 155 additions and 145 deletions.
2 changes: 1 addition & 1 deletion src/components/periodic-report/dto/periodic-report.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ class PeriodicReport extends Resource {
@Field()
readonly skippedReason: SecuredStringNullable;

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

@SensitivityField({
description: "Based on the project's sensitivity",
Expand Down
238 changes: 129 additions & 109 deletions src/components/periodic-report/periodic-report.repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,13 @@ import {
CalendarDate,
generateId,
ID,
NotFoundException,
Range,
ServerException,
Session,
UnsecuredDto,
} from '~/common';
import { DtoRepository } from '~/core/database';
import { ChangesOf } from '~/core/database/changes';
import {
ACTIVE,
createNode,
Expand Down Expand Up @@ -65,120 +66,139 @@ export class PeriodicReportRepository extends DtoRepository<
}

async merge(input: MergePeriodicReports) {
const Report = resolveReportType(input);
try {
const Report = resolveReportType(input);

// Create IDs here that will feed into the reports that are new.
// If only neo4j had a nanoid generator natively.
const intervals = await Promise.all(
input.intervals.map(async (interval) => ({
tempId: await generateId(),
start: interval.start,
end: interval.end,
tempFileId: await generateId(),
})),
);
// Create IDs here that will feed into the reports that are new.
// If only neo4j had a nanoid generator natively.
const intervals = await Promise.all(
input.intervals.map(async (interval) => ({
tempId: await generateId(),
start: interval.start,
end: interval.end,
tempFileId: await generateId(),
})),
);

const isProgress = input.type === ReportType.Progress;
const extraCreateOptions = isProgress
? this.progressRepo.getCreateOptions(input)
: {};
const isProgress = input.type === ReportType.Progress;
const extraCreateOptions = isProgress
? this.progressRepo.getCreateOptions(input)
: {};

const query = this.db
.query()
// before interval list, so it's the same time across reports
.with('datetime() as now')
.matchNode('parent', 'BaseNode', { id: input.parent })
.unwind(intervals, 'interval')
.comment('Stop processing this row if the report already exists')
.subQuery('parent, interval', (sub) =>
sub
.match([
[
node('parent'),
relation('out', '', 'report', ACTIVE),
node('node', `${input.type}Report`),
],
[
node('node'),
relation('out', '', 'start', ACTIVE),
node('', 'Property', { value: variable('interval.start') }),
],
[
node('node'),
relation('out', '', 'end', ACTIVE),
node('', 'Property', { value: variable('interval.end') }),
],
])
// Invert zero rows into one row
// We want to continue out of this sub-query having 1 row when
// the report doesn't exist.
// However, the match above gives us zero rows in this case.
// Use count() to get us back to 1 row, and to create a temp list
// of how many rows we want (0 if report exists, 1 if it doesn't).
// Then use UNWIND to convert this list into rows.
.with('CASE WHEN count(node) = 0 THEN [true] ELSE [] END as rows')
.raw('UNWIND rows as row')
// nonsense value, the 1 row returned is what is important, not this column
.return('true as itIsNew'),
)
.apply(
await createNode(Report as typeof IPeriodicReport, {
baseNodeProps: {
id: variable('interval.tempId'),
createdAt: variable('now'),
...extraCreateOptions.baseNodeProps,
},
initialProps: {
type: input.type,
start: variable('interval.start'),
end: variable('interval.end'),
skippedReason: null,
receivedDate: null,
reportFile: variable('interval.tempFileId'),
...extraCreateOptions.initialProps,
},
}),
)
.apply(
createRelationships(Report, 'in', {
report: variable('parent'),
}),
)
.apply(isProgress ? this.progressRepo.amendAfterCreateNode() : undefined)
// rename node to report, so we can call create node again for the file
.with('now, interval, node as report')
.apply(
await createNode(File, {
initialProps: {
name: variable('apoc.temporal.format(interval.end, "date")'),
},
baseNodeProps: {
id: variable('interval.tempFileId'),
createdAt: variable('now'),
},
}),
)
.apply(
createRelationships(File, {
in: { reportFileNode: variable('report') },
out: { createdBy: ['User', input.session.userId] },
}),
)
.return<{ id: ID; interval: Range<CalendarDate> }>(
'report.id as id, interval',
);
return await query.run();
const query = this.db
.query()
// before interval list, so it's the same time across reports
.with('datetime() as now')
.matchNode('parent', 'BaseNode', { id: input.parent })
.unwind(intervals, 'interval')
.comment('Stop processing this row if the report already exists')
.subQuery('parent, interval', (sub) =>
sub
.match([
[
node('parent'),
relation('out', '', 'report', ACTIVE),
node('node', `${input.type}Report`),
],
[
node('node'),
relation('out', '', 'start', ACTIVE),
node('', 'Property', { value: variable('interval.start') }),
],
[
node('node'),
relation('out', '', 'end', ACTIVE),
node('', 'Property', { value: variable('interval.end') }),
],
])
// Invert zero rows into one row
// We want to continue out of this sub-query having 1 row when
// the report doesn't exist.
// However, the match above gives us zero rows in this case.
// Use count() to get us back to 1 row, and to create a temp list
// of how many rows we want (0 if report exists, 1 if it doesn't).
// Then use UNWIND to convert this list into rows.
.with('CASE WHEN count(node) = 0 THEN [true] ELSE [] END as rows')
.raw('UNWIND rows as row')
// nonsense value, the 1 row returned is what is important, not this column
.return('true as itIsNew'),
)
.apply(
await createNode(Report as typeof IPeriodicReport, {
baseNodeProps: {
id: variable('interval.tempId'),
createdAt: variable('now'),
...extraCreateOptions.baseNodeProps,
},
initialProps: {
type: input.type,
start: variable('interval.start'),
end: variable('interval.end'),
skippedReason: null,
receivedDate: null,
reportFile: variable('interval.tempFileId'),
...extraCreateOptions.initialProps,
},
}),
)
.apply(
createRelationships(Report, 'in', {
report: variable('parent'),
}),
)
.apply(
isProgress ? this.progressRepo.amendAfterCreateNode() : undefined,
)
// rename node to report, so we can call create node again for the file
.with('now, interval, node as report')
.apply(
await createNode(File, {
initialProps: {
name: variable('apoc.temporal.format(interval.end, "date")'),
},
baseNodeProps: {
id: variable('interval.tempFileId'),
createdAt: variable('now'),
},
}),
)
.apply(
createRelationships(File, {
in: { reportFileNode: variable('report') },
out: { createdBy: ['User', input.session.userId] },
}),
)
.return<{ id: ID; interval: Range<CalendarDate> }>(
'report.id as id, interval',
);

return await query.run();
} catch (exception) {
throw new ServerException('Could not create periodic reports', exception);
}
}

async update<T extends PeriodicReport | UnsecuredDto<PeriodicReport>>(
existing: T,
simpleChanges: Omit<
ChangesOf<PeriodicReport, UpdatePeriodicReportInput>,
'reportFile'
> &
Partial<Pick<PeriodicReport, 'start' | 'end'>>,
async update(
changes: Omit<UpdatePeriodicReportInput, 'reportFile'> &
Pick<PeriodicReport, 'start' | 'end'>,
session: Session,
) {
return await this.updateProperties(existing, simpleChanges);
const { id, ...simpleChanges } = changes;

await this.updateProperties({ id }, simpleChanges);

return await this.readOne(id, session);
}

async readOne(id: ID, session: Session) {
if (!id) {
throw new NotFoundException(
'No periodic report id to search for',
'periodicReport.id',
);
}

return await super.readOne(id, session);
}

async list(input: PeriodicReportListInput, session: Session) {
Expand Down
60 changes: 25 additions & 35 deletions src/components/periodic-report/periodic-report.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,8 @@ import {
CalendarDate,
DateInterval,
ID,
NotFoundException,
ObjectView,
Range,
ServerException,
Session,
UnsecuredDto,
} from '~/common';
Expand Down Expand Up @@ -44,36 +42,42 @@ export class PeriodicReportService {
if (input.intervals.length === 0) {
return;
}
try {
const result = await this.repo.merge(input);
this.logger.info(`Merged ${input.type.toLowerCase()} reports`, {
existing: input.intervals.length - result.length,
new: result.length,
parent: input.parent,
newIntervals: result.map(({ interval }) =>
DateInterval.fromObject(interval).toISO(),
),
});
} catch (exception) {
throw new ServerException('Could not create periodic reports', exception);
}
const result = await this.repo.merge(input);
this.logger.info(`Merged ${input.type.toLowerCase()} reports`, {
existing: input.intervals.length - result.length,
new: result.length,
parent: input.parent,
newIntervals: result.map(({ interval }) =>
DateInterval.fromObject(interval).toISO(),
),
});
}

async update(input: UpdatePeriodicReportInput, session: Session) {
const currentRaw = await this.repo.readOne(input.id, session);
const current = this.secure(currentRaw, session);
const current = await this.repo.readOne(input.id, session);
const changes = this.repo.getActualChanges(current, input);
this.privileges
.for(session, resolveReportType(current), currentRaw)
.for(session, resolveReportType(current), current)
.verifyChanges(changes);

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

const updated = await this.repo.update(current, simpleChanges);
const updated = this.secure(
await this.repo.update(
{
id: current.id,
start: current.start,
end: current.end,
...simpleChanges,
},
session,
),
session,
);

if (reportFile) {
const file = await this.files.updateDefinedFile(
current.reportFile,
this.secure(current, session).reportFile,
'file',
reportFile,
session,
Expand All @@ -96,17 +100,6 @@ export class PeriodicReportService {
session: Session,
_view?: ObjectView,
): Promise<PeriodicReport> {
this.logger.debug(`read one`, {
id,
userId: session.userId,
});
if (!id) {
throw new NotFoundException(
'No periodic report id to search for',
'periodicReport.id',
);
}

const result = await this.repo.readOne(id, session);
return this.secure(result, session);
}
Expand Down Expand Up @@ -232,10 +225,7 @@ export class PeriodicReportService {
// no change
return;
}
await this.repo.update(report, {
start: at,
end: at,
});
await this.repo.update({ id: report.id, start: at, end: at }, session);
} else {
await this.merge({
intervals: [{ start: at, end: at }],
Expand Down

0 comments on commit 4aeaf58

Please sign in to comment.