Skip to content

Commit 90647c3

Browse files
authored
Merge pull request #2884 from SeedCompany/0649-add-field-countries-to-partner
2 parents a834cf5 + 590f62c commit 90647c3

File tree

7 files changed

+88
-1
lines changed

7 files changed

+88
-1
lines changed

src/components/location/dto/location.dto.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
Secured,
1111
SecuredEnum,
1212
SecuredProperty,
13+
SecuredPropertyList,
1314
SecuredProps,
1415
SecuredString,
1516
SecuredStringNullable,
@@ -54,6 +55,11 @@ export class Location extends Resource {
5455
})
5556
export class SecuredLocation extends SecuredProperty(Location) {}
5657

58+
@ObjectType({
59+
description: SecuredPropertyList.descriptionFor('a list of locations'),
60+
})
61+
export class SecuredLocations extends SecuredPropertyList(Location) {}
62+
5763
declare module '~/core/resources/map' {
5864
interface ResourceMap {
5965
Location: typeof Location;

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { Transform, Type } from 'class-transformer';
33
import { Matches, ValidateNested } from 'class-validator';
44
import { uniq } from 'lodash';
55
import { ID, IdField, IdOf, IsId, NameField } from '../../../common';
6+
import { Location } from '../../../components/location';
67
import { FieldRegion } from '../../field-region';
78
import type { Language } from '../../language';
89
import { FinancialReportingType } from '../../partnership/dto/financial-reporting-type';
@@ -43,6 +44,11 @@ export abstract class CreatePartner {
4344
@IdField({ nullable: true })
4445
readonly languageOfWiderCommunicationId?: IdOf<Language> | null;
4546

47+
@Field(() => [IDType], { nullable: true })
48+
@IsId({ each: true })
49+
@Transform(({ value }) => uniq(value))
50+
readonly countries?: ReadonlyArray<IdOf<Location>> = [];
51+
4652
@Field(() => [IDType], { nullable: true })
4753
@IsId({ each: true })
4854
@Transform(({ value }) => uniq(value))

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
Sensitivity,
2020
SensitivityField,
2121
} from '../../../common';
22+
import { Location } from '../../../components/location';
2223
import { ScopedRole } from '../../authorization';
2324
import { FieldRegion } from '../../field-region';
2425
import type { Language } from '../../language';
@@ -78,6 +79,7 @@ export class Partner extends Interfaces {
7879
readonly languageOfWiderCommunication: Secured<IdOf<Language> | null>;
7980

8081
readonly fieldRegions: Required<Secured<ReadonlyArray<IdOf<FieldRegion>>>>;
82+
readonly countries: Required<Secured<ReadonlyArray<IdOf<Location>>>>;
8183

8284
@DateTimeField()
8385
readonly modifiedAt: DateTime;

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { Transform, Type } from 'class-transformer';
33
import { Matches, ValidateNested } from 'class-validator';
44
import { uniq } from 'lodash';
55
import { ID, IdField, IdOf, IsId, NameField } from '../../../common';
6+
import { Location } from '../../../components/location';
67
import { FieldRegion } from '../../field-region';
78
import type { Language } from '../../language';
89
import { FinancialReportingType } from '../../partnership/dto/financial-reporting-type';
@@ -43,6 +44,11 @@ export abstract class UpdatePartner {
4344
@IdField({ nullable: true })
4445
readonly languageOfWiderCommunicationId?: IdOf<Language> | null;
4546

47+
@Field(() => [IDType], { nullable: true })
48+
@IsId({ each: true })
49+
@Transform(({ value }) => (value ? uniq(value) : undefined))
50+
readonly countries?: ReadonlyArray<IdOf<Location>>;
51+
4652
@Field(() => [IDType], { nullable: true })
4753
@IsId({ each: true })
4854
@Transform(({ value }) => (value ? uniq(value) : undefined))

src/components/partner/partner.repository.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ export class PartnerRepository extends DtoRepository<
6464
input.languageOfWiderCommunicationId,
6565
],
6666
fieldRegions: ['FieldRegion', input.fieldRegions],
67+
countries: ['Location', input.countries],
6768
}),
6869
)
6970
.return<{ id: ID }>('node.id as id')
@@ -114,6 +115,15 @@ export class PartnerRepository extends DtoRepository<
114115
])
115116
.return(collect('fieldRegions.id').as('fieldRegionsIds')),
116117
)
118+
.subQuery('node', (sub) =>
119+
sub
120+
.match([
121+
node('node'),
122+
relation('out', '', 'countries'),
123+
node('countries', 'Location'),
124+
])
125+
.return(collect('countries.id').as('countriesIds')),
126+
)
117127
.apply(matchProps())
118128
.optionalMatch([
119129
node('node'),
@@ -137,6 +147,7 @@ export class PartnerRepository extends DtoRepository<
137147
pointOfContact: 'pointOfContact.id',
138148
languageOfWiderCommunication: 'languageOfWiderCommunication.id',
139149
fieldRegions: 'fieldRegionsIds',
150+
countries: 'countriesIds',
140151
scope: 'scopedRoles',
141152
pinned: 'exists((:User { id: $requestingUser })-[:pinned]->(node))',
142153
}).as('dto'),

