Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add /query API endpoint #125

Merged
merged 8 commits into from
Apr 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
thehenrytsai marked this conversation as resolved.
Show resolved Hide resolved
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) => {

thehenrytsai marked this conversation as resolved.
Show resolved Hide resolved
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
Loading