Skip to content

Commit

Permalink
Add /query API endpoint (#125)
Browse files Browse the repository at this point in the history
Co-authored-by: Henry Tsai <[email protected]>
  • Loading branch information
csuwildcat and thehenrytsai authored Apr 27, 2024
1 parent 183d943 commit 3db86d6
Show file tree
Hide file tree
Showing 3 changed files with 98 additions and 5 deletions.
1 change: 0 additions & 1 deletion .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
"version": "0.2.0",
"configurations": [
{
"runtimeVersion": "18",
"type": "node",
"request": "launch",
"name": "Tests - Node",
Expand Down
43 changes: 40 additions & 3 deletions src/http-api.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { type Dwn, RecordsRead, type RecordsReadReply } from '@tbd54566975/dwn-sdk-js';
import { type Dwn, RecordsRead, RecordsQuery } from '@tbd54566975/dwn-sdk-js';

import cors from 'cors';
import type { Express, Request, Response } from 'express';
Expand Down Expand Up @@ -86,7 +86,8 @@ export class HttpApi {
);
}

/* setupRoutes configures the HTTP server's request handlers
/**
* Configures the HTTP server's request handlers.
*/
#setupRoutes(): void {
this.#api.get('/health', (_req, res) => {
Expand All @@ -107,7 +108,7 @@ export class HttpApi {
const record = await RecordsRead.create({
filter: { recordId: req.params.id },
});
const reply = (await this.dwn.processMessage(req.params.did, record.toJSON())) as RecordsReadReply;
const reply = await this.dwn.processMessage(req.params.did, record.message);

if (reply.status.code === 200) {
if (reply?.record?.data) {
Expand All @@ -128,6 +129,42 @@ export class HttpApi {
}
});

this.#api.get('/:did/query', async (req, res) => {

try {
// builds a nested object from flat keys with dot notation which may share the same parent path
// e.g. "filter.protocol=foo&filter.protocolPath=bar" becomes
// {
// filter: {
// protocol: 'foo',
// protocolPath: 'bar'
// }
// }
const recordsQueryOptions = {} as any;
for (const param in req.query) {
const keys = param.split('.');
const lastKey = keys.pop();
const lastLevelObject = keys.reduce((obj, key) => obj[key] = obj[key] || {}, recordsQueryOptions)
lastLevelObject[lastKey] = req.query[param];
}

const recordsQuery = await RecordsQuery.create({
filter: recordsQueryOptions.filter,
pagination: recordsQueryOptions.pagination,
dateSort: recordsQueryOptions.dateSort,
});

// should always return a 200 status code with a JSON response
const reply = await this.dwn.processMessage(req.params.did, recordsQuery.message);

res.setHeader('content-type', 'application/json');
return res.json(reply);
} catch (error) {
// error should only occur when we are unable to create the RecordsQuery message internally, making it a client error
return res.status(400).send(error);
}
});

this.#api.get('/', (_req, res) => {
// return a plain text string
res.setHeader('content-type', 'text/plain');
Expand Down
59 changes: 58 additions & 1 deletion tests/http-api.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@ import sinon from 'sinon';
import {
Cid,
DataStream,
DwnErrorCode,
RecordsQuery,
RecordsRead,
TestDataGenerator,
Time,
} from '@tbd54566975/dwn-sdk-js';
import type { Dwn, Persona } from '@tbd54566975/dwn-sdk-js';
import type { Dwn, DwnError, Persona, RecordsQueryReply } from '@tbd54566975/dwn-sdk-js';

import { expect } from 'chai';
import type { Server } from 'http';
Expand Down Expand Up @@ -529,6 +530,62 @@ describe('http api', function () {
});
});

describe('/:did/query', function () {
it('returns record data if record is published', async function () {
const filePath = './fixtures/test.jpeg';
const {
cid: expectedCid,
size,
stream,
} = await getFileAsReadStream(filePath);

const { recordsWrite } = await createRecordsWriteMessage(alice, {
dataCid: expectedCid,
dataSize: size,
published: true,
});

const requestId = uuidv4();
const dwnRequest = createJsonRpcRequest(requestId, 'dwn.processMessage', {
message: recordsWrite.toJSON(),
target: alice.did,
});

const response = await fetch('http://localhost:3000', {
method: 'POST',
headers: {
'dwn-request': JSON.stringify(dwnRequest),
},
body: stream,
});

expect(response.status).to.equal(200);

const body = (await response.json()) as JsonRpcResponse;
expect(body.id).to.equal(requestId);
expect(body.error).to.not.exist;

const { reply } = body.result;
expect(reply.status.code).to.equal(202);

const { entries } = await fetch(
`http://localhost:3000/${alice.did}/query?filter.recordId=${recordsWrite.message.recordId}&other.random.param=unused-value`,
).then(response => response.json()) as RecordsQueryReply;

expect(entries?.length).to.equal(1);
});

it('should return 400 if user provide invalid query', async function () {
const response = await fetch(
`http://localhost:3000/${alice.did}/query?filter=invalid-filter`,
);
expect(response.status).to.equal(400);

const responseBody = await response.json() as DwnError;
expect(responseBody.code).to.equal(DwnErrorCode.SchemaValidatorAdditionalPropertyNotAllowed);
});
});

describe('/info', function () {
it('verify /info has some of the fields it is supposed to have', async function () {
const resp = await fetch(`http://localhost:3000/info`);
Expand Down

0 comments on commit 3db86d6

Please sign in to comment.