Skip to content

Commit

Permalink
Record.store(), Record.import(), and roles API support (#385)
Browse files Browse the repository at this point in the history
- Add `store()` method to the `api` `Record` class.
- Add `import()` method to the `api` `Record` class.

These now allow to import and store initial writes as well as the current state write in local and remote DWNs.
  • Loading branch information
csuwildcat authored and finn-block committed Mar 19, 2024
1 parent 6e308be commit 02c473e
Show file tree
Hide file tree
Showing 12 changed files with 1,321 additions and 139 deletions.
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,10 @@ Each `Record` instance has the following instance methods:
- **`text`** - _`function`_: returns the data as a string.
- **`send`** - _`function`_: sends the record the instance represents to the DWeb Node endpoints of a provided DID.
- **`update`** - _`function`_: takes in a new request object matching the expected method signature of a `write` and overwrites the record. This is a convenience method that allows you to easily overwrite records with less verbosity.
- **`store`** - _`function`_: stores the record in the local DWN instance, offering the following options:
- `import`: imports the record as with an owner-signed override (still subject to Protocol rules, when a record is Protocol-based)
- **`import`** - _`function`_: signs a record with an owner override to import the record into the local DWN instance:
- `store` - _`boolean`_: when false is passed, the record will only be signed with an owner override, not stored in the local DWN instance. Defaults to `true`.

### **`web5.dwn.records.query(request)`**

Expand Down
29 changes: 18 additions & 11 deletions packages/agent/src/dwn-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ type DwnMessage = {
data?: Blob;
}

const dwnMessageCreators = {
const dwnMessageConstructors = {
[DwnInterfaceName.Events + DwnMethodName.Get] : EventsGet,
[DwnInterfaceName.Messages + DwnMethodName.Get] : MessagesGet,
[DwnInterfaceName.Records + DwnMethodName.Read] : RecordsRead,
Expand Down Expand Up @@ -245,14 +245,14 @@ export class DwnManager {
request: ProcessDwnRequest
}) {
const { request } = options;

const rawMessage = request.rawMessage as any;
let readableStream: Readable | undefined;

// TODO: Consider refactoring to move data transformations imposed by fetch() limitations to the HTTP transport-related methods.
if (request.messageType === 'RecordsWrite') {
const messageOptions = request.messageOptions as RecordsWriteOptions;

if (request.dataStream && !messageOptions.data) {
if (request.dataStream && !messageOptions?.data) {
const { dataStream } = request;
let isomorphicNodeReadable: Readable;

Expand All @@ -266,21 +266,28 @@ export class DwnManager {
readableStream = webReadableToIsomorphicNodeReadable(forProcessMessage);
}

// @ts-ignore
messageOptions.dataCid = await Cid.computeDagPbCidFromStream(isomorphicNodeReadable);
// @ts-ignore
messageOptions.dataSize ??= isomorphicNodeReadable['bytesRead'];
if (!rawMessage) {
// @ts-ignore
messageOptions.dataCid = await Cid.computeDagPbCidFromStream(isomorphicNodeReadable);
// @ts-ignore
messageOptions.dataSize ??= isomorphicNodeReadable['bytesRead'];
}
}
}

const dwnSigner = await this.constructDwnSigner(request.author);

const messageCreator = dwnMessageCreators[request.messageType];
const dwnMessage = await messageCreator.create({
const dwnMessageConstructor = dwnMessageConstructors[request.messageType];
const dwnMessage = rawMessage ? await dwnMessageConstructor.parse(rawMessage) : await dwnMessageConstructor.create({
...<any>request.messageOptions,
signer: dwnSigner
});

if (dwnMessageConstructor === RecordsWrite){
if (request.signAsOwner) {
await (dwnMessage as RecordsWrite).signAsOwner(dwnSigner);
}
}

return { message: dwnMessage.message, dataStream: readableStream };
}

Expand Down Expand Up @@ -411,7 +418,7 @@ export class DwnManager {

const dwnSigner = await this.constructDwnSigner(author);

const messageCreator = dwnMessageCreators[messageType];
const messageCreator = dwnMessageConstructors[messageType];

const dwnMessage = await messageCreator.create({
...<any>messageOptions,
Expand Down
4 changes: 3 additions & 1 deletion packages/agent/src/types/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,8 +75,10 @@ export type DwnRequest = {
*/
export type ProcessDwnRequest = DwnRequest & {
dataStream?: Blob | ReadableStream | Readable;
messageOptions: unknown;
rawMessage?: unknown;
messageOptions?: unknown;
store?: boolean;
signAsOwner?: boolean;
};

export type SendDwnRequest = DwnRequest & (ProcessDwnRequest | { messageCid: string })
Expand Down
147 changes: 119 additions & 28 deletions packages/agent/tests/dwn-manager.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,26 +95,33 @@ describe('DwnManager', () => {
});

describe('processRequest()', () => {
let identity: ManagedIdentity;
let alice: ManagedIdentity;
let bob: ManagedIdentity;

beforeEach(async () => {
await testAgent.clearStorage();
await testAgent.createAgentDid();
// Creates a new Identity to author the DWN messages.
identity = await testAgent.agent.identityManager.create({
alice = await testAgent.agent.identityManager.create({
name : 'Alice',
didMethod : 'key',
kms : 'local'
});

bob = await testAgent.agent.identityManager.create({
name : 'Bob',
didMethod : 'key',
kms : 'local'
});
});

it('handles EventsGet', async () => {
const testCursor = 'foo';

// Attempt to process the EventsGet.
let eventsGetResponse = await testAgent.agent.dwnManager.processRequest({
author : identity.did,
target : identity.did,
author : alice.did,
target : alice.did,
messageType : 'EventsGet',
messageOptions : {
cursor: testCursor,
Expand All @@ -140,8 +147,8 @@ describe('DwnManager', () => {

// Write a record to use for the MessagesGet test.
let { message, reply: { status: writeStatus } } = await testAgent.agent.dwnManager.processRequest({
author : identity.did,
target : identity.did,
author : alice.did,
target : alice.did,
messageType : 'RecordsWrite',
messageOptions : {
dataFormat : 'text/plain',
Expand All @@ -157,8 +164,8 @@ describe('DwnManager', () => {

// Attempt to process the MessagesGet.
let messagesGetResponse = await testAgent.agent.dwnManager.processRequest({
author : identity.did,
target : identity.did,
author : alice.did,
target : alice.did,
messageType : 'MessagesGet',
messageOptions : {
messageCids: [messageCid]
Expand Down Expand Up @@ -187,8 +194,8 @@ describe('DwnManager', () => {

it('handles ProtocolsConfigure', async () => {
let protocolsConfigureResponse = await testAgent.agent.dwnManager.processRequest({
author : identity.did,
target : identity.did,
author : alice.did,
target : alice.did,
messageType : 'ProtocolsConfigure',
messageOptions : {
definition: emailProtocolDefinition
Expand All @@ -211,8 +218,8 @@ describe('DwnManager', () => {
it('handles ProtocolsQuery', async () => {
// Configure a protocol to use for the ProtocolsQuery test.
let protocolsConfigureResponse = await testAgent.agent.dwnManager.processRequest({
author : identity.did,
target : identity.did,
author : alice.did,
target : alice.did,
messageType : 'ProtocolsConfigure',
messageOptions : {
definition: emailProtocolDefinition
Expand All @@ -222,8 +229,8 @@ describe('DwnManager', () => {

// Attempt to query for the protocol that was just configured.
let protocolsQueryResponse = await testAgent.agent.dwnManager.processRequest({
author : identity.did,
target : identity.did,
author : alice.did,
target : alice.did,
messageType : 'ProtocolsQuery',
messageOptions : {
filter: { protocol: emailProtocolDefinition.protocol },
Expand Down Expand Up @@ -252,8 +259,8 @@ describe('DwnManager', () => {

// Write a record that can be deleted.
let { message, reply: { status: writeStatus } } = await testAgent.agent.dwnManager.processRequest({
author : identity.did,
target : identity.did,
author : alice.did,
target : alice.did,
messageType : 'RecordsWrite',
messageOptions : {
dataFormat : 'text/plain',
Expand All @@ -266,8 +273,8 @@ describe('DwnManager', () => {

// Attempt to process the RecordsRead.
const deleteResponse = await testAgent.agent.dwnManager.processRequest({
author : identity.did,
target : identity.did,
author : alice.did,
target : alice.did,
messageType : 'RecordsDelete',
messageOptions : {
recordId: writeMessage.recordId
Expand All @@ -294,8 +301,8 @@ describe('DwnManager', () => {

// Write a record that can be queried for.
let { message, reply: { status: writeStatus } } = await testAgent.agent.dwnManager.processRequest({
author : identity.did,
target : identity.did,
author : alice.did,
target : alice.did,
messageType : 'RecordsWrite',
messageOptions : {
dataFormat : 'text/plain',
Expand All @@ -308,8 +315,8 @@ describe('DwnManager', () => {

// Attempt to process the RecordsQuery.
const queryResponse = await testAgent.agent.dwnManager.processRequest({
author : identity.did,
target : identity.did,
author : alice.did,
target : alice.did,
messageType : 'RecordsQuery',
messageOptions : {
filter: {
Expand Down Expand Up @@ -343,8 +350,8 @@ describe('DwnManager', () => {

// Write a record that can be read.
let { message, reply: { status: writeStatus } } = await testAgent.agent.dwnManager.processRequest({
author : identity.did,
target : identity.did,
author : alice.did,
target : alice.did,
messageType : 'RecordsWrite',
messageOptions : {
dataFormat : 'text/plain',
Expand All @@ -357,8 +364,8 @@ describe('DwnManager', () => {

// Attempt to process the RecordsRead.
const readResponse = await testAgent.agent.dwnManager.processRequest({
author : identity.did,
target : identity.did,
author : alice.did,
target : alice.did,
messageType : 'RecordsRead',
messageOptions : {
filter: {
Expand Down Expand Up @@ -391,8 +398,8 @@ describe('DwnManager', () => {

// Attempt to process the RecordsWrite
let writeResponse = await testAgent.agent.dwnManager.processRequest({
author : identity.did,
target : identity.did,
author : alice.did,
target : alice.did,
messageType : 'RecordsWrite',
messageOptions : {
dataFormat: 'text/plain'
Expand All @@ -414,6 +421,90 @@ describe('DwnManager', () => {
expect(writeReply).to.have.property('status');
expect(writeReply.status.code).to.equal(202);
});

it('handles RecordsWrite messages to sign as owner', async () => {
// bob authors a public record to his dwn
const dataStream = new Blob([ Convert.string('Hello, world!').toUint8Array() ]);

const bobWrite = await testAgent.agent.dwnManager.processRequest({
author : bob.did,
target : bob.did,
messageType : 'RecordsWrite',
messageOptions : {
published : true,
schema : 'foo/bar',
dataFormat : 'text/plain'
},
dataStream,
});
expect(bobWrite.reply.status.code).to.equal(202);
const message = bobWrite.message as RecordsWriteMessage;

// alice queries bob's DWN for the record
const queryBobResponse = await testAgent.agent.dwnManager.processRequest({
messageType : 'RecordsQuery',
author : alice.did,
target : bob.did,
messageOptions : {
filter: {
recordId: message.recordId
}
}
});
let reply = queryBobResponse.reply as RecordsQueryReply;
expect(reply.status.code).to.equal(200);
expect(reply.entries!.length).to.equal(1);
expect(reply.entries![0].recordId).to.equal(message.recordId);

// alice attempts to process the rawMessage as is without signing it, should fail
let aliceWrite = await testAgent.agent.dwnManager.processRequest({
messageType : 'RecordsWrite',
author : alice.did,
target : alice.did,
rawMessage : message,
dataStream,
});
expect(aliceWrite.reply.status.code).to.equal(401);

// alice queries to make sure the record is not saved on her dwn
let queryAliceResponse = await testAgent.agent.dwnManager.processRequest({
messageType : 'RecordsQuery',
author : alice.did,
target : alice.did,
messageOptions : {
filter: {
recordId: message.recordId
}
}
});
expect(queryAliceResponse.reply.status.code).to.equal(200);
expect(queryAliceResponse.reply.entries!.length).to.equal(0);

// alice attempts to process the rawMessage again this time marking it to be signed as owner
aliceWrite = await testAgent.agent.dwnManager.processRequest({
messageType : 'RecordsWrite',
author : alice.did,
target : alice.did,
rawMessage : message,
signAsOwner : true,
dataStream,
});
expect(aliceWrite.reply.status.code).to.equal(202);

// alice now queries for the record, it should be there
queryAliceResponse = await testAgent.agent.dwnManager.processRequest({
messageType : 'RecordsQuery',
author : alice.did,
target : alice.did,
messageOptions : {
filter: {
recordId: message.recordId
}
}
});
expect(queryAliceResponse.reply.status.code).to.equal(200);
expect(queryAliceResponse.reply.entries!.length).to.equal(1);
});
});

describe('sendDwnRequest()', () => {
Expand Down
1 change: 1 addition & 0 deletions packages/api/src/dwn-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -366,6 +366,7 @@ export class DwnApi {
const { entries, status, cursor } = reply;

const records = entries.map((entry: RecordsQueryReplyEntry) => {

const recordOptions = {
/**
* Extract the `author` DID from the record entry since records may be signed by the
Expand Down
Loading

0 comments on commit 02c473e

Please sign in to comment.