From 6268326fe83e908b6d7ad045cae44aa6dc4e4609 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20K=C3=B6nig?= <33655937+jkoenig134@users.noreply.github.com> Date: Wed, 30 Oct 2024 12:15:21 +0100 Subject: [PATCH] Add IdentityMetadata Routes (#301) * feat: add IdentityMetadataController * feat: add sdk support * test: add tests for identity metadata * chore: start openapi doc * fix: use noContent for delete * chore: add more tests * chore: expect 204 * chore: 204 for delete idm * feat: update openapi spec * fix: make key optional * fix: add UpsertIdentityMetadataRequest * chore: update description * chore: move to object --- packages/sdk/src/ConnectorClient.ts | 3 + .../src/endpoints/IdentityMetadataEndpoint.ts | 16 ++ packages/sdk/src/endpoints/index.ts | 1 + .../ConnectorIdentityMetadata.ts | 5 + .../sdk/src/types/identityMetadata/index.ts | 2 + .../requests/UpsertIdentityMetadataRequest.ts | 5 + packages/sdk/src/types/index.ts | 1 + .../controllers/IdentityMetadataController.ts | 31 ++++ src/modules/coreHttpApi/openapi.yml | 170 +++++++++++++++++- test/identityMetadata.test.ts | 67 +++++++ test/lib/validation.ts | 1 + test/spec.test.ts | 3 +- 12 files changed, 301 insertions(+), 4 deletions(-) create mode 100644 packages/sdk/src/endpoints/IdentityMetadataEndpoint.ts create mode 100644 packages/sdk/src/types/identityMetadata/ConnectorIdentityMetadata.ts create mode 100644 packages/sdk/src/types/identityMetadata/index.ts create mode 100644 packages/sdk/src/types/identityMetadata/requests/UpsertIdentityMetadataRequest.ts create mode 100644 src/modules/coreHttpApi/controllers/IdentityMetadataController.ts create mode 100644 test/identityMetadata.test.ts diff --git a/packages/sdk/src/ConnectorClient.ts b/packages/sdk/src/ConnectorClient.ts index ac56dbe5..472f93e2 100644 --- a/packages/sdk/src/ConnectorClient.ts +++ b/packages/sdk/src/ConnectorClient.ts @@ -5,6 +5,7 @@ import { AttributesEndpoint, ChallengesEndpoint, FilesEndpoint, + IdentityMetadataEndpoint, IncomingRequestsEndpoint, MessagesEndpoint, MonitoringEndpoint, @@ -19,6 +20,7 @@ export class ConnectorClient { public readonly attributes: AttributesEndpoint; public readonly challenges: ChallengesEndpoint; public readonly files: FilesEndpoint; + public readonly identityMetadata: IdentityMetadataEndpoint; public readonly incomingRequests: IncomingRequestsEndpoint; public readonly messages: MessagesEndpoint; public readonly monitoring: MonitoringEndpoint; @@ -43,6 +45,7 @@ export class ConnectorClient { this.attributes = new AttributesEndpoint(axiosInstance); this.challenges = new ChallengesEndpoint(axiosInstance); this.files = new FilesEndpoint(axiosInstance); + this.identityMetadata = new IdentityMetadataEndpoint(axiosInstance); this.incomingRequests = new IncomingRequestsEndpoint(axiosInstance); this.messages = new MessagesEndpoint(axiosInstance); this.monitoring = new MonitoringEndpoint(axiosInstance); diff --git a/packages/sdk/src/endpoints/IdentityMetadataEndpoint.ts b/packages/sdk/src/endpoints/IdentityMetadataEndpoint.ts new file mode 100644 index 00000000..8d679229 --- /dev/null +++ b/packages/sdk/src/endpoints/IdentityMetadataEndpoint.ts @@ -0,0 +1,16 @@ +import { ConnectorHttpResponse, ConnectorIdentityMetadata, UpsertIdentityMetadataRequest } from "../types"; +import { Endpoint } from "./Endpoint"; + +export class IdentityMetadataEndpoint extends Endpoint { + public async upsertIdentityMetadata(request: UpsertIdentityMetadataRequest): Promise> { + return await this.put("/api/v2/IdentityMetadata", request); + } + + public async getIdentityMetadata(reference: string, key?: string): Promise> { + return await this.get("/api/v2/IdentityMetadata", { reference: reference, key: key }); + } + + public async deleteIdentityMetadata(reference: string, key?: string): Promise> { + return await this.delete("/api/v2/IdentityMetadata", { reference: reference, key: key }, 204); + } +} diff --git a/packages/sdk/src/endpoints/index.ts b/packages/sdk/src/endpoints/index.ts index 52384240..a32f274b 100644 --- a/packages/sdk/src/endpoints/index.ts +++ b/packages/sdk/src/endpoints/index.ts @@ -2,6 +2,7 @@ export * from "./AccountEndpoint"; export * from "./AttributesEndpoint"; export * from "./ChallengesEndpoint"; export * from "./FilesEndpoint"; +export * from "./IdentityMetadataEndpoint"; export * from "./IncomingRequestsEndpoint"; export * from "./MessagesEndpoint"; export * from "./MonitoringEndpoint"; diff --git a/packages/sdk/src/types/identityMetadata/ConnectorIdentityMetadata.ts b/packages/sdk/src/types/identityMetadata/ConnectorIdentityMetadata.ts new file mode 100644 index 00000000..d1c1d0c0 --- /dev/null +++ b/packages/sdk/src/types/identityMetadata/ConnectorIdentityMetadata.ts @@ -0,0 +1,5 @@ +export interface ConnectorIdentityMetadata { + reference: string; + key?: string; + value: unknown; +} diff --git a/packages/sdk/src/types/identityMetadata/index.ts b/packages/sdk/src/types/identityMetadata/index.ts new file mode 100644 index 00000000..24d0617a --- /dev/null +++ b/packages/sdk/src/types/identityMetadata/index.ts @@ -0,0 +1,2 @@ +export * from "./ConnectorIdentityMetadata"; +export * from "./requests/UpsertIdentityMetadataRequest"; diff --git a/packages/sdk/src/types/identityMetadata/requests/UpsertIdentityMetadataRequest.ts b/packages/sdk/src/types/identityMetadata/requests/UpsertIdentityMetadataRequest.ts new file mode 100644 index 00000000..5351ad1c --- /dev/null +++ b/packages/sdk/src/types/identityMetadata/requests/UpsertIdentityMetadataRequest.ts @@ -0,0 +1,5 @@ +export interface UpsertIdentityMetadataRequest { + reference: string; + key?: string; + value: unknown; +} diff --git a/packages/sdk/src/types/index.ts b/packages/sdk/src/types/index.ts index 1f515491..47f99dbb 100644 --- a/packages/sdk/src/types/index.ts +++ b/packages/sdk/src/types/index.ts @@ -4,6 +4,7 @@ export * from "./challenges"; export * from "./ConnectorError"; export * from "./ConnectorHttpResponse"; export * from "./files"; +export * from "./identityMetadata"; export * from "./messages"; export * from "./relationships"; export * from "./relationshipTemplates"; diff --git a/src/modules/coreHttpApi/controllers/IdentityMetadataController.ts b/src/modules/coreHttpApi/controllers/IdentityMetadataController.ts new file mode 100644 index 00000000..11e75f4c --- /dev/null +++ b/src/modules/coreHttpApi/controllers/IdentityMetadataController.ts @@ -0,0 +1,31 @@ +import { ConsumptionServices } from "@nmshd/runtime"; +import { Inject } from "@nmshd/typescript-ioc"; +import { Accept, DELETE, GET, PUT, Path, QueryParam } from "@nmshd/typescript-rest"; +import { Envelope } from "../../../infrastructure"; +import { BaseController } from "../common/BaseController"; + +@Path("/api/v2/IdentityMetadata") +export class IdentityMetadataController extends BaseController { + public constructor(@Inject private readonly consumptionServices: ConsumptionServices) { + super(); + } + + @PUT + @Accept("application/json") + public async upsertIdentityMetadata(request: any): Promise { + const result = await this.consumptionServices.identityMetadata.upsertIdentityMetadata(request); + return this.ok(result); + } + + @GET + public async getIdentityMetadata(@QueryParam("reference") reference: string, @QueryParam("key") key?: string): Promise { + const result = await this.consumptionServices.identityMetadata.getIdentityMetadata({ reference, key }); + return this.ok(result); + } + + @DELETE + public async deleteIdentityMetadata(@QueryParam("reference") reference: string, @QueryParam("key") key?: string): Promise { + const result = await this.consumptionServices.identityMetadata.deleteIdentityMetadata({ reference, key }); + return this.noContent(result); + } +} diff --git a/src/modules/coreHttpApi/openapi.yml b/src/modules/coreHttpApi/openapi.yml index b1f29902..b9b0651d 100644 --- a/src/modules/coreHttpApi/openapi.yml +++ b/src/modules/coreHttpApi/openapi.yml @@ -2158,6 +2158,131 @@ paths: 404: $ref: "#/components/responses/NotFound" + # ------------------- IdentityMetadata ------------------- + + /api/v2/IdentityMetadata: + put: + operationId: upsertIdentityMetadata + description: Creates or updates an IdentityMetadata object for the specified `reference` and `key` combination. + tags: + - IdentityMetadata + requestBody: + content: + application/json: + schema: + type: object + $ref: "#/components/schemas/UpsertIdentityMetadataRequest" + responses: + 200: + description: Success + content: + application/json: + schema: + type: object + properties: + result: + nullable: false + $ref: "#/components/schemas/IdentityMetadata" + required: + - result + headers: + X-Response-Duration-ms: + schema: + $ref: "#/components/schemas/HeaderContent_X-Response-Duration-ms" + X-Response-Time: + schema: + $ref: "#/components/schemas/HeaderContent_X-Response-Time" + 400: + $ref: "#/components/responses/BadRequest" + 401: + $ref: "#/components/responses/Unauthorized" + 403: + $ref: "#/components/responses/Forbidden" + 404: + $ref: "#/components/responses/NotFound" + + get: + operationId: getIdentityMetadata + description: Fetches the IdentityMetadata with the given `reference` and `key` combination. + tags: + - IdentityMetadata + parameters: + - in: query + name: reference + description: The reference of the IdentityMetadata. + required: true + schema: + $ref: "#/components/schemas/Address" + - in: query + name: key + description: The optional key of the IdentityMetadata. + required: false + schema: + type: string + responses: + 200: + description: Success + content: + application/json: + schema: + type: object + properties: + result: + nullable: false + $ref: "#/components/schemas/IdentityMetadata" + required: + - result + headers: + X-Response-Duration-ms: + schema: + $ref: "#/components/schemas/HeaderContent_X-Response-Duration-ms" + X-Response-Time: + schema: + $ref: "#/components/schemas/HeaderContent_X-Response-Time" + 401: + $ref: "#/components/responses/Unauthorized" + 403: + $ref: "#/components/responses/Forbidden" + 404: + $ref: "#/components/responses/NotFound" + + delete: + operationId: deleteIdentityMetadata + description: Deletes the IdentityMetadata with the given `reference` and `key` combination. + tags: + - IdentityMetadata + parameters: + - in: query + name: reference + description: The reference of the IdentityMetadata. + required: true + schema: + $ref: "#/components/schemas/Address" + - in: query + name: key + description: The optional key of the IdentityMetadata. + required: false + schema: + type: string + responses: + 204: + description: Success + headers: + X-Response-Duration-ms: + schema: + $ref: "#/components/schemas/HeaderContent_X-Response-Duration-ms" + X-Response-Time: + schema: + $ref: "#/components/schemas/HeaderContent_X-Response-Time" + 400: + $ref: "#/components/responses/BadRequest" + 401: + $ref: "#/components/responses/Unauthorized" + 403: + $ref: "#/components/responses/Forbidden" + 404: + $ref: "#/components/responses/NotFound" + # ------------------- Messages ------------------- /api/v2/Messages: @@ -2190,8 +2315,8 @@ paths: nullable: false example: "@type": Mail - to: [did:e:_________________________________] - cc: [did:e:_________________________________] + to: [did:e::dids:<22-characters>] + cc: [did:e::dids:<22-characters>] subject: Subject body: Body description: Either a Mail, a Request, a ResponseWrapper, a Notification or an ArbitraryMessageContent must be provided as the 'content' of the Message. @@ -4439,7 +4564,7 @@ components: type: string format: Address minLength: 35 - example: did:e:_________________________________ + example: did:e::dids:<22-characters> AttributeID: type: string @@ -5805,6 +5930,45 @@ components: type: object description: The arbitrary JSON object which should be shared between creator of the Token and the recipient. + IdentityMetadata: + type: object + additionalProperties: false + properties: + reference: + allOf: + - $ref: "#/components/schemas/Address" + nullable: false + description: The address of the identity for that the metadata is stored for. + key: + type: string + nullable: true + description: An optional key to identify the metadata. Can be used to store multiple metadata entries for the same identity. There can be at most one IdentityMetadata per `reference` and `key` combination. + value: + type: object + example: { "key": "value" } + description: The metadata value as a JSON object. + required: + - reference + - value + + UpsertIdentityMetadataRequest: + type: object + properties: + reference: + allOf: + - $ref: "#/components/schemas/Address" + nullable: false + key: + type: string + nullable: true + value: + type: object + nullable: false + example: { "key": "value" } + required: + - reference + - value + # ------------------- General ------------------- ErrorContent: diff --git a/test/identityMetadata.test.ts b/test/identityMetadata.test.ts new file mode 100644 index 00000000..4bde0dc4 --- /dev/null +++ b/test/identityMetadata.test.ts @@ -0,0 +1,67 @@ +import { Random, RandomCharacterRange } from "@nmshd/transport"; +import { ConnectorClientWithMetadata, Launcher } from "./lib/Launcher"; +import { ValidationSchema } from "./lib/validation"; + +const launcher = new Launcher(); +let client1: ConnectorClientWithMetadata; + +beforeAll(async () => { + [client1] = await launcher.launch(1); +}, 30000); + +afterAll(() => launcher.stop()); + +describe("IdentityMetadata", () => { + test.each([ + { + reference: "did:e:localhost:dids:1234567890abcdef123456", + key: undefined, + value: "value" + }, + { + reference: "did:e:localhost:dids:1234567890abcdef123456", + key: undefined, + value: { a: "json" } + }, + { + reference: "did:e:localhost:dids:1234567890abcdef123456", + key: "key", + value: "value" + } + ])("should upsert an IdentityMetadata with key '$key' and value '$value'", async (data) => { + const result = await client1.identityMetadata.upsertIdentityMetadata(data); + expect(result).toBeSuccessful(ValidationSchema.IdentityMetadata); + + const identityMetadata = result.result; + expect(identityMetadata.reference.toString()).toStrictEqual(data.reference); + expect(identityMetadata.key).toStrictEqual(data.key); + expect(identityMetadata.value).toStrictEqual(data.value); + }); + + test("should get an IdentityMetadata", async () => { + const reference = await generateReference(); + await client1.identityMetadata.upsertIdentityMetadata({ reference: reference, value: "value" }); + + const result = await client1.identityMetadata.getIdentityMetadata(reference); + expect(result).toBeSuccessful(ValidationSchema.IdentityMetadata); + + const identityMetadata = result.result; + expect(identityMetadata.value).toBe("value"); + }); + + test("should delete an IdentityMetadata", async () => { + const reference = await generateReference(); + await client1.identityMetadata.upsertIdentityMetadata({ reference: reference, value: "value" }); + + const result = await client1.identityMetadata.deleteIdentityMetadata(reference); + expect(result).toBeSuccessfulVoidResult(); + + const getResult = await client1.identityMetadata.getIdentityMetadata(reference); + expect(getResult).toBeAnError("IdentityMetadata not found. Make sure the ID exists and the record is not expired.", "error.runtime.recordNotFound"); + }); +}); + +async function generateReference(): Promise { + const identityPart = await Random.string(22, `${RandomCharacterRange.Digit}abcdef`); + return `did:e:localhost:dids:${identityPart}`; +} diff --git a/test/lib/validation.ts b/test/lib/validation.ts index fd5a5885..2a4cd150 100644 --- a/test/lib/validation.ts +++ b/test/lib/validation.ts @@ -13,6 +13,7 @@ export enum ValidationSchema { Error = "ConnectorError", File = "ConnectorFile", Files = "ConnectorFiles", + IdentityMetadata = "ConnectorIdentityMetadata", Message = "ConnectorMessage", Messages = "ConnectorMessages", MessageWithAttachments = "ConnectorMessageWithAttachments", diff --git a/test/spec.test.ts b/test/spec.test.ts index 401173a9..1e680994 100644 --- a/test/spec.test.ts +++ b/test/spec.test.ts @@ -59,7 +59,8 @@ describe("test openapi spec against routes", () => { "/api/v2/Attributes/ValidateIQLQuery": { post: "200" }, "/api/v2/Challenges/Validate": { post: "200" }, "/api/v2/Relationships/{param}": { delete: "204" }, - "/api/v2/Attributes/{param}": { delete: "204" } + "/api/v2/Attributes/{param}": { delete: "204" }, + "/api/v2/IdentityMetadata": { delete: "204" } }; /* eslint-enable @typescript-eslint/naming-convention */