Skip to content

Commit

Permalink
Add a helper method for generating a PaginationCursor (#513)
Browse files Browse the repository at this point in the history
Currently the only way to get a pagination cursor is from a query response that has more than the number of records you've queried for.

However there could be some cases where the user wants to keep track of the last record they have retrieved and retrieve subsequent records.

Added some helper methods to `agent` in order to build/return the data needed, and a convenience method to the `Record` class in `api` that utilizes them.
  • Loading branch information
LiranCohen authored May 8, 2024
1 parent ca7f53b commit 82fe049
Show file tree
Hide file tree
Showing 7 changed files with 207 additions and 44 deletions.
9 changes: 9 additions & 0 deletions .changeset/odd-eels-rest.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
"@web5/identity-agent": patch
"@web5/proxy-agent": patch
"@web5/user-agent": patch
"@web5/agent": patch
"@web5/api": patch
---

Add a helper methods for generating a PaginationCursor from `api` without importing `dwn-sdk-js` directly
32 changes: 30 additions & 2 deletions packages/agent/src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import type { DidUrlDereferencer } from '@web5/dids';
import type { RecordsWriteMessage } from '@tbd54566975/dwn-sdk-js';
import type { PaginationCursor, RecordsWriteMessage } from '@tbd54566975/dwn-sdk-js';

import { DateSort } from '@tbd54566975/dwn-sdk-js';
import { Readable } from '@web5/common';
import { utils as didUtils } from '@web5/dids';
import { ReadableWebToNodeStream } from 'readable-web-to-node-stream';
import { DwnInterfaceName, DwnMethodName, RecordsWrite } from '@tbd54566975/dwn-sdk-js';
import { DwnInterfaceName, DwnMethodName, Message, RecordsWrite } from '@tbd54566975/dwn-sdk-js';

export function blobToIsomorphicNodeReadable(blob: Blob): Readable {
return webReadableToIsomorphicNodeReadable(blob.stream() as ReadableStream<any>);
Expand Down Expand Up @@ -55,6 +56,33 @@ export function isRecordsWrite(obj: unknown): obj is RecordsWrite {
);
}

/**
* Get the CID of the given RecordsWriteMessage.
*/
export function getRecordMessageCid(message: RecordsWriteMessage): Promise<string> {
return Message.getCid(message);
}

/**
* Get the pagination cursor for the given RecordsWriteMessage and DateSort.
*
* @param message The RecordsWriteMessage for which to get the pagination cursor.
* @param dateSort The date sort that will be used in the query or subscription to which the cursor will be applied.
*/
export async function getPaginationCursor(message: RecordsWriteMessage, dateSort: DateSort): Promise<PaginationCursor> {
const value = dateSort === DateSort.CreatedAscending || dateSort === DateSort.CreatedDescending ?
message.descriptor.dateCreated : message.descriptor.datePublished;

if (value === undefined) {
throw new Error('The dateCreated or datePublished property is missing from the record descriptor.');
}

return {
messageCid: await getRecordMessageCid(message),
value
};
}

export function webReadableToIsomorphicNodeReadable(webReadable: ReadableStream<any>) {
return new ReadableWebToNodeStream(webReadable);
}
80 changes: 80 additions & 0 deletions packages/agent/tests/utils.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { expect } from 'chai';

import { DateSort, Message, TestDataGenerator } from '@tbd54566975/dwn-sdk-js';
import { getPaginationCursor, getRecordAuthor, getRecordMessageCid } from '../src/utils.js';

describe('Utils', () => {
describe('getPaginationCursor', () => {
it('should return a PaginationCursor object', async () => {
// create a RecordWriteMessage object which is published
const { message } = await TestDataGenerator.generateRecordsWrite({
published: true,
});

const messageCid = await Message.getCid(message);

// Published Ascending DateSort will get the datePublished as the cursor value
const datePublishedAscendingCursor = await getPaginationCursor(message, DateSort.PublishedAscending);
expect(datePublishedAscendingCursor).to.deep.equal({
value: message.descriptor.datePublished,
messageCid,
});

// Published Descending DateSort will get the datePublished as the cursor value
const datePublishedDescendingCursor = await getPaginationCursor(message, DateSort.PublishedDescending);
expect(datePublishedDescendingCursor).to.deep.equal({
value: message.descriptor.datePublished,
messageCid,
});

// Created Ascending DateSort will get the dateCreated as the cursor value
const dateCreatedAscendingCursor = await getPaginationCursor(message, DateSort.CreatedAscending);
expect(dateCreatedAscendingCursor).to.deep.equal({
value: message.descriptor.dateCreated,
messageCid,
});

// Created Descending DateSort will get the dateCreated as the cursor value
const dateCreatedDescendingCursor = await getPaginationCursor(message, DateSort.CreatedDescending);
expect(dateCreatedDescendingCursor).to.deep.equal({
value: message.descriptor.dateCreated,
messageCid,
});
});

it('should fail for DateSort with PublishedAscending or PublishedDescending if the record is not published', async () => {
// create a RecordWriteMessage object which is not published
const { message } = await TestDataGenerator.generateRecordsWrite();

// Published Ascending DateSort will get the datePublished as the cursor value
try {
await getPaginationCursor(message, DateSort.PublishedAscending);
expect.fail('Expected getPaginationCursor to throw an error');
} catch(error: any) {
expect(error.message).to.include('The dateCreated or datePublished property is missing from the record descriptor.');
}
});
});

describe('getRecordMessageCid', () => {
it('should get the CID of a RecordsWriteMessage', async () => {
// create a RecordWriteMessage object
const { message } = await TestDataGenerator.generateRecordsWrite();
const messageCid = await Message.getCid(message);

const messageCidFromFunction = await getRecordMessageCid(message);
expect(messageCidFromFunction).to.equal(messageCid);
});
});

describe('getRecordAuthor', () => {
it('should get the author of a RecordsWriteMessage', async () => {
// create a RecordWriteMessage object
const { message, author } = await TestDataGenerator.generateRecordsWrite();

const authorFromFunction = getRecordAuthor(message);
expect(authorFromFunction).to.not.be.undefined;
expect(authorFromFunction!).to.equal(author.did);
});
});
});
4 changes: 2 additions & 2 deletions packages/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -77,11 +77,11 @@
"node": ">=18.0.0"
},
"dependencies": {
"@web5/agent": "0.3.2",
"@web5/agent": "0.3.4",
"@web5/common": "1.0.0",
"@web5/crypto": "1.0.0",
"@web5/dids": "1.0.1",
"@web5/user-agent": "0.3.2"
"@web5/user-agent": "0.3.4"
},
"devDependencies": {
"@playwright/test": "1.40.1",
Expand Down
15 changes: 14 additions & 1 deletion packages/api/src/record.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,16 @@
/// <reference types="@tbd54566975/dwn-sdk-js" />

import type { Readable } from '@web5/common';
import type {
import {
Web5Agent,
DwnMessage,
DwnMessageParams,
DwnResponseStatus,
ProcessDwnRequest,
DwnMessageDescriptor,
getPaginationCursor,
DwnDateSort,
DwnPaginationCursor
} from '@web5/agent';

import { DwnInterface } from '@web5/agent';
Expand Down Expand Up @@ -576,6 +579,16 @@ export class Record implements RecordModel {
return str;
}

/**
* Returns a pagination cursor for the current record given a sort order.
*
* @param sort the sort order to use for the pagination cursor.
* @returns A promise that resolves to a pagination cursor for the current record.
*/
async paginationCursor(sort: DwnDateSort): Promise<DwnPaginationCursor> {
return getPaginationCursor(this.rawMessage, sort);
}

/**
* Update the current record on the DWN.
* @param params - Parameters to update the record.
Expand Down
69 changes: 68 additions & 1 deletion packages/api/tests/record.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { expect } from 'chai';
import { NodeStream } from '@web5/common';
import { utils as didUtils } from '@web5/dids';
import { Web5UserAgent } from '@web5/user-agent';
import { DwnConstant, DwnEncryptionAlgorithm, DwnInterface, DwnKeyDerivationScheme, dwnMessageConstructors, PlatformAgentTestHarness } from '@web5/agent';
import { DwnConstant, DwnDateSort, DwnEncryptionAlgorithm, DwnInterface, DwnKeyDerivationScheme, dwnMessageConstructors, PlatformAgentTestHarness } from '@web5/agent';
import { Record } from '../src/record.js';
import { DwnApi } from '../src/dwn-api.js';
import { dataToBlob } from '../src/utils.js';
Expand All @@ -16,6 +16,7 @@ import emailProtocolDefinition from './fixtures/protocol-definitions/email.json'
// NOTE: @noble/secp256k1 requires globalThis.crypto polyfill for node.js <=18: https://github.com/paulmillr/noble-secp256k1/blob/main/README.md#usage
// Remove when we move off of node.js v18 to v20, earliest possible time would be Oct 2023: https://github.com/nodejs/release#release-schedule
import { webcrypto } from 'node:crypto';
import { Message } from '@tbd54566975/dwn-sdk-js';
// @ts-ignore
if (!globalThis.crypto) globalThis.crypto = webcrypto;

Expand Down Expand Up @@ -2825,4 +2826,70 @@ describe('Record', () => {
});
});
});

describe('paginationCursor', () => {
it('should return a cursor for pagination', async () => {
// Create a record that is not published.
const { status, record } = await dwnAlice.records.write({
data : 'Hello, world!',
message : {
schema : 'foo/bar',
dataFormat : 'text/plain'
}
});

expect(status.code).to.equal(202);
const messageCid = await Message.getCid(record['rawMessage']);

const paginationCursorCreatedAscending = await record.paginationCursor(DwnDateSort.CreatedAscending);
expect(paginationCursorCreatedAscending).to.be.deep.equal({
messageCid,
value: record.dateCreated,
});

const paginationCursorCreatedDescending = await record.paginationCursor(DwnDateSort.CreatedDescending);
expect(paginationCursorCreatedDescending).to.be.deep.equal({
messageCid,
value: record.dateCreated,
});
});

it('should return a cursor for pagination for a published record', async () => {
// Create a record that is not published.
const { status, record } = await dwnAlice.records.write({
data : 'Hello, world!',
message : {
published : true,
schema : 'foo/bar',
dataFormat : 'text/plain'
}
});
expect(status.code).to.equal(202);
const messageCid = await Message.getCid(record['rawMessage']);

const paginationCursorCreatedAscending = await record.paginationCursor(DwnDateSort.CreatedAscending);
expect(paginationCursorCreatedAscending).to.be.deep.equal({
messageCid,
value: record.dateCreated,
});

const paginationCursorCreatedDescending = await record.paginationCursor(DwnDateSort.CreatedDescending);
expect(paginationCursorCreatedDescending).to.be.deep.equal({
messageCid,
value: record.dateCreated,
});

const paginationCursorPublishedAscending = await record.paginationCursor(DwnDateSort.PublishedAscending);
expect(paginationCursorPublishedAscending).to.be.deep.equal({
messageCid,
value: record.datePublished,
});

const paginationCursorPublishedDescending = await record.paginationCursor(DwnDateSort.PublishedDescending);
expect(paginationCursorPublishedDescending).to.be.deep.equal({
messageCid,
value: record.datePublished,
});
});
});
});
42 changes: 4 additions & 38 deletions pnpm-lock.yaml

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

0 comments on commit 82fe049

Please sign in to comment.