Skip to content

Commit

Permalink
Uniqueness of key for RelationshipAttributes (#319)
Browse files Browse the repository at this point in the history
* refactor: preapare CreateAttributeRequestItemProcessor to add more validation

* feat: add validation for CreateAttributeRequestItemProcessor

* test: CreateAttributeRequestItemProcessor key validation

* fix: uniqueness for key only instead of pair of owner and key

* fix: failing test

* test: ensure key uniqueness in attributes.test

* test: ensure key uniqueness in CreateRelationshipAttributeRequestItemDVO.test

* feat: use database language every database understands

* test: ensure key uniqueness only for constant ownership

* fix: too late clean up of Attributes

* feat: ensure key uniqueness only for RelationshipAttributes that are not in deletion in any way

* refactor: extract function call as auxiliary function

* refactor: put afterEach block inside describe block

* feat: ensure key uniqueness for Recipient within CreateAttributeRequestItemProcessor

* refactor: auxiliary function can be applied to queries as well

* feat: ensure key uniqueness for Sender within ReadAttributeRequestItemProcessor

* feat: ensure key uniqueness only for constant value type

* fix: failing tests with async canCreateOutgoingRequestItem method

* feat: key uniqueness for Recipient within ReadAttributeRequestItemProcessor

* feat: ensure key uniquess for empty owner as placeholder as well

* refactor: simplify error message

* refactor: move validation to canAccept method

* refactor: standardize error messages

* test: ensure key uniqueness with empty owner

* test: move tests to canAccept block

* fix: call accept instead of canAccept

* test: refactor canAccept test with SuccessfulValidationResult

* test: make tests independent of each other

* feat: ensure key uniqueness for empty owner queried with ReadAttributeRequestItem

* feat: ensure key uniqueness on Sender site of ProposeAttributeRequestItemProcessor

* feat: ensure key uniqueness on Recipient site of ProposeAttributeRequestItemProcessor

* refactor: standardize error messages

* feat: prohibit Requests that create more than one RelationshipAttribute with same key

* fix: title type

* test: key uniqueness of incomingRequestsController

* test: key uniqueness of OutgoingRequestsController

* refactor: remove recursion of IncomingRequestsController

* refactor: keep RequestItemGroup in mind

* refactor: use auxiliary method for clearness

* feat: handle empty owner in IncomingRequestsController and OutgoingRequestsController

* test: ensure key uniqueness for empty owner

* refactor: move validation from canDecide to canAccept

* fix: failing IncomingRequestsController test

* test: ensure key uniqueness with RequestItemGroups

* fix: wrong DecideRequestParameters for RequestItemGroups

* feat: use already known separation sequence

* feat: give better validation function name

* refactor: do not break existing code blocks anymore

* fix: error due to incorrect merging

* test: ensure key uniqueness with ProposeAttributeRequestItem

* chore: descriptive values should not start with capital letter

* refactor: rename error code

* feat: distinguish between user mistake and deformed Request

* feat: throwing instead of returning error if Request is deformed

* feat: reuse key uniqueness error

* feat: distinguish between deformed Requests and wrong accept parameters

* test: RequestItem cannot be accepted due to incorrect params of User

* fix: failing test due to void return

* feat: self-explanatory variable names

* refactor: more renaming of variables

* refactor: unify renaming

* feat: simplify error message for unknown recipient of Request

* feat: incorporate some review comments

* fix: failing test due to wrong error format

* feat: incorporate review comment

* refactor: prepare reusability of code

* fix: error due to merge

* feat: use JSON.stringify() instead of own method for extracting identifiers

* refactor: reduce redundancy by move auxiliary functions to different file

* feat: inform user what key would be redundantly in use

* feat: incorporate some review comments

* feat: incoporate review comments

* refactor: change order of thrown errors

* test: can propose and query RelationshipAttribute with same key but different value type

* feat: incorporate review comments

* refactor: simplify extraction of fragments of mustBeAcceptedItems

* refactor: further simplification of extraction of fragments of mustBeAcceptedItems

* refactor: simplify extraction of fragments of accepte items

* fix: returned instead of continued

* test: RequestItem cannot be accepted if key uniqueness is violated regardless of value of mustBeAccepted

---------

Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
Co-authored-by: Milena Czierlinski <milena.czierlinski@js-soft.com>
  • Loading branch information
3 people authored and sebbi08 committed Dec 18, 2024
1 parent d9aaa08 commit cc9ec2e
Show file tree
Hide file tree
Showing 19 changed files with 1,321 additions and 101 deletions.
4 changes: 4 additions & 0 deletions packages/consumption/src/consumption/ConsumptionCoreErrors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,10 @@ class Requests {
return new CoreError("error.consumption.requests.peerIsInDeletion", message);
}

public violatedKeyUniquenessOfRelationshipAttributes(message: string) {
return new CoreError("error.consumption.requests.violatedKeyUniquenessOfRelationshipAttributes", message);
}

public inheritedFromItem(message: string) {
return new ApplicationError("error.consumption.requests.validation.inheritedFromItem", message);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1294,6 +1294,25 @@ export class AttributesController extends ConsumptionBaseController {
return ownSharedAttributeSuccessors;
}

public async getRelationshipAttributesOfValueTypeToPeerWithGivenKeyAndOwner(key: string, owner: CoreAddress, valueType: string, peer: CoreAddress): Promise<LocalAttribute[]> {
return await this.getLocalAttributes({
"content.@type": "RelationshipAttribute",
"content.owner": owner.toString(),
"content.key": key,
"content.value.@type": valueType,
"shareInfo.peer": peer.toString(),
"shareInfo.thirdPartyAddress": { $exists: false },
"deletionInfo.deletionStatus": {
$nin: [
LocalAttributeDeletionStatus.ToBeDeleted,
LocalAttributeDeletionStatus.ToBeDeletedByPeer,
LocalAttributeDeletionStatus.DeletedByPeer,
LocalAttributeDeletionStatus.DeletedByOwner
]
}
});
}

public async getAttributeTagCollection(): Promise<AttributeTagCollection> {
const backboneTagCollection = (await this.attributeTagClient.getTagCollection()).value;
return AttributeTagCollection.from(backboneTagCollection);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { RequestItemProcessorRegistry } from "../itemProcessors/RequestItemProce
import { ILocalRequestSource, LocalRequest } from "../local/LocalRequest";
import { LocalRequestStatus } from "../local/LocalRequestStatus";
import { LocalResponse, LocalResponseSource } from "../local/LocalResponse";
import { validateKeyUniquenessOfRelationshipAttributesWithinIncomingRequest } from "../utility/validateRelationshipAttributesWithinRequest";
import { DecideRequestParametersValidator } from "./DecideRequestParametersValidator";
import { CheckPrerequisitesOfIncomingRequestParameters, ICheckPrerequisitesOfIncomingRequestParameters } from "./checkPrerequisites/CheckPrerequisitesOfIncomingRequestParameters";
import { CompleteIncomingRequestParameters, ICompleteIncomingRequestParameters } from "./complete/CompleteIncomingRequestParameters";
Expand Down Expand Up @@ -166,7 +167,15 @@ export class IncomingRequestsController extends ConsumptionBaseController {
}

public async canAccept(params: DecideRequestParametersJSON): Promise<ValidationResult> {
return await this.canDecide({ ...params, accept: true });
const canDecideResult = await this.canDecide({ ...params, accept: true });

if (canDecideResult.isError()) return canDecideResult;

const request = await this.getOrThrow(params.requestId);
const keyUniquenessValidationResult = validateKeyUniquenessOfRelationshipAttributesWithinIncomingRequest(request.content.items, params.items, this.identity.address);
if (keyUniquenessValidationResult.isError()) return keyUniquenessValidationResult;

return canDecideResult;
}

public async canReject(params: DecideRequestParametersJSON): Promise<ValidationResult> {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
import { CreateAttributeAcceptResponseItem, CreateAttributeRequestItem, IdentityAttribute, RejectResponseItem, Request, ResponseItemResult } from "@nmshd/content";
import {
CreateAttributeAcceptResponseItem,
CreateAttributeRequestItem,
IdentityAttribute,
RejectResponseItem,
RelationshipAttribute,
Request,
ResponseItemResult
} from "@nmshd/content";
import { CoreAddress } from "@nmshd/core-types";
import { ConsumptionCoreErrors } from "../../../../consumption/ConsumptionCoreErrors";
import { LocalAttribute } from "../../../attributes";
Expand All @@ -8,11 +16,7 @@ import { GenericRequestItemProcessor } from "../GenericRequestItemProcessor";
import { LocalRequestInfo } from "../IRequestItemProcessor";

export class CreateAttributeRequestItemProcessor extends GenericRequestItemProcessor<CreateAttributeRequestItem> {
public override canCreateOutgoingRequestItem(
requestItem: CreateAttributeRequestItem,
_request?: Request,
recipient?: CoreAddress
): ValidationResult | Promise<ValidationResult> {
public override async canCreateOutgoingRequestItem(requestItem: CreateAttributeRequestItem, _request?: Request, recipient?: CoreAddress): Promise<ValidationResult> {
const recipientIsAttributeOwner = requestItem.attribute.owner.equals(recipient);
const senderIsAttributeOwner = requestItem.attribute.owner.equals(this.currentIdentityAddress);
const ownerIsEmptyString = requestItem.attribute.owner.toString() === "";
Expand All @@ -30,30 +34,68 @@ export class CreateAttributeRequestItemProcessor extends GenericRequestItemProce
);
}

if (typeof recipient !== "undefined") {
return ValidationResult.error(
ConsumptionCoreErrors.requests.invalidRequestItem(
"The owner of the provided IdentityAttribute for the `attribute` property can only be the address of the recipient or an empty string. The latter will default to the address of the recipient."
)
);
}

if (!(recipientIsAttributeOwner || senderIsAttributeOwner || ownerIsEmptyString)) {
return ValidationResult.error(
ConsumptionCoreErrors.requests.invalidRequestItem(
"The owner of the provided RelationshipAttribute for the `attribute` property can only be the address of the sender, the address of the recipient or an empty string. The latter will default to the address of the recipient."
)
);
}

if (typeof recipient !== "undefined") {
const relationshipAttributesWithSameKey = await this.consumptionController.attributes.getRelationshipAttributesOfValueTypeToPeerWithGivenKeyAndOwner(
requestItem.attribute.key,
ownerIsEmptyString ? recipient : requestItem.attribute.owner,
requestItem.attribute.value.toJSON()["@type"],
recipient
);

if (relationshipAttributesWithSameKey.length !== 0) {
return ValidationResult.error(
ConsumptionCoreErrors.requests.invalidRequestItem(
"The owner of the provided IdentityAttribute for the `attribute` property can only be the Recipient's Address or an empty string. The latter will default to the Recipient's Address."
`The creation of the provided RelationshipAttribute cannot be requested because there is already a RelationshipAttribute in the context of this Relationship with the same key '${requestItem.attribute.key}', owner and value type.`
)
);
}
}

return ValidationResult.error(
ConsumptionCoreErrors.requests.invalidRequestItem(
"The owner of the provided IdentityAttribute for the `attribute` property can only be an empty string. It will default to the Recipient's Address."
)
return ValidationResult.success();
}

public override async canAccept(requestItem: CreateAttributeRequestItem, _params: AcceptRequestItemParametersJSON, requestInfo: LocalRequestInfo): Promise<ValidationResult> {
if (requestItem.attribute instanceof RelationshipAttribute) {
const ownerIsEmptyString = requestItem.attribute.owner.toString() === "";

const relationshipAttributesWithSameKey = await this.consumptionController.attributes.getRelationshipAttributesOfValueTypeToPeerWithGivenKeyAndOwner(
requestItem.attribute.key,
ownerIsEmptyString ? this.currentIdentityAddress : requestItem.attribute.owner,
requestItem.attribute.value.toJSON()["@type"],
requestInfo.peer
);
}

if (recipientIsAttributeOwner || senderIsAttributeOwner || ownerIsEmptyString) {
return ValidationResult.success();
if (relationshipAttributesWithSameKey.length !== 0) {
if (requestItem.mustBeAccepted) {
throw ConsumptionCoreErrors.requests.violatedKeyUniquenessOfRelationshipAttributes(
`The provided RelationshipAttribute cannot be created because there is already a RelationshipAttribute in the context of this Relationship with the same key '${requestItem.attribute.key}', owner and value type.`
);
}

return ValidationResult.error(
ConsumptionCoreErrors.requests.invalidAcceptParameters(
`This CreateAttributeRequestItem cannot be accepted as the provided RelationshipAttribute cannot be created because there is already a RelationshipAttribute in the context of this Relationship with the same key '${requestItem.attribute.key}', owner and value type.`
)
);
}
}

return ValidationResult.error(
ConsumptionCoreErrors.requests.invalidRequestItem(
"The owner of the provided RelationshipAttribute for the `attribute` property can only be the address of the sender, recipient or an empty string. The latter will default to the address of the recipient."
)
);
return ValidationResult.success();
}

public override async accept(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import validateQuery from "../utility/validateQuery";
import { AcceptProposeAttributeRequestItemParameters, AcceptProposeAttributeRequestItemParametersJSON } from "./AcceptProposeAttributeRequestItemParameters";

export class ProposeAttributeRequestItemProcessor extends GenericRequestItemProcessor<ProposeAttributeRequestItem, AcceptProposeAttributeRequestItemParametersJSON> {
public override canCreateOutgoingRequestItem(requestItem: ProposeAttributeRequestItem, _request: Request, recipient?: CoreAddress): ValidationResult {
public override async canCreateOutgoingRequestItem(requestItem: ProposeAttributeRequestItem, _request: Request, recipient?: CoreAddress): Promise<ValidationResult> {
const queryValidationResult = this.validateQuery(requestItem, recipient);
if (queryValidationResult.isError()) {
return queryValidationResult;
Expand All @@ -46,6 +46,23 @@ export class ProposeAttributeRequestItemProcessor extends GenericRequestItemProc
return proposedAttributeMatchesWithQueryValidationResult;
}

if (requestItem.query instanceof RelationshipAttributeQuery && typeof recipient !== "undefined") {
const relationshipAttributesWithSameKey = await this.consumptionController.attributes.getRelationshipAttributesOfValueTypeToPeerWithGivenKeyAndOwner(
requestItem.query.key,
recipient,
requestItem.query.attributeCreationHints.valueType,
recipient
);

if (relationshipAttributesWithSameKey.length !== 0) {
return ValidationResult.error(
ConsumptionCoreErrors.requests.invalidRequestItem(
`The creation of the proposed RelationshipAttribute cannot be requested because there is already a RelationshipAttribute in the context of this Relationship with the same key '${requestItem.query.key}', owner and value type.`
)
);
}
}

return ValidationResult.success();
}

Expand Down Expand Up @@ -157,6 +174,29 @@ export class ProposeAttributeRequestItemProcessor extends GenericRequestItemProc
const answerToQueryValidationResult = validateAttributeMatchesWithQuery(requestItem.query, attribute, this.currentIdentityAddress, requestInfo.peer);
if (answerToQueryValidationResult.isError()) return answerToQueryValidationResult;

if (requestItem.query instanceof RelationshipAttributeQuery) {
const relationshipAttributesWithSameKey = await this.consumptionController.attributes.getRelationshipAttributesOfValueTypeToPeerWithGivenKeyAndOwner(
requestItem.query.key,
this.currentIdentityAddress,
requestItem.query.attributeCreationHints.valueType,
requestInfo.peer
);

if (relationshipAttributesWithSameKey.length !== 0) {
if (requestItem.mustBeAccepted) {
throw ConsumptionCoreErrors.requests.violatedKeyUniquenessOfRelationshipAttributes(
`The queried RelationshipAttribute cannot be created because there is already a RelationshipAttribute in the context of this Relationship with the same key '${requestItem.query.key}', owner and value type.`
);
}

return ValidationResult.error(
ConsumptionCoreErrors.requests.invalidAcceptParameters(
`This ProposeAttributeRequestItem cannot be accepted as the queried RelationshipAttribute cannot be created because there is already a RelationshipAttribute in the context of this Relationship with the same key '${requestItem.query.key}', owner and value type.`
)
);
}
}

return ValidationResult.success();
}

Expand Down
Loading

0 comments on commit cc9ec2e

Please sign in to comment.