From a9561dac7ba5f04e9e8a63601f24e6b16e124e53 Mon Sep 17 00:00:00 2001 From: Vittorio Caprio Date: Thu, 25 Jul 2024 12:39:25 +0200 Subject: [PATCH] IMN-657 GET EService descriptor by ID (#712) Co-authored-by: Eric Camellini --- packages/api-clients/src/bffApi.ts | 2 +- packages/bff/src/model/api/apiConverter.ts | 2 +- .../converters/catalogClientApiConverter.ts | 219 ++++++++++++++++++ .../converters/tenantClientApiConverters.ts | 64 +++++ packages/bff/src/model/domain/errors.ts | 44 +++- packages/bff/src/model/modelMappingUtils.ts | 59 +++++ packages/bff/src/model/validators.ts | 69 +++++- packages/bff/src/routers/catalogRouter.ts | 47 +++- packages/bff/src/services/catalogService.ts | 205 +++++++++++++--- packages/bff/src/utilities/errorMappers.ts | 5 +- 10 files changed, 665 insertions(+), 51 deletions(-) create mode 100644 packages/bff/src/model/api/converters/catalogClientApiConverter.ts create mode 100644 packages/bff/src/model/api/converters/tenantClientApiConverters.ts create mode 100644 packages/bff/src/model/modelMappingUtils.ts diff --git a/packages/api-clients/src/bffApi.ts b/packages/api-clients/src/bffApi.ts index 0adb168537..87b1d37001 100644 --- a/packages/api-clients/src/bffApi.ts +++ b/packages/api-clients/src/bffApi.ts @@ -3,7 +3,7 @@ import { QueryParametersByAlias } from "./utils.js"; type BffApi = typeof bffApi.eservicesApi.api; -export type GetCatalogQueryParam = QueryParametersByAlias< +export type BffGetCatalogQueryParam = QueryParametersByAlias< BffApi, "getEServicesCatalog" >; diff --git a/packages/bff/src/model/api/apiConverter.ts b/packages/bff/src/model/api/apiConverter.ts index 9a03dc48be..ac8cf6d802 100644 --- a/packages/bff/src/model/api/apiConverter.ts +++ b/packages/bff/src/model/api/apiConverter.ts @@ -42,7 +42,7 @@ export function toDescriptorWithOnlyAttributes( } export function toEserviceCatalogProcessQueryParams( - queryParams: bffApi.GetCatalogQueryParam + queryParams: bffApi.BffGetCatalogQueryParam ): catalogApi.GetCatalogQueryParam { return { ...queryParams, diff --git a/packages/bff/src/model/api/converters/catalogClientApiConverter.ts b/packages/bff/src/model/api/converters/catalogClientApiConverter.ts new file mode 100644 index 0000000000..e153a0344c --- /dev/null +++ b/packages/bff/src/model/api/converters/catalogClientApiConverter.ts @@ -0,0 +1,219 @@ +/* eslint-disable functional/immutable-data */ +/* eslint-disable max-params */ +import { DescriptorWithOnlyAttributes } from "pagopa-interop-agreement-lifecycle"; +import { + agreementApi, + attributeRegistryApi, + bffApi, + catalogApi, + tenantApi, +} from "pagopa-interop-api-clients"; +import { EServiceAttribute, unsafeBrandId } from "pagopa-interop-models"; +import { attributeNotExists } from "../../domain/errors.js"; +import { getTenantEmail, isUpgradable } from "../../modelMappingUtils.js"; +import { catalogApiDescriptorState } from "../apiTypes.js"; + +export function toEserviceCatalogProcessQueryParams( + queryParams: bffApi.BffGetCatalogQueryParam +): catalogApi.GetCatalogQueryParam { + return { + ...queryParams, + eservicesIds: [], + name: queryParams.q, + }; +} + +export function toBffCatalogApiEService( + eservice: catalogApi.EService, + producerTenant: tenantApi.Tenant, + hasCertifiedAttributes: boolean, + isRequesterEqProducer: boolean, + activeDescriptor?: catalogApi.EServiceDescriptor, + agreement?: agreementApi.Agreement +): bffApi.CatalogEService { + const partialEnhancedEservice = { + id: eservice.id, + name: eservice.name, + description: eservice.description, + producer: { + id: eservice.producerId, + name: producerTenant.name, + }, + isMine: isRequesterEqProducer, + hasCertifiedAttributes, + }; + + return { + ...partialEnhancedEservice, + ...(activeDescriptor + ? { + activeDescriptor: { + id: activeDescriptor.id, + version: activeDescriptor.version, + audience: activeDescriptor.audience, + state: activeDescriptor.state, + }, + } + : {}), + ...(agreement + ? { + agreement: { + id: agreement.id, + state: agreement.state, + canBeUpgraded: isUpgradable(eservice, agreement), + }, + } + : {}), + }; +} + +export function toBffCatalogApiDescriptorAttribute( + attributes: attributeRegistryApi.Attribute[], + descriptorAttributes: catalogApi.Attribute[] +): bffApi.DescriptorAttribute[] { + return descriptorAttributes.map((attribute) => { + const foundAttribute = attributes.find((att) => att.id === attribute.id); + if (!foundAttribute) { + throw attributeNotExists(unsafeBrandId(attribute.id)); + } + + return { + id: attribute.id, + name: foundAttribute.name, + description: foundAttribute.description, + explicitAttributeVerification: attribute.explicitAttributeVerification, + }; + }); +} + +export function toBffCatalogApiDescriptorDoc( + document: catalogApi.EServiceDoc +): bffApi.EServiceDoc { + return { + id: document.id, + name: document.name, + contentType: document.contentType, + prettyName: document.prettyName, + }; +} + +export function toBffCatalogApiEserviceRiskAnalysis( + riskAnalysis: catalogApi.EServiceRiskAnalysis +): bffApi.EServiceRiskAnalysis { + const answers: bffApi.RiskAnalysisForm["answers"] = + riskAnalysis.riskAnalysisForm.singleAnswers + .concat( + riskAnalysis.riskAnalysisForm.multiAnswers.flatMap((multiAnswer) => + multiAnswer.values.map((answerValue) => ({ + id: multiAnswer.id, + value: answerValue, + key: multiAnswer.key, + })) + ) + ) + .reduce((answers: bffApi.RiskAnalysisForm["answers"], answer) => { + const key = answer.key; + if (answers[key] && answer.value) { + answers[key] = [...answers[key], answer.value]; + } else { + answers[key] = []; + } + + return answers; + }, {}); + + const riskAnalysisForm: bffApi.RiskAnalysisForm = { + riskAnalysisId: riskAnalysis.id, + version: riskAnalysis.riskAnalysisForm.version, + answers, + }; + + return { + id: riskAnalysis.id, + name: riskAnalysis.name, + createdAt: riskAnalysis.createdAt, + riskAnalysisForm, + }; +} + +export function toBffCatalogApiProducerDescriptorEService( + eservice: catalogApi.EService, + producer: tenantApi.Tenant +): bffApi.ProducerDescriptorEService { + const producerMail = getTenantEmail(producer); + + const notDraftDecriptors: bffApi.CompactDescriptor[] = + eservice.descriptors.filter( + (d) => d.state !== catalogApiDescriptorState.DRAFT + ); + + const draftDescriptor: bffApi.CompactDescriptor | undefined = + eservice.descriptors.find( + (d) => d.state === catalogApiDescriptorState.DRAFT + ); + + return { + id: eservice.id, + name: eservice.name, + description: eservice.description, + technology: eservice.technology, + mode: eservice.mode, + mail: producerMail && { + address: producerMail.address, + description: producerMail.description, + }, + draftDescriptor, + riskAnalysis: eservice.riskAnalysis.map( + toBffCatalogApiEserviceRiskAnalysis + ), + descriptors: notDraftDecriptors, + }; +} + +export function toEserviceAttribute( + attributes: catalogApi.Attribute[] +): EServiceAttribute[] { + return attributes.map((attribute) => ({ + ...attribute, + id: unsafeBrandId(attribute.id), + })); +} + +export function toDescriptorWithOnlyAttributes( + descriptor: catalogApi.EServiceDescriptor +): DescriptorWithOnlyAttributes { + return { + ...descriptor, + attributes: { + certified: descriptor.attributes.certified.map(toEserviceAttribute), + declared: descriptor.attributes.declared.map(toEserviceAttribute), + verified: descriptor.attributes.verified.map(toEserviceAttribute), + }, + }; +} + +export function toBffCatalogApiDescriptorAttributes( + attributes: attributeRegistryApi.Attribute[], + descriptor: catalogApi.EServiceDescriptor +): bffApi.DescriptorAttributes { + return { + certified: [ + toBffCatalogApiDescriptorAttribute( + attributes, + descriptor.attributes.certified.flat() + ), + ], + declared: [ + toBffCatalogApiDescriptorAttribute( + attributes, + descriptor.attributes.declared.flat() + ), + ], + verified: [ + toBffCatalogApiDescriptorAttribute( + attributes, + descriptor.attributes.verified.flat() + ), + ], + }; +} diff --git a/packages/bff/src/model/api/converters/tenantClientApiConverters.ts b/packages/bff/src/model/api/converters/tenantClientApiConverters.ts new file mode 100644 index 0000000000..6555ce74e1 --- /dev/null +++ b/packages/bff/src/model/api/converters/tenantClientApiConverters.ts @@ -0,0 +1,64 @@ +import { TenantWithOnlyAttributes } from "pagopa-interop-agreement-lifecycle"; +import { tenantApi } from "pagopa-interop-api-clients"; +import { + CertifiedTenantAttribute, + DeclaredTenantAttribute, + TenantAttribute, + VerifiedTenantAttribute, + tenantAttributeType, + unsafeBrandId, +} from "pagopa-interop-models"; + +export function toTenantAttribute( + att: tenantApi.TenantAttribute +): TenantAttribute[] { + const certified: CertifiedTenantAttribute | undefined = att.certified && { + id: unsafeBrandId(att.certified.id), + type: tenantAttributeType.CERTIFIED, + revocationTimestamp: att.certified.revocationTimestamp + ? new Date(att.certified.revocationTimestamp) + : undefined, + assignmentTimestamp: new Date(att.certified.assignmentTimestamp), + }; + + const verified: VerifiedTenantAttribute | undefined = att.verified && { + id: unsafeBrandId(att.verified.id), + type: tenantAttributeType.VERIFIED, + assignmentTimestamp: new Date(att.verified.assignmentTimestamp), + verifiedBy: att.verified.verifiedBy.map((v) => ({ + id: v.id, + verificationDate: new Date(v.verificationDate), + expirationDate: v.expirationDate ? new Date(v.expirationDate) : undefined, + extensionDate: v.extensionDate ? new Date(v.extensionDate) : undefined, + })), + revokedBy: att.verified.revokedBy.map((r) => ({ + id: r.id, + verificationDate: new Date(r.verificationDate), + revocationDate: new Date(r.revocationDate), + expirationDate: r.expirationDate ? new Date(r.expirationDate) : undefined, + extensionDate: r.extensionDate ? new Date(r.extensionDate) : undefined, + })), + }; + + const declared: DeclaredTenantAttribute | undefined = att.declared && { + id: unsafeBrandId(att.declared.id), + type: tenantAttributeType.DECLARED, + assignmentTimestamp: new Date(att.declared.assignmentTimestamp), + revocationTimestamp: att.declared.revocationTimestamp + ? new Date(att.declared.revocationTimestamp) + : undefined, + }; + + return [certified, verified, declared].filter( + (a): a is TenantAttribute => !!a + ); +} + +export function toTenantWithOnlyAttributes( + tenant: tenantApi.Tenant +): TenantWithOnlyAttributes { + return { + ...tenant, + attributes: tenant.attributes.map(toTenantAttribute).flat(), + }; +} diff --git a/packages/bff/src/model/domain/errors.ts b/packages/bff/src/model/domain/errors.ts index 513d0f0880..173acc1404 100644 --- a/packages/bff/src/model/domain/errors.ts +++ b/packages/bff/src/model/domain/errors.ts @@ -2,15 +2,19 @@ import { ApiError, PurposeId, makeApiProblemBuilder, + AttributeId, } from "pagopa-interop-models"; export const errorCodes = { purposeNotFound: "0001", - missingClaim: "0002", - tenantLoginNotAllowed: "0003", - tokenVerificationFailed: "0004", - userNotFound: "0005", - selfcareEntityNotFilled: "0006", + userNotFound: "0002", + selfcareEntityNotFilled: "0003", + descriptorNotFound: "0004", + attributeNotExists: "0005", + invalidEserviceRequester: "0006", + missingClaim: "0007", + tenantLoginNotAllowed: "0008", + tokenVerificationFailed: "0009", }; export type ErrorCodes = keyof typeof errorCodes; @@ -47,6 +51,36 @@ export function purposeNotFound(purposeId: PurposeId): ApiError { }); } +export function invalidEServiceRequester( + eServiceId: string, + requesterId: string +): ApiError { + return new ApiError({ + detail: `EService ${eServiceId} does not belong to producer ${requesterId}`, + code: "invalidEserviceRequester", + title: `Invalid eservice requester`, + }); +} + +export function eserviceDescriptorNotFound( + eServiceId: string, + descriptorId: string +): ApiError { + return new ApiError({ + detail: `Descriptor ${descriptorId} not found in Eservice ${eServiceId}`, + code: "descriptorNotFound", + title: `Descriptor not found`, + }); +} + +export function attributeNotExists(id: AttributeId): ApiError { + return new ApiError({ + detail: `Attribute ${id} does not exist in the attribute registry`, + code: "attributeNotExists", + title: "Attribute not exists", + }); +} + export function missingClaim(claimName: string): ApiError { return new ApiError({ detail: `Claim ${claimName} has not been passed`, diff --git a/packages/bff/src/model/modelMappingUtils.ts b/packages/bff/src/model/modelMappingUtils.ts new file mode 100644 index 0000000000..618cd6a8d6 --- /dev/null +++ b/packages/bff/src/model/modelMappingUtils.ts @@ -0,0 +1,59 @@ +import { + agreementApi, + catalogApi, + tenantApi, +} from "pagopa-interop-api-clients"; +import { + agreementApiState, + catalogApiDescriptorState, +} from "./api/apiTypes.js"; + +/* + This file contains commons utility functions + used to pick or transform data from model to another. +*/ + +const activeDescriptorStatesFilter: catalogApi.EServiceDescriptorState[] = [ + catalogApiDescriptorState.PUBLISHED, + catalogApiDescriptorState.SUSPENDED, + catalogApiDescriptorState.DEPRECATED, +]; + +export function getLatestActiveDescriptor( + eservice: catalogApi.EService +): catalogApi.EServiceDescriptor | undefined { + return eservice.descriptors + .filter((d) => activeDescriptorStatesFilter.includes(d.state)) + .sort((a, b) => Number(a.version) - Number(b.version)) + .at(-1); +} + +export function getTenantEmail( + tenant: tenantApi.Tenant +): tenantApi.Mail | undefined { + return tenant.mails.find( + (m) => m.kind === tenantApi.MailKind.Values.CONTACT_EMAIL + ); +} + +export function isUpgradable( + eservice: catalogApi.EService, + agreement: agreementApi.Agreement +): boolean { + const eserviceDescriptor = eservice.descriptors.find( + (e) => e.id === agreement.descriptorId + ); + + return ( + eserviceDescriptor !== undefined && + eservice.descriptors + .filter((d) => Number(d.version) > Number(eserviceDescriptor.version)) + .find( + (d) => + (d.state === catalogApiDescriptorState.PUBLISHED || + d.state === catalogApiDescriptorState.SUSPENDED) && + (agreement.state === agreementApiState.ACTIVE || + agreement.state === agreementApiState.SUSPENDED) + ) !== undefined + ); +} diff --git a/packages/bff/src/model/validators.ts b/packages/bff/src/model/validators.ts index cbd8d11bbc..f11f0bace7 100644 --- a/packages/bff/src/model/validators.ts +++ b/packages/bff/src/model/validators.ts @@ -1,15 +1,74 @@ import { certifiedAttributesSatisfied } from "pagopa-interop-agreement-lifecycle"; -import { catalogApi, tenantApi } from "pagopa-interop-api-clients"; import { - toDescriptorWithOnlyAttributes, - toTenantWithOnlyAttributes, -} from "./api/apiConverter.js"; + agreementApi, + catalogApi, + tenantApi, +} from "pagopa-interop-api-clients"; +import { TenantId } from "pagopa-interop-models"; +import { + agreementApiState, + catalogApiDescriptorState, +} from "./api/apiTypes.js"; +import { toDescriptorWithOnlyAttributes } from "./api/converters/catalogClientApiConverter.js"; +import { toTenantWithOnlyAttributes } from "./api/converters/tenantClientApiConverters.js"; +import { invalidEServiceRequester } from "./domain/errors.js"; + +const subscribedAgreementStates: agreementApi.AgreementState[] = [ + agreementApiState.PENDING, + agreementApiState.ACTIVE, + agreementApiState.SUSPENDED, +]; export const catalogProcessApiEServiceDescriptorCertifiedAttributesSatisfied = ( - descriptor: catalogApi.EServiceDescriptor, + descriptor: catalogApi.EServiceDescriptor | undefined, tenant: tenantApi.Tenant ): boolean => + descriptor !== undefined && certifiedAttributesSatisfied( toDescriptorWithOnlyAttributes(descriptor), toTenantWithOnlyAttributes(tenant) ); + +export function isAgreementSubscribed( + agreement: agreementApi.Agreement | undefined +): boolean { + return !!agreement && subscribedAgreementStates.includes(agreement.state); +} + +export function isAgreementUpgradable( + eservice: catalogApi.EService, + agreement: agreementApi.Agreement +): boolean { + const eserviceDescriptor = eservice.descriptors.find( + (e) => e.id === agreement.descriptorId + ); + + return ( + eserviceDescriptor !== undefined && + eservice.descriptors + .filter((d) => Number(d.version) > Number(eserviceDescriptor.version)) + .find( + (d) => + (d.state === catalogApiDescriptorState.PUBLISHED || + d.state === catalogApiDescriptorState.SUSPENDED) && + (agreement.state === agreementApiState.ACTIVE || + agreement.state === agreementApiState.SUSPENDED) + ) !== undefined + ); +} + +export function isRequesterEserviceProducer( + requesterId: string, + eservice: catalogApi.EService +): boolean { + return requesterId === eservice.producerId; +} + +export function assertRequesterIsProducer( + requesterId: TenantId, + eservice: catalogApi.EService +): void { + if (!isRequesterEserviceProducer(requesterId, eservice)) { + throw invalidEServiceRequester(eservice.id, requesterId); + } +} diff --git a/packages/bff/src/routers/catalogRouter.ts b/packages/bff/src/routers/catalogRouter.ts index 6058bca860..d897322355 100644 --- a/packages/bff/src/routers/catalogRouter.ts +++ b/packages/bff/src/routers/catalogRouter.ts @@ -10,7 +10,7 @@ import { bffApi } from "pagopa-interop-api-clients"; import { unsafeBrandId } from "pagopa-interop-models"; import { PagoPAInteropBeClients } from "../providers/clientProvider.js"; import { catalogServiceBuilder } from "../services/catalogService.js"; -import { toEserviceCatalogProcessQueryParams } from "../model/api/apiConverter.js"; +import { toEserviceCatalogProcessQueryParams } from "../model/api/converters/catalogClientApiConverter.js"; import { makeApiProblem } from "../model/domain/errors.js"; import { bffGetCatalogErrorMapper, @@ -24,6 +24,7 @@ const catalogRouter = ( catalogProcessClient, tenantProcessClient, agreementProcessClient, + attributeProcessClient, }: PagoPAInteropBeClients ): ZodiosRouter => { const catalogRouter = ctx.router(bffApi.eservicesApi.api, { @@ -33,7 +34,8 @@ const catalogRouter = ( const catalogService = catalogServiceBuilder( catalogProcessClient, tenantProcessClient, - agreementProcessClient + agreementProcessClient, + attributeProcessClient ); catalogRouter @@ -55,12 +57,45 @@ const catalogRouter = ( } }) .get("/producers/eservices", async (_req, res) => res.status(501).send()) - .get("/producers/eservices/:eserviceId", async (_req, res) => - res.status(501).send() - ) + .get("/producers/eservices/:eserviceId", async (req, res) => { + const ctx = fromBffAppContext(req.ctx, req.headers); + try { + const response = await catalogService.getProducerEServiceDetails( + req.params.eserviceId, + ctx + ); + return res.status(200).json(response).send(); + } catch (error) { + const errorRes = makeApiProblem( + error, + bffGetCatalogErrorMapper, + ctx.logger + ); + return res.status(errorRes.status).json(errorRes).end(); + } + }) .get( "/producers/eservices/:eserviceId/descriptors/:descriptorId", - async (_req, res) => res.status(501).send() + async (req, res) => { + const ctx = fromBffAppContext(req.ctx, req.headers); + + try { + const response = await catalogService.getProducerEServiceDescriptor( + unsafeBrandId(req.params.eserviceId), + unsafeBrandId(req.params.descriptorId), + ctx + ); + return res.status(200).json(response).send(); + } catch (error) { + const errorRes = makeApiProblem( + error, + bffGetCatalogErrorMapper, + ctx.logger, + `Error retrieving producer descriptor ${req.params.descriptorId} for eservice ${req.params.eserviceId}` + ); + return res.status(errorRes.status).json(errorRes).end(); + } + } ) .get( "/catalog/eservices/:eserviceId/descriptor/:descriptorId", diff --git a/packages/bff/src/services/catalogService.ts b/packages/bff/src/services/catalogService.ts index 3105958873..02aeeee65b 100644 --- a/packages/bff/src/services/catalogService.ts +++ b/packages/bff/src/services/catalogService.ts @@ -1,37 +1,35 @@ /* eslint-disable @typescript-eslint/explicit-function-return-type */ +import { + attributeRegistryApi, + bffApi, + catalogApi, + tenantApi, +} from "pagopa-interop-api-clients"; +import { DescriptorId, EServiceId } from "pagopa-interop-models"; import { WithLogger } from "pagopa-interop-commons"; -import { catalogApi, tenantApi, bffApi } from "pagopa-interop-api-clients"; -import { match } from "ts-pattern"; -import { EServiceId } from "pagopa-interop-models"; -import { toBffCatalogApiEServiceResponse } from "../model/api/apiConverter.js"; -import { catalogProcessApiEServiceDescriptorCertifiedAttributesSatisfied } from "../model/validators.js"; +import { + toBffCatalogApiDescriptorAttributes, + toBffCatalogApiDescriptorDoc, + toBffCatalogApiEService, + toBffCatalogApiEserviceRiskAnalysis, + toBffCatalogApiProducerDescriptorEService, +} from "../model/api/converters/catalogClientApiConverter.js"; + +import { eserviceDescriptorNotFound } from "../model/domain/errors.js"; +import { getLatestActiveDescriptor } from "../model/modelMappingUtils.js"; +import { + assertRequesterIsProducer, + catalogProcessApiEServiceDescriptorCertifiedAttributesSatisfied, +} from "../model/validators.js"; import { AgreementProcessClient, + AttributeProcessClient, CatalogProcessClient, TenantProcessClient, } from "../providers/clientProvider.js"; import { BffAppContext, Headers } from "../utilities/context.js"; -import { catalogApiDescriptorState } from "../model/api/apiTypes.js"; import { getLatestAgreement } from "./agreementService.js"; -function activeDescriptorStateFilter( - descriptor: catalogApi.EServiceDescriptor -): boolean { - return match(descriptor.state) - .with( - catalogApiDescriptorState.PUBLISHED, - catalogApiDescriptorState.SUSPENDED, - catalogApiDescriptorState.DEPRECATED, - () => true - ) - .with( - catalogApiDescriptorState.DRAFT, - catalogApiDescriptorState.ARCHIVED, - () => false - ) - .exhaustive(); -} - export type CatalogService = ReturnType; const enhanceCatalogEService = @@ -59,11 +57,7 @@ const enhanceCatalogEService = }) : producerTenant; - const latestActiveDescriptor: catalogApi.EServiceDescriptor | undefined = - eservice.descriptors - .filter(activeDescriptorStateFilter) - .sort((a, b) => Number(a.version) - Number(b.version)) - .at(-1); + const latestActiveDescriptor = getLatestActiveDescriptor(eservice); const latestAgreement = await getLatestAgreement( agreementProcessClient, @@ -80,7 +74,7 @@ const enhanceCatalogEService = requesterTenant ); - return toBffCatalogApiEServiceResponse( + return toBffCatalogApiEService( eservice, producerTenant, hasCertifiedAttributes, @@ -90,10 +84,69 @@ const enhanceCatalogEService = ); }; +const getBulkAttributes = async ( + attributeProcessClient: AttributeProcessClient, + headers: Headers, + descriptorAttributeIds: string[] +) => { + // Fetch all attributes in a recursive way + const attributesBulk = async ( + offset: number, + result: attributeRegistryApi.Attribute[] + ): Promise => { + const attributes = await attributeProcessClient.getBulkedAttributes( + descriptorAttributeIds, + { + headers, + queries: { + limit: 50, + offset, + }, + } + ); + + if (attributes.totalCount <= 50) { + return result.concat(attributes.results); + } else { + return await attributesBulk(offset + 50, result); + } + }; + + return await attributesBulk(0, []); +}; + +const retrieveEserviceDescriptor = ( + eservice: catalogApi.EService, + descriptorId: DescriptorId +): catalogApi.EServiceDescriptor => { + const descriptor = eservice.descriptors.find((e) => e.id === descriptorId); + + if (!descriptor) { + throw eserviceDescriptorNotFound(eservice.id, descriptorId); + } + + return descriptor; +}; + +const getAttributeIds = ( + descriptor: catalogApi.EServiceDescriptor +): string[] => [ + ...descriptor.attributes.certified.flatMap((atts) => + atts.map((att) => att.id) + ), + ...descriptor.attributes.declared.flatMap((atts) => + atts.map((att) => att.id) + ), + ...descriptor.attributes.verified.flatMap((atts) => + atts.map((att) => att.id) + ), +]; + export function catalogServiceBuilder( catalogProcessClient: CatalogProcessClient, tenantProcessClient: TenantProcessClient, - agreementProcessClient: AgreementProcessClient + agreementProcessClient: AgreementProcessClient, + attributeProcessClient: AttributeProcessClient ) { return { getCatalog: async ( @@ -133,9 +186,97 @@ export function catalogServiceBuilder( totalCount: eservicesResponse.totalCount, }, }; - return response; }, + getProducerEServiceDescriptor: async ( + eserviceId: EServiceId, + descriptorId: DescriptorId, + context: WithLogger + ): Promise => { + const requesterId = context.authData.organizationId; + const headers = context.headers; + + const eservice: catalogApi.EService = + await catalogProcessClient.getEServiceById({ + params: { + eServiceId: eserviceId, + }, + headers, + }); + + assertRequesterIsProducer(requesterId, eservice); + + const descriptor = retrieveEserviceDescriptor(eservice, descriptorId); + + const descriptorAttributeIds = getAttributeIds(descriptor); + + const attributes = await getBulkAttributes( + attributeProcessClient, + headers, + descriptorAttributeIds + ); + + const descriptorAttributes = toBffCatalogApiDescriptorAttributes( + attributes, + descriptor + ); + + const requesterTenant = await tenantProcessClient.tenant.getTenant({ + headers, + params: { + id: requesterId, + }, + }); + + return { + id: descriptor.id, + version: descriptor.version, + description: descriptor.description, + interface: + descriptor.interface && + toBffCatalogApiDescriptorDoc(descriptor.interface), + docs: descriptor.docs.map(toBffCatalogApiDescriptorDoc), + state: descriptor.state, + audience: descriptor.audience, + voucherLifespan: descriptor.voucherLifespan, + dailyCallsPerConsumer: descriptor.dailyCallsPerConsumer, + dailyCallsTotal: descriptor.dailyCallsTotal, + agreementApprovalPolicy: descriptor.agreementApprovalPolicy, + attributes: descriptorAttributes, + eservice: toBffCatalogApiProducerDescriptorEService( + eservice, + requesterTenant + ), + }; + }, + getProducerEServiceDetails: async ( + eServiceId: string, + context: WithLogger + ): Promise => { + const requesterId = context.authData.organizationId; + const headers = context.headers; + + const eservice: catalogApi.EService = + await catalogProcessClient.getEServiceById({ + params: { + eServiceId, + }, + headers, + }); + + assertRequesterIsProducer(requesterId, eservice); + + return { + id: eservice.id, + name: eservice.name, + description: eservice.description, + technology: eservice.technology, + mode: eservice.mode, + riskAnalysis: eservice.riskAnalysis.map( + toBffCatalogApiEserviceRiskAnalysis + ), + }; + }, updateEServiceDescription: async ( headers: Headers, eServiceId: EServiceId, diff --git a/packages/bff/src/utilities/errorMappers.ts b/packages/bff/src/utilities/errorMappers.ts index 2b5371d105..acd1403cbc 100644 --- a/packages/bff/src/utilities/errorMappers.ts +++ b/packages/bff/src/utilities/errorMappers.ts @@ -13,7 +13,10 @@ const { } = constants; export const bffGetCatalogErrorMapper = (error: ApiError): number => - match(error.code).otherwise(() => HTTP_STATUS_INTERNAL_SERVER_ERROR); + match(error.code) + .with("descriptorNotFound", () => HTTP_STATUS_NOT_FOUND) + .with("invalidEserviceRequester", () => HTTP_STATUS_FORBIDDEN) + .otherwise(() => HTTP_STATUS_INTERNAL_SERVER_ERROR); export const reversePurposeUpdateErrorMapper = ( error: ApiError