Skip to content

Commit 82fe049

Browse files
authored
Add a helper method for generating a PaginationCursor (#513)
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.
1 parent ca7f53b commit 82fe049

File tree

7 files changed

+207
-44
lines changed

7 files changed

+207
-44
lines changed

.changeset/odd-eels-rest.md

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
"@web5/identity-agent": patch
3+
"@web5/proxy-agent": patch
4+
"@web5/user-agent": patch
5+
"@web5/agent": patch
6+
"@web5/api": patch
7+
---
8+
9+
Add a helper methods for generating a PaginationCursor from `api` without importing `dwn-sdk-js` directly

packages/agent/src/utils.ts

+30-2
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import type { DidUrlDereferencer } from '@web5/dids';
2-
import type { RecordsWriteMessage } from '@tbd54566975/dwn-sdk-js';
2+
import type { PaginationCursor, RecordsWriteMessage } from '@tbd54566975/dwn-sdk-js';
33

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

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

59+
/**
60+
* Get the CID of the given RecordsWriteMessage.
61+
*/
62+
export function getRecordMessageCid(message: RecordsWriteMessage): Promise<string> {
63+
return Message.getCid(message);
64+
}
65+
66+
/**
67+
* Get the pagination cursor for the given RecordsWriteMessage and DateSort.
68+
*
69+
* @param message The RecordsWriteMessage for which to get the pagination cursor.
70+
* @param dateSort The date sort that will be used in the query or subscription to which the cursor will be applied.
71+
*/
72+
export async function getPaginationCursor(message: RecordsWriteMessage, dateSort: DateSort): Promise<PaginationCursor> {
73+
const value = dateSort === DateSort.CreatedAscending || dateSort === DateSort.CreatedDescending ?
74+
message.descriptor.dateCreated : message.descriptor.datePublished;
75+
76+
if (value === undefined) {
77+
throw new Error('The dateCreated or datePublished property is missing from the record descriptor.');
78+
}
79+
80+
return {
81+
messageCid: await getRecordMessageCid(message),
82+
value
83+
};
84+
}
85+
5886
export function webReadableToIsomorphicNodeReadable(webReadable: ReadableStream<any>) {
5987
return new ReadableWebToNodeStream(webReadable);
6088
}

packages/agent/tests/utils.spec.ts

+80
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import { expect } from 'chai';
2+
3+
import { DateSort, Message, TestDataGenerator } from '@tbd54566975/dwn-sdk-js';
4+
import { getPaginationCursor, getRecordAuthor, getRecordMessageCid } from '../src/utils.js';
5+
6+
describe('Utils', () => {
7+
describe('getPaginationCursor', () => {
8+
it('should return a PaginationCursor object', async () => {
9+
// create a RecordWriteMessage object which is published
10+
const { message } = await TestDataGenerator.generateRecordsWrite({
11+
published: true,
12+
});
13+
14+
const messageCid = await Message.getCid(message);
15+
16+
// Published Ascending DateSort will get the datePublished as the cursor value
17+
const datePublishedAscendingCursor = await getPaginationCursor(message, DateSort.PublishedAscending);
18+
expect(datePublishedAscendingCursor).to.deep.equal({
19+
value: message.descriptor.datePublished,
20+
messageCid,
21+
});
22+
23+
// Published Descending DateSort will get the datePublished as the cursor value
24+
const datePublishedDescendingCursor = await getPaginationCursor(message, DateSort.PublishedDescending);
25+
expect(datePublishedDescendingCursor).to.deep.equal({
26+
value: message.descriptor.datePublished,
27+
messageCid,
28+
});
29+
30+
// Created Ascending DateSort will get the dateCreated as the cursor value
31+
const dateCreatedAscendingCursor = await getPaginationCursor(message, DateSort.CreatedAscending);
32+
expect(dateCreatedAscendingCursor).to.deep.equal({
33+
value: message.descriptor.dateCreated,
34+
messageCid,
35+
});
36+
37+
// Created Descending DateSort will get the dateCreated as the cursor value
38+
const dateCreatedDescendingCursor = await getPaginationCursor(message, DateSort.CreatedDescending);
39+
expect(dateCreatedDescendingCursor).to.deep.equal({
40+
value: message.descriptor.dateCreated,
41+
messageCid,
42+
});
43+
});
44+
45+
it('should fail for DateSort with PublishedAscending or PublishedDescending if the record is not published', async () => {
46+
// create a RecordWriteMessage object which is not published
47+
const { message } = await TestDataGenerator.generateRecordsWrite();
48+
49+
// Published Ascending DateSort will get the datePublished as the cursor value
50+
try {
51+
await getPaginationCursor(message, DateSort.PublishedAscending);
52+
expect.fail('Expected getPaginationCursor to throw an error');
53+
} catch(error: any) {
54+
expect(error.message).to.include('The dateCreated or datePublished property is missing from the record descriptor.');
55+
}
56+
});
57+
});
58+
59+
describe('getRecordMessageCid', () => {
60+
it('should get the CID of a RecordsWriteMessage', async () => {
61+
// create a RecordWriteMessage object
62+
const { message } = await TestDataGenerator.generateRecordsWrite();
63+
const messageCid = await Message.getCid(message);
64+
65+
const messageCidFromFunction = await getRecordMessageCid(message);
66+
expect(messageCidFromFunction).to.equal(messageCid);
67+
});
68+
});
69+
70+
describe('getRecordAuthor', () => {
71+
it('should get the author of a RecordsWriteMessage', async () => {
72+
// create a RecordWriteMessage object
73+
const { message, author } = await TestDataGenerator.generateRecordsWrite();
74+
75+
const authorFromFunction = getRecordAuthor(message);
76+
expect(authorFromFunction).to.not.be.undefined;
77+
expect(authorFromFunction!).to.equal(author.did);
78+
});
79+
});
80+
});

packages/api/package.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -77,11 +77,11 @@
7777
"node": ">=18.0.0"
7878
},
7979
"dependencies": {
80-
"@web5/agent": "0.3.2",
80+
"@web5/agent": "0.3.4",
8181
"@web5/common": "1.0.0",
8282
"@web5/crypto": "1.0.0",
8383
"@web5/dids": "1.0.1",
84-
"@web5/user-agent": "0.3.2"
84+
"@web5/user-agent": "0.3.4"
8585
},
8686
"devDependencies": {
8787
"@playwright/test": "1.40.1",

packages/api/src/record.ts

+14-1
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,16 @@
55
/// <reference types="@tbd54566975/dwn-sdk-js" />
66

77
import type { Readable } from '@web5/common';
8-
import type {
8+
import {
99
Web5Agent,
1010
DwnMessage,
1111
DwnMessageParams,
1212
DwnResponseStatus,
1313
ProcessDwnRequest,
1414
DwnMessageDescriptor,
15+
getPaginationCursor,
16+
DwnDateSort,
17+
DwnPaginationCursor
1518
} from '@web5/agent';
1619

1720
import { DwnInterface } from '@web5/agent';
@@ -576,6 +579,16 @@ export class Record implements RecordModel {
576579
return str;
577580
}
578581

582+
/**
583+
* Returns a pagination cursor for the current record given a sort order.
584+
*
585+
* @param sort the sort order to use for the pagination cursor.
586+
* @returns A promise that resolves to a pagination cursor for the current record.
587+
*/
588+
async paginationCursor(sort: DwnDateSort): Promise<DwnPaginationCursor> {
589+
return getPaginationCursor(this.rawMessage, sort);
590+
}
591+
579592
/**
580593
* Update the current record on the DWN.
581594
* @param params - Parameters to update the record.

packages/api/tests/record.spec.ts

+68-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { expect } from 'chai';
55
import { NodeStream } from '@web5/common';
66
import { utils as didUtils } from '@web5/dids';
77
import { Web5UserAgent } from '@web5/user-agent';
8-
import { DwnConstant, DwnEncryptionAlgorithm, DwnInterface, DwnKeyDerivationScheme, dwnMessageConstructors, PlatformAgentTestHarness } from '@web5/agent';
8+
import { DwnConstant, DwnDateSort, DwnEncryptionAlgorithm, DwnInterface, DwnKeyDerivationScheme, dwnMessageConstructors, PlatformAgentTestHarness } from '@web5/agent';
99
import { Record } from '../src/record.js';
1010
import { DwnApi } from '../src/dwn-api.js';
1111
import { dataToBlob } from '../src/utils.js';
@@ -16,6 +16,7 @@ import emailProtocolDefinition from './fixtures/protocol-definitions/email.json'
1616
// NOTE: @noble/secp256k1 requires globalThis.crypto polyfill for node.js <=18: https://github.com/paulmillr/noble-secp256k1/blob/main/README.md#usage
1717
// 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
1818
import { webcrypto } from 'node:crypto';
19+
import { Message } from '@tbd54566975/dwn-sdk-js';
1920
// @ts-ignore
2021
if (!globalThis.crypto) globalThis.crypto = webcrypto;
2122

@@ -2825,4 +2826,70 @@ describe('Record', () => {
28252826
});
28262827
});
28272828
});
2829+
2830+
describe('paginationCursor', () => {
2831+
it('should return a cursor for pagination', async () => {
2832+
// Create a record that is not published.
2833+
const { status, record } = await dwnAlice.records.write({
2834+
data : 'Hello, world!',
2835+
message : {
2836+
schema : 'foo/bar',
2837+
dataFormat : 'text/plain'
2838+
}
2839+
});
2840+
2841+
expect(status.code).to.equal(202);
2842+
const messageCid = await Message.getCid(record['rawMessage']);
2843+
2844+
const paginationCursorCreatedAscending = await record.paginationCursor(DwnDateSort.CreatedAscending);
2845+
expect(paginationCursorCreatedAscending).to.be.deep.equal({
2846+
messageCid,
2847+
value: record.dateCreated,
2848+
});
2849+
2850+
const paginationCursorCreatedDescending = await record.paginationCursor(DwnDateSort.CreatedDescending);
2851+
expect(paginationCursorCreatedDescending).to.be.deep.equal({
2852+
messageCid,
2853+
value: record.dateCreated,
2854+
});
2855+
});
2856+
2857+
it('should return a cursor for pagination for a published record', async () => {
2858+
// Create a record that is not published.
2859+
const { status, record } = await dwnAlice.records.write({
2860+
data : 'Hello, world!',
2861+
message : {
2862+
published : true,
2863+
schema : 'foo/bar',
2864+
dataFormat : 'text/plain'
2865+
}
2866+
});
2867+
expect(status.code).to.equal(202);
2868+
const messageCid = await Message.getCid(record['rawMessage']);
2869+
2870+
const paginationCursorCreatedAscending = await record.paginationCursor(DwnDateSort.CreatedAscending);
2871+
expect(paginationCursorCreatedAscending).to.be.deep.equal({
2872+
messageCid,
2873+
value: record.dateCreated,
2874+
});
2875+
2876+
const paginationCursorCreatedDescending = await record.paginationCursor(DwnDateSort.CreatedDescending);
2877+
expect(paginationCursorCreatedDescending).to.be.deep.equal({
2878+
messageCid,
2879+
value: record.dateCreated,
2880+
});
2881+
2882+
const paginationCursorPublishedAscending = await record.paginationCursor(DwnDateSort.PublishedAscending);
2883+
expect(paginationCursorPublishedAscending).to.be.deep.equal({
2884+
messageCid,
2885+
value: record.datePublished,
2886+
});
2887+
2888+
const paginationCursorPublishedDescending = await record.paginationCursor(DwnDateSort.PublishedDescending);
2889+
expect(paginationCursorPublishedDescending).to.be.deep.equal({
2890+
messageCid,
2891+
value: record.datePublished,
2892+
});
2893+
});
2894+
});
28282895
});

pnpm-lock.yaml

+4-38
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)