Skip to content

Commit 5e2aa50

Browse files
committed
Merge branch 'develop'
2 parents 14ee548 + 1713766 commit 5e2aa50

15 files changed

+154
-36
lines changed

src/common/secured-mapper.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
import { DataLoader } from '@seedcompany/data-loader';
2+
import { NotFoundException } from './exceptions';
3+
import { ID } from './id-field';
14
import { Secured, UnwrapSecured } from './secured-property';
25

36
/**
@@ -15,3 +18,35 @@ export async function mapSecuredValue<T extends Secured<any>, S>(
1518
const mapped = await mapper(value);
1619
return { ...rest, value: mapped };
1720
}
21+
22+
/**
23+
* A helper to hydrate a secured id list with a DataLoader.
24+
*/
25+
export async function loadSecuredIds<TID extends ID, Res>(
26+
loader: DataLoader<Res, ID>,
27+
input: Secured<readonly TID[]>,
28+
): Promise<Required<Secured<readonly Res[]>>> {
29+
const { value: ids, ...rest } = input;
30+
const value = await loadManyIgnoreMissingThrowAny(loader, ids ?? []);
31+
return { ...rest, value };
32+
}
33+
34+
/**
35+
* A helper to load many keys from a DataLoader,
36+
* ignoring missing keys and throwing any other errors.
37+
*/
38+
export async function loadManyIgnoreMissingThrowAny<Key, Res>(
39+
loader: DataLoader<Res, Key>,
40+
keys: readonly Key[],
41+
): Promise<Required<readonly Res[]>> {
42+
const loaded = (await loader.loadMany(keys)).flatMap((item) => {
43+
if (item instanceof NotFoundException) {
44+
return [];
45+
} else if (item instanceof Error) {
46+
throw item;
47+
}
48+
49+
return item;
50+
});
51+
return loaded;
52+
}

src/components/changeset/enforce-changeset-editable.pipe.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,12 @@ import {
44
DataLoaderStrategy,
55
} from '@seedcompany/data-loader';
66
import { isPlainObject } from 'lodash';
7-
import { ID, InputException, isIdLike } from '../../common';
7+
import {
8+
ID,
9+
InputException,
10+
isIdLike,
11+
loadManyIgnoreMissingThrowAny,
12+
} from '~/common';
813
import { GqlContextHost, NotGraphQLContext } from '../../core';
914
import { ResourceLoaderRegistry } from '../../core/resources/loader.registry';
1015
import { Changeset } from './dto';
@@ -61,11 +66,8 @@ export class EnforceChangesetEditablePipe implements PipeTransform {
6166
this.loaderRegistry.loaders.get('Changeset')!.factory;
6267
const loader = await this.loaderContext.getLoader(loaderFactory, context);
6368

64-
const changesets = await loader.loadMany(ids);
69+
const changesets = await loadManyIgnoreMissingThrowAny(loader, ids);
6570
for (const changeset of changesets) {
66-
if (changeset instanceof Error) {
67-
throw changeset;
68-
}
6971
if (!changeset.editable) {
7072
throw new InputException('Changeset is not editable');
7173
}

src/components/field-region/dto/field-region.dto.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
Resource,
99
Secured,
1010
SecuredProperty,
11+
SecuredPropertyList,
1112
SecuredProps,
1213
SecuredString,
1314
} from '../../../common';
@@ -34,6 +35,11 @@ export class FieldRegion extends Resource {
3435
})
3536
export class SecuredFieldRegion extends SecuredProperty(FieldRegion) {}
3637

