Skip to content

Commit 9a12d10

Browse files
authored
Merge pull request #3331 from SeedCompany/reextract-pnps
2 parents 631ddba + 66d6bb4 commit 9a12d10

File tree

6 files changed

+105
-34
lines changed

6 files changed

+105
-34
lines changed

src/components/product-progress/handlers/extract-pnp-progress.handler.ts

Lines changed: 52 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
import { mapOf } from '@seedcompany/common';
2+
import { oneLine } from 'common-tags';
23
import { EventsHandler, ILogger, Logger } from '~/core';
34
import { ReportType } from '../../periodic-report/dto';
45
import { PeriodicReportUploadedEvent } from '../../periodic-report/events';
56
import { ProductService } from '../../product';
6-
import { ProducibleType } from '../../product/dto';
7+
import { ProducibleType, ProductStep } from '../../product/dto';
78
import { isScriptureEqual } from '../../scripture';
89
import { ProgressReportVariantProgress as Progress } from '../dto';
910
import { ProductProgressService } from '../product-progress.service';
11+
import { StepNotPlannedException } from '../step-not-planned.exception';
1012
import { StepProgressExtractor } from '../step-progress-extractor.service';
1113

1214
@EventsHandler(PeriodicReportUploadedEvent)
@@ -62,7 +64,7 @@ export class ExtractPnpProgressHandler {
6264
if (row.story) {
6365
const productId = storyProducts.get(row.story);
6466
if (productId) {
65-
return { productId, steps };
67+
return { extracted: row, productId, steps };
6668
}
6769
}
6870

@@ -73,15 +75,19 @@ export class ExtractPnpProgressHandler {
7375
isScriptureEqual(ref.scriptureRanges, row.scripture),
7476
);
7577
if (exactScriptureMatch) {
76-
return { productId: exactScriptureMatch.id, steps };
78+
return { extracted: row, productId: exactScriptureMatch.id, steps };
7779
}
7880

7981
const unspecifiedScriptureMatch = scriptureProducts.find(
8082
(ref) =>
8183
ref.book === row.bookName && ref.totalVerses === row.totalVerses,
8284
);
8385
if (unspecifiedScriptureMatch) {
84-
return { productId: unspecifiedScriptureMatch.id, steps };
86+
return {
87+
extracted: row,
88+
productId: unspecifiedScriptureMatch.id,
89+
steps,
90+
};
8591
}
8692
}
8793

