Skip to content

Commit 27eb957

Browse files
CarsonFbryanjnelson
andcommitted
Refactor EngagementService
Move constraints & separate reads/writes into neo4j repo. Make `secure` sync. Remove unnecessary logs & try/catches & comments. Co-authored-by: Bryan Nelson <[email protected]>
1 parent 9cdb25f commit 27eb957

File tree

3 files changed

+241
-302
lines changed

3 files changed

+241
-302
lines changed

src/components/engagement/engagement.repository.ts

Lines changed: 194 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,10 @@ import { difference, pickBy, upperFirst } from 'lodash';
1212
import { DateTime } from 'luxon';
1313
import { MergeExclusive } from 'type-fest';
1414
import {
15+
DuplicateException,
1516
generateId,
1617
ID,
18+
InputException,
1719
labelForView,
1820
NotFoundException,
1921
ObjectView,
@@ -24,7 +26,7 @@ import {
2426
viewOfChangeset,
2527
} from '~/common';
2628
import { CommonRepository, OnIndex } from '~/core/database';
27-
import { ChangesOf, getChanges } from '~/core/database/changes';
29+
import { getChanges } from '~/core/database/changes';
2830
import {
2931
ACTIVE,
3032
coalesce,
@@ -46,11 +48,13 @@ import {
4648
whereNotDeletedInChangeset,
4749
} from '~/core/database/query';
4850
import { Privileges } from '../authorization';
51+
import { FileService } from '../file';
4952
import { FileId } from '../file/dto';
5053
import {
5154
languageFilters,
5255
languageSorters,
5356
} from '../language/language.repository';
57+
import { Location } from '../location/dto';
5458
import {
5559
matchCurrentDue,
5660
progressReportSorters,
@@ -59,6 +63,7 @@ import { ProjectType } from '../project/dto';
5963
import { projectFilters } from '../project/project-filters.query';
6064
import { projectSorters } from '../project/project.repository';
6165
import { userFilters } from '../user';
66+
import { User } from '../user/dto';
6267
import {
6368
CreateInternshipEngagement,
6469
CreateLanguageEngagement,
@@ -80,7 +85,10 @@ export type LanguageOrEngagementId = MergeExclusive<
8085

8186
@Injectable()
8287
export class EngagementRepository extends CommonRepository {
83-
constructor(private readonly privileges: Privileges) {
88+
constructor(
89+
private readonly privileges: Privileges,
90+
private readonly files: FileService,
91+
) {
8492
super();
8593
}
8694

@@ -192,10 +200,9 @@ export class EngagementRepository extends CommonRepository {
192200
);
193201
}
194202

195-
// CREATE ///////////////////////////////////////////////////////////
196-
197203
async createLanguageEngagement(
198204
input: CreateLanguageEngagement,
205+
session: Session,
199206
changeset?: ID,
200207
) {
201208
const pnpId = await generateId<FileId>();
@@ -219,6 +226,17 @@ export class EngagementRepository extends CommonRepository {
219226
canDelete: true,
220227
};
221228

229+
await this.verifyRelationshipEligibility(
230+
projectId,
231+
languageId,
232+
false,
233+
changeset,
234+
);
235+
236+
if (input.firstScripture) {
237+
await this.verifyFirstScripture({ languageId });
238+
}
239+
222240
const query = this.db
223241
.query()
224242
.apply(await createNode(LanguageEngagement, { initialProps }))
@@ -238,11 +256,26 @@ export class EngagementRepository extends CommonRepository {
238256
throw new ServerException('Could not create Language Engagement');
239257
}
240258

241-
return { id: result.id, pnpId };
259+
await this.files.createDefinedFile(
260+
pnpId,
261+
`PNP`,
262+
session,
263+
result.id,
264+
'pnp',
265+
input.pnp,
266+
'engagement.pnp',
267+
);
268+
269+
return (await this.readOne(
270+
result.id,
271+
session,
272+
viewOfChangeset(changeset),
273+
)) as UnsecuredDto<LanguageEngagement>;
242274
}
243275

244276
async createInternshipEngagement(
245277
input: CreateInternshipEngagement,
278+
session: Session,
246279
changeset?: ID,
247280
) {
248281
const growthPlanId = await generateId<FileId>();
@@ -268,6 +301,13 @@ export class EngagementRepository extends CommonRepository {
268301
canDelete: true,
269302
};
270303

304+
await this.verifyRelationshipEligibility(
305+
projectId,
306+
internId,
307+
true,
308+
changeset,
309+
);
310+
271311
const query = this.db
272312
.query()
273313
.apply(await createNode(InternshipEngagement, { initialProps }))
@@ -287,67 +327,124 @@ export class EngagementRepository extends CommonRepository {
287327
.return<{ id: ID }>('node.id as id');
288328
const result = await query.first();
289329
if (!result) {
290-
throw new NotFoundException();
330+
if (mentorId && !(await this.getBaseNode(mentorId, User))) {
331+
throw new NotFoundException(
332+
'Could not find mentor',
333+
'engagement.mentorId',
334+
);
335+
}
336+
337+
if (
338+
countryOfOriginId &&
339+
!(await this.getBaseNode(countryOfOriginId, Location))
340+
) {
341+
throw new NotFoundException(
342+
'Could not find country of origin',
343+
'engagement.countryOfOriginId',
344+
);
345+
}
346+
347+
throw new ServerException('Could not create Internship Engagement');
291348
}
292349

293-
return { id: result.id, growthPlanId };
350+
await this.files.createDefinedFile(
351+
growthPlanId,
352+
`Growth Plan`,
353+
session,
354+
result.id,
355+
'growthPlan',
356+
input.growthPlan,
357+
'engagement.growthPlan',
358+
);
359+
360+
return (await this.readOne(
361+
result.id,
362+
session,
363+
viewOfChangeset(changeset),
364+
)) as UnsecuredDto<InternshipEngagement>;
294365
}
295366

296-
// UPDATE ///////////////////////////////////////////////////////////
297-
298367
getActualLanguageChanges = getChanges(LanguageEngagement);
299368

300369
async updateLanguage(
301-
existing: LanguageEngagement | UnsecuredDto<LanguageEngagement>,
302-
changes: ChangesOf<LanguageEngagement, UpdateLanguageEngagement>,
370+
changes: UpdateLanguageEngagement,
371+
session: Session,
303372
changeset?: ID,
304-
): Promise<void> {
305-
const { pnp, ...simpleChanges } = changes;
373+
) {
374+
const { id, pnp, ...simpleChanges } = changes;
375+
376+
if (pnp) {
377+
const engagement = await this.readOne(id, session);
378+
if (engagement.pnp) {
379+
await this.files.createFileVersion(
380+
{
381+
...pnp,
382+
parentId: engagement.pnp.id,
383+
},
384+
session,
385+
);
386+
}
387+
}
388+
389+
if (changes.firstScripture) {
390+
await this.verifyFirstScripture({ engagementId: id });
391+
}
306392

307393
await this.db.updateProperties({
308394
type: LanguageEngagement,
309-
object: existing,
395+
object: { id },
310396
changes: simpleChanges,
311397
changeset,
312398
});
399+
400+
return await this.readOne(id, session);
313401
}
314402

315403
getActualInternshipChanges = getChanges(InternshipEngagement);
316404

317405
async updateInternship(
318-
existing: InternshipEngagement | UnsecuredDto<InternshipEngagement>,
319-
changes: ChangesOf<InternshipEngagement, UpdateInternshipEngagement>,
406+
changes: UpdateInternshipEngagement,
407+
session: Session,
320408
changeset?: ID,
321-
): Promise<void> {
322-
const {
323-
mentorId,
324-
countryOfOriginId,
325-
growthPlan: _,
326-
...simpleChanges
327-
} = changes;
409+
) {
410+
const { id, mentorId, countryOfOriginId, growthPlan, ...simpleChanges } =
411+
changes;
412+
413+
if (growthPlan) {
414+
const engagement = await this.readOne(id, session);
415+
if (engagement.growthPlan) {
416+
await this.files.createFileVersion(
417+
{
418+
...growthPlan,
419+
parentId: engagement.growthPlan.id,
420+
},
421+
session,
422+
);
423+
}
424+
}
328425

329426
if (mentorId !== undefined) {
330-
await this.updateRelation('mentor', 'User', existing.id, mentorId);
427+
await this.updateRelation('mentor', 'User', id, mentorId);
331428
}
332429

333430
if (countryOfOriginId !== undefined) {
334431
await this.updateRelation(
335432
'countryOfOrigin',
336433
'Location',
337-
existing.id,
434+
id,
338435
countryOfOriginId,
339436
);
340437
}
341438

342439
await this.db.updateProperties({
343440
type: InternshipEngagement,
344-
object: existing,
441+
object: { id },
345442
changes: simpleChanges,
346443
changeset,
347444
});
348-
}
349445

350-
// LIST ///////////////////////////////////////////////////////////
446+
return await this.readOne(id, session);
447+
}
351448

352449
async list(input: EngagementListInput, session: Session, changeset?: ID) {
353450
const result = await this.db
@@ -425,18 +522,19 @@ export class EngagementRepository extends CommonRepository {
425522
return rows.map((r) => r.id);
426523
}
427524

428-
async verifyRelationshipEligibility(
525+
protected async verifyRelationshipEligibility(
429526
projectId: ID,
430527
otherId: ID,
431-
isTranslation: boolean,
432-
property: 'language' | 'intern',
528+
isInternship: boolean,
433529
changeset?: ID,
434530
) {
435-
return await this.db
531+
const property = isInternship ? 'intern' : 'language';
532+
533+
const result = await this.db
436534
.query()
437535
.optionalMatch(node('project', 'Project', { id: projectId }))
438536
.optionalMatch(
439-
node('other', isTranslation ? 'Language' : 'User', {
537+
node('other', !isInternship ? 'Language' : 'User', {
440538
id: otherId,
441539
}),
442540
)
@@ -465,9 +563,48 @@ export class EngagementRepository extends CommonRepository {
465563
engagement?: Node;
466564
}>()
467565
.first();
566+
567+
if (!result?.project) {
568+
throw new NotFoundException(
569+
'Could not find project',
570+
'engagement.projectId',
571+
);
572+
}
573+
574+
const isActuallyInternship =
575+
result.project.properties.type === ProjectType.Internship;
576+
if (isActuallyInternship !== isInternship) {
577+
throw new InputException(
578+
`Only ${
579+
isInternship ? 'Internship' : 'Language'
580+
} Engagements can be created on ${
581+
isInternship ? 'Internship' : 'Translation'
582+
} Projects`,
583+
`engagement.${property}Id`,
584+
);
585+
}
586+
587+
const label = isInternship ? 'person' : 'language';
588+
if (!result?.other) {
589+
throw new NotFoundException(
590+
`Could not find ${label}`,
591+
`engagement.${property}Id`,
592+
);
593+
}
594+
595+
if (result.engagement) {
596+
throw new DuplicateException(
597+
`engagement.${property}Id`,
598+
`Engagement for this project and ${label} already exists`,
599+
);
600+
}
601+
602+
return result;
468603
}
469604

470-
async doesLanguageHaveExternalFirstScripture(id: LanguageOrEngagementId) {
605+
private async doesLanguageHaveExternalFirstScripture(
606+
id: LanguageOrEngagementId,
607+
) {
471608
const result = await this.db
472609
.query()
473610
.apply(this.matchLanguageOrEngagement(id))
@@ -481,7 +618,9 @@ export class EngagementRepository extends CommonRepository {
481618
return !!result;
482619
}
483620

484-
async doOtherEngagementsHaveFirstScripture(id: LanguageOrEngagementId) {
621+
private async doOtherEngagementsHaveFirstScripture(
622+
id: LanguageOrEngagementId,
623+
) {
485624
const result = await this.db
486625
.query()
487626
.apply(this.matchLanguageOrEngagement(id))
@@ -513,6 +652,26 @@ export class EngagementRepository extends CommonRepository {
513652
: query.match([node('language', 'Language', { id: languageId })]);
514653
}
515654

655+
/**
656+
* if firstScripture is true, validate that the engagement
657+
* is the only engagement for the language that has firstScripture=true
658+
* that the language doesn't have hasExternalFirstScripture=true
659+
*/
660+
private async verifyFirstScripture(id: LanguageOrEngagementId) {
661+
if (await this.doesLanguageHaveExternalFirstScripture(id)) {
662+
throw new InputException(
663+
'First scripture has already been marked as having been done externally',
664+
'languageEngagement.firstScripture',
665+
);
666+
}
667+
if (await this.doOtherEngagementsHaveFirstScripture(id)) {
668+
throw new InputException(
669+
'Another engagement has already been marked as having done the first scripture',
670+
'languageEngagement.firstScripture',
671+
);
672+
}
673+
}
674+
516675
@OnIndex()
517676
private createIndexes() {
518677
return this.getConstraintsFor(IEngagement);

0 commit comments

Comments
 (0)