diff --git a/packages/api-clients/open-api/bffApi.yml b/packages/api-clients/open-api/bffApi.yml index eecf8dccf4..867172b12e 100644 --- a/packages/api-clients/open-api/bffApi.yml +++ b/packages/api-clients/open-api/bffApi.yml @@ -15996,6 +15996,173 @@ paths: schema: type: integer description: Time interval in milliseconds. Allowed requests will be constantly replenished during the interval. At the end of the interval the max allowed requests will be available + "/catalog/eservices/templates": + parameters: + - $ref: "#/components/parameters/CorrelationIdHeader" + get: + tags: + - eserviceTemplates + summary: Retrieves EService templates catalog + description: Retrieves EService templates catalog + operationId: getEServiceTemplatesCatalog + parameters: + - in: query + name: q + description: Query to filter EService template by name + schema: + type: string + - in: query + name: creatorsIds + description: comma separated sequence of creators IDs + schema: + type: array + items: + type: string + format: uuid + default: [] + explode: false + - in: query + name: offset + required: true + schema: + type: integer + format: int32 + minimum: 0 + - in: query + name: limit + required: true + schema: + type: integer + format: int32 + minimum: 1 + maximum: 50 + responses: + "200": + description: A list of EService templates + content: + application/json: + schema: + $ref: "#/components/schemas/CatalogEServiceTemplates" + headers: + "X-Rate-Limit-Limit": + schema: + type: integer + description: Max allowed requests within time interval + "X-Rate-Limit-Remaining": + schema: + type: integer + description: Remaining requests within time interval + "X-Rate-Limit-Interval": + schema: + type: integer + description: Time interval in milliseconds. Allowed requests will be constantly replenished during the interval. At the end of the interval the max allowed requests will be available + "400": + description: Invalid input + headers: + "X-Rate-Limit-Limit": + schema: + type: integer + description: Max allowed requests within time interval + "X-Rate-Limit-Remaining": + schema: + type: integer + description: Remaining requests within time interval + "X-Rate-Limit-Interval": + schema: + type: integer + description: Time interval in milliseconds. Allowed requests will be constantly replenished during the interval. At the end of the interval the max allowed requests will be available + content: + application/json: + schema: + $ref: "#/components/schemas/Problem" + "429": + description: Too Many Requests + content: + application/json: + schema: + $ref: "#/components/schemas/Problem" + headers: + "X-Rate-Limit-Limit": + schema: + type: integer + description: Max allowed requests within time interval + "X-Rate-Limit-Remaining": + schema: + type: integer + description: Remaining requests within time interval + "X-Rate-Limit-Interval": + schema: + type: integer + description: Time interval in milliseconds. Allowed requests will be constantly replenished during the interval. At the end of the interval the max allowed requests will be available + "/producers/eservices/templates": + parameters: + - $ref: "#/components/parameters/CorrelationIdHeader" + get: + tags: + - eserviceTemplates + summary: Retrieves Producer EService templates + description: Retrieves Producer EService templates + operationId: getProducerEServices + parameters: + - in: query + name: q + description: Query to filter EServices templates by name + schema: + type: string + - in: query + name: offset + required: true + schema: + type: integer + format: int32 + minimum: 0 + - in: query + name: limit + required: true + schema: + type: integer + format: int32 + minimum: 1 + maximum: 50 + responses: + "200": + description: A list of EServices templates + content: + application/json: + schema: + $ref: "#/components/schemas/ProducerEServiceTemplates" + headers: + "X-Rate-Limit-Limit": + schema: + type: integer + description: Max allowed requests within time interval + "X-Rate-Limit-Remaining": + schema: + type: integer + description: Remaining requests within time interval + "X-Rate-Limit-Interval": + schema: + type: integer + description: Time interval in milliseconds. Allowed requests will be constantly replenished during the interval. At the end of the interval the max allowed requests will be available + "429": + description: Too Many Requests + content: + application/json: + schema: + $ref: "#/components/schemas/Problem" + headers: + "X-Rate-Limit-Limit": + schema: + type: integer + description: Max allowed requests within time interval + "X-Rate-Limit-Remaining": + schema: + type: integer + description: Remaining requests within time interval + "X-Rate-Limit-Interval": + schema: + type: integer + description: Time interval in milliseconds. Allowed requests will be constantly replenished during the interval. At the end of the interval the max allowed requests will be available "/status": get: security: [] @@ -19232,6 +19399,69 @@ components: required: - results - totalCount + CatalogEServiceTemplate: + type: object + additionalProperties: false + required: + - id + - name + - description + - creator + - publishedVersion + properties: + id: + type: string + format: uuid + name: + type: string + description: + type: string + creator: + $ref: "#/components/schemas/CompactOrganization" + publishedVersion: + $ref: "#/components/schemas/CompactEServiceTemplateVersion" + ProducerEServiceTemplate: + type: object + additionalProperties: false + required: + - id + - name + properties: + id: + type: string + format: uuid + name: + type: string + activeVersion: + $ref: "#/components/schemas/CompactEServiceTemplateVersion" + draftVersion: + $ref: "#/components/schemas/CompactEServiceTemplateVersion" + CatalogEServiceTemplates: + type: object + additionalProperties: false + properties: + results: + type: array + items: + $ref: "#/components/schemas/CatalogEServiceTemplate" + pagination: + $ref: "#/components/schemas/Pagination" + required: + - results + - pagination + ProducerEServiceTemplates: + type: object + additionalProperties: false + properties: + results: + type: array + items: + $ref: "#/components/schemas/ProducerEServiceTemplate" + pagination: + $ref: "#/components/schemas/Pagination" + required: + - results + - pagination Problem: type: object additionalProperties: false diff --git a/packages/api-clients/open-api/eserviceTemplateApi.yml b/packages/api-clients/open-api/eserviceTemplateApi.yml index f75f80bc5f..5765762d57 100644 --- a/packages/api-clients/open-api/eserviceTemplateApi.yml +++ b/packages/api-clients/open-api/eserviceTemplateApi.yml @@ -43,8 +43,8 @@ paths: schema: type: string - in: query - name: producersIds - description: comma separated sequence of producers IDs + name: eserviceTemplatesIds + description: comma separated sequence of eservice templates IDs schema: type: array items: diff --git a/packages/backend-for-frontend/src/api/eserviceTemplateApiConverter.ts b/packages/backend-for-frontend/src/api/eserviceTemplateApiConverter.ts index 0884a89e84..6da3c5b40f 100644 --- a/packages/backend-for-frontend/src/api/eserviceTemplateApiConverter.ts +++ b/packages/backend-for-frontend/src/api/eserviceTemplateApiConverter.ts @@ -4,7 +4,19 @@ import { tenantApi, } from "pagopa-interop-api-clients"; import { genericError } from "pagopa-interop-models"; +import { catalogEServiceTemplatePublishedVersionNotFound } from "../model/errors.js"; import { toBffCatalogApiEserviceRiskAnalysis } from "./catalogApiConverter.js"; +import { toBffCompactOrganization } from "./agreementApiConverter.js"; + +export function toBffCompactEServiceTemplateVersion( + eserviceTemplateVersion: eserviceTemplateApi.EServiceTemplateVersion +): bffApi.CompactEServiceTemplateVersion { + return { + id: eserviceTemplateVersion.id, + version: eserviceTemplateVersion.version, + state: eserviceTemplateVersion.state, + }; +} export function toBffEServiceTemplateApiEServiceTemplateDetails( eserviceTemplate: eserviceTemplateApi.EServiceTemplate, @@ -16,19 +28,65 @@ export function toBffEServiceTemplateApiEServiceTemplateDetails( audienceDescription: eserviceTemplate.audienceDescription, eserviceDescription: eserviceTemplate.eserviceDescription, technology: eserviceTemplate.technology, - creator: { - id: creator.id, - name: creator.name, - }, + creator: toBffCompactOrganization(creator), mode: eserviceTemplate.mode, riskAnalysis: eserviceTemplate.riskAnalysis.map( toBffCatalogApiEserviceRiskAnalysis ), - versions: eserviceTemplate.versions, + versions: eserviceTemplate.versions.map( + toBffCompactEServiceTemplateVersion + ), isSignalHubEnabled: eserviceTemplate.isSignalHubEnabled, }; } +export function toBffCatalogEServiceTemplate( + eserviceTemplate: eserviceTemplateApi.EServiceTemplate, + creator: tenantApi.Tenant +): bffApi.CatalogEServiceTemplate { + const publishedVersion = eserviceTemplate.versions.find( + (v) => + v.state === + eserviceTemplateApi.EServiceTemplateVersionState.Values.PUBLISHED + ); + + if (!publishedVersion) { + throw catalogEServiceTemplatePublishedVersionNotFound(eserviceTemplate.id); + } + + return { + id: eserviceTemplate.id, + name: eserviceTemplate.name, + description: eserviceTemplate.audienceDescription, + creator: toBffCompactOrganization(creator), + publishedVersion: toBffCompactEServiceTemplateVersion(publishedVersion), + }; +} + +export function toBffProducerEServiceTemplate( + eserviceTemplate: eserviceTemplateApi.EServiceTemplate +): bffApi.ProducerEServiceTemplate { + const activeVersion = eserviceTemplate.versions.find( + (v) => + v.state === + eserviceTemplateApi.EServiceTemplateVersionState.Values.PUBLISHED || + v.state === + eserviceTemplateApi.EServiceTemplateVersionState.Values.SUSPENDED + ); + + const draftVersion = eserviceTemplate.versions.find( + (v) => + v.state === eserviceTemplateApi.EServiceTemplateVersionState.Values.DRAFT + ); + + return { + id: eserviceTemplate.id, + name: eserviceTemplate.name, + activeVersion, + draftVersion, + }; +} + export const toBffCreatedEServiceTemplateVersion = ( eserviceTemplate: eserviceTemplateApi.EServiceTemplate ): bffApi.CreatedEServiceTemplateVersion => { diff --git a/packages/backend-for-frontend/src/model/errors.ts b/packages/backend-for-frontend/src/model/errors.ts index 6fb6892aa6..354cd91940 100644 --- a/packages/backend-for-frontend/src/model/errors.ts +++ b/packages/backend-for-frontend/src/model/errors.ts @@ -52,6 +52,7 @@ export const errorCodes = { eserviceDelegated: "0043", delegatedEserviceNotExportable: "0044", eserviceTemplateVersionNotFound: "0045", + catalogEServiceTemplatePublishedVersionNotFound: "0046", }; export type ErrorCodes = keyof typeof errorCodes; @@ -450,3 +451,13 @@ export function eserviceTemplateVersionNotFound( title: "EService template version not found", }); } + +export function catalogEServiceTemplatePublishedVersionNotFound( + eserviceTemplateId: string +): ApiError { + return new ApiError({ + detail: `Published version not found in catalog Eservice template ${eserviceTemplateId}`, + code: "catalogEServiceTemplatePublishedVersionNotFound", + title: "Catalog EService template published version not found", + }); +} diff --git a/packages/backend-for-frontend/src/routers/eserviceTemplateRouter.ts b/packages/backend-for-frontend/src/routers/eserviceTemplateRouter.ts index 02afcd283b..c30cc5cbf6 100644 --- a/packages/backend-for-frontend/src/routers/eserviceTemplateRouter.ts +++ b/packages/backend-for-frontend/src/routers/eserviceTemplateRouter.ts @@ -12,7 +12,10 @@ import { PagoPAInteropBeClients } from "../clients/clientsProvider.js"; import { eserviceTemplateServiceBuilder } from "../services/eserviceTemplateService.js"; import { fromBffAppContext } from "../utilities/context.js"; import { emptyErrorMapper, makeApiProblem } from "../model/errors.js"; -import { bffGetEServiceTemplateErrorMapper } from "../utilities/errorMappers.js"; +import { + bffGetCatalogEServiceTemplateErrorMapper, + bffGetEServiceTemplateErrorMapper, +} from "../utilities/errorMappers.js"; import { toBffCreatedEServiceTemplateVersion } from "../api/eserviceTemplateApiConverter.js"; const eserviceTemplateRouter = ( @@ -283,6 +286,61 @@ const eserviceTemplateRouter = ( } } ) + .get("/catalog/eservices/templates", async (req, res) => { + const ctx = fromBffAppContext(req.ctx, req.headers); + const { q, creatorsIds, offset, limit } = req.query; + + try { + const response = + await eserviceTemplateService.getCatalogEServiceTemplates( + q, + creatorsIds, + offset, + limit, + ctx + ); + + return res + .status(200) + .send(bffApi.CatalogEServiceTemplates.parse(response)); + } catch (error) { + const errorRes = makeApiProblem( + error, + bffGetCatalogEServiceTemplateErrorMapper, + ctx.logger, + ctx.correlationId, + "Error retrieving Catalog eservice templates" + ); + return res.status(errorRes.status).send(errorRes); + } + }) + .get("/producers/eservices/templates", async (req, res) => { + const ctx = fromBffAppContext(req.ctx, req.headers); + const { q, offset, limit } = req.query; + + try { + const response = + await eserviceTemplateService.getProducerEServiceTemplates( + q, + offset, + limit, + ctx + ); + + return res + .status(200) + .send(bffApi.ProducerEServiceTemplates.parse(response)); + } catch (error) { + const errorRes = makeApiProblem( + error, + emptyErrorMapper, + ctx.logger, + ctx.correlationId, + "Error retrieving producer eservice templates" + ); + return res.status(errorRes.status).send(errorRes); + } + }) .post( "/eservices/templates/:eServiceTemplateId/versions/:eServiceTemplateVersionId/quotas/update", async (req, res) => { diff --git a/packages/backend-for-frontend/src/services/eserviceTemplateService.ts b/packages/backend-for-frontend/src/services/eserviceTemplateService.ts index cb6f6f7425..39bdae3f37 100644 --- a/packages/backend-for-frontend/src/services/eserviceTemplateService.ts +++ b/packages/backend-for-frontend/src/services/eserviceTemplateService.ts @@ -6,7 +6,11 @@ import { EServiceTemplateVersionId, RiskAnalysisId, } from "pagopa-interop-models"; -import { bffApi, eserviceTemplateApi } from "pagopa-interop-api-clients"; +import { + bffApi, + eserviceTemplateApi, + tenantApi, +} from "pagopa-interop-api-clients"; import { AttributeProcessClient, EServiceTemplateProcessClient, @@ -17,8 +21,15 @@ import { toBffCatalogApiDescriptorAttributes, toBffCatalogApiDescriptorDoc, } from "../api/catalogApiConverter.js"; -import { toBffEServiceTemplateApiEServiceTemplateDetails } from "../api/eserviceTemplateApiConverter.js"; -import { eserviceTemplateVersionNotFound } from "../model/errors.js"; +import { + toBffCatalogEServiceTemplate, + toBffEServiceTemplateApiEServiceTemplateDetails, + toBffProducerEServiceTemplate, +} from "../api/eserviceTemplateApiConverter.js"; +import { + eserviceTemplateVersionNotFound, + tenantNotFound, +} from "../model/errors.js"; import { getAllBulkAttributes } from "./attributeService.js"; export function eserviceTemplateServiceBuilder( @@ -264,6 +275,86 @@ export function eserviceTemplateServiceBuilder( ), }; }, + getCatalogEServiceTemplates: async ( + name: string | undefined, + creatorsIds: string[], + offset: number, + limit: number, + { headers, logger }: WithLogger + ): Promise => { + logger.info( + `Retrieving Catalog EService templates for name = ${name}, creatorsIds = ${creatorsIds}, offset = ${offset}, limit = ${limit}` + ); + const eserviceTemplatesResponse: eserviceTemplateApi.EServiceTemplates = + await eserviceTemplateClient.getEServiceTemplates({ + headers, + queries: { + name, + states: [ + eserviceTemplateApi.EServiceTemplateVersionState.Values.PUBLISHED, + ], + creatorsIds, + limit, + offset, + }, + }); + + const creatorTenantsMap = await getTenantsFromEServiceTemplates( + tenantProcessClient, + eserviceTemplatesResponse.results, + headers + ); + + const results = eserviceTemplatesResponse.results.map((template) => { + const creator = creatorTenantsMap.get(template.creatorId); + + if (!creator) { + throw tenantNotFound(template.creatorId); + } + + return toBffCatalogEServiceTemplate(template, creator); + }); + + return { + results, + pagination: { + offset, + limit, + totalCount: eserviceTemplatesResponse.totalCount, + }, + }; + }, + getProducerEServiceTemplates: async ( + name: string | undefined, + offset: number, + limit: number, + { headers, logger, authData }: WithLogger + ): Promise => { + logger.info( + `Retrieving EService templates for creator ${authData.organizationId}, for name = ${name}, offset = ${offset}, limit = ${limit}` + ); + const eserviceTemplatesResponse: eserviceTemplateApi.EServiceTemplates = + await eserviceTemplateClient.getEServiceTemplates({ + headers, + queries: { + name, + creatorsIds: [authData.organizationId], + limit, + offset, + }, + }); + + return { + results: eserviceTemplatesResponse.results.map( + toBffProducerEServiceTemplate + ), + pagination: { + offset, + limit, + totalCount: eserviceTemplatesResponse.totalCount, + }, + }; + }, createEServiceTemplateEServiceRiskAnalysis: async ( eServiceTemplateId: EServiceTemplateId, seed: bffApi.EServiceRiskAnalysisSeed, @@ -360,6 +451,24 @@ export const retrieveEServiceTemplateVersion = ( return eserviceTemplateVersion; }; +async function getTenantsFromEServiceTemplates( + tenantClient: TenantProcessClient, + eserviceTemplates: eserviceTemplateApi.EServiceTemplate[], + headers: BffAppContext["headers"] +): Promise> { + const creatorsIds = Array.from( + new Set(eserviceTemplates.map((t) => t.creatorId)) + ); + + const tenants = await Promise.all( + creatorsIds.map(async (id) => + tenantClient.tenant.getTenant({ headers, params: { id } }) + ) + ); + + return new Map(tenants.map((t) => [t.id, t])); +} + const getAttributeIds = ( eserviceTemplateVersion: eserviceTemplateApi.EServiceTemplateVersion ): string[] => [ diff --git a/packages/backend-for-frontend/src/utilities/errorMappers.ts b/packages/backend-for-frontend/src/utilities/errorMappers.ts index 475dae75af..db46f2de20 100644 --- a/packages/backend-for-frontend/src/utilities/errorMappers.ts +++ b/packages/backend-for-frontend/src/utilities/errorMappers.ts @@ -214,3 +214,14 @@ export const bffGetEServiceTemplateErrorMapper = ( match(error.code) .with("eserviceTemplateVersionNotFound", () => HTTP_STATUS_NOT_FOUND) .otherwise(() => HTTP_STATUS_INTERNAL_SERVER_ERROR); + +export const bffGetCatalogEServiceTemplateErrorMapper = ( + error: ApiError +): number => + match(error.code) + .with( + "tenantNotFound", + "catalogEServiceTemplatePublishedVersionNotFound", + () => HTTP_STATUS_NOT_FOUND + ) + .otherwise(() => HTTP_STATUS_INTERNAL_SERVER_ERROR); diff --git a/packages/eservice-template-process/src/routers/EServiceTemplateRouter.ts b/packages/eservice-template-process/src/routers/EServiceTemplateRouter.ts index b40db76635..7a1e67e2e1 100644 --- a/packages/eservice-template-process/src/routers/EServiceTemplateRouter.ts +++ b/packages/eservice-template-process/src/routers/EServiceTemplateRouter.ts @@ -12,7 +12,11 @@ import { fromAppContext, } from "pagopa-interop-commons"; import { eserviceTemplateApi } from "pagopa-interop-api-clients"; -import { unsafeBrandId } from "pagopa-interop-models"; +import { + EServiceTemplateId, + TenantId, + unsafeBrandId, +} from "pagopa-interop-models"; import { config } from "../config/config.js"; import { readModelServiceBuilder } from "../services/readModelService.js"; import { eserviceTemplateServiceBuilder } from "../services/eserviceTemplateService.js"; @@ -36,6 +40,7 @@ import { updateDraftTemplateVersionErrorMapper, } from "../utilities/errorMappers.js"; import { + apiEServiceTemplateVersionStateToEServiceTemplateVersionState, apiDescriptorStateToDescriptorState, eserviceTemplateInstanceToApiEServiceTemplateInstance, eserviceTemplateToApiEServiceTemplate, @@ -77,8 +82,54 @@ const eserviceTemplatesRouter = ( }) .get( "/eservices/templates", - authorizationMiddleware([ADMIN_ROLE]), - async (_req, res) => res.status(504) + authorizationMiddleware([ + ADMIN_ROLE, + API_ROLE, + SECURITY_ROLE, + M2M_ROLE, + SUPPORT_ROLE, + ]), + async (req, res) => { + const ctx = fromAppContext(req.ctx); + + try { + const { + name, + creatorsIds, + eserviceTemplatesIds, + states, + offset, + limit, + } = req.query; + + const eserviceTemplates = + await eserviceTemplateService.getEServiceTemplates( + { + eserviceTemplatesIds: + eserviceTemplatesIds.map(unsafeBrandId), + creatorsIds: creatorsIds.map(unsafeBrandId), + states: states.map( + apiEServiceTemplateVersionStateToEServiceTemplateVersionState + ), + name, + }, + offset, + limit, + ctx + ); + + return res.status(200).send( + eserviceTemplateApi.EServiceTemplates.parse({ + results: eserviceTemplates.results.map( + eserviceTemplateToApiEServiceTemplate + ), + totalCount: eserviceTemplates.totalCount, + }) + ); + } catch (error) { + return res.status(500).send(); + } + } ) .post( "/eservices/templates", diff --git a/packages/eservice-template-process/src/services/eserviceTemplateService.ts b/packages/eservice-template-process/src/services/eserviceTemplateService.ts index 20735327d9..5f5166a1a8 100644 --- a/packages/eservice-template-process/src/services/eserviceTemplateService.ts +++ b/packages/eservice-template-process/src/services/eserviceTemplateService.ts @@ -23,12 +23,9 @@ import { EServiceTemplateVersionId, EServiceTemplateVersionState, eserviceTemplateVersionState, + ListResult, unsafeBrandId, WithMetadata, -} from "pagopa-interop-models"; -import { match } from "ts-pattern"; -import { eserviceTemplateApi } from "pagopa-interop-api-clients"; -import { EServiceAttribute, RiskAnalysis, RiskAnalysisId, @@ -37,8 +34,9 @@ import { TenantKind, eserviceMode, generateId, - ListResult, } from "pagopa-interop-models"; +import { match } from "ts-pattern"; +import { eserviceTemplateApi } from "pagopa-interop-api-clients"; import { attributeNotFound, eServiceTemplateDuplicate, @@ -47,14 +45,6 @@ import { eserviceTemplateWithoutPublishedVersion, inconsistentDailyCalls, notValidEServiceTemplateVersionState, -} from "../model/domain/errors.js"; -import { - toCreateEventEServiceTemplateVersionActivated, - toCreateEventEServiceTemplateVersionSuspended, - toCreateEventEServiceTemplateNameUpdated, - toCreateEventEServiceTemplateDraftVersionUpdated, -} from "../model/domain/toEvent.js"; -import { versionAttributeGroupSupersetMissingInAttributesSeed, inconsistentAttributesSeedGroupsCount, unchangedAttributes, @@ -64,6 +54,10 @@ import { eserviceTemaplateRiskAnalysisNameDuplicate, } from "../model/domain/errors.js"; import { + toCreateEventEServiceTemplateVersionActivated, + toCreateEventEServiceTemplateVersionSuspended, + toCreateEventEServiceTemplateNameUpdated, + toCreateEventEServiceTemplateDraftVersionUpdated, toCreateEventEServiceTemplateAudienceDescriptionUpdated, toCreateEventEServiceTemplateEServiceDescriptionUpdated, toCreateEventEServiceTemplateVersionQuotasUpdated, @@ -73,6 +67,8 @@ import { toCreateEventEServiceTemplateRiskAnalysisUpdated, toCreateEventEServiceTemplateDeleted, toCreateEventEServiceTemplateDraftVersionDeleted, + toCreateEventEServiceTemplateAdded, + toCreateEventEServiceTemplateDraftUpdated, } from "../model/domain/toEvent.js"; import { config } from "../config/config.js"; import { @@ -80,22 +76,21 @@ import { apiEServiceModeToEServiceMode, apiTechnologyToTechnology, } from "../model/domain/apiConverter.js"; -import { - toCreateEventEServiceTemplateAdded, - toCreateEventEServiceTemplateDraftUpdated, -} from "../model/domain/toEvent.js"; import { ApiGetEServiceTemplateIstancesFilters, EServiceTemplateInstance, } from "../model/domain/models.js"; +import { + GetEServiceTemplatesFilters, + ReadModelService, +} from "./readModelService.js"; import { assertIsDraftTemplate, assertIsReceiveTemplate, assertTenantKindExists, + assertRequesterEServiceTemplateCreator, + assertIsDraftEserviceTemplate, } from "./validators.js"; -import { ReadModelService } from "./readModelService.js"; -import { assertRequesterEServiceTemplateCreator } from "./validators.js"; -import { assertIsDraftEserviceTemplate } from "./validators.js"; export const retrieveEServiceTemplate = async ( eserviceTemplateId: EServiceTemplateId, @@ -705,7 +700,6 @@ export function eserviceTemplateServiceBuilder( return applyVisibilityToEServiceTemplate(eserviceTemplate.data, authData); }, - async deleteEServiceTemplateVersion( eserviceTemplateId: EServiceTemplateId, eserviceTemplateVersionId: EServiceTemplateVersionId, @@ -1216,6 +1210,31 @@ export function eserviceTemplateServiceBuilder( return updatedEServiceTemplate; }, + async getEServiceTemplates( + filters: GetEServiceTemplatesFilters, + offset: number, + limit: number, + { authData, logger }: WithLogger + ): Promise> { + logger.info( + `Getting EServices templates with name = ${filters.name}, ids = ${filters.eserviceTemplatesIds}, creators = ${filters.creatorsIds}, states = ${filters.states}, limit = ${limit}, offset = ${offset}` + ); + + const { results, totalCount } = + await readModelService.getEServiceTemplates( + filters, + offset, + limit, + authData + ); + + return { + results: results.map((eserviceTemplate) => + applyVisibilityToEServiceTemplate(eserviceTemplate, authData) + ), + totalCount, + }; + }, async getEServiceTemplateIstances( eserviceTemplateId: EServiceTemplateId, filters: ApiGetEServiceTemplateIstancesFilters, diff --git a/packages/eservice-template-process/src/services/readModelService.ts b/packages/eservice-template-process/src/services/readModelService.ts index 0f50877312..8d9e8c6d7c 100644 --- a/packages/eservice-template-process/src/services/readModelService.ts +++ b/packages/eservice-template-process/src/services/readModelService.ts @@ -1,6 +1,10 @@ import { + AuthData, EServiceTemplateCollection, + hasPermission, + ReadModelFilter, ReadModelRepository, + userRoles, TenantCollection, } from "pagopa-interop-commons"; import { @@ -8,11 +12,13 @@ import { AttributeId, EServiceTemplate, EServiceTemplateId, + EServiceTemplateVersionState, ListResult, Tenant, TenantId, TenantReadModel, WithMetadata, + eserviceTemplateVersionState, descriptorState, genericInternalError, } from "pagopa-interop-models"; @@ -23,6 +29,13 @@ import { EServiceTemplateInstance, } from "../model/domain/models.js"; +export type GetEServiceTemplatesFilters = { + name?: string; + eserviceTemplatesIds: EServiceTemplateId[]; + creatorsIds: TenantId[]; + states: EServiceTemplateVersionState[]; +}; + async function getEServiceTemplate( eserviceTemplates: EServiceTemplateCollection, filter: Filter>> @@ -131,6 +144,109 @@ export function readModelServiceBuilder({ return result.data; }, + async getEServiceTemplates( + filters: GetEServiceTemplatesFilters, + offset: number, + limit: number, + authData: AuthData + ): Promise> { + const { eserviceTemplatesIds, creatorsIds, states, name } = filters; + + const nameFilter: ReadModelFilter = name + ? { + "data.name": { + $regex: ReadModelRepository.escapeRegExp(name), + $options: "i", + }, + } + : {}; + + const idsFilter: ReadModelFilter = + ReadModelRepository.arrayToFilter(eserviceTemplatesIds, { + "data.id": { $in: eserviceTemplatesIds }, + }); + + const creatorsIdsFilter: ReadModelFilter = + ReadModelRepository.arrayToFilter(creatorsIds, { + "data.creatorId": { $in: creatorsIds }, + }); + + const templateStateFilter: ReadModelFilter = + ReadModelRepository.arrayToFilter(states, { + "data.versions.state": { $in: states }, + }); + + const visibilityFilter: ReadModelFilter = hasPermission( + [userRoles.ADMIN_ROLE, userRoles.API_ROLE, userRoles.SUPPORT_ROLE], + authData + ) + ? { + $or: [ + { "data.creatorId": authData.organizationId }, + { "data.versions.1": { $exists: true } }, + { + "data.versions": { $size: 1 }, + "data.versions.0.state": { + $ne: eserviceTemplateVersionState.draft, + }, + }, + ], + } + : { + $or: [ + { "data.versions.1": { $exists: true } }, + { + "data.versions": { $size: 1 }, + "data.versions.0.state": { + $ne: eserviceTemplateVersionState.draft, + }, + }, + ], + }; + + const aggregationPipeline = [ + { $match: nameFilter }, + { $match: idsFilter }, + { $match: creatorsIdsFilter }, + { $match: templateStateFilter }, + { $match: visibilityFilter }, + { + $project: { + data: 1, + computedColumn: { $toLower: ["$data.name"] }, + }, + }, + { + $sort: { computedColumn: 1 }, + }, + ]; + + const data = await eserviceTemplates + .aggregate( + [...aggregationPipeline, { $skip: offset }, { $limit: limit }], + { allowDiskUse: true } + ) + .toArray(); + + const result = z + .array(EServiceTemplate) + .safeParse(data.map((d) => d.data)); + if (!result.success) { + throw genericInternalError( + `Unable to parse eservice templates items: result ${JSON.stringify( + result + )} - data ${JSON.stringify(data)} ` + ); + } + + return { + results: result.data, + totalCount: await ReadModelRepository.getTotalCount( + eserviceTemplates, + aggregationPipeline + ), + }; + }, async getEServiceTemplateInstances({ eserviceTemplate, filters, diff --git a/packages/eservice-template-process/test/getEServiceTemplates.test.ts b/packages/eservice-template-process/test/getEServiceTemplates.test.ts index d813ec9381..ada21274fd 100644 --- a/packages/eservice-template-process/test/getEServiceTemplates.test.ts +++ b/packages/eservice-template-process/test/getEServiceTemplates.test.ts @@ -1,7 +1,657 @@ -import { describe, expect, it } from "vitest"; +/* eslint-disable functional/no-let */ +import { genericLogger, AuthData, userRoles } from "pagopa-interop-commons"; +import { + getMockAuthData, + getMockDocument, + getMockEServiceTemplate, + getMockEServiceTemplateVersion, + getMockTenant, +} from "pagopa-interop-commons-test"; +import { + TenantId, + generateId, + eserviceTemplateVersionState, + Tenant, + EServiceTemplate, + EServiceTemplateVersion, +} from "pagopa-interop-models"; +import { beforeEach, expect, describe, it } from "vitest"; +import { + addOneEServiceTemplate, + addOneTenant, + eserviceTemplateService, +} from "./utils.js"; -describe("getEServiceTemplates", () => { - it("shoud pass", () => { - expect(true).toBe(true); +describe("get eservices", () => { + let organizationId1: TenantId; + let organizationId2: TenantId; + let organizationId3: TenantId; + let eserviceTemplate1: EServiceTemplate; + let eserviceTemplate2: EServiceTemplate; + let eserviceTemplate3: EServiceTemplate; + let eserviceTemplate4: EServiceTemplate; + let eserviceTemplate5: EServiceTemplate; + const mockEServiceTemplateVersion = getMockEServiceTemplateVersion(); + const mockEServiceTemplate = getMockEServiceTemplate(); + const mockDocument = getMockDocument(); + + beforeEach(async () => { + organizationId1 = generateId(); + organizationId2 = generateId(); + organizationId3 = generateId(); + + const eserviceTemplateVersion1: EServiceTemplateVersion = { + ...mockEServiceTemplateVersion, + id: generateId(), + interface: mockDocument, + state: eserviceTemplateVersionState.published, + }; + + eserviceTemplate1 = { + ...mockEServiceTemplate, + id: generateId(), + name: "eservice 001 test", + versions: [eserviceTemplateVersion1], + creatorId: organizationId1, + }; + await addOneEServiceTemplate(eserviceTemplate1); + + const eserviceTemplateVersion2: EServiceTemplateVersion = { + ...mockEServiceTemplateVersion, + id: generateId(), + interface: mockDocument, + state: eserviceTemplateVersionState.published, + }; + + eserviceTemplate2 = { + ...mockEServiceTemplate, + id: generateId(), + name: "eservice template 002 test", + versions: [eserviceTemplateVersion2], + creatorId: organizationId1, + }; + await addOneEServiceTemplate(eserviceTemplate2); + + const eserviceTemplateVersion3: EServiceTemplateVersion = { + ...mockEServiceTemplateVersion, + id: generateId(), + interface: mockDocument, + state: eserviceTemplateVersionState.published, + }; + eserviceTemplate3 = { + ...mockEServiceTemplate, + id: generateId(), + name: "eservice template 003 test", + versions: [eserviceTemplateVersion3], + creatorId: organizationId1, + }; + await addOneEServiceTemplate(eserviceTemplate3); + + const eserviceTemplateVersion4: EServiceTemplateVersion = { + ...mockEServiceTemplateVersion, + id: generateId(), + interface: mockDocument, + state: eserviceTemplateVersionState.suspended, + }; + eserviceTemplate4 = { + ...mockEServiceTemplate, + id: generateId(), + name: "eservice template 004 test", + creatorId: organizationId2, + versions: [eserviceTemplateVersion4], + }; + await addOneEServiceTemplate(eserviceTemplate4); + + const eserviceTemplateVersion5: EServiceTemplateVersion = { + ...mockEServiceTemplateVersion, + id: generateId(), + interface: mockDocument, + state: eserviceTemplateVersionState.suspended, + }; + eserviceTemplate5 = { + ...mockEServiceTemplate, + id: generateId(), + name: "eservice template 005", + creatorId: organizationId2, + versions: [eserviceTemplateVersion5], + }; + await addOneEServiceTemplate(eserviceTemplate5); + + const tenant: Tenant = { + ...getMockTenant(), + id: organizationId3, + }; + await addOneTenant(tenant); + }); + it("should get the eService templates if they exist (parameters: eserviceTemplatesIds)", async () => { + const result = await eserviceTemplateService.getEServiceTemplates( + { + eserviceTemplatesIds: [eserviceTemplate1.id, eserviceTemplate2.id], + creatorsIds: [], + states: [], + }, + 0, + 50, + { + authData: getMockAuthData(), + correlationId: generateId(), + logger: genericLogger, + serviceName: "", + } + ); + expect(result.totalCount).toBe(2); + expect(result.results).toEqual([eserviceTemplate1, eserviceTemplate2]); + }); + it("should get the eServices templates if they exist (parameters: creatorsIds)", async () => { + const result = await eserviceTemplateService.getEServiceTemplates( + { + eserviceTemplatesIds: [], + creatorsIds: [organizationId1], + states: [], + }, + 0, + 50, + { + authData: getMockAuthData(), + correlationId: generateId(), + logger: genericLogger, + serviceName: "", + } + ); + expect(result.totalCount).toBe(3); + expect(result.results).toEqual([ + eserviceTemplate1, + eserviceTemplate2, + eserviceTemplate3, + ]); + }); + it("should get the eServices templates if they exist (parameters: states)", async () => { + const result = await eserviceTemplateService.getEServiceTemplates( + { + eserviceTemplatesIds: [], + creatorsIds: [], + states: [eserviceTemplateVersionState.published], + }, + 0, + 50, + { + authData: getMockAuthData(), + correlationId: generateId(), + logger: genericLogger, + serviceName: "", + } + ); + expect(result.totalCount).toBe(3); + expect(result.results).toEqual([ + eserviceTemplate1, + eserviceTemplate2, + eserviceTemplate3, + ]); + }); + it("should get the eServices templates if they exist (parameters: name)", async () => { + const result = await eserviceTemplateService.getEServiceTemplates( + { + eserviceTemplatesIds: [], + creatorsIds: [], + states: [], + name: "test", + }, + 0, + 50, + { + authData: getMockAuthData(), + correlationId: generateId(), + logger: genericLogger, + serviceName: "", + } + ); + expect(result.totalCount).toBe(4); + expect(result.results).toEqual([ + eserviceTemplate1, + eserviceTemplate2, + eserviceTemplate3, + eserviceTemplate4, + ]); + }); + it("should get the eServices templates if they exist (parameters: states, name)", async () => { + const result = await eserviceTemplateService.getEServiceTemplates( + { + eserviceTemplatesIds: [], + creatorsIds: [], + states: [eserviceTemplateVersionState.published], + name: "test", + }, + 0, + 50, + { + authData: getMockAuthData(), + correlationId: generateId(), + logger: genericLogger, + serviceName: "", + } + ); + expect(result.totalCount).toBe(3); + expect(result.results).toEqual([ + eserviceTemplate1, + eserviceTemplate2, + eserviceTemplate3, + ]); + }); + it("should not get the eServices templates if they don't exist (parameters: states, name)", async () => { + const result = await eserviceTemplateService.getEServiceTemplates( + { + eserviceTemplatesIds: [], + creatorsIds: [], + states: [eserviceTemplateVersionState.deprecated], + name: "test", + }, + 0, + 50, + { + authData: getMockAuthData(), + correlationId: generateId(), + logger: genericLogger, + serviceName: "", + } + ); + expect(result.totalCount).toBe(0); + expect(result.results).toEqual([]); + }); + it("should get the eServices templates if they exist (parameters: creatorsIds, states, name)", async () => { + const result = await eserviceTemplateService.getEServiceTemplates( + { + eserviceTemplatesIds: [], + creatorsIds: [organizationId2], + states: [eserviceTemplateVersionState.suspended], + name: "test", + }, + 0, + 50, + { + authData: getMockAuthData(), + correlationId: generateId(), + logger: genericLogger, + serviceName: "", + } + ); + expect(result.totalCount).toBe(1); + expect(result.results).toEqual([eserviceTemplate4]); + }); + it("should not get the eServices templates if they don't exist (parameters: producersIds, states, name)", async () => { + const result = await eserviceTemplateService.getEServiceTemplates( + { + eserviceTemplatesIds: [], + creatorsIds: [organizationId2], + states: [eserviceTemplateVersionState.published], + name: "not-existing", + }, + 0, + 50, + { + authData: getMockAuthData(), + correlationId: generateId(), + logger: genericLogger, + serviceName: "", + } + ); + expect(result.totalCount).toBe(0); + expect(result.results).toEqual([]); + }); + it("should get the eServices templates if they exist (pagination: limit)", async () => { + const result = await eserviceTemplateService.getEServiceTemplates( + { + eserviceTemplatesIds: [], + creatorsIds: [], + states: [], + }, + 0, + 2, + { + authData: getMockAuthData(), + correlationId: generateId(), + logger: genericLogger, + serviceName: "", + } + ); + + expect(result.totalCount).toBe(5); + expect(result.results.length).toBe(2); + }); + it("should get the eServices templates if they exist (pagination: offset, limit)", async () => { + const result = await eserviceTemplateService.getEServiceTemplates( + { + eserviceTemplatesIds: [], + creatorsIds: [], + states: [], + }, + 4, + 4, + { + authData: getMockAuthData(), + correlationId: generateId(), + logger: genericLogger, + serviceName: "", + } + ); + expect(result.totalCount).toBe(5); + expect(result.results.length).toBe(1); + }); + it("should include eservice templates with no versions (requester is the creator, admin)", async () => { + const eserviceTemplate6: EServiceTemplate = { + ...mockEServiceTemplate, + id: generateId(), + name: "eservice template 006", + creatorId: organizationId1, + versions: [], + }; + const authData: AuthData = { + ...getMockAuthData(organizationId1), + userRoles: [userRoles.ADMIN_ROLE], + }; + await addOneEServiceTemplate(eserviceTemplate6); + const result = await eserviceTemplateService.getEServiceTemplates( + { + eserviceTemplatesIds: [], + creatorsIds: [], + states: [], + }, + 0, + 50, + { + authData, + correlationId: generateId(), + logger: genericLogger, + serviceName: "", + } + ); + expect(result.totalCount).toBe(6); + expect(result.results).toEqual([ + eserviceTemplate1, + eserviceTemplate2, + eserviceTemplate3, + eserviceTemplate4, + eserviceTemplate5, + eserviceTemplate6, + ]); + }); + it("should include eservice templates whose only version is draft (requester is the creator, admin)", async () => { + const eserviceTemplateVersion6: EServiceTemplateVersion = { + ...mockEServiceTemplateVersion, + id: generateId(), + state: eserviceTemplateVersionState.draft, + }; + const eserviceTemplate6: EServiceTemplate = { + ...mockEServiceTemplate, + id: generateId(), + name: "eservice template 006", + creatorId: organizationId1, + versions: [eserviceTemplateVersion6], + }; + const authData: AuthData = { + ...getMockAuthData(organizationId1), + userRoles: [userRoles.ADMIN_ROLE], + }; + await addOneEServiceTemplate(eserviceTemplate6); + const result = await eserviceTemplateService.getEServiceTemplates( + { + eserviceTemplatesIds: [], + creatorsIds: [], + states: [], + }, + 0, + 50, + { + authData, + correlationId: generateId(), + logger: genericLogger, + serviceName: "", + } + ); + expect(result.totalCount).toBe(6); + expect(result.results).toEqual([ + eserviceTemplate1, + eserviceTemplate2, + eserviceTemplate3, + eserviceTemplate4, + eserviceTemplate5, + eserviceTemplate6, + ]); + }); + it("should not include eservice templates whose only version is draft (requester is the creator, not admin nor api, nor support)", async () => { + const eserviceTemplateVersion6: EServiceTemplateVersion = { + ...mockEServiceTemplateVersion, + id: generateId(), + state: eserviceTemplateVersionState.draft, + }; + const eserviceTemplate6: EServiceTemplate = { + ...mockEServiceTemplate, + id: generateId(), + name: "eservice template 006", + creatorId: organizationId1, + versions: [eserviceTemplateVersion6], + }; + const authData: AuthData = { + ...getMockAuthData(organizationId1), + userRoles: [userRoles.SECURITY_ROLE], + }; + await addOneEServiceTemplate(eserviceTemplate6); + const result = await eserviceTemplateService.getEServiceTemplates( + { + eserviceTemplatesIds: [], + creatorsIds: [], + states: [], + }, + 0, + 50, + { + authData, + correlationId: generateId(), + logger: genericLogger, + serviceName: "", + } + ); + expect(result.totalCount).toBe(5); + expect(result.results).toEqual([ + eserviceTemplate1, + eserviceTemplate2, + eserviceTemplate3, + eserviceTemplate4, + eserviceTemplate5, + ]); + }); + it("should not include eservice templates whose only version is draft (requester is not the creator)", async () => { + const eserviceTemplateVersion6: EServiceTemplateVersion = { + ...mockEServiceTemplateVersion, + id: generateId(), + state: eserviceTemplateVersionState.draft, + }; + const eserviceTemplate6: EServiceTemplate = { + ...mockEServiceTemplate, + id: generateId(), + name: "eservice template 006", + creatorId: organizationId1, + versions: [eserviceTemplateVersion6], + }; + const authData: AuthData = { + ...getMockAuthData(), + userRoles: [userRoles.SECURITY_ROLE], + }; + await addOneEServiceTemplate(eserviceTemplate6); + const result = await eserviceTemplateService.getEServiceTemplates( + { + eserviceTemplatesIds: [], + creatorsIds: [], + states: [], + }, + 0, + 50, + { + authData, + correlationId: generateId(), + logger: genericLogger, + serviceName: "", + } + ); + expect(result.totalCount).toBe(5); + expect(result.results).toEqual([ + eserviceTemplate1, + eserviceTemplate2, + eserviceTemplate3, + eserviceTemplate4, + eserviceTemplate5, + ]); + }); + it("should not filter out %s versions if the eservice template has both of draft and published versions (requester is the creator, admin)", async () => { + const eserviceTemplateVersion6a: EServiceTemplateVersion = { + ...mockEServiceTemplateVersion, + id: generateId(), + interface: mockDocument, + publishedAt: new Date(), + state: eserviceTemplateVersionState.published, + }; + const eserviceTemplateVersion6b: EServiceTemplateVersion = { + ...mockEServiceTemplateVersion, + id: generateId(), + version: 2, + state: eserviceTemplateVersionState.draft, + }; + const eserviceTemplate6: EServiceTemplate = { + ...mockEServiceTemplate, + id: generateId(), + name: "eservice template 006", + creatorId: organizationId1, + versions: [eserviceTemplateVersion6a, eserviceTemplateVersion6b], + }; + const authData: AuthData = { + ...getMockAuthData(organizationId1), + userRoles: [userRoles.ADMIN_ROLE], + }; + await addOneEServiceTemplate(eserviceTemplate6); + const result = await eserviceTemplateService.getEServiceTemplates( + { + eserviceTemplatesIds: [], + creatorsIds: [], + states: [], + }, + 0, + 50, + { + authData, + correlationId: generateId(), + logger: genericLogger, + serviceName: "", + } + ); + expect(result.totalCount).toBe(6); + expect(result.results).toEqual([ + eserviceTemplate1, + eserviceTemplate2, + eserviceTemplate3, + eserviceTemplate4, + eserviceTemplate5, + eserviceTemplate6, + ]); + }); + it("should filter out draft versions if the eservice has both draft and published versions (requester is the creator, but not admin nor api, nor support)", async () => { + const eserviceTemplateVersion6a: EServiceTemplateVersion = { + ...mockEServiceTemplateVersion, + id: generateId(), + interface: mockDocument, + publishedAt: new Date(), + state: eserviceTemplateVersionState.published, + }; + const eserviceTemplateVersion6b: EServiceTemplateVersion = { + ...mockEServiceTemplateVersion, + id: generateId(), + version: 2, + state: eserviceTemplateVersionState.draft, + }; + const eserviceTemplate6: EServiceTemplate = { + ...mockEServiceTemplate, + id: generateId(), + name: "eservice template 006", + creatorId: organizationId1, + versions: [eserviceTemplateVersion6a, eserviceTemplateVersion6b], + }; + const authData: AuthData = { + ...getMockAuthData(organizationId1), + userRoles: [userRoles.SECURITY_ROLE], + }; + await addOneEServiceTemplate(eserviceTemplate6); + const result = await eserviceTemplateService.getEServiceTemplates( + { + eserviceTemplatesIds: [], + creatorsIds: [], + states: [], + }, + 0, + 50, + { + authData, + correlationId: generateId(), + logger: genericLogger, + serviceName: "", + } + ); + expect(result.totalCount).toBe(6); + expect(result.results).toEqual([ + eserviceTemplate1, + eserviceTemplate2, + eserviceTemplate3, + eserviceTemplate4, + eserviceTemplate5, + { ...eserviceTemplate6, versions: [eserviceTemplateVersion6a] }, + ]); + }); + it("should filter out draft versions if the eservice template has both of draft and published versions (requester is not the creator)", async () => { + const eserviceTemplateVersion6a: EServiceTemplateVersion = { + ...mockEServiceTemplateVersion, + id: generateId(), + interface: mockDocument, + publishedAt: new Date(), + state: eserviceTemplateVersionState.published, + }; + const eserviceTemplateVersion6b: EServiceTemplateVersion = { + ...mockEServiceTemplateVersion, + id: generateId(), + version: 2, + state: eserviceTemplateVersionState.draft, + }; + const eserviceTemplate6: EServiceTemplate = { + ...mockEServiceTemplate, + id: generateId(), + name: "eservice template 006", + creatorId: organizationId1, + versions: [eserviceTemplateVersion6a, eserviceTemplateVersion6b], + }; + const authData: AuthData = { + ...getMockAuthData(), + userRoles: [userRoles.ADMIN_ROLE], + }; + await addOneEServiceTemplate(eserviceTemplate6); + const result = await eserviceTemplateService.getEServiceTemplates( + { + eserviceTemplatesIds: [], + creatorsIds: [], + states: [], + }, + 0, + 50, + { + authData, + correlationId: generateId(), + logger: genericLogger, + serviceName: "", + } + ); + expect(result.totalCount).toBe(6); + expect(result.results).toEqual([ + eserviceTemplate1, + eserviceTemplate2, + eserviceTemplate3, + eserviceTemplate4, + eserviceTemplate5, + { ...eserviceTemplate6, versions: [eserviceTemplateVersion6a] }, + ]); }); }); diff --git a/packages/eservice-template-process/test/utils.ts b/packages/eservice-template-process/test/utils.ts index 528b8f8437..c4b368bcfe 100644 --- a/packages/eservice-template-process/test/utils.ts +++ b/packages/eservice-template-process/test/utils.ts @@ -13,12 +13,12 @@ import { EServiceTemplate, EServiceTemplateEvent, EServiceTemplateId, + toEServiceTemplateV2, RiskAnalysis, Tenant, toReadModelTenant, - EServiceTemplateVersion, - toEServiceTemplateV2, toReadModelAttribute, + EServiceTemplateVersion, toReadModelEService, } from "pagopa-interop-models"; import { riskAnalysisFormToRiskAnalysisFormToValidate } from "pagopa-interop-commons";