@@ -95,16 +101,48 @@ export class ExtractPnpProgressHandler {
95101

96102
// Update progress for report & product
97103
await Promise.all(
98-
updates.map(async (input) => {
99-
await this.progress.update(
100-
{
101-
...input,
102-
reportId: event.report.id,
103-
// TODO this seems fine for now as only this variant will upload PnPs.
104-
variant: Progress.FallbackVariant,
105-
},
106-
event.session,
107-
);
104+
updates.map(async ({ extracted, ...input }) => {
105+
try {
106+
await this.progress.update(
107+
{
108+
...input,
109+
reportId: event.report.id,
110+
// TODO this seems fine for now as only this variant will upload PnPs.
111+
variant: Progress.FallbackVariant,
112+
},
113+
event.session,
114+
);
115+
} catch (e) {
116+
if (
117+
!(
118+
e instanceof AggregateError &&
119+
e.message === 'Invalid Progress Input'
120+
)
121+
) {
122+
throw e;
123+
}
124+
for (const error of e.errors) {
125+
if (!(error instanceof StepNotPlannedException)) {
126+
continue;
127+
}
128+
const stepLabel = ProductStep.entry(error.step).label;
129+
// kinda. close enough, I think, we give the cell ref as well.
130+
const goalLabel = extracted.bookName ?? extracted.story;
131+
result.addProblem({
132+
severity: 'Error',
133+
groups: [
134+
'Step is not planned',
135+
`_${goalLabel}_ has progress reported on steps that have not been declared to be worked in this engagement`,
136+
`_${goalLabel}_ has not declared _${stepLabel}_ \`${extracted.cell.ref}\` as a step that will be worked in this engagement`,
137+
],
138+
message: oneLine`
139+
Please update the goal in CORD to mark this step as planned
140+
or upload an updated PnP file to the "Planning Spreadsheet" on the engagement page.
141+
`,
142+
source: extracted.cell,
143+
});
144+
}
145+
}
108146
}),
109147
);
110148
}

src/components/product-progress/product-progress.service.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -152,17 +152,21 @@ export class ProductProgressService {
152152
);
153153
}
154154

155-
input.steps.forEach((step, index) => {
155+
const errors = input.steps.flatMap((step, index) => {
156156
if (!scope.steps.includes(step.step)) {
157-
throw new StepNotPlannedException(input.productId, step.step, index);
157+
return new StepNotPlannedException(input.productId, step.step, index);
158158
}
159159
if (step.completed && step.completed > scope.progressTarget) {
160-
throw new InputException(
160+
return new InputException(
161161
"Completed value cannot exceed product's progress target",
162162
`steps.${index}.completed`,
163163
);
164164
}
165+
return [];
165166
});
167+
if (errors.length > 0) {
168+
throw new AggregateError(errors, 'Invalid Progress Input');
169+
}
166170

167171
const progress = await this.repo.update(input);
168172
return this.secure(progress, this.privilegesFor(session, scope))!;

src/components/product-progress/step-progress-extractor.service.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ type ExtractedRow = MergeExclusive<
3939
*/
4040
rowIndex: number;
4141
steps: ReadonlyArray<{ step: Step; completed?: number | null }>;
42+
cell: Cell<ProgressSheet>;
4243
};
4344

4445
@Injectable()
@@ -100,6 +101,7 @@ const parseProgressRow =
100101
rowIndex: rowIndex + 1,
101102
order: index + 1,
102103
steps,
104+
cell,
103105
};
104106

105107
if (sheet.isOBS()) {

src/components/progress-report/migrations/reextract-all-progress-reports.migration.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { FileVersion } from '../../file/dto';
88
import { PeriodicReportUploadedEvent } from '../../periodic-report/events';
99
import { ProgressReport } from '../dto';
1010

11-
@Migration('2024-11-11T16:00:04')
11+
@Migration('2024-11-11T16:00:06')
1212
export class ReextractPnpProgressReportsMigration extends BaseMigration {
1313
constructor(
1414
private readonly eventBus: IEventBus,
@@ -26,7 +26,7 @@ export class ReextractPnpProgressReportsMigration extends BaseMigration {
2626
try {
2727
const pnp = this.files.asDownloadable(fv);
2828
const event = new PeriodicReportUploadedEvent(report, pnp, session);
29-
await this.eventBus.publish(event);
29+
await this.db.conn.runInTransaction(() => this.eventBus.publish(event));
3030
} catch (e) {
3131
this.logger.error('Failed to re-extract PnP', {
3232
report: report.id,

src/core/exception/exception.normalizer.ts

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,12 +42,14 @@ export interface ExceptionJson {
4242
stack: string;
4343
code: string;
4444
codes: ReadonlySet<string>;
45+
/** From {@link AggregateError.errors} */
46+
aggregatees?: readonly ExceptionJson[];
4547
[key: string]: unknown;
4648
}
4749

4850
/**
4951
* Denote normalization has already happened for this error.
50-
* So the GQL server can know acturately if needs to normalize or not.
52+
* So the GQL server can know accurately if it needs to normalize or not.
5153
*/
5254
export class NormalizedException extends Error {
5355
constructor(readonly normalized: ExceptionJson) {
@@ -132,6 +134,26 @@ export class ExceptionNormalizer {
132134
};
133135
}
134136

137+
if (
138+
ex instanceof AggregateError &&
139+
// not subclassed
140+
ex.name === 'AggregateError'
141+
) {
142+
const aggregatees = ex.errors.map((e) =>
143+
this.normalize({
144+
...params,
145+
ex: e,
146+
}),
147+
);
148+
return {
149+
aggregatees,
150+
// shrug?
151+
codes: [
152+
aggregatees.every((e) => e.codes.has('Client')) ? 'Client' : 'Server',
153+
],
154+
};
155+
}
156+
135157
const gqlContext =
136158
context && context.getType() === 'graphql'
137159
? GqlExecutionContext.create(context as any)

src/core/graphql/graphql-error-formatter.ts

Lines changed: 19 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ export class GraphqlErrorFormatter {
3434
() =>
3535
({ result, setResult }) => {
3636
if (result.length > 0) {
37-
const errors = result.map((error) => this.formatError(error));
37+
const errors = result.flatMap((error) => this.formatError(error));
3838
setResult(errors);
3939
}
4040
};
@@ -43,7 +43,9 @@ export class GraphqlErrorFormatter {
4343
onExecuteDone: (params) =>
4444
handleStreamOrSingleExecutionResult(params, ({ result, setResult }) => {
4545
if (result.errors && result.errors.length > 0) {
46-
const errors = result.errors.map((error) => this.formatError(error));
46+
const errors = result.errors.flatMap((error) =>
47+
this.formatError(error),
48+
);
4749
setResult({ ...result, errors });
4850
}
4951
}),
@@ -71,20 +73,23 @@ export class GraphqlErrorFormatter {
7173
this.filter.logIt(normalized, error.originalError ?? error);
7274
}
7375

74-
const { message, stack, code: _, ...extensions } = normalized;
75-
const { codes } = extensions;
76+
// Unwrap AggregateError's errors to flat gql errors
77+
return (normalized.aggregatees ?? [normalized]).map((innerEx) => {
78+
const { message, stack, code: _, ...extensions } = innerEx;
79+
const { codes } = extensions;
7680

77-
// Schema & validation errors don't have meaningful stack traces, so remove them
78-
const worthlessTrace = codes.has('Validation') || codes.has('GraphQL');
79-
if (!worthlessTrace) {
80-
extensions.stacktrace = stack.split('\n');
81-
}
81+
// Schema & validation errors don't have meaningful stack traces, so remove them
82+
const worthlessTrace = codes.has('Validation') || codes.has('GraphQL');
83+
if (!worthlessTrace) {
84+
extensions.stacktrace = stack.split('\n');
85+
}
8286

83-
return new GraphQLError(message, {
84-
nodes: error.nodes,
85-
positions: error.positions,
86-
path: error.path,
87-
extensions: { ...error.extensions, ...extensions },
87+
return new GraphQLError(message, {
88+
nodes: error.nodes,
89+
positions: error.positions,
90+
path: error.path,
91+
extensions: { ...error.extensions, ...extensions },
92+
});
8893
});
8994
};
9095
}

0 commit comments

Comments
 (0)