Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion src/components/periodic-report/dto/periodic-report.dto.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Field, InterfaceType, ObjectType } from '@nestjs/graphql';
import { type MergeExclusive } from 'type-fest';
import {
Calculated,
CalendarDate,
Expand All @@ -15,8 +16,14 @@ import { e } from '~/core/gel';
import { RegisterResource } from '~/core/resources';
import { type ScopedRole } from '../../authorization/dto';
import { type DefinedFile } from '../../file/dto';
import { ProgressReport } from '../../progress-report/dto';
import { ReportType } from './report-type.enum';

export type AnyReport = MergeExclusive<
FinancialReport,
MergeExclusive<NarrativeReport, ProgressReport>
>;

@RegisterResource({ db: e.PeriodicReport })
@Calculated()
@InterfaceType({
Expand Down Expand Up @@ -44,7 +51,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 Expand Up @@ -83,6 +90,12 @@ export class NarrativeReport extends PeriodicReport {
})
export class SecuredPeriodicReport extends SecuredProperty(PeriodicReport) {}

export const ReportConcretes = {
Financial: FinancialReport,
Narrative: NarrativeReport,
Progress: ProgressReport,
};

declare module '~/core/resources/map' {
interface ResourceMap {
PeriodicReport: typeof PeriodicReport;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
import { type DateTimeUnit } from 'luxon';
import { type CalendarDate, DateInterval, type ID, type Range } from '~/common';
import { type ReportType } from '../dto';
import { type PeriodicReportService } from '../periodic-report.service';
import { type PeriodicReportRepository } from '../periodic-report.repository';

export type Intervals = [
updated: DateInterval | null,
previous: DateInterval | null,
];

export abstract class AbstractPeriodicReportSync {
constructor(protected readonly periodicReports: PeriodicReportService) {}
constructor(
protected readonly periodicReportsRepo: PeriodicReportRepository,
) {}

protected async sync(
parent: ID,
Expand All @@ -20,9 +22,12 @@ export abstract class AbstractPeriodicReportSync {
},
finalAt?: CalendarDate,
) {
await this.periodicReports.delete(parent, type, diff.removals);
if (!diff) {
return;
}
await this.periodicReportsRepo.delete(parent, type, diff.removals);

await this.periodicReports.merge({
await this.periodicReportsRepo.merge({
type,
parent,
intervals: diff.additions,
Expand All @@ -31,7 +36,7 @@ export abstract class AbstractPeriodicReportSync {
if (!finalAt) {
return;
}
await this.periodicReports.mergeFinalReport(parent, type, finalAt);
await this.periodicReportsRepo.mergeFinalReport(parent, type, finalAt);
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
import { type DateTimeUnit } from 'luxon';
import { type DateInterval } from '~/common';
import { EventsHandler, type IEventHandler, ILogger, Logger } from '~/core';
import {
ConfigService,
EventsHandler,
type IEventHandler,
ILogger,
Logger,
} from '~/core';
import { projectRange } from '../../project/dto';
import { ProjectUpdatedEvent } from '../../project/events';
import { ReportPeriod, ReportType } from '../dto';
import { PeriodicReportService } from '../periodic-report.service';
import { PeriodicReportRepository } from '../periodic-report.repository';
import {
AbstractPeriodicReportSync,
type Intervals,
Expand All @@ -18,13 +24,18 @@ export class SyncPeriodicReportsToProjectDateRange
implements IEventHandler<SubscribedEvent>
{
constructor(
periodicReports: PeriodicReportService,
periodicReportsRepo: PeriodicReportRepository,
private readonly config: ConfigService,
@Logger('periodic-reports:project-sync') private readonly logger: ILogger,
) {
super(periodicReports);
super(periodicReportsRepo);
}

async handle(event: SubscribedEvent) {
if (this.config.databaseEngine === 'gel') {
return;
}

this.logger.debug('Project mutation, syncing periodic reports', {
...event,
event: event.constructor.name,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import { Settings } from 'luxon';
import { DateInterval, type UnsecuredDto } from '~/common';
import { EventsHandler, type IEventHandler, ILogger, Logger } from '~/core';
import {
ConfigService,
EventsHandler,
type IEventHandler,
ILogger,
Logger,
} from '~/core';
import { EngagementService } from '../../engagement';
import { type Engagement, engagementRange } from '../../engagement/dto';
import {
Expand All @@ -9,7 +15,7 @@ import {
} from '../../engagement/events';
import { ProjectUpdatedEvent } from '../../project/events';
import { ReportType } from '../dto';
import { PeriodicReportService } from '../periodic-report.service';
import { PeriodicReportRepository } from '../periodic-report.repository';
import {
AbstractPeriodicReportSync,
type Intervals,
Expand All @@ -30,14 +36,19 @@ export class SyncProgressReportToEngagementDateRange
implements IEventHandler<SubscribedEvent>
{
constructor(
periodicReports: PeriodicReportService,
periodicReportsRepo: PeriodicReportRepository,
private readonly config: ConfigService,
private readonly engagements: EngagementService,
@Logger('progress-report:engagement-sync') private readonly logger: ILogger,
) {
super(periodicReports);
super(periodicReportsRepo);
}

async handle(event: SubscribedEvent) {
if (this.config.databaseEngine === 'gel') {
return;
}

// Only LanguageEngagements
if (
!(
Expand Down
171 changes: 171 additions & 0 deletions src/components/periodic-report/periodic-report.gel.repository.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
import { Injectable } from '@nestjs/common';
import { type Without } from 'type-fest/source/merge-exclusive';
import {
CalendarDate,
EnhancedResource,
type ID,
type PublicOf,
type UnsecuredDto,
} from '~/common';
import { castToEnum, e, RepoFor } from '~/core/gel';
import { type ProgressReport } from '../progress-report/dto';
import {
type FinancialReport,
IPeriodicReport,
type NarrativeReport,
ReportType,
resolveReportType,
} from './dto';
import { type PeriodicReportRepository } from './periodic-report.repository';

@Injectable()
export class PeriodicReportGelRepository
extends RepoFor(IPeriodicReport, {
hydrate: (periodicReport) => ({
...periodicReport['*'],
type: castToEnum(periodicReport.__type__.name.slice(9, -7), ReportType),
reportFile: true,
sensitivity: periodicReport.container.is(e.Project.ContextAware)
.sensitivity,
scope: false,
parent: e.select({
identity: periodicReport.id,
labels: e.array_agg(e.set(periodicReport.__type__.name.slice(9, null))),
properties: e.select({
id: periodicReport.id,
createdAt: periodicReport.createdAt,
}),
}),
}),
omit: ['create'],
})
implements PublicOf<PeriodicReportRepository>
{
//TODO - this is close but the hydrate for ProgressReport needs worked on
async matchCurrentDue(parentId: ID, reportType: ReportType) {
const enhancedResource = EnhancedResource.of(
resolveReportType({ type: reportType }),
);
const resource = e.cast(enhancedResource.db, e.uuid(parentId));
const report = e.select(resource, (report) => ({
...this.hydrate(report),
filter: e.all(
e.set(
e.op(resource.id, '=', report.container.id),
e.op(report.end, '<', CalendarDate.now()),
),
),
order_by: [
{ expression: report.end, direction: e.DESC },
{ expression: report.start, direction: e.ASC },
],
}));
return await this.db.run(report);
}

getCurrentDue(
_parentId: ID,
_reportType: ReportType,
): Promise<
| UnsecuredDto<
| (Without<
| (Without<FinancialReport, NarrativeReport> & NarrativeReport)
| (Without<NarrativeReport, FinancialReport> & FinancialReport),
ProgressReport
> &
ProgressReport)
| (Without<
ProgressReport,
| (Without<FinancialReport, NarrativeReport> & NarrativeReport)
| (Without<NarrativeReport, FinancialReport> & FinancialReport)
> &
(
| (Without<FinancialReport, NarrativeReport> & NarrativeReport)
| (Without<NarrativeReport, FinancialReport> & FinancialReport)
))
>
| undefined
> {
//TODO - is this needed? How does this differ from matchCurrentDue?
throw new Error('Method not implemented.');
}

//TODO - this is close but the hydrate for ProgressReport needs worked on
async getByDate(parentId: ID, date: CalendarDate, reportType: ReportType) {
const enhancedResource = EnhancedResource.of(
resolveReportType({ type: reportType }),
);
const resource = e.cast(enhancedResource.db, e.uuid(parentId));
const report = e.select(resource, (report) => ({
...this.hydrate(report),
filter: e.all(
e.set(
e.op(resource.id, '=', report.container.id),
e.op(report.start, '<=', date),
e.op(report.end, '>=', date),
),
),
}));
return await this.db.run(report);
}

//TODO - this is close but the hydrate for ProgressReport needs worked on
async getNextDue(parentId: ID, reportType: ReportType) {
const enhancedResource = EnhancedResource.of(
resolveReportType({ type: reportType }),
);
const resource = e.cast(enhancedResource.db, e.uuid(parentId));
const report = e.select(resource, (report) => ({
...this.hydrate(report),
filter: e.all(
e.set(
e.op(resource.id, '=', report.container.id),
e.op(report.end, '>', CalendarDate.now()),
),
),
order_by: { expression: report.end, direction: e.ASC },
}));

return await this.db.run(report);
}

//TODO - this is close but the hydrate for ProgressReport needs worked on
async getLatestReportSubmitted(parentId: ID, reportType: ReportType) {
const enhancedResource = EnhancedResource.of(
resolveReportType({ type: reportType }),
);
const resource = e.cast(enhancedResource.db, e.uuid(parentId));
const report = e.select(resource, (report) => ({
...this.hydrate(report),
filter: e.all(
e.set(
e.op(resource.id, '=', report.container.id),
e.op('exists', report.reportFile),
),
),
order_by: { expression: report.start, direction: e.DESC },
}));

return await this.db.run(report);
}

//TODO - this is close but the hydrate for ProgressReport needs worked on
async getFinalReport(parentId: ID, reportType: ReportType) {
const enhancedResource = EnhancedResource.of(
resolveReportType({ type: reportType }),
);
const resource = e.cast(enhancedResource.db, e.uuid(parentId));
const report = e.select(resource, (report) => ({
...this.hydrate(report),
filter: e.all(
e.set(
e.op(resource.id, '=', report.container.id),
e.op(report.start, '=', report.end),
),
),
order_by: { expression: report.start, direction: e.DESC },
}));

return await this.db.run(report);
}
}
4 changes: 3 additions & 1 deletion src/components/periodic-report/periodic-report.module.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { forwardRef, Module } from '@nestjs/common';
import { splitDb } from '~/core';
import { AuthorizationModule } from '../authorization/authorization.module';
import { EngagementModule } from '../engagement/engagement.module';
import { FileModule } from '../file/file.module';
Expand All @@ -7,6 +8,7 @@ import { ProjectModule } from '../project/project.module';
import * as handlers from './handlers';
import { PeriodicReportParentResolver } from './periodic-report-parent.resolver';
import { PeriodicReportProjectConnectionResolver } from './periodic-report-project-connection.resolver';
import { PeriodicReportGelRepository } from './periodic-report.gel.repository';
import { PeriodicReportLoader } from './periodic-report.loader';
import { PeriodicReportRepository } from './periodic-report.repository';
import { PeriodicReportResolver } from './periodic-report.resolver';
Expand All @@ -25,7 +27,7 @@ import { PeriodicReportService } from './periodic-report.service';
PeriodicReportResolver,
PeriodicReportProjectConnectionResolver,
PeriodicReportParentResolver,
PeriodicReportRepository,
splitDb(PeriodicReportRepository, PeriodicReportGelRepository),
PeriodicReportLoader,
...Object.values(handlers),
],
Expand Down
Loading
Loading