From 3ff1ff861275e7998721264196922faf9b99eb59 Mon Sep 17 00:00:00 2001 From: Milena Czierlinski <146972016+Milena-Czierlinski@users.noreply.github.com> Date: Tue, 14 Jan 2025 11:06:43 +0100 Subject: [PATCH] Extend RequestConfig of DeciderModule depending on existence of Relationship (#387) * feat: add RelationshipRequestConfig * feat: add checkRelationshipCompatibility * test: RelationshipRequestConfig * feat: resolve todos --------- Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com> --- packages/runtime/src/modules/DeciderModule.ts | 66 ++++++-- .../src/modules/decide/RequestConfig.ts | 6 +- .../test/modules/DeciderModule.test.ts | 144 ++++++++++++++++++ 3 files changed, 203 insertions(+), 13 deletions(-) diff --git a/packages/runtime/src/modules/DeciderModule.ts b/packages/runtime/src/modules/DeciderModule.ts index fbc861781..60fdca051 100644 --- a/packages/runtime/src/modules/DeciderModule.ts +++ b/packages/runtime/src/modules/DeciderModule.ts @@ -1,5 +1,6 @@ +import { ApplicationError } from "@js-soft/ts-utils"; import { LocalRequestStatus } from "@nmshd/consumption"; -import { RequestItemGroupJSON, RequestItemJSONDerivations } from "@nmshd/content"; +import { isRequestItemDerivation, RequestItemGroupJSON, RequestItemJSONDerivations } from "@nmshd/content"; import { CoreDate } from "@nmshd/core-types"; import { IncomingRequestStatusChangedEvent, @@ -103,12 +104,19 @@ export class DeciderModule extends RuntimeModule { const requestConfigElement = automationConfigElement.requestConfig; const responseConfigElement = automationConfigElement.responseConfig; - const generalRequestIsCompatible = checkGeneralRequestCompatibility(requestConfigElement, request); + const services = await this.runtime.getServices(event.eventTargetAddress); + const generalRequestIsCompatible = await checkGeneralRequestCompatibility(requestConfigElement, request, services); + + if (generalRequestIsCompatible instanceof Error) { + this.logger.error(generalRequestIsCompatible); + break; + } + if (!generalRequestIsCompatible) { continue; } - const updatedRequestItemParameters = checkRequestItemCompatibilityAndApplyResponseConfig( + const updatedRequestItemParameters = await checkRequestItemCompatibilityAndApplyResponseConfig( itemsOfRequest, decideRequestItemParameters, requestConfigElement, @@ -260,14 +268,14 @@ function createEmptyDecideRequestItemParameters(array: any[]): { items: any[] } }; } -function checkGeneralRequestCompatibility(requestConfigElement: RequestConfig, request: LocalRequestDTO): boolean { +async function checkGeneralRequestCompatibility(requestConfigElement: RequestConfig, request: LocalRequestDTO, services: RuntimeServices): Promise { let generalRequestPartOfConfigElement = requestConfigElement; if (isRequestItemDerivationConfig(requestConfigElement)) { generalRequestPartOfConfigElement = filterConfigElementByPrefix(requestConfigElement, false); } - return checkCompatibility(generalRequestPartOfConfigElement, request); + return await checkCompatibility(generalRequestPartOfConfigElement, request, services); } function filterConfigElementByPrefix(requestItemConfigElement: RequestItemDerivationConfig, includePrefix: boolean): Record { @@ -287,8 +295,15 @@ function filterConfigElementByPrefix(requestItemConfigElement: RequestItemDeriva return filteredRequestItemConfigElement; } -function checkCompatibility(requestConfigElement: RequestConfig, requestOrRequestItem: LocalRequestDTO | RequestItemJSONDerivations): boolean { +async function checkCompatibility(requestConfigElement: RequestConfig, requestOrRequestItem: LocalRequestDTO, services: RuntimeServices): Promise; +async function checkCompatibility(requestConfigElement: RequestConfig, requestOrRequestItem: RequestItemJSONDerivations): Promise; +async function checkCompatibility( + requestConfigElement: RequestConfig, + requestOrRequestItem: LocalRequestDTO | RequestItemJSONDerivations, + services?: RuntimeServices +): Promise { let compatible = true; + for (const property in requestConfigElement) { const unformattedRequestConfigProperty = requestConfigElement[property as keyof RequestConfig]; if (typeof unformattedRequestConfigProperty === "undefined") { @@ -296,6 +311,19 @@ function checkCompatibility(requestConfigElement: RequestConfig, requestOrReques } const requestConfigProperty = makeObjectsToStrings(unformattedRequestConfigProperty); + if (property === "relationshipAlreadyExists") { + if (isRequestItemDerivation(requestOrRequestItem)) { + return Error("The RelationshipRequestConfig 'relationshipAlreadyExists' is compared to a RequestItem, but should be compared to a Request."); + } + + const relationshipCompatibility = await checkRelationshipCompatibility(requestConfigProperty, requestOrRequestItem as LocalRequestDTO, services!); + if (relationshipCompatibility instanceof ApplicationError) return relationshipCompatibility; + + compatible &&= relationshipCompatibility; + if (!compatible) break; + continue; + } + const unformattedRequestProperty = getNestedProperty(requestOrRequestItem, property); if (typeof unformattedRequestProperty === "undefined") { compatible = false; @@ -338,6 +366,20 @@ function getNestedProperty(object: any, path: string): any { return nestedProperty; } +async function checkRelationshipCompatibility(relationshipRequestConfig: boolean, request: LocalRequestDTO, services: RuntimeServices): Promise { + let relationshipExists = false; + + const relationshipResult = await services.transportServices.relationships.getRelationshipByAddress({ address: request.peer }); + if (relationshipResult.isError && relationshipResult.error.code !== "error.runtime.recordNotFound") return relationshipResult.error; + + if (relationshipResult.isSuccess) { + relationshipExists = true; + } + + const compatible = relationshipExists === relationshipRequestConfig; + return compatible; +} + function checkTagCompatibility(requestConfigTags: string[], requestTags: string[]): boolean { const atLeastOneMatchingTag = requestConfigTags.some((tag) => requestTags.includes(tag)); return atLeastOneMatchingTag; @@ -354,16 +396,16 @@ function checkDateCompatibility(requestConfigDate: string, requestDate: string): return CoreDate.from(requestDate).equals(CoreDate.from(requestConfigDate)); } -function checkRequestItemCompatibilityAndApplyResponseConfig( +async function checkRequestItemCompatibilityAndApplyResponseConfig( itemsOfRequest: (RequestItemJSONDerivations | RequestItemGroupJSON)[], parametersToDecideRequest: any, requestConfigElement: RequestItemDerivationConfig, responseConfigElement: ResponseConfig -): { items: any[] } { +): Promise<{ items: any[] }> { for (let i = 0; i < itemsOfRequest.length; i++) { const item = itemsOfRequest[i]; if (item["@type"] === "RequestItemGroup") { - checkRequestItemCompatibilityAndApplyResponseConfig( + await checkRequestItemCompatibilityAndApplyResponseConfig( (item as RequestItemGroupJSON).items, parametersToDecideRequest.items[i], requestConfigElement, @@ -374,7 +416,7 @@ function checkRequestItemCompatibilityAndApplyResponseConfig( if (alreadyDecidedByOtherConfig) continue; if (isRequestItemDerivationConfig(requestConfigElement)) { - const requestItemIsCompatible = checkRequestItemCompatibility(requestConfigElement, item as RequestItemJSONDerivations); + const requestItemIsCompatible = await checkRequestItemCompatibility(requestConfigElement, item as RequestItemJSONDerivations); if (!requestItemIsCompatible) continue; } @@ -395,7 +437,7 @@ function checkRequestItemCompatibilityAndApplyResponseConfig( return parametersToDecideRequest; } -function checkRequestItemCompatibility(requestConfigElement: RequestItemDerivationConfig, requestItem: RequestItemJSONDerivations): boolean { +async function checkRequestItemCompatibility(requestConfigElement: RequestItemDerivationConfig, requestItem: RequestItemJSONDerivations): Promise { const requestItemPartOfConfigElement = filterConfigElementByPrefix(requestConfigElement, true); - return checkCompatibility(requestItemPartOfConfigElement, requestItem); + return await checkCompatibility(requestItemPartOfConfigElement, requestItem); } diff --git a/packages/runtime/src/modules/decide/RequestConfig.ts b/packages/runtime/src/modules/decide/RequestConfig.ts index 67d2f5aac..2cf0dea21 100644 --- a/packages/runtime/src/modules/decide/RequestConfig.ts +++ b/packages/runtime/src/modules/decide/RequestConfig.ts @@ -10,6 +10,10 @@ export interface GeneralRequestConfig { "content.metadata"?: object | object[]; } +export interface RelationshipRequestConfig extends GeneralRequestConfig { + relationshipAlreadyExists?: boolean; +} + export interface RequestItemConfig extends GeneralRequestConfig { "content.item.@type"?: string | string[]; "content.item.mustBeAccepted"?: boolean; @@ -142,4 +146,4 @@ export function isRequestItemDerivationConfig(input: any): input is RequestItemD return Object.keys(input).some((key) => key.startsWith("content.item.")); } -export type RequestConfig = GeneralRequestConfig | RequestItemDerivationConfig; +export type RequestConfig = GeneralRequestConfig | RelationshipRequestConfig | RequestItemDerivationConfig; diff --git a/packages/runtime/test/modules/DeciderModule.test.ts b/packages/runtime/test/modules/DeciderModule.test.ts index 045c80a84..211b4323f 100644 --- a/packages/runtime/test/modules/DeciderModule.test.ts +++ b/packages/runtime/test/modules/DeciderModule.test.ts @@ -589,6 +589,150 @@ describe("DeciderModule", () => { }); }); + describe("RelationshipRequestConfig", () => { + test("decides a Request on new Relationship given an according RelationshipRequestConfig", async () => { + const deciderConfig: DeciderModuleConfigurationOverwrite = { + automationConfig: [ + { + requestConfig: { + relationshipAlreadyExists: false + }, + responseConfig: { + accept: false + } + } + ] + }; + const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig }))[0]; + + const request = Request.from({ + items: [{ "@type": "AuthenticationRequestItem", mustBeAccepted: false }] + }); + const template = ( + await sender.transport.relationshipTemplates.createOwnRelationshipTemplate({ + content: RelationshipTemplateContent.from({ + onNewRelationship: request + }).toJSON(), + expiresAt: CoreDate.utc().add({ hours: 1 }).toISOString() + }) + ).value; + await recipient.transport.relationshipTemplates.loadPeerRelationshipTemplate({ reference: template.truncatedReference }); + const receivedRequestResult = await recipient.consumption.incomingRequests.received({ + receivedRequest: request.toJSON(), + requestSourceId: template.id + }); + await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); + + await expect(recipient.eventBus).toHavePublished( + RelationshipTemplateProcessedEvent, + (e) => e.data.result === RelationshipTemplateProcessedResult.RequestAutomaticallyDecided && e.data.template.id === template.id + ); + }); + + test("decides a Request on existing Relationship given an according RelationshipRequestConfig", async () => { + const deciderConfig: DeciderModuleConfigurationOverwrite = { + automationConfig: [ + { + requestConfig: { + relationshipAlreadyExists: true + }, + responseConfig: { + accept: true + } + } + ] + }; + const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig }))[0]; + await establishRelationship(sender.transport, recipient.transport); + + const message = await exchangeMessage(sender.transport, recipient.transport); + const receivedRequestResult = await recipient.consumption.incomingRequests.received({ + receivedRequest: { + "@type": "Request", + items: [{ "@type": "AuthenticationRequestItem", mustBeAccepted: false }] + }, + requestSourceId: message.id + }); + await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); + + await expect(recipient.eventBus).toHavePublished( + MessageProcessedEvent, + (e) => e.data.result === MessageProcessedResult.RequestAutomaticallyDecided && e.data.message.id === message.id + ); + }); + + test("doesn't decide a Request on new Relationship given a contrary RelationshipRequestConfig", async () => { + const deciderConfig: DeciderModuleConfigurationOverwrite = { + automationConfig: [ + { + requestConfig: { + relationshipAlreadyExists: true + }, + responseConfig: { + accept: true + } + } + ] + }; + const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig }))[0]; + + const request = Request.from({ + items: [{ "@type": "AuthenticationRequestItem", mustBeAccepted: false }] + }); + const template = ( + await sender.transport.relationshipTemplates.createOwnRelationshipTemplate({ + content: RelationshipTemplateContent.from({ + onNewRelationship: request + }).toJSON(), + expiresAt: CoreDate.utc().add({ hours: 1 }).toISOString() + }) + ).value; + await recipient.transport.relationshipTemplates.loadPeerRelationshipTemplate({ reference: template.truncatedReference }); + const receivedRequestResult = await recipient.consumption.incomingRequests.received({ + receivedRequest: request.toJSON(), + requestSourceId: template.id + }); + await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); + + await expect(recipient.eventBus).toHavePublished( + RelationshipTemplateProcessedEvent, + (e) => e.data.result === RelationshipTemplateProcessedResult.ManualRequestDecisionRequired && e.data.template.id === template.id + ); + }); + + test("doesn't decide a Request on existing Relationship given a contrary RelationshipRequestConfig", async () => { + const deciderConfig: DeciderModuleConfigurationOverwrite = { + automationConfig: [ + { + requestConfig: { + relationshipAlreadyExists: false + }, + responseConfig: { + accept: true + } + } + ] + }; + const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig }))[0]; + await establishRelationship(sender.transport, recipient.transport); + + const message = await exchangeMessage(sender.transport, recipient.transport); + const receivedRequestResult = await recipient.consumption.incomingRequests.received({ + receivedRequest: { + "@type": "Request", + items: [{ "@type": "AuthenticationRequestItem", mustBeAccepted: false }] + }, + requestSourceId: message.id + }); + await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id }); + + await expect(recipient.eventBus).toHavePublished( + MessageProcessedEvent, + (e) => e.data.result === MessageProcessedResult.ManualRequestDecisionRequired && e.data.message.id === message.id + ); + }); + }); + describe("RequestItemConfig", () => { test("rejects a RequestItem given a RequestItemConfig with all fields set", async () => { const deciderConfig: DeciderModuleConfigurationOverwrite = {