Skip to content

Commit

Permalink
Extend RequestConfig of DeciderModule depending on existence of Relat…
Browse files Browse the repository at this point in the history
…ionship (#387)

* feat: add RelationshipRequestConfig

* feat: add checkRelationshipCompatibility

* test: RelationshipRequestConfig

* feat: resolve todos

---------

Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
  • Loading branch information
Milena-Czierlinski and mergify[bot] authored Jan 14, 2025
1 parent b91afb4 commit 3ff1ff8
Show file tree
Hide file tree
Showing 3 changed files with 203 additions and 13 deletions.
66 changes: 54 additions & 12 deletions packages/runtime/src/modules/DeciderModule.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -103,12 +104,19 @@ export class DeciderModule extends RuntimeModule<DeciderModuleConfiguration> {
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,
Expand Down Expand Up @@ -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<boolean | Error> {
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<string, any> {
Expand All @@ -287,15 +295,35 @@ function filterConfigElementByPrefix(requestItemConfigElement: RequestItemDeriva
return filteredRequestItemConfigElement;
}

function checkCompatibility(requestConfigElement: RequestConfig, requestOrRequestItem: LocalRequestDTO | RequestItemJSONDerivations): boolean {
async function checkCompatibility(requestConfigElement: RequestConfig, requestOrRequestItem: LocalRequestDTO, services: RuntimeServices): Promise<boolean | Error>;
async function checkCompatibility(requestConfigElement: RequestConfig, requestOrRequestItem: RequestItemJSONDerivations): Promise<boolean>;
async function checkCompatibility(
requestConfigElement: RequestConfig,
requestOrRequestItem: LocalRequestDTO | RequestItemJSONDerivations,
services?: RuntimeServices
): Promise<boolean | Error> {
let compatible = true;

for (const property in requestConfigElement) {
const unformattedRequestConfigProperty = requestConfigElement[property as keyof RequestConfig];
if (typeof unformattedRequestConfigProperty === "undefined") {
continue;
}
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;
Expand Down Expand Up @@ -338,6 +366,20 @@ function getNestedProperty(object: any, path: string): any {
return nestedProperty;
}

async function checkRelationshipCompatibility(relationshipRequestConfig: boolean, request: LocalRequestDTO, services: RuntimeServices): Promise<boolean | ApplicationError> {
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;
Expand All @@ -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,
Expand All @@ -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;
}

Expand All @@ -395,7 +437,7 @@ function checkRequestItemCompatibilityAndApplyResponseConfig(
return parametersToDecideRequest;
}

function checkRequestItemCompatibility(requestConfigElement: RequestItemDerivationConfig, requestItem: RequestItemJSONDerivations): boolean {
async function checkRequestItemCompatibility(requestConfigElement: RequestItemDerivationConfig, requestItem: RequestItemJSONDerivations): Promise<boolean> {
const requestItemPartOfConfigElement = filterConfigElementByPrefix(requestConfigElement, true);
return checkCompatibility(requestItemPartOfConfigElement, requestItem);
return await checkCompatibility(requestItemPartOfConfigElement, requestItem);
}
6 changes: 5 additions & 1 deletion packages/runtime/src/modules/decide/RequestConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
144 changes: 144 additions & 0 deletions packages/runtime/test/modules/DeciderModule.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down

0 comments on commit 3ff1ff8

Please sign in to comment.