Skip to content

Commit 3ff1ff8

Browse files
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>
1 parent b91afb4 commit 3ff1ff8

File tree

3 files changed

+203
-13
lines changed

3 files changed

+203
-13
lines changed

packages/runtime/src/modules/DeciderModule.ts

Lines changed: 54 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1+
import { ApplicationError } from "@js-soft/ts-utils";
12
import { LocalRequestStatus } from "@nmshd/consumption";
2-
import { RequestItemGroupJSON, RequestItemJSONDerivations } from "@nmshd/content";
3+
import { isRequestItemDerivation, RequestItemGroupJSON, RequestItemJSONDerivations } from "@nmshd/content";
34
import { CoreDate } from "@nmshd/core-types";
45
import {
56
IncomingRequestStatusChangedEvent,
@@ -103,12 +104,19 @@ export class DeciderModule extends RuntimeModule<DeciderModuleConfiguration> {
103104
const requestConfigElement = automationConfigElement.requestConfig;
104105
const responseConfigElement = automationConfigElement.responseConfig;
105106

106-
const generalRequestIsCompatible = checkGeneralRequestCompatibility(requestConfigElement, request);
107+
const services = await this.runtime.getServices(event.eventTargetAddress);
108+
const generalRequestIsCompatible = await checkGeneralRequestCompatibility(requestConfigElement, request, services);
109+
110+
if (generalRequestIsCompatible instanceof Error) {
111+
this.logger.error(generalRequestIsCompatible);
112+
break;
113+
}
114+
107115
if (!generalRequestIsCompatible) {
108116
continue;
109117
}
110118

111-
const updatedRequestItemParameters = checkRequestItemCompatibilityAndApplyResponseConfig(
119+
const updatedRequestItemParameters = await checkRequestItemCompatibilityAndApplyResponseConfig(
112120
itemsOfRequest,
113121
decideRequestItemParameters,
114122
requestConfigElement,
@@ -260,14 +268,14 @@ function createEmptyDecideRequestItemParameters(array: any[]): { items: any[] }
260268
};
261269
}
262270

263-
function checkGeneralRequestCompatibility(requestConfigElement: RequestConfig, request: LocalRequestDTO): boolean {
271+
async function checkGeneralRequestCompatibility(requestConfigElement: RequestConfig, request: LocalRequestDTO, services: RuntimeServices): Promise<boolean | Error> {
264272
let generalRequestPartOfConfigElement = requestConfigElement;
265273

266274
if (isRequestItemDerivationConfig(requestConfigElement)) {
267275
generalRequestPartOfConfigElement = filterConfigElementByPrefix(requestConfigElement, false);
268276
}
269277

270-
return checkCompatibility(generalRequestPartOfConfigElement, request);
278+
return await checkCompatibility(generalRequestPartOfConfigElement, request, services);
271279
}
272280

273281
function filterConfigElementByPrefix(requestItemConfigElement: RequestItemDerivationConfig, includePrefix: boolean): Record<string, any> {
@@ -287,15 +295,35 @@ function filterConfigElementByPrefix(requestItemConfigElement: RequestItemDeriva
287295
return filteredRequestItemConfigElement;
288296
}
289297

290-
function checkCompatibility(requestConfigElement: RequestConfig, requestOrRequestItem: LocalRequestDTO | RequestItemJSONDerivations): boolean {
298+
async function checkCompatibility(requestConfigElement: RequestConfig, requestOrRequestItem: LocalRequestDTO, services: RuntimeServices): Promise<boolean | Error>;
299+
async function checkCompatibility(requestConfigElement: RequestConfig, requestOrRequestItem: RequestItemJSONDerivations): Promise<boolean>;
300+
async function checkCompatibility(
301+
requestConfigElement: RequestConfig,
302+
requestOrRequestItem: LocalRequestDTO | RequestItemJSONDerivations,
303+
services?: RuntimeServices
304+
): Promise<boolean | Error> {
291305
let compatible = true;
306+
292307
for (const property in requestConfigElement) {
293308
const unformattedRequestConfigProperty = requestConfigElement[property as keyof RequestConfig];
294309
if (typeof unformattedRequestConfigProperty === "undefined") {
295310
continue;
296311
}
297312
const requestConfigProperty = makeObjectsToStrings(unformattedRequestConfigProperty);
298313

314+
if (property === "relationshipAlreadyExists") {
315+
if (isRequestItemDerivation(requestOrRequestItem)) {
316+
return Error("The RelationshipRequestConfig 'relationshipAlreadyExists' is compared to a RequestItem, but should be compared to a Request.");
317+
}
318+
319+
const relationshipCompatibility = await checkRelationshipCompatibility(requestConfigProperty, requestOrRequestItem as LocalRequestDTO, services!);
320+
if (relationshipCompatibility instanceof ApplicationError) return relationshipCompatibility;
321+
322+
compatible &&= relationshipCompatibility;
323+
if (!compatible) break;
324+
continue;
325+
}
326+
299327
const unformattedRequestProperty = getNestedProperty(requestOrRequestItem, property);
300328
if (typeof unformattedRequestProperty === "undefined") {
301329
compatible = false;
@@ -338,6 +366,20 @@ function getNestedProperty(object: any, path: string): any {
338366
return nestedProperty;
339367
}
340368

369+
async function checkRelationshipCompatibility(relationshipRequestConfig: boolean, request: LocalRequestDTO, services: RuntimeServices): Promise<boolean | ApplicationError> {
370+
let relationshipExists = false;
371+
372+
const relationshipResult = await services.transportServices.relationships.getRelationshipByAddress({ address: request.peer });
373+
if (relationshipResult.isError && relationshipResult.error.code !== "error.runtime.recordNotFound") return relationshipResult.error;
374+
375+
if (relationshipResult.isSuccess) {
376+
relationshipExists = true;
377+
}
378+
379+
const compatible = relationshipExists === relationshipRequestConfig;
380+
return compatible;
381+
}
382+
341383
function checkTagCompatibility(requestConfigTags: string[], requestTags: string[]): boolean {
342384
const atLeastOneMatchingTag = requestConfigTags.some((tag) => requestTags.includes(tag));
343385
return atLeastOneMatchingTag;
@@ -354,16 +396,16 @@ function checkDateCompatibility(requestConfigDate: string, requestDate: string):
354396
return CoreDate.from(requestDate).equals(CoreDate.from(requestConfigDate));
355397
}
356398

357-
function checkRequestItemCompatibilityAndApplyResponseConfig(
399+
async function checkRequestItemCompatibilityAndApplyResponseConfig(
358400
itemsOfRequest: (RequestItemJSONDerivations | RequestItemGroupJSON)[],
359401
parametersToDecideRequest: any,
360402
requestConfigElement: RequestItemDerivationConfig,
361403
responseConfigElement: ResponseConfig
362-
): { items: any[] } {
404+
): Promise<{ items: any[] }> {
363405
for (let i = 0; i < itemsOfRequest.length; i++) {
364406
const item = itemsOfRequest[i];
365407
if (item["@type"] === "RequestItemGroup") {
366-
checkRequestItemCompatibilityAndApplyResponseConfig(
408+
await checkRequestItemCompatibilityAndApplyResponseConfig(
367409
(item as RequestItemGroupJSON).items,
368410
parametersToDecideRequest.items[i],
369411
requestConfigElement,
@@ -374,7 +416,7 @@ function checkRequestItemCompatibilityAndApplyResponseConfig(
374416
if (alreadyDecidedByOtherConfig) continue;
375417

376418
if (isRequestItemDerivationConfig(requestConfigElement)) {
377-
const requestItemIsCompatible = checkRequestItemCompatibility(requestConfigElement, item as RequestItemJSONDerivations);
419+
const requestItemIsCompatible = await checkRequestItemCompatibility(requestConfigElement, item as RequestItemJSONDerivations);
378420
if (!requestItemIsCompatible) continue;
379421
}
380422

@@ -395,7 +437,7 @@ function checkRequestItemCompatibilityAndApplyResponseConfig(
395437
return parametersToDecideRequest;
396438
}
397439

398-
function checkRequestItemCompatibility(requestConfigElement: RequestItemDerivationConfig, requestItem: RequestItemJSONDerivations): boolean {
440+
async function checkRequestItemCompatibility(requestConfigElement: RequestItemDerivationConfig, requestItem: RequestItemJSONDerivations): Promise<boolean> {
399441
const requestItemPartOfConfigElement = filterConfigElementByPrefix(requestConfigElement, true);
400-
return checkCompatibility(requestItemPartOfConfigElement, requestItem);
442+
return await checkCompatibility(requestItemPartOfConfigElement, requestItem);
401443
}

packages/runtime/src/modules/decide/RequestConfig.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ export interface GeneralRequestConfig {
1010
"content.metadata"?: object | object[];
1111
}
1212

13+
export interface RelationshipRequestConfig extends GeneralRequestConfig {
14+
relationshipAlreadyExists?: boolean;
15+
}
16+
1317
export interface RequestItemConfig extends GeneralRequestConfig {
1418
"content.item.@type"?: string | string[];
1519
"content.item.mustBeAccepted"?: boolean;
@@ -142,4 +146,4 @@ export function isRequestItemDerivationConfig(input: any): input is RequestItemD
142146
return Object.keys(input).some((key) => key.startsWith("content.item."));
143147
}
144148

145-
export type RequestConfig = GeneralRequestConfig | RequestItemDerivationConfig;
149+
export type RequestConfig = GeneralRequestConfig | RelationshipRequestConfig | RequestItemDerivationConfig;

packages/runtime/test/modules/DeciderModule.test.ts

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -589,6 +589,150 @@ describe("DeciderModule", () => {
589589
});
590590
});
591591

592+
describe("RelationshipRequestConfig", () => {
593+
test("decides a Request on new Relationship given an according RelationshipRequestConfig", async () => {
594+
const deciderConfig: DeciderModuleConfigurationOverwrite = {
595+
automationConfig: [
596+
{
597+
requestConfig: {
598+
relationshipAlreadyExists: false
599+
},
600+
responseConfig: {
601+
accept: false
602+
}
603+
}
604+
]
605+
};
606+
const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig }))[0];
607+
608+
const request = Request.from({
609+
items: [{ "@type": "AuthenticationRequestItem", mustBeAccepted: false }]
610+
});
611+
const template = (
612+
await sender.transport.relationshipTemplates.createOwnRelationshipTemplate({
613+
content: RelationshipTemplateContent.from({
614+
onNewRelationship: request
615+
}).toJSON(),
616+
expiresAt: CoreDate.utc().add({ hours: 1 }).toISOString()
617+
})
618+
).value;
619+
await recipient.transport.relationshipTemplates.loadPeerRelationshipTemplate({ reference: template.truncatedReference });
620+
const receivedRequestResult = await recipient.consumption.incomingRequests.received({
621+
receivedRequest: request.toJSON(),
622+
requestSourceId: template.id
623+
});
624+
await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id });
625+
626+
await expect(recipient.eventBus).toHavePublished(
627+
RelationshipTemplateProcessedEvent,
628+
(e) => e.data.result === RelationshipTemplateProcessedResult.RequestAutomaticallyDecided && e.data.template.id === template.id
629+
);
630+
});
631+
632+
test("decides a Request on existing Relationship given an according RelationshipRequestConfig", async () => {
633+
const deciderConfig: DeciderModuleConfigurationOverwrite = {
634+
automationConfig: [
635+
{
636+
requestConfig: {
637+
relationshipAlreadyExists: true
638+
},
639+
responseConfig: {
640+
accept: true
641+
}
642+
}
643+
]
644+
};
645+
const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig }))[0];
646+
await establishRelationship(sender.transport, recipient.transport);
647+
648+
const message = await exchangeMessage(sender.transport, recipient.transport);
649+
const receivedRequestResult = await recipient.consumption.incomingRequests.received({
650+
receivedRequest: {
651+
"@type": "Request",
652+
items: [{ "@type": "AuthenticationRequestItem", mustBeAccepted: false }]
653+
},
654+
requestSourceId: message.id
655+
});
656+
await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id });
657+
658+
await expect(recipient.eventBus).toHavePublished(
659+
MessageProcessedEvent,
660+
(e) => e.data.result === MessageProcessedResult.RequestAutomaticallyDecided && e.data.message.id === message.id
661+
);
662+
});
663+
664+
test("doesn't decide a Request on new Relationship given a contrary RelationshipRequestConfig", async () => {
665+
const deciderConfig: DeciderModuleConfigurationOverwrite = {
666+
automationConfig: [
667+
{
668+
requestConfig: {
669+
relationshipAlreadyExists: true
670+
},
671+
responseConfig: {
672+
accept: true
673+
}
674+
}
675+
]
676+
};
677+
const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig }))[0];
678+
679+
const request = Request.from({
680+
items: [{ "@type": "AuthenticationRequestItem", mustBeAccepted: false }]
681+
});
682+
const template = (
683+
await sender.transport.relationshipTemplates.createOwnRelationshipTemplate({
684+
content: RelationshipTemplateContent.from({
685+
onNewRelationship: request
686+
}).toJSON(),
687+
expiresAt: CoreDate.utc().add({ hours: 1 }).toISOString()
688+
})
689+
).value;
690+
await recipient.transport.relationshipTemplates.loadPeerRelationshipTemplate({ reference: template.truncatedReference });
691+
const receivedRequestResult = await recipient.consumption.incomingRequests.received({
692+
receivedRequest: request.toJSON(),
693+
requestSourceId: template.id
694+
});
695+
await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id });
696+
697+
await expect(recipient.eventBus).toHavePublished(
698+
RelationshipTemplateProcessedEvent,
699+
(e) => e.data.result === RelationshipTemplateProcessedResult.ManualRequestDecisionRequired && e.data.template.id === template.id
700+
);
701+
});
702+
703+
test("doesn't decide a Request on existing Relationship given a contrary RelationshipRequestConfig", async () => {
704+
const deciderConfig: DeciderModuleConfigurationOverwrite = {
705+
automationConfig: [
706+
{
707+
requestConfig: {
708+
relationshipAlreadyExists: false
709+
},
710+
responseConfig: {
711+
accept: true
712+
}
713+
}
714+
]
715+
};
716+
const recipient = (await runtimeServiceProvider.launch(1, { enableDeciderModule: true, configureDeciderModule: deciderConfig }))[0];
717+
await establishRelationship(sender.transport, recipient.transport);
718+
719+
const message = await exchangeMessage(sender.transport, recipient.transport);
720+
const receivedRequestResult = await recipient.consumption.incomingRequests.received({
721+
receivedRequest: {
722+
"@type": "Request",
723+
items: [{ "@type": "AuthenticationRequestItem", mustBeAccepted: false }]
724+
},
725+
requestSourceId: message.id
726+
});
727+
await recipient.consumption.incomingRequests.checkPrerequisites({ requestId: receivedRequestResult.value.id });
728+
729+
await expect(recipient.eventBus).toHavePublished(
730+
MessageProcessedEvent,
731+
(e) => e.data.result === MessageProcessedResult.ManualRequestDecisionRequired && e.data.message.id === message.id
732+
);
733+
});
734+
});
735+
592736
describe("RequestItemConfig", () => {
593737
test("rejects a RequestItem given a RequestItemConfig with all fields set", async () => {
594738
const deciderConfig: DeciderModuleConfigurationOverwrite = {

0 commit comments

Comments
 (0)