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 a helper method for generating a PaginationCursor #513

Merged
merged 11 commits into from
May 8, 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
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
shamilovtim marked this conversation as resolved.
Show resolved Hide resolved
---

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);
shamilovtim marked this conversation as resolved.
Show resolved Hide resolved
}

/**
* 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);
shamilovtim marked this conversation as resolved.
Show resolved Hide resolved
}

/**
* 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.

Loading