Skip to content

Commit

Permalink
Add IdentityMetadata Routes (#301)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
jkoenig134 authored Oct 30, 2024
1 parent bf4f026 commit 6268326
Show file tree
Hide file tree
Showing 12 changed files with 301 additions and 4 deletions.
3 changes: 3 additions & 0 deletions packages/sdk/src/ConnectorClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
AttributesEndpoint,
ChallengesEndpoint,
FilesEndpoint,
IdentityMetadataEndpoint,
IncomingRequestsEndpoint,
MessagesEndpoint,
MonitoringEndpoint,
Expand All @@ -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;
Expand All @@ -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);
Expand Down
16 changes: 16 additions & 0 deletions packages/sdk/src/endpoints/IdentityMetadataEndpoint.ts
Original file line number Diff line number Diff line change
@@ -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<ConnectorHttpResponse<ConnectorIdentityMetadata>> {
return await this.put("/api/v2/IdentityMetadata", request);
}

public async getIdentityMetadata(reference: string, key?: string): Promise<ConnectorHttpResponse<ConnectorIdentityMetadata>> {
return await this.get("/api/v2/IdentityMetadata", { reference: reference, key: key });
}

public async deleteIdentityMetadata(reference: string, key?: string): Promise<ConnectorHttpResponse<void>> {
return await this.delete("/api/v2/IdentityMetadata", { reference: reference, key: key }, 204);
}
}
1 change: 1 addition & 0 deletions packages/sdk/src/endpoints/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export interface ConnectorIdentityMetadata {
reference: string;
key?: string;
value: unknown;
}
2 changes: 2 additions & 0 deletions packages/sdk/src/types/identityMetadata/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from "./ConnectorIdentityMetadata";
export * from "./requests/UpsertIdentityMetadataRequest";
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export interface UpsertIdentityMetadataRequest {
reference: string;
key?: string;
value: unknown;
}
1 change: 1 addition & 0 deletions packages/sdk/src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
31 changes: 31 additions & 0 deletions src/modules/coreHttpApi/controllers/IdentityMetadataController.ts
Original file line number Diff line number Diff line change
@@ -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<Envelope> {
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<Envelope> {
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<void> {
const result = await this.consumptionServices.identityMetadata.deleteIdentityMetadata({ reference, key });
return this.noContent(result);
}
}
170 changes: 167 additions & 3 deletions src/modules/coreHttpApi/openapi.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -2190,8 +2315,8 @@ paths:
nullable: false
example:
"@type": Mail
to: [did:e:_________________________________]
cc: [did:e:_________________________________]
to: [did:e:<base-url>:dids:<22-characters>]
cc: [did:e:<base-url>: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.
Expand Down Expand Up @@ -4439,7 +4564,7 @@ components:
type: string
format: Address
minLength: 35
example: did:e:_________________________________
example: did:e:<base-url>:dids:<22-characters>

AttributeID:
type: string
Expand Down Expand Up @@ -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:
Expand Down
67 changes: 67 additions & 0 deletions test/identityMetadata.test.ts
Original file line number Diff line number Diff line change
@@ -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<string> {
const identityPart = await Random.string(22, `${RandomCharacterRange.Digit}abcdef`);
return `did:e:localhost:dids:${identityPart}`;
}
1 change: 1 addition & 0 deletions test/lib/validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export enum ValidationSchema {
Error = "ConnectorError",
File = "ConnectorFile",
Files = "ConnectorFiles",
IdentityMetadata = "ConnectorIdentityMetadata",
Message = "ConnectorMessage",
Messages = "ConnectorMessages",
MessageWithAttachments = "ConnectorMessageWithAttachments",
Expand Down
3 changes: 2 additions & 1 deletion test/spec.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 */

Expand Down

0 comments on commit 6268326

Please sign in to comment.