38+
@ObjectType({
39+
description: SecuredPropertyList.descriptionFor('a list of field regions'),
40+
})
41+
export class SecuredFieldRegions extends SecuredPropertyList(FieldRegion) {}
42+
3743
declare module '~/core/resources/map' {
3844
interface ResourceMap {
3945
FieldRegion: typeof FieldRegion;

src/components/partner/dto/create-partner.dto.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
import { Field, InputType, ObjectType } from '@nestjs/graphql';
1+
import { Field, ID as IDType, InputType, ObjectType } from '@nestjs/graphql';
22
import { Transform, Type } from 'class-transformer';
33
import { Matches, ValidateNested } from 'class-validator';
44
import { uniq } from 'lodash';
5-
import { ID, IdField, IdOf, NameField } from '../../../common';
5+
import { ID, IdField, IdOf, IsId, NameField } from '../../../common';
6+
import { FieldRegion } from '../../field-region';
67
import type { Language } from '../../language';
78
import { FinancialReportingType } from '../../partnership/dto/financial-reporting-type';
89
import { PartnerType } from './partner-type.enum';
@@ -41,6 +42,11 @@ export abstract class CreatePartner {
4142

4243
@IdField({ nullable: true })
4344
readonly languageOfWiderCommunicationId?: IdOf<Language> | null;
45+
46+
@Field(() => [IDType], { nullable: true })
47+
@IsId({ each: true })
48+
@Transform(({ value }) => uniq(value))
49+
readonly fieldRegions?: ReadonlyArray<IdOf<FieldRegion>> = [];
4450
}
4551

4652
@InputType()

src/components/partner/dto/partner.dto.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
SensitivityField,
2121
} from '../../../common';
2222
import { ScopedRole } from '../../authorization';
23+
import { FieldRegion } from '../../field-region';
2324
import type { Language } from '../../language';
2425
import { FinancialReportingType } from '../../partnership/dto/financial-reporting-type';
2526
import { Pinnable } from '../../pin/dto';
@@ -76,6 +77,8 @@ export class Partner extends Interfaces {
7677

7778
readonly languageOfWiderCommunication: Secured<IdOf<Language> | null>;
7879

80+
readonly fieldRegions: Required<Secured<ReadonlyArray<IdOf<FieldRegion>>>>;
81+
7982
@DateTimeField()
8083
readonly modifiedAt: DateTime;
8184

src/components/partner/dto/update-partner.dto.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
import { Field, InputType, ObjectType } from '@nestjs/graphql';
1+
import { Field, ID as IDType, InputType, ObjectType } from '@nestjs/graphql';
22
import { Transform, Type } from 'class-transformer';
33
import { Matches, ValidateNested } from 'class-validator';
44
import { uniq } from 'lodash';
5-
import { ID, IdField, IdOf, NameField } from '../../../common';
5+
import { ID, IdField, IdOf, IsId, NameField } from '../../../common';
6+
import { FieldRegion } from '../../field-region';
67
import type { Language } from '../../language';
78
import { FinancialReportingType } from '../../partnership/dto/financial-reporting-type';
89
import { PartnerType } from './partner-type.enum';
@@ -41,6 +42,11 @@ export abstract class UpdatePartner {
4142

4243
@IdField({ nullable: true })
4344
readonly languageOfWiderCommunicationId?: IdOf<Language> | null;
45+
46+
@Field(() => [IDType], { nullable: true })
47+
@IsId({ each: true })
48+
@Transform(({ value }) => (value ? uniq(value) : undefined))
49+
readonly fieldRegions?: ReadonlyArray<IdOf<FieldRegion>>;
4450
}
4551

4652
@InputType()

src/components/partner/partner.repository.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { ID, ServerException, Session, UnsecuredDto } from '../../common';
55
import { DtoRepository } from '../../core';
66
import {
77
ACTIVE,
8+
collect,
89
createNode,
910
createRelationships,
1011
filter as filters,
@@ -62,6 +63,7 @@ export class PartnerRepository extends DtoRepository<
6263
'Language',
6364
input.languageOfWiderCommunicationId,
6465
],
66+
fieldRegions: ['FieldRegion', input.fieldRegions],
6567
}),
6668
)
6769
.return<{ id: ID }>('node.id as id')
@@ -103,6 +105,15 @@ export class PartnerRepository extends DtoRepository<
103105
.raw('WHERE size(projList) = 0')
104106
.return(`'High' as sensitivity`),
105107
)
108+
.subQuery('node', (sub) =>
109+
sub
110+
.match([
111+
node('node'),
112+
relation('out', '', 'fieldRegions'),
113+
node('fieldRegions', 'FieldRegion'),
114+
])
115+
.return(collect('fieldRegions.id').as('fieldRegionsIds')),
116+
)
106117
.apply(matchProps())
107118
.optionalMatch([
108119
node('node'),
@@ -125,6 +136,7 @@ export class PartnerRepository extends DtoRepository<
125136
organization: 'organization.id',
126137
pointOfContact: 'pointOfContact.id',
127138
languageOfWiderCommunication: 'languageOfWiderCommunication.id',
139+
fieldRegions: 'fieldRegionsIds',
128140
scope: 'scopedRoles',
129141
pinned: 'exists((:User { id: $requestingUser })-[:pinned]->(node))',
130142
}).as('dto'),

