diff --git a/dbschema/migrations/00021-m1ud7t6.edgeql b/dbschema/migrations/00021-m1ud7t6.edgeql new file mode 100644 index 0000000000..ab1fd34cd1 --- /dev/null +++ b/dbschema/migrations/00021-m1ud7t6.edgeql @@ -0,0 +1,12 @@ +CREATE MIGRATION m1ud7t6egltmyyxi7xts3rea6bkapucm6qx7araq7eedpoee3ujyba + ONTO m17rl5lo5wwq63duwmpqjtdqfuhq3luocyigofuobamdcu6l4dlepq +{ + ALTER TYPE default::Partner { + CREATE LINK parent: default::Partner { + CREATE CONSTRAINT std::exclusive; + }; + CREATE MULTI LINK strategicAlliances: default::Partner { + CREATE CONSTRAINT std::exclusive; + }; + }; +}; diff --git a/dbschema/partner.gel b/dbschema/partner.gel index 207be6945a..2050574a45 100644 --- a/dbschema/partner.gel +++ b/dbschema/partner.gel @@ -41,6 +41,13 @@ module default { multi languagesOfConsulting: Language; multi fieldRegions: FieldRegion; multi countries: Location; + multi strategicAlliances: Partner { + constraint exclusive; + }; + + parent: Partner { + constraint exclusive; + }; startDate: cal::local_date; diff --git a/src/components/partner/dto/create-partner.dto.ts b/src/components/partner/dto/create-partner.dto.ts index cfaf81bbec..b060891057 100644 --- a/src/components/partner/dto/create-partner.dto.ts +++ b/src/components/partner/dto/create-partner.dto.ts @@ -50,6 +50,14 @@ export abstract class CreatePartner { @IdField({ nullable: true }) readonly languageOfWiderCommunicationId?: ID<'Language'> | null; + @IdField({ nullable: true }) + readonly parentId?: ID<'Partner'> | null; + + @Field(() => [IDType], { nullable: true }) + @IsId({ each: true }) + @Transform(({ value }) => uniq(value)) + readonly strategicAlliances?: ReadonlyArray> = []; + @Field(() => [IDType], { nullable: true }) @IsId({ each: true }) @Transform(({ value }) => uniq(value)) diff --git a/src/components/partner/dto/partner.dto.ts b/src/components/partner/dto/partner.dto.ts index b410a35a2b..f705799487 100644 --- a/src/components/partner/dto/partner.dto.ts +++ b/src/components/partner/dto/partner.dto.ts @@ -9,6 +9,7 @@ import { SecuredBoolean, SecuredDateNullable, SecuredProperty, + SecuredPropertyList, SecuredStringNullable, Sensitivity, SensitivityField, @@ -66,6 +67,9 @@ export class Partner extends Interfaces { >; readonly countries: Required>>>; + readonly strategicAlliances: Required< + Secured>> + >; readonly languagesOfConsulting: Required< Secured>> @@ -87,6 +91,8 @@ export class Partner extends Interfaces { @Field() readonly departmentIdBlock: SecuredFinanceDepartmentIdBlockNullable; + + readonly parent: Secured | null>; } @ObjectType({ @@ -94,6 +100,11 @@ export class Partner extends Interfaces { }) export class SecuredPartner extends SecuredProperty(Partner) {} +@ObjectType({ + description: SecuredPropertyList.descriptionFor('a list of partners'), +}) +export class SecuredPartners extends SecuredPropertyList(Partner) {} + declare module '~/core/resources/map' { interface ResourceMap { Partner: typeof Partner; diff --git a/src/components/partner/dto/update-partner.dto.ts b/src/components/partner/dto/update-partner.dto.ts index 071e95372d..4d35cd38f3 100644 --- a/src/components/partner/dto/update-partner.dto.ts +++ b/src/components/partner/dto/update-partner.dto.ts @@ -48,6 +48,13 @@ export abstract class UpdatePartner { @IdField({ nullable: true }) readonly languageOfWiderCommunicationId?: ID<'Language'> | null; + @IdField({ nullable: true }) + readonly parentId?: ID<'Partner'> | null; + + @ListField(() => IDType, { optional: true }) + @IsId({ each: true }) + readonly strategicAlliances?: ReadonlyArray>; + @ListField(() => IDType, { optional: true }) @IsId({ each: true }) readonly countries?: ReadonlyArray>; diff --git a/src/components/partner/partner.gel.repository.ts b/src/components/partner/partner.gel.repository.ts index 03f8f051bb..a1ead15a28 100644 --- a/src/components/partner/partner.gel.repository.ts +++ b/src/components/partner/partner.gel.repository.ts @@ -18,6 +18,8 @@ export class PartnerGelRepository countries: true, languagesOfConsulting: true, departmentIdBlock: departmentIdBlock.hydrate, + strategicAlliances: true, + parent: true, }), omit: ['create', 'update'], }) diff --git a/src/components/partner/partner.repository.ts b/src/components/partner/partner.repository.ts index 77657c4efa..5fa7761dda 100644 --- a/src/components/partner/partner.repository.ts +++ b/src/components/partner/partner.repository.ts @@ -93,6 +93,8 @@ export class PartnerRepository extends DtoRepository(Partner) { fieldRegions: ['FieldRegion', input.fieldRegions], countries: ['Location', input.countries], languagesOfConsulting: ['Language', input.languagesOfConsulting], + strategicAlliances: ['Partner', input.strategicAlliances], + parent: ['Partner', input.parentId], }), ) .apply(departmentIdBlockUtils.createMaybe(input.departmentIdBlock)) @@ -118,11 +120,23 @@ export class PartnerRepository extends DtoRepository(Partner) { countries, languagesOfConsulting, departmentIdBlock, + strategicAlliances, + parentId, ...simpleChanges } = changes; await this.updateProperties({ id }, simpleChanges); + if (parentId !== undefined) { + if (parentId === id) { + throw new InputException( + 'A partner cannot be its own parent organization', + 'partner.parent', + ); + } + await this.updateRelation('parent', 'Partner', changes.id, parentId); + } + if (pointOfContactId !== undefined) { await this.updateRelation( 'pointOfContact', @@ -169,6 +183,26 @@ export class PartnerRepository extends DtoRepository(Partner) { } } + if (strategicAlliances) { + if (strategicAlliances.includes(changes.id)) { + throw new InputException( + 'A partner cannot be its own strategic ally', + 'partner.strategicAlliances', + ); + } + try { + await this.updateRelationList({ + id: changes.id, + relation: 'strategicAlliances', + newList: strategicAlliances, + }); + } catch (e) { + throw e instanceof InputException + ? e.withField('partner.strategicAlliances') + : e; + } + } + if (languagesOfConsulting) { try { await this.updateRelationList({ @@ -257,6 +291,26 @@ export class PartnerRepository extends DtoRepository(Partner) { ), ), ) + .subQuery('node', (sub) => + sub + .match([ + node('node'), + relation('out', '', 'strategicAlliances'), + node('strategicAlliances', 'Partner'), + ]) + .return( + collect('strategicAlliances { .id }').as('strategicAlliances'), + ), + ) + .subQuery('node', (sub) => + sub + .optionalMatch([ + node('node'), + relation('out', '', 'parent', ACTIVE), + node('parent', 'Partner'), + ]) + .return('parent { .id } as parent'), + ) .apply(matchProps()) .optionalMatch([ node('node'), @@ -288,6 +342,8 @@ export class PartnerRepository extends DtoRepository(Partner) { departmentIdBlock: 'departmentIdBlock', scope: 'scopedRoles', pinned, + parent: 'parent { .id }', + strategicAlliances: 'strategicAlliances', }).as('dto'), ); } diff --git a/src/components/partner/partner.resolver.ts b/src/components/partner/partner.resolver.ts index b285523d24..16d34d44e1 100644 --- a/src/components/partner/partner.resolver.ts +++ b/src/components/partner/partner.resolver.ts @@ -42,6 +42,8 @@ import { Partner, PartnerListInput, PartnerListOutput, + SecuredPartner, + SecuredPartners, UpdatePartnerInput, UpdatePartnerOutput, } from './dto'; @@ -60,6 +62,25 @@ export class PartnerResolver { return await partners.load(id); } + @ResolveField(() => SecuredPartner) + async parent( + @Parent() partner: Partner, + @Loader(PartnerLoader) partners: LoaderOf, + ): Promise { + return await mapSecuredValue(partner.parent, ({ id }) => partners.load(id)); + } + + @ResolveField(() => SecuredPartners) + async strategicAlliances( + @Parent() partner: Partner, + @Loader(PartnerLoader) loader: LoaderOf, + ): Promise { + return await loadSecuredIds(loader, { + ...partner.strategicAlliances, + value: partner.strategicAlliances.value?.map((alliance) => alliance.id), + }); + } + @Query(() => PartnerListOutput, { description: 'Look up partners', }) diff --git a/src/components/partner/partner.service.ts b/src/components/partner/partner.service.ts index 3ae475a65c..2c94218c14 100644 --- a/src/components/partner/partner.service.ts +++ b/src/components/partner/partner.service.ts @@ -111,6 +111,19 @@ export class PartnerService { }; } + if (input.strategicAlliances?.includes(input.id)) { + throw new InputException( + 'A partner cannot be its own strategic ally', + 'partner.strategicAlliances', + ); + } + if (input.parentId && input.parentId === input.id) { + throw new InputException( + 'A partner cannot be its own parent organization', + 'partner.parent', + ); + } + const { departmentIdBlock, ...simpleInput } = input; const simpleChanges = this.repo.getActualChanges(partner, simpleInput); const changes = {