Skip to content

Commit

Permalink
feat: add createResource endpoint
Browse files Browse the repository at this point in the history
Signed-off-by: Sownak Roy <[email protected]>
  • Loading branch information
sownak committed Dec 9, 2024
1 parent 8e48a0e commit e1fab41
Show file tree
Hide file tree
Showing 8 changed files with 1,538 additions and 231 deletions.
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

47 changes: 46 additions & 1 deletion src/app.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import express from 'express';
import express, { Request, Response } from 'express';
import Helmet from 'helmet';
import swaggerUI from 'swagger-ui-express';

Expand All @@ -8,6 +8,7 @@ import { DidController } from './controllers/did.js';
import { CheqdController } from './controllers/cheqd.js';
import { ResourceController } from './controllers/resource.js';
import { CheqdRegistrar } from './service/cheqd.js';
import { MethodSpecificIdAlgo, VerificationMethods } from '@cheqd/sdk';

class App {
public express: express.Application;
Expand All @@ -29,6 +30,27 @@ class App {
private routes() {
const app = this.express;
const URL_PREFIX = '/1.0';
const staticTraits = {
cheqd: {
updatable: true,
deactivatable: true,
enumerable: true,
historyAvailable: true,
humanReadable: false,
},
};
const properties = {
cheqd: {
supportedVerificationMethods: [
VerificationMethods.Ed255192020,
VerificationMethods.Ed255192018,
VerificationMethods.JWK,
],
supportedAlgorithm: [MethodSpecificIdAlgo.Base58, MethodSpecificIdAlgo.Uuid],
localStoreTTL: process.env.LOCAL_STORE_TTL,
},
};
const didMethods = ['cheqd'];

app.get('/', (req, res) => res.redirect('api-docs'));

Expand Down Expand Up @@ -58,6 +80,29 @@ class App {
new ResourceController().create
);

app.post(
`${URL_PREFIX}/createResource`,
ResourceController.createResourceValidator,
DidController.commonValidator,
new ResourceController().createResource
);
app.post(
`${URL_PREFIX}/updateResource`,
ResourceController.createResourceValidator,
DidController.commonValidator,
new ResourceController().create
);

app.get(`${URL_PREFIX}/methods`, (req: Request, res: Response) => {
res.status(200).json(didMethods);
});
app.get(`${URL_PREFIX}/properties`, (req: Request, res: Response) => {
res.status(200).json(properties);
});
app.get(`${URL_PREFIX}/traits`, (req: Request, res: Response) => {
res.status(200).json(staticTraits);
});

// cheqd-helpers
app.get(`${URL_PREFIX}/key-pair`, new CheqdController().generateKeys);
app.get(`${URL_PREFIX}/did-document`, CheqdController.didDocValidator, new CheqdController().generateDidDoc);
Expand Down
142 changes: 135 additions & 7 deletions src/controllers/resource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { v4 } from 'uuid';
import { fromString } from 'uint8arrays';

import { CheqdRegistrar, CheqdResolver, NetworkType } from '../service/cheqd.js';
import { IResourceCreateRequest, IState } from '../types/types.js';
import { IResourceCreateRequest, IResourceCreateRequestV1, IResourceUpdateRequest, IState } from '../types/types.js';
import { Messages } from '../types/constants.js';
import { convertToSignInfo } from '../helpers/helpers.js';
import { Responses } from '../helpers/response.js';
Expand All @@ -29,13 +29,29 @@ export class ResourceController {
check('alsoKnownAs.*.uri').isString().withMessage(Messages.Invalid),
check('alsoKnownAs.*.description').isString().withMessage(Messages.Invalid),
];
public static createResourceValidator = [
check('did').exists().isString().contains('did:cheqd').withMessage(Messages.InvalidDid),
check('jobId')
.custom((value, { req }) => {
if (!value && !(req.body.name && req.body.type && req.body.content)) return false;
return true;
})
.withMessage('name, type and content are required'),
check('name').optional().isString().withMessage(Messages.Invalid),
check('type').optional().isString().withMessage(Messages.Invalid),
check('content').exists().isString().withMessage(Messages.Invalid),
check('relativeDidUrl').optional().isString().contains('/resources/').withMessage(Messages.InvalidDidUrl),
check('alsoKnownAs').optional().isArray().withMessage(Messages.Invalid),
check('alsoKnownAs.*.uri').isString().withMessage(Messages.Invalid),
check('alsoKnownAs.*.description').isString().withMessage(Messages.Invalid),
];

public async create(request: Request, response: Response) {
const result = validationResult(request);
if (!result.isEmpty()) {
return response
.status(400)
.json(Responses.GetInvalidResourceResponse({}, request.body.secret, result.array()[0].msg));
.json(Responses.GetInvalidResourceResponseV1({}, request.body.secret, result.array()[0].msg));
}

const { did } = request.params;
Expand All @@ -48,7 +64,7 @@ export class ResourceController {
version,
secret = {},
options = {},
} = request.body as IResourceCreateRequest;
} = request.body as IResourceCreateRequestV1;

let resourcePayload: Partial<MsgCreateResourcePayload> = {};
try {
Expand Down Expand Up @@ -84,7 +100,7 @@ export class ResourceController {
} else if (!data) {
return response
.status(400)
.json(Responses.GetInvalidResourceResponse({}, secret, Messages.InvalidResource));
.json(Responses.GetInvalidResourceResponseV1({}, secret, Messages.InvalidResource));
} else {
jobId = v4();

Expand All @@ -108,7 +124,7 @@ export class ResourceController {
return response
.status(200)
.json(
Responses.GetResourceActionSignatureResponse(
Responses.GetResourceActionSignatureResponseV1(
jobId,
resolvedDocument.verificationMethod,
resourcePayload
Expand All @@ -120,11 +136,13 @@ export class ResourceController {
await CheqdRegistrar.instance.connect(options);
const result = await CheqdRegistrar.instance.createResource(signInputs, resourcePayload);
if (result.code == 0) {
return response.status(201).json(Responses.GetResourceSuccessResponse(jobId, secret, resourcePayload));
return response
.status(201)
.json(Responses.GetResourceSuccessResponseV1(jobId, secret, resourcePayload));
} else {
return response
.status(400)
.json(Responses.GetInvalidResourceResponse(resourcePayload, secret, Messages.InvalidResource));
.json(Responses.GetInvalidResourceResponseV1(resourcePayload, secret, Messages.InvalidResource));
}
} catch (error) {
return response.status(500).json({
Expand All @@ -139,4 +157,114 @@ export class ResourceController {
});
}
}
public async createResource(request: Request, response: Response) {
const result = validationResult(request);
if (!result.isEmpty()) {
return response
.status(400)
.json(Responses.GetInvalidResourceResponse('', {}, request.body.secret, result.array()[0].msg));
}

let {
did,
jobId,
content,
name,
type,
version,
secret = {},
options = {},
} = request.body as IResourceCreateRequest;

let resourcePayload: Partial<MsgCreateResourcePayload> = {};
try {
// check if did is registered on the ledger
let resolvedDocument = await CheqdResolver(did);
if (!resolvedDocument?.didDocument || resolvedDocument.didDocumentMetadata.deactivated) {
return response
.status(400)
.send(Responses.GetInvalidResponse({ id: did }, secret, Messages.DidNotFound));
} else {
resolvedDocument = resolvedDocument.didDocument;
}

// Validate and get store data if any
if (jobId) {
const storeData = LocalStore.instance.getResource(jobId);
if (!storeData) {
return response.status(400).json(Responses.GetJobExpiredResponse(jobId));
} else if (storeData.state == IState.Finished) {
return response.status(201).json({
jobId,
didUrlState: {
resourceId: storeData.resource.id,
state: IState.Finished,
secret,
resource: storeData.resource,
},
});
}

resourcePayload = storeData.resource;
resourcePayload.data = new Uint8Array(Object.values(resourcePayload.data!));
} else if (!content) {
return response
.status(400)
.json(Responses.GetInvalidResourceResponse('', {}, secret, Messages.InvalidContent));
} else {
jobId = v4();

resourcePayload = {
collectionId: did.split(':').pop()!,
id: v4(),
name,
resourceType: type,
version: version,
data: fromString(content, 'base64'),
};
}

let signInputs: SignInfo[];

if (secret.signingResponse) {
signInputs = convertToSignInfo(secret.signingResponse);
} else {
LocalStore.instance.setResource(jobId, { resource: resourcePayload, state: IState.Action });
return response
.status(200)
.json(
Responses.GetResourceActionSignatureResponse(
jobId,
resolvedDocument.verificationMethod,
did,
resourcePayload
)
);
}

options.network = options.network || (did.split(':')[2] as NetworkType);
await CheqdRegistrar.instance.connect(options);
const result = await CheqdRegistrar.instance.createResource(signInputs, resourcePayload);
if (result.code == 0) {
return response
.status(201)
.json(Responses.GetResourceSuccessResponse(jobId, secret, did, resourcePayload));
} else {
return response
.status(400)
.json(Responses.GetInvalidResourceResponse(did, resourcePayload, secret, Messages.InvalidResource));
}
} catch (error) {
return response.status(500).json({
jobId,
didUrlState: {
state: IState.Failed,
reason: Messages.Internal,
description: Messages.TryAgain + error,
secret,
resourcePayload,
},
});
}
}
}
77 changes: 74 additions & 3 deletions src/helpers/response.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ export class Responses {
};
}

static GetResourceActionSignatureResponse(
static GetResourceActionSignatureResponseV1(
jobId: string,
verificationMethod: VerificationMethod[],
resource: Partial<MsgCreateResourcePayload>
Expand Down Expand Up @@ -129,6 +129,38 @@ export class Responses {
},
};
}
static GetResourceActionSignatureResponse(
jobId: string,
verificationMethod: VerificationMethod[],
did: string,
resource: Partial<MsgCreateResourcePayload>
) {
const signingRequest = verificationMethod.map((method) => {
return {
kid: method.id,
type: method.type,
alg: 'EdDSA',
serializedPayload: toString(
MsgCreateResourcePayload.encode(MsgCreateResourcePayload.fromPartial(resource)).finish(),
'base64pad'
),
};
});

return {
jobId,
didUrlState: {
didUrl: did + '/resources/' + resource.id,
state: IState.Action,
action: IAction.GetSignature,
description: Messages.GetSignature,
signingRequest,
secret: {
signingResponse: [Messages.SigingResponse],
},
},
};
}

static GetInvalidResponse(didDocument: DIDDocument | undefined, secret: Record<string, any> = {}, error: string) {
return {
Expand Down Expand Up @@ -167,7 +199,7 @@ export class Responses {
};
}

static GetResourceSuccessResponse(
static GetResourceSuccessResponseV1(
jobId: string,
secret: Record<string, any>,
resourcePayload: Partial<MsgCreateResourcePayload>
Expand All @@ -182,8 +214,29 @@ export class Responses {
},
};
}
static GetResourceSuccessResponse(
jobId: string,
secret: Record<string, any>,
did: string,
resourcePayload: Partial<MsgCreateResourcePayload>
) {
return {
jobId,
didUrlState: {
didUrl: did + '/resources/' + resourcePayload.id || '',
state: IState.Finished,
secret,
content: resourcePayload.data,
name: resourcePayload.name,
type: resourcePayload.resourceType,
version: resourcePayload.version,
},
didRegistrationMetadata: {},
contentMetadata: {},
};
}

static GetInvalidResourceResponse(
static GetInvalidResourceResponseV1(
resourcePayload: Partial<MsgCreateResourcePayload> = {},
secret: Record<string, any> = {},
error: string
Expand All @@ -200,4 +253,22 @@ export class Responses {
},
};
}
static GetInvalidResourceResponse(
did: string,
resourcePayload: Partial<MsgCreateResourcePayload> = {},
secret: Record<string, any> = {},
error: string
) {
return {
jobId: null,
didUrlState: {
didUrl: did + '/resources/' + resourcePayload.id,
state: IState.Failed,
reason: Messages.Invalid,
description: Messages.Invalid + ': ' + error,
secret,
resourcePayload,
},
};
}
}
Loading

0 comments on commit e1fab41

Please sign in to comment.