src/components/partner/partner.resolver.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
import { Loader, LoaderOf } from '../../core';
2020
import { FieldRegionLoader, SecuredFieldRegions } from '../field-region';
2121
import { LanguageLoader, SecuredLanguageNullable } from '../language';
22+
import { LocationLoader, SecuredLocations } from '../location';
2223
import { OrganizationLoader, SecuredOrganization } from '../organization';
2324
import { PartnerLoader, PartnerService } from '../partner';
2425
import {
@@ -103,6 +104,14 @@ export class PartnerResolver {
103104
return await loadSecuredIds(loader, partner.fieldRegions);
104105
}
105106

107+
@ResolveField(() => SecuredLocations)
108+
async countries(
109+
@Parent() partner: Partner,
110+
@Loader(LocationLoader) loader: LoaderOf<LocationLoader>,
111+
): Promise<SecuredLocations> {
112+
return await loadSecuredIds(loader, partner.countries);
113+
}
114+
106115
@ResolveField(() => SecuredProjectList, {
107116
description: 'The list of projects the partner has a partnership with.',
108117
})

src/components/partner/partner.service.ts

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,20 @@ import { forwardRef, Inject, Injectable } from '@nestjs/common';
22
import {
33
DuplicateException,
44
ID,
5+
IdOf,
56
InputException,
7+
loadManyIgnoreMissingThrowAny,
68
NotFoundException,
79
ObjectView,
810
ServerException,
911
Session,
1012
UnauthorizedException,
1113
UnsecuredDto,
1214
} from '../../common';
13-
import { HandleIdLookup, ILogger, Logger } from '../../core';
15+
import { HandleIdLookup, ILogger, Logger, ResourceLoader } from '../../core';
1416
import { mapListResults } from '../../core/database/results';
1517
import { Privileges } from '../authorization';
18+
import { Location, LocationLoader, LocationType } from '../location';
1619
import { FinancialReportingType } from '../partnership/dto/financial-reporting-type';
1720
import {
1821
IProject,
@@ -38,6 +41,7 @@ export class PartnerService {
3841
@Inject(forwardRef(() => ProjectService))
3942
private readonly projectService: ProjectService & {},
4043
private readonly repo: PartnerRepository,
44+
private readonly resourceLoader: ResourceLoader,
4145
) {}
4246

4347
async create(input: CreatePartner, session: Session): Promise<Partner> {
@@ -55,6 +59,10 @@ export class PartnerService {
5559
);
5660
}
5761

62+
if (input.countries) {
63+
await this.verifyCountries(input.countries);
64+
}
65+
5866
const id = await this.repo.create(input, session);
5967

6068
this.logger.debug(`Partner created`, { id });
@@ -125,6 +133,7 @@ export class PartnerService {
125133
pointOfContactId,
126134
languageOfWiderCommunicationId,
127135
fieldRegions,
136+
countries,
128137
...simpleChanges
129138
} = changes;
130139

@@ -148,6 +157,22 @@ export class PartnerService {
148157
);
149158
}
150159

160+
if (countries) {
161+
await this.verifyCountries(countries);
162+
163+
try {
164+
await this.repo.updateRelationList({
165+
id: partner.id,
166+
relation: 'countries',
167+
newList: countries,
168+
});
169+
} catch (e) {
170+
throw e instanceof InputException
171+
? e.withField('partner.countries')
172+
: e;
173+
}
174+
}
175+
151176
if (fieldRegions) {
152177
try {
153178
await this.repo.updateRelationList({
@@ -234,4 +259,26 @@ export class PartnerService {
234259
? false
235260
: true;
236261
}
262+
263+
private async verifyCountries(ids: ReadonlyArray<IdOf<Location>>) {
264+
const loader = await this.resourceLoader.getLoader(LocationLoader);
265+
const locations = await loadManyIgnoreMissingThrowAny(loader, ids);
266+
const invalidIds = locations.flatMap((location) =>
267+
location.type.value !== 'Country' ? location.id : [],
268+
);
269+
if (invalidIds.length === 0) {
270+
return;
271+
}
272+
const ex = new LocationTypeException([LocationType.Country], invalidIds);
273+
throw ex.withField('partner.countries');
274+
}
275+
}
276+
277+
class LocationTypeException extends InputException {
278+
constructor(
279+
readonly allowedTypes: readonly LocationType[],
280+
readonly invalidIds: ID[],
281+
) {
282+
super('Given locations do not match the expected type');
283+
}
237284
}

0 commit comments

Comments
 (0)