src/components/partner/partner.resolver.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,13 @@ import {
1111
ID,
1212
IdArg,
1313
ListArg,
14+
loadSecuredIds,
1415
LoggedInSession,
1516
mapSecuredValue,
1617
Session,
1718
} from '../../common';
1819
import { Loader, LoaderOf } from '../../core';
20+
import { FieldRegionLoader, SecuredFieldRegions } from '../field-region';
1921
import { LanguageLoader, SecuredLanguageNullable } from '../language';
2022
import { OrganizationLoader, SecuredOrganization } from '../organization';
2123
import { PartnerLoader, PartnerService } from '../partner';
@@ -93,6 +95,14 @@ export class PartnerResolver {
9395
);
9496
}
9597

98+
@ResolveField(() => SecuredFieldRegions)
99+
async fieldRegions(
100+
@Parent() partner: Partner,
101+
@Loader(FieldRegionLoader) loader: LoaderOf<FieldRegionLoader>,
102+
): Promise<SecuredFieldRegions> {
103+
return await loadSecuredIds(loader, partner.fieldRegions);
104+
}
105+
96106
@ResolveField(() => SecuredProjectList, {
97107
description: 'The list of projects the partner has a partnership with.',
98108
})

src/components/partner/partner.service.ts

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -99,12 +99,12 @@ export class PartnerService {
9999
}
100100

101101
async update(input: UpdatePartner, session: Session): Promise<Partner> {
102-
const object = await this.readOne(input.id, session);
102+
const partner = await this.readOne(input.id, session);
103103

104104
if (
105105
!this.validateFinancialReportingType(
106-
input.financialReportingTypes ?? object.financialReportingTypes.value,
107-
input.types ?? object.types.value,
106+
input.financialReportingTypes ?? partner.financialReportingTypes.value,
107+
input.types ?? partner.types.value,
108108
)
109109
) {
110110
if (input.financialReportingTypes && input.types) {
@@ -119,28 +119,44 @@ export class PartnerService {
119119
};
120120
}
121121

122-
const changes = this.repo.getActualChanges(object, input);
123-
this.privileges.for(session, Partner, object).verifyChanges(changes);
122+
const changes = this.repo.getActualChanges(partner, input);
123+
this.privileges.for(session, Partner, partner).verifyChanges(changes);
124124
const {
125125
pointOfContactId,
126126
languageOfWiderCommunicationId,
127+
fieldRegions,
127128
...simpleChanges
128129
} = changes;
129130

130-
await this.repo.updateProperties(object, simpleChanges);
131+
await this.repo.updateProperties(partner, simpleChanges);
131132

132133
if (pointOfContactId) {
133134
await this.repo.updatePointOfContact(input.id, pointOfContactId, session);
134135
}
136+
135137
if (languageOfWiderCommunicationId) {
136138
await this.repo.updateRelation(
137139
'languageOfWiderCommunication',
138140
'Language',
139-
object.id,
141+
partner.id,
140142
languageOfWiderCommunicationId,
141143
);
142144
}
143145

146+
if (fieldRegions) {
147+
try {
148+
await this.repo.updateRelationList({
149+
id: partner.id,
150+
relation: 'fieldRegions',
151+
newList: fieldRegions,
152+
});
153+
} catch (e) {
154+
throw e instanceof InputException
155+
? e.withField('partner.fieldRegions')
156+
: e;
157+
}
158+
}
159+
144160
return await this.readOne(input.id, session);
145161
}
146162

src/components/product-progress/progress-report-connection.resolver.ts

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { Args, Parent, ResolveField, Resolver } from '@nestjs/graphql';
22
import { sortBy } from 'lodash';
3-
import { ClientException } from '~/common';
3+
import { loadManyIgnoreMissingThrowAny } from '~/common';
44
import { Loader, LoaderOf } from '~/core';
55
import { ProgressReport } from '../progress-report/dto';
66
import {
@@ -30,16 +30,11 @@ export class ProgressReportConnectionResolver {
3030
@Loader(ProductProgressByReportLoader)
3131
loader: LoaderOf<ProductProgressByReportLoader>,
3232
): Promise<ReadonlyArray<readonly ProductProgress[]>> {
33-
const detailsOrErrors = await loader.loadMany(
33+
const detailsOrErrors = await loadManyIgnoreMissingThrowAny(
34+
loader,
3435
Progress.Variants.map((variant) => ({ report, variant })),
3536
);
3637
const details = detailsOrErrors.flatMap((entry) => {
37-
if (entry instanceof Error) {
38-
if (entry instanceof ClientException) {
39-
return [];
40-
}
41-
throw entry;
42-
}
4338
if (entry.details.length === 0) {
4439
return [];
4540
}

0 commit comments

Comments
 (0)