Skip to content

Commit f725e28

Browse files
committed
Persist & serve pnp progress extraction results
1 parent 5716355 commit f725e28

9 files changed

+182
-2
lines changed

src/app.module.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import { PartnershipProducingMediumModule } from './components/partnership-produ
2424
import { PartnershipModule } from './components/partnership/partnership.module';
2525
import { PeriodicReportModule } from './components/periodic-report/periodic-report.module';
2626
import { PinModule } from './components/pin/pin.module';
27+
import { PnpExtractionResultModule } from './components/pnp/extraction-result/pnp-extraction-result.module';
2728
import { PostModule } from './components/post/post.module';
2829
import { ProductProgressModule } from './components/product-progress/product-progress.module';
2930
import { ProductModule } from './components/product/product.module';
@@ -87,6 +88,7 @@ if (process.env.NODE_ENV !== 'production') {
8788
PartnershipProducingMediumModule,
8889
ProgressReportModule,
8990
PromptsModule,
91+
PnpExtractionResultModule,
9092
],
9193
})
9294
export class AppModule {}
Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { LazyGetter as Once } from 'lazy-get-decorator';
22
import { Session } from '~/common';
3-
import { Downloadable, FileNode } from '../../file/dto';
3+
import { Downloadable, FileVersion } from '../../file/dto';
44
import { PnpProgressExtractionResult } from '../../pnp/extraction-result';
55
import { PeriodicReport } from '../dto';
66

@@ -10,11 +10,13 @@ import { PeriodicReport } from '../dto';
1010
export class PeriodicReportUploadedEvent {
1111
constructor(
1212
readonly report: PeriodicReport,
13-
readonly file: Downloadable<FileNode>,
13+
readonly file: Downloadable<FileVersion>,
1414
readonly session: Session,
1515
) {}
1616

17+
pnpResultUsed = false;
1718
@Once() get pnpResult() {
19+
this.pnpResultUsed = true;
1820
return new PnpProgressExtractionResult(this.file.id);
1921
}
2022
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { Parent, ResolveField, Resolver } from '@nestjs/graphql';
2+
import { Loader, LoaderOf } from '@seedcompany/data-loader';
3+
import { ProgressReport } from '../../progress-report/dto';
4+
import { PnpProgressExtractionResult } from './extraction-result.dto';
5+
import { PnpExtractionResultLoader } from './pnp-extraction-result.loader';
6+
7+
@Resolver(ProgressReport)
8+
export class PnpExtractionResultProgressReportConnectionResolver {
9+
@ResolveField(() => PnpProgressExtractionResult, {
10+
nullable: true,
11+
})
12+
async pnpExtractionResult(
13+
@Parent() report: ProgressReport,
14+
@Loader(() => PnpExtractionResultLoader)
15+
loader: LoaderOf<PnpExtractionResultLoader>,
16+
): Promise<PnpProgressExtractionResult | null> {
17+
const fileId = report.reportFile.value;
18+
if (!fileId) {
19+
return null;
20+
}
21+
const { result } = await loader.load(fileId);
22+
return result;
23+
}
24+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { Injectable } from '@nestjs/common';
2+
import { ID, NotImplementedException } from '~/common';
3+
import { CommonRepository } from '~/core/edgedb';
4+
import { PnpExtractionResult } from './extraction-result.dto';
5+
import { PnpExtractionResultLoadResult } from './pnp-extraction-result.loader';
6+
7+
@Injectable()
8+
export class PnpExtractionResultRepository extends CommonRepository {
9+
async read(
10+
files: ReadonlyArray<ID<'File'>>,
11+
): Promise<readonly PnpExtractionResultLoadResult[]> {
12+
throw new NotImplementedException().with(files);
13+
}
14+
15+
async save(
16+
file: ID<'FileVersion'>,
17+
result: PnpExtractionResult,
18+
): Promise<void> {
19+
throw new NotImplementedException().with(file, result);
20+
}
21+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { DataLoaderStrategy } from '@seedcompany/data-loader';
2+
import { ID } from '~/common';
3+
import { LoaderFactory } from '~/core';
4+
import { PnpExtractionResult } from './extraction-result.dto';
5+
import { PnpExtractionResultRepository } from './pnp-extraction-result.edgedb.repository';
6+
7+
export interface PnpExtractionResultLoadResult {
8+
id: ID<'File'>;
9+
result: PnpExtractionResult | null;
10+
}
11+
12+
@LoaderFactory()
13+
export class PnpExtractionResultLoader
14+
implements DataLoaderStrategy<PnpExtractionResultLoadResult, ID<'File'>>
15+
{
16+
constructor(private readonly repo: PnpExtractionResultRepository) {}
17+
18+
async loadMany(files: ReadonlyArray<ID<'File'>>) {
19+
return await this.repo.read(files);
20+
}
21+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { Module } from '@nestjs/common';
2+
import { splitDb2 } from '~/core';
3+
import { PnpExtractionResultProgressReportConnectionResolver } from './pnp-extraction-result-progress-report-connection.resolver';
4+
import { PnpExtractionResultRepository } from './pnp-extraction-result.edgedb.repository';
5+
import { PnpExtractionResultLoader } from './pnp-extraction-result.loader';
6+
import { PnpExtractionResultNeo4jRepository } from './pnp-extraction-result.neo4j.repository';
7+
import { SaveProgressExtractionResultHandler } from './save-progress-extraction-result.handler';
8+
9+
@Module({
10+
providers: [
11+
PnpExtractionResultProgressReportConnectionResolver,
12+
PnpExtractionResultLoader,
13+
SaveProgressExtractionResultHandler,
14+
splitDb2(PnpExtractionResultRepository, {
15+
edge: PnpExtractionResultRepository,
16+
neo4j: PnpExtractionResultNeo4jRepository,
17+
}),
18+
],
19+
})
20+
export class PnpExtractionResultModule {}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import { Injectable } from '@nestjs/common';
2+
import { groupToMapBy, mapKeys } from '@seedcompany/common';
3+
import { inArray, node, relation } from 'cypher-query-builder';
4+
import { SetNonNullable } from 'type-fest';
5+
import { ID, PublicOf } from '~/common';
6+
import { CommonRepository } from '~/core/database';
7+
import { apoc, merge } from '~/core/database/query';
8+
import {
9+
PnpExtractionResult,
10+
PnpProblemSeverity as Severity,
11+
} from './extraction-result.dto';
12+
import { PnpExtractionResultRepository } from './pnp-extraction-result.edgedb.repository';
13+
import { PnpExtractionResultLoadResult } from './pnp-extraction-result.loader';
14+
15+
@Injectable()
16+
export class PnpExtractionResultNeo4jRepository
17+
extends CommonRepository
18+
implements PublicOf<PnpExtractionResultRepository>
19+
{
20+
async read(files: ReadonlyArray<ID<'File'>>) {
21+
const found = await this.db
22+
.query()
23+
.match([
24+
node('file', 'File'),
25+
relation('out', '', 'pnpExtractionResult'),
26+
node('result', 'PnpExtractionResult'),
27+
])
28+
.where({ 'file.id': inArray(files) })
29+
.return<SetNonNullable<PnpExtractionResultLoadResult>>([
30+
'file.id as id',
31+
merge('result', {
32+
problems: apoc.convert.fromJsonList('result.problems'),
33+
}).as('result'),
34+
])
35+
.run();
36+
const map = mapKeys.fromList(found, (r) => r.id).asMap;
37+
return files.map((id) => ({
38+
id,
39+
result: map.get(id)?.result ?? null,
40+
}));
41+
}
42+
43+
async save(file: ID<'FileVersion'>, result: PnpExtractionResult) {
44+
const bySev = groupToMapBy(result.problems, (p) => p.severity);
45+
const stats = Object.fromEntries(
46+
[...Severity].flatMap((severity) => [
47+
[`result.has${severity}`, bySev.has(severity)],
48+
[`result.count${severity}`, bySev.get(severity)?.length ?? 0],
49+
]),
50+
);
51+
52+
await this.db
53+
.query()
54+
.match([
55+
node('file', 'File'),
56+
relation('in', 'parent'),
57+
node('', 'FileVersion', { id: file }),
58+
])
59+
.merge([
60+
node('file'),
61+
relation('out', '', 'pnpExtractionResult'),
62+
node('result', 'PnpExtractionResult'),
63+
])
64+
.apply((q) => {
65+
q.params.addParam(result.problems, 'problems');
66+
})
67+
.setValues(stats)
68+
.setVariables({
69+
'result.problems': 'apoc.convert.toJson($problems)',
70+
})
71+
.executeAndLogStats();
72+
}
73+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { EventsHandler } from '~/core';
2+
import { PeriodicReportUploadedEvent } from '../../periodic-report/events';
3+
import { PnpExtractionResultRepository } from './pnp-extraction-result.edgedb.repository';
4+
5+
@EventsHandler([PeriodicReportUploadedEvent, -1])
6+
export class SaveProgressExtractionResultHandler {
7+
constructor(private readonly repo: PnpExtractionResultRepository) {}
8+
9+
async handle(event: PeriodicReportUploadedEvent) {
10+
if (!event.pnpResultUsed) {
11+
return;
12+
}
13+
14+
await this.repo.save(event.file.id, event.pnpResult);
15+
}
16+
}

src/core/database/query/cypher-functions.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ export const apoc = {
7676
convert: {
7777
/** Converts Neo4j node to object/map of the node's properties */
7878
toMap: fn1('apoc.convert.toMap'),
79+
fromJsonList: fn1('apoc.convert.fromJsonList'),
7980
},
8081
create: {
8182
setLabels: (node: ExpressionInput, labels: readonly string[]) =>

0 commit comments

Comments
 (0)