@leordev-web5/api
Version:
SDK for accessing the features and capabilities of Web5
549 lines • 28 kB
JavaScript
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __rest = (this && this.__rest) || function (s, e) {
var t = {};
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
t[p] = s[p];
if (s != null && typeof Object.getOwnPropertySymbols === "function")
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
t[p[i]] = s[p[i]];
}
return t;
};
import { Convert, NodeStream, removeUndefinedProperties, Stream } from '@web5/common';
import { DwnInterfaceName, DwnMethodName } from '@tbd54566975/dwn-sdk-js';
import { dataToBlob } from './utils.js';
import { SendCache } from './send-cache.js';
/**
* Record wrapper class with convenience methods to send and update,
* aside from manipulating and reading the record data.
*
* Note: The `messageTimestamp` of the most recent RecordsWrite message is
* logically equivalent to the date/time at which a Record was most
* recently modified. Since this Record class implementation is
* intended to simplify the developer experience of working with
* logical records (and not individual DWN messages) the
* `messageTimestamp` is mapped to `dateModified`.
*
* @beta
*/
export class Record {
// Getters for immutable DWN Record properties.
/** Record's signatures attestation */
get attestation() { return this._attestation; }
/** Record's signatures attestation */
get authorization() { return this._authorization; }
/** DID that signed the record. */
get author() { return this._author; }
/** Record's context ID */
get contextId() { return this._contextId; }
/** Record's data format */
get dataFormat() { return this._descriptor.dataFormat; }
/** Record's creation date */
get dateCreated() { return this._descriptor.dateCreated; }
/** Record's encryption */
get encryption() { return this._encryption; }
/** Record's initial write if the record has been updated */
get initialWrite() { return this._initialWrite; }
/** Record's ID */
get id() { return this._recordId; }
/** Interface is always `Records` */
get interface() { return this._descriptor.interface; }
/** Method is always `Write` */
get method() { return this._descriptor.method; }
/** Record's parent ID */
get parentId() { return this._descriptor.parentId; }
/** Record's protocol */
get protocol() { return this._descriptor.protocol; }
/** Record's protocol path */
get protocolPath() { return this._descriptor.protocolPath; }
/** Role under which the author is writing the record */
get protocolRole() { return this._protocolRole; }
/** Record's recipient */
get recipient() { return this._descriptor.recipient; }
/** Record's schema */
get schema() { return this._descriptor.schema; }
// Getters for mutable DWN Record properties.
/** Record's CID */
get dataCid() { return this._descriptor.dataCid; }
/** Record's data size */
get dataSize() { return this._descriptor.dataSize; }
/** Record's modified date */
get dateModified() { return this._descriptor.messageTimestamp; }
/** Record's published date */
get datePublished() { return this._descriptor.datePublished; }
/** Record's published status */
get messageTimestamp() { return this._descriptor.messageTimestamp; }
/** Record's published status (true/false) */
get published() { return this._descriptor.published; }
/**
* Returns a copy of the raw `RecordsWriteMessage` that was used to create the current `Record` instance.
*/
get rawMessage() {
const message = JSON.parse(JSON.stringify({
contextId: this._contextId,
recordId: this._recordId,
descriptor: this._descriptor,
attestation: this._attestation,
authorization: this._authorization,
encryption: this._encryption,
}));
removeUndefinedProperties(message);
return message;
}
constructor(agent, options) {
this._agent = agent;
/** Store the author DID that originally signed the message as a convenience for developers, so
* that they don't have to decode the signer's DID from the JWS. */
this._author = options.author;
/** Store the currently `connectedDid` so that subsequent message signing is done with the
* connected DID's keys and DWN requests target the connected DID's DWN. */
this._connectedDid = options.connectedDid;
/** If the record was queried or read from a remote DWN, the `remoteOrigin` DID will be
* defined. This value is used to send subsequent read requests to the same remote DWN in the
* event the record's data payload was too large to be returned in query results. or must be
* read again (e.g., if the data stream is consumed). */
this._remoteOrigin = options.remoteOrigin;
// RecordsWriteMessage properties.
this._attestation = options.attestation;
this._authorization = options.authorization;
this._contextId = options.contextId;
this._descriptor = options.descriptor;
this._encryption = options.encryption;
this._initialWrite = options.initialWrite;
this._recordId = options.recordId;
this._protocolRole = options.protocolRole;
if (options.encodedData) {
// If `encodedData` is set, then it is expected that:
// type is Blob if the Record object was instantiated by dwn.records.create()/write().
// type is Base64 URL encoded string if the Record object was instantiated by dwn.records.query().
// If it is a string, we need to Base64 URL decode to bytes and instantiate a Blob.
this._encodedData = (typeof options.encodedData === 'string') ?
new Blob([Convert.base64Url(options.encodedData).toUint8Array()], { type: this.dataFormat }) :
options.encodedData;
}
if (options.data) {
// If the record was created from a RecordsRead reply then it will have a `data` property.
// If the `data` property is a web ReadableStream, convert it to a Node.js Readable.
this._readableStream = Stream.isReadableStream(options.data) ?
NodeStream.fromWebReadable({ readableStream: options.data }) :
options.data;
}
}
/**
* Returns the data of the current record.
* If the record data is not available, it attempts to fetch the data from the DWN.
* @returns a data stream with convenience methods such as `blob()`, `json()`, `text()`, and `stream()`, similar to the fetch API response
* @throws `Error` if the record has already been deleted.
*
* @beta
*/
get data() {
// eslint-disable-next-line @typescript-eslint/no-this-alias
const self = this; // Capture the context of the `Record` instance.
const dataObj = {
/**
* Returns the data of the current record as a `Blob`.
*
* @returns A promise that resolves to a Blob containing the record's data.
* @throws If the record data is not available or cannot be converted to a `Blob`.
*
* @beta
*/
blob() {
return __awaiter(this, void 0, void 0, function* () {
return new Blob([yield NodeStream.consumeToBytes({ readable: yield this.stream() })], { type: self.dataFormat });
});
},
/**
* Returns the data of the current record as a `Uint8Array`.
*
* @returns A Promise that resolves to a `Uint8Array` containing the record's data bytes.
* @throws If the record data is not available or cannot be converted to a byte array.
*
* @beta
*/
bytes() {
return __awaiter(this, void 0, void 0, function* () {
return yield NodeStream.consumeToBytes({ readable: yield this.stream() });
});
},
/**
* Parses the data of the current record as JSON and returns it as a JavaScript object.
*
* @returns A Promise that resolves to a JavaScript object parsed from the record's JSON data.
* @throws If the record data is not available, not in JSON format, or cannot be parsed.
*
* @beta
*/
json() {
return __awaiter(this, void 0, void 0, function* () {
return yield NodeStream.consumeToJson({ readable: yield this.stream() });
});
},
/**
* Returns the data of the current record as a `string`.
*
* @returns A promise that resolves to a `string` containing the record's text data.
* @throws If the record data is not available or cannot be converted to text.
*
* @beta
*/
text() {
return __awaiter(this, void 0, void 0, function* () {
return yield NodeStream.consumeToText({ readable: yield this.stream() });
});
},
/**
* Provides a `Readable` stream containing the record's data.
*
* @returns A promise that resolves to a Node.js `Readable` stream of the record's data.
* @throws If the record data is not available in-memory and cannot be fetched.
*
* @beta
*/
stream() {
return __awaiter(this, void 0, void 0, function* () {
if (self._encodedData) {
/** If `encodedData` is set, it indicates that the Record was instantiated by
* `dwn.records.create()`/`dwn.records.write()` or the record's data payload was small
* enough to be returned in `dwn.records.query()` results. In either case, the data is
* already available in-memory and can be returned as a Node.js `Readable` stream. */
self._readableStream = NodeStream.fromWebReadable({ readableStream: self._encodedData.stream() });
}
else if (!NodeStream.isReadable({ readable: self._readableStream })) {
/** If the data stream for this `Record` instance has already been partially or fully
* consumed, then the data must be fetched again from either: */
self._readableStream = self._remoteOrigin ?
// A. ...a remote DWN if the record was originally queried from a remote DWN.
yield self.readRecordData({ target: self._remoteOrigin, isRemote: true }) :
// B. ...a local DWN if the record was originally queried from the local DWN.
yield self.readRecordData({ target: self._connectedDid, isRemote: false });
}
if (!self._readableStream) {
throw new Error('Record data is not available.');
}
return self._readableStream;
});
},
then(...callbacks) {
return this.stream().then(...callbacks);
},
catch(callback) {
return dataObj.then().catch(callback);
},
};
return dataObj;
}
/**
* Stores the current record state as well as any initial write to the owner's DWN.
*
* @param importRecord - if true, the record will signed by the owner before storing it to the owner's DWN. Defaults to false.
* @returns the status of the store request
*
* @beta
*/
store(importRecord = false) {
return __awaiter(this, void 0, void 0, function* () {
// if we are importing the record we sign it as the owner
return this.processRecord({ signAsOwner: importRecord, store: true });
});
}
/**
* Signs the current record state as well as any initial write and optionally stores it to the owner's DWN.
* This is useful when importing a record that was signed by someone else int your own DWN.
*
* @param store - if true, the record will be stored to the owner's DWN after signing. Defaults to true.
* @returns the status of the import request
*
* @beta
*/
import(store = true) {
return __awaiter(this, void 0, void 0, function* () {
return this.processRecord({ store, signAsOwner: true });
});
}
/**
* Send the current record to a remote DWN by specifying their DID
* If no DID is specified, the target is assumed to be the owner (connectedDID).
* If an initial write is present and the Record class send cache has no awareness of it, the initial write is sent first
* (vs waiting for the regular DWN sync)
* @param target - the optional DID to send the record to, if none is set it is sent to the connectedDid
* @returns the status of the send record request
* @throws `Error` if the record has already been deleted.
*
* @beta
*/
send(target) {
return __awaiter(this, void 0, void 0, function* () {
const initialWrite = this._initialWrite;
target !== null && target !== void 0 ? target : (target = this._connectedDid);
// Is there an initial write? Do we know if we've already sent it to this target?
if (initialWrite && !Record._sendCache.check(this._recordId, target)) {
// We do have an initial write, so prepare it for sending to the target.
const rawMessage = Object.assign({}, initialWrite);
removeUndefinedProperties(rawMessage);
const initialState = {
messageType: DwnInterfaceName.Records + DwnMethodName.Write,
author: this._connectedDid,
target: target,
rawMessage
};
yield this._agent.sendDwnRequest(initialState);
// Set the cache to maintain awareness that we don't need to send the initial write next time.
Record._sendCache.set(this._recordId, target);
}
// Prepare the current state for sending to the target
const latestState = {
messageType: DwnInterfaceName.Records + DwnMethodName.Write,
author: this._connectedDid,
dataStream: yield this.data.blob(),
target: target
};
latestState.rawMessage = Object.assign({}, this.rawMessage);
const { reply } = yield this._agent.sendDwnRequest(latestState);
return reply;
});
}
/**
* Returns a JSON representation of the Record instance.
* It's called by `JSON.stringify(...)` automatically.
*/
toJSON() {
return {
attestation: this.attestation,
author: this.author,
authorization: this.authorization,
contextId: this.contextId,
dataCid: this.dataCid,
dataFormat: this.dataFormat,
dataSize: this.dataSize,
dateCreated: this.dateCreated,
messageTimestamp: this.dateModified,
datePublished: this.datePublished,
encryption: this.encryption,
interface: this.interface,
method: this.method,
parentId: this.parentId,
protocol: this.protocol,
protocolPath: this.protocolPath,
protocolRole: this.protocolRole,
published: this.published,
recipient: this.recipient,
recordId: this.id,
schema: this.schema
};
}
/**
* Convenience method to return the string representation of the Record instance.
* Called automatically in string concatenation, String() type conversion, and template literals.
*/
toString() {
let str = `Record: {\n`;
str += ` ID: ${this.id}\n`;
str += this.contextId ? ` Context ID: ${this.contextId}\n` : '';
str += this.protocol ? ` Protocol: ${this.protocol}\n` : '';
str += this.schema ? ` Schema: ${this.schema}\n` : '';
str += ` Data CID: ${this.dataCid}\n`;
str += ` Data Format: ${this.dataFormat}\n`;
str += ` Data Size: ${this.dataSize}\n`;
str += ` Created: ${this.dateCreated}\n`;
str += ` Modified: ${this.dateModified}\n`;
str += `}`;
return str;
}
/**
* Update the current record on the DWN.
* @param options - options to update the record, including the new data
* @returns the status of the update request
* @throws `Error` if the record has already been deleted.
*
* @beta
*/
update(options = {}) {
return __awaiter(this, void 0, void 0, function* () {
// Map Record class `dateModified` property to DWN SDK `messageTimestamp`.
const _a = options, { dateModified } = _a, updateOptions = __rest(_a, ["dateModified"]);
updateOptions.messageTimestamp = dateModified;
// Begin assembling update message.
let updateMessage = Object.assign(Object.assign({}, this._descriptor), updateOptions);
let dataBlob;
if (options.data !== undefined) {
// If `data` is being updated then `dataCid` and `dataSize` must be undefined and the `data`
// property is passed as a top-level property to `agent.processDwnRequest()`.
delete updateMessage.dataCid;
delete updateMessage.dataSize;
delete updateMessage.data;
({ dataBlob } = dataToBlob(options.data, updateMessage.dataFormat));
}
// Throw an error if an attempt is made to modify immutable properties. `data` has already been handled.
const mutableDescriptorProperties = new Set(['data', 'dataCid', 'dataSize', 'datePublished', 'messageTimestamp', 'published']);
Record.verifyPermittedMutation(Object.keys(options), mutableDescriptorProperties);
// If a new `dateModified` was not provided, remove the equivalent `messageTimestamp` property from from the
// updateMessage to let the DWN SDK auto-fill. This is necessary because otherwise DWN SDK throws an
// Error 409 Conflict due to attempting to overwrite a record when the `messageTimestamp` values are identical.
if (options.dateModified === undefined) {
delete updateMessage.messageTimestamp;
}
// If `published` is set to false, ensure that `datePublished` is undefined. Otherwise, DWN SDK's schema validation
// will throw an error if `published` is false but `datePublished` is set.
if (options.published === false && updateMessage.datePublished !== undefined) {
delete updateMessage.datePublished;
}
// Set the record ID and context ID, if any.
updateMessage.recordId = this._recordId;
updateMessage.contextId = this._contextId;
const messageOptions = Object.assign({}, updateMessage);
const agentResponse = yield this._agent.processDwnRequest({
author: this._connectedDid,
dataStream: dataBlob,
messageOptions,
messageType: DwnInterfaceName.Records + DwnMethodName.Write,
target: this._connectedDid,
});
const { message, reply: { status } } = agentResponse;
const responseMessage = message;
if (200 <= status.code && status.code <= 299) {
// copy the original raw message to the initial write before we update the values.
if (!this._initialWrite) {
this._initialWrite = Object.assign({}, this.rawMessage);
}
// Only update the local Record instance mutable properties if the record was successfully (over)written.
this._authorization = responseMessage.authorization;
this._protocolRole = messageOptions.protocolRole;
mutableDescriptorProperties.forEach(property => {
this._descriptor[property] = responseMessage.descriptor[property];
});
// Cache data.
if (options.data !== undefined) {
this._encodedData = dataBlob;
}
}
return { status };
});
}
// Handles the various conditions around there being an initial write, whether to store initial/current state,
// and whether to add an owner signature to the initial write to enable storage when protocol rules require it.
processRecord({ store, signAsOwner }) {
return __awaiter(this, void 0, void 0, function* () {
// if there is an initial write and we haven't already processed it, we first process it and marked it as such.
if (this._initialWrite && ((signAsOwner && !this._initialWriteSigned) || (store && !this._initialWriteStored))) {
const initialWriteRequest = {
messageType: DwnInterfaceName.Records + DwnMethodName.Write,
rawMessage: this.initialWrite,
author: this._connectedDid,
target: this._connectedDid,
signAsOwner,
store,
};
// Process the prepared initial write, with the options set for storing and/or signing as the owner.
const agentResponse = yield this._agent.processDwnRequest(initialWriteRequest);
const { message, reply: { status } } = agentResponse;
const responseMessage = message;
// If we are signing as owner, make sure to update the initial write's authorization, because now it will have the owner's signature on it
// set the stored or signed status to true so we don't process it again.
if (200 <= status.code && status.code <= 299) {
if (store)
this._initialWriteStored = true;
if (signAsOwner) {
this._initialWriteSigned = true;
this.initialWrite.authorization = responseMessage.authorization;
}
}
}
// Now that we've processed a potential initial write, we can process the current record state.
const requestOptions = {
messageType: DwnInterfaceName.Records + DwnMethodName.Write,
rawMessage: this.rawMessage,
author: this._connectedDid,
target: this._connectedDid,
dataStream: yield this.data.blob(),
signAsOwner,
store,
};
const agentResponse = yield this._agent.processDwnRequest(requestOptions);
const { message, reply: { status } } = agentResponse;
const responseMessage = message;
if (200 <= status.code && status.code <= 299) {
// If we are signing as the owner, make sure to update the current record state's authorization, because now it will have the owner's signature on it.
if (signAsOwner)
this._authorization = responseMessage.authorization;
}
return { status };
});
}
/**
* Fetches the record's data from the specified DWN.
*
* This private method is called when the record data is not available in-memory
* and needs to be fetched from either a local or a remote DWN.
* It makes a read request to the specified DWN and processes the response to provide
* a Node.js `Readable` stream of the record's data.
*
* @param target - The DID of the DWN to fetch the data from.
* @param isRemote - Indicates whether the target DWN is a remote node.
* @returns A Promise that resolves to a Node.js `Readable` stream of the record's data.
* @throws If there is an error while fetching or processing the data from the DWN.
*
* @beta
*/
readRecordData({ target, isRemote }) {
return __awaiter(this, void 0, void 0, function* () {
const readRequest = {
author: this._connectedDid,
messageOptions: { filter: { recordId: this.id } },
messageType: DwnInterfaceName.Records + DwnMethodName.Read,
target,
};
const agentResponsePromise = isRemote ?
this._agent.sendDwnRequest(readRequest) :
this._agent.processDwnRequest(readRequest);
try {
const { reply: { record } } = yield agentResponsePromise;
const dataStream = record.data;
// If the data stream is a web ReadableStream, convert it to a Node.js Readable.
const nodeReadable = Stream.isReadableStream(dataStream) ?
NodeStream.fromWebReadable({ readableStream: dataStream }) :
dataStream;
return nodeReadable;
}
catch (error) {
throw new Error(`Error encountered while attempting to read data: ${error.message}`);
}
});
}
/**
* Verifies if the properties to be mutated are mutable.
*
* This private method is used to ensure that only mutable properties of the `Record` instance
* are being changed. It checks whether the properties specified for mutation are among the
* set of properties that are allowed to be modified. If any of the properties to be mutated
* are not in the set of mutable properties, the method throws an error.
*
* @param propertiesToMutate - An iterable of property names that are intended to be mutated.
* @param mutableDescriptorProperties - A set of property names that are allowed to be mutated.
*
* @throws If any of the properties in `propertiesToMutate` are not in `mutableDescriptorProperties`.
*
* @beta
*/
static verifyPermittedMutation(propertiesToMutate, mutableDescriptorProperties) {
for (const property of propertiesToMutate) {
if (!mutableDescriptorProperties.has(property)) {
throw new Error(`${property} is an immutable property. Its value cannot be changed.`);
}
}
}
}
// Cache to minimize the amount of redundant two-phase commits we do in store() and send()
// Retains awareness of the last 100 records stored/sent for up to 100 target DIDs each.
Record._sendCache = SendCache;
//# sourceMappingURL=record.js.map