UNPKG

@leordev-web5/api

Version:

SDK for accessing the features and capabilities of Web5

512 lines (448 loc) 16.6 kB
import type { DwnResponse, Web5Agent } from '@web5/agent'; import type { RecordsQueryReply, RecordsReadOptions, ProtocolsQueryReply, RecordsQueryOptions, RecordsWriteMessage, RecordsWriteOptions, RecordsDeleteOptions, ProtocolsQueryOptions, RecordsQueryReplyEntry, ProtocolsConfigureMessage, ProtocolsConfigureOptions, ProtocolsConfigureDescriptor, } from '@tbd54566975/dwn-sdk-js'; import { isEmptyObject } from '@web5/common'; import { DwnInterfaceName, DwnMethodName, RecordsWrite } from '@tbd54566975/dwn-sdk-js'; import { Record } from './record.js'; import { dataToBlob } from './utils.js'; import { Protocol } from './protocol.js'; /** * Status code and detailed message for a response. * * @beta */ export type ResponseStatus = { status: { code: number; detail: string; }; }; /** * Request to setup a protocol with its definitions * * @beta */ export type ProtocolsConfigureRequest = { message: Omit<ProtocolsConfigureOptions, 'signer'>; } /** * Response for the protocol configure request * * @beta */ export type ProtocolsConfigureResponse = ResponseStatus & { protocol?: Protocol; } /** * Represents each entry on the protocols query reply * * @beta */ export type ProtocolsQueryReplyEntry = { descriptor: ProtocolsConfigureDescriptor; }; /** * Request to query protocols * * @beta */ export type ProtocolsQueryRequest = { from?: string; message: Omit<ProtocolsQueryOptions, 'signer'> } /** * Response with the retrieved protocols * * @beta */ export type ProtocolsQueryResponse = ResponseStatus & { protocols: Protocol[]; } /** * Type alias for {@link RecordsWriteRequest} * * @beta */ export type RecordsCreateRequest = RecordsWriteRequest; /** * Type alias for {@link RecordsWriteResponse} * * @beta */ export type RecordsCreateResponse = RecordsWriteResponse; /** * Request to create a record from an existing one (useful for updating an existing record) * * @beta */ export type RecordsCreateFromRequest = { author: string; data: unknown; message?: Omit<RecordsWriteOptions, 'signer'>; record: Record; } /** * Request to delete a record from the DWN * * @beta */ export type RecordsDeleteRequest = { from?: string; message: Omit<RecordsDeleteOptions, 'signer'>; } /** * Request to query records from the DWN * * @beta */ export type RecordsQueryRequest = { /** The from property indicates the DID to query from and return results. */ from?: string; message: Omit<RecordsQueryOptions, 'signer'>; } /** * Response for the query request * * @beta */ export type RecordsQueryResponse = ResponseStatus & { records?: Record[] /** If there are additional results, the messageCid of the last record will be returned as a pagination cursor. */ cursor?: string; }; /** * Request to read a record from the DWN * * @beta */ export type RecordsReadRequest = { /** The from property indicates the DID to read from and return results fro. */ from?: string; message: Omit<RecordsReadOptions, 'signer'>; } /** * Response for the read request * * @beta */ export type RecordsReadResponse = ResponseStatus & { record: Record; }; /** * Request to write a record to the DWN * * @beta */ export type RecordsWriteRequest = { data: unknown; message?: Omit<Partial<RecordsWriteOptions>, 'signer'>; store?: boolean; } /** * Response for the write request * * @beta */ export type RecordsWriteResponse = ResponseStatus & { record?: Record }; /** * Interface to interact with DWN Records and Protocols * * @beta */ export class DwnApi { private agent: Web5Agent; private connectedDid: string; constructor(options: { agent: Web5Agent, connectedDid: string }) { this.agent = options.agent; this.connectedDid = options.connectedDid; } /** * API to interact with DWN protocols (e.g., `dwn.protocols.configure()`). */ get protocols() { return { /** * Configure method, used to setup a new protocol (or update) with the passed definitions */ configure: async (request: ProtocolsConfigureRequest): Promise<ProtocolsConfigureResponse> => { const agentResponse = await this.agent.processDwnRequest({ target : this.connectedDid, author : this.connectedDid, messageOptions : request.message, messageType : DwnInterfaceName.Protocols + DwnMethodName.Configure }); const { message, messageCid, reply: { status }} = agentResponse; const response: ProtocolsConfigureResponse = { status }; if (status.code < 300) { const metadata = { author: this.connectedDid, messageCid }; response.protocol = new Protocol(this.agent, message as ProtocolsConfigureMessage, metadata); } return response; }, /** * Query the available protocols */ query: async (request: ProtocolsQueryRequest): Promise<ProtocolsQueryResponse> => { const agentRequest = { author : this.connectedDid, messageOptions : request.message, messageType : DwnInterfaceName.Protocols + DwnMethodName.Query, target : request.from || this.connectedDid }; let agentResponse: DwnResponse; if (request.from) { agentResponse = await this.agent.sendDwnRequest(agentRequest); } else { agentResponse = await this.agent.processDwnRequest(agentRequest); } const reply = agentResponse.reply as ProtocolsQueryReply; const { entries = [], status } = reply; const protocols = entries.map((entry: ProtocolsQueryReplyEntry) => { const metadata = { author: this.connectedDid, }; // FIXME: dwn-sdk-js actually returns the entire ProtocolsConfigure message, // but the type claims that it returns the message without authorization. // When dwn-sdk-js fixes the type, we should remove `as ProtocolsConfigureMessage` return new Protocol(this.agent, entry as ProtocolsConfigureMessage, metadata); }); return { protocols, status }; } }; } /** * API to interact with DWN records (e.g., `dwn.records.create()`). */ get records() { return { /** * Alias for the `write` method */ create: async (request: RecordsCreateRequest): Promise<RecordsCreateResponse> => { return this.records.write(request); }, /** * Write a record based on an existing one (useful for updating an existing record) */ createFrom: async (request: RecordsCreateFromRequest): Promise<RecordsWriteResponse> => { const { author: inheritedAuthor, ...inheritedProperties } = request.record.toJSON(); // If `data` is being updated then `dataCid` and `dataSize` must not be present. if (request.data !== undefined) { delete inheritedProperties.dataCid; delete inheritedProperties.dataSize; } // 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 (request.message?.published === false && inheritedProperties.datePublished !== undefined) { delete inheritedProperties.datePublished; delete inheritedProperties.published; } // If the request changes the `author` or message `descriptor` then the deterministic `recordId` will change. // As a result, we will discard the `recordId` if either of these changes occur. if (!isEmptyObject(request.message) || (request.author && request.author !== inheritedAuthor)) { delete inheritedProperties.recordId; } return this.records.write({ data : request.data, message : { ...inheritedProperties, ...request.message, }, }); }, /** * Delete a record */ delete: async (request: RecordsDeleteRequest): Promise<ResponseStatus> => { const agentRequest = { /** * The `author` is the DID that will sign the message and must be the DID the Web5 app is * connected with and is authorized to access the signing private key of. */ author : this.connectedDid, messageOptions : request.message, messageType : DwnInterfaceName.Records + DwnMethodName.Delete, /** * The `target` is the DID of the DWN tenant under which the delete will be executed. * If `from` is provided, the delete operation will be executed on a remote DWN. * Otherwise, the record will be deleted on the local DWN. */ target : request.from || this.connectedDid }; let agentResponse: DwnResponse; if (request.from) { agentResponse = await this.agent.sendDwnRequest(agentRequest); } else { agentResponse = await this.agent.processDwnRequest(agentRequest); } const { reply: { status } } = agentResponse; return { status }; }, /** * Query a single or multiple records based on the given filter */ query: async (request: RecordsQueryRequest): Promise<RecordsQueryResponse> => { const agentRequest = { /** * The `author` is the DID that will sign the message and must be the DID the Web5 app is * connected with and is authorized to access the signing private key of. */ author : this.connectedDid, messageOptions : request.message, messageType : DwnInterfaceName.Records + DwnMethodName.Query, /** * The `target` is the DID of the DWN tenant under which the query will be executed. * If `from` is provided, the query operation will be executed on a remote DWN. * Otherwise, the local DWN will be queried. */ target : request.from || this.connectedDid }; let agentResponse: DwnResponse; if (request.from) { agentResponse = await this.agent.sendDwnRequest(agentRequest); } else { agentResponse = await this.agent.processDwnRequest(agentRequest); } const reply = agentResponse.reply as RecordsQueryReply; 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 * tenant owner or any other entity. */ author : RecordsWrite.getAuthor(entry), /** * Set the `connectedDid` to currently connected DID so that subsequent calls to * {@link Record} instance methods, such as `record.update()` are executed on the * local DWN even if the record was returned by a query of a remote DWN. */ connectedDid : this.connectedDid, /** * If the record was returned by a query of a remote DWN, set the `remoteOrigin` to * the DID of the DWN that returned the record. The `remoteOrigin` property will be used * to determine which DWN to send subsequent read requests to in the event the data * payload exceeds the threshold for being returned with queries. */ remoteOrigin : request.from, ...entry as RecordsWriteMessage }; const record = new Record(this.agent, recordOptions); return record; }); return { records, status, cursor }; }, /** * Read a single record based on the given filter */ read: async (request: RecordsReadRequest): Promise<RecordsReadResponse> => { const agentRequest = { /** * The `author` is the DID that will sign the message and must be the DID the Web5 app is * connected with and is authorized to access the signing private key of. */ author : this.connectedDid, messageOptions : request.message, messageType : DwnInterfaceName.Records + DwnMethodName.Read, /** * The `target` is the DID of the DWN tenant under which the read will be executed. * If `from` is provided, the read operation will be executed on a remote DWN. * Otherwise, the read will occur on the local DWN. */ target : request.from || this.connectedDid }; let agentResponse: DwnResponse; if (request.from) { agentResponse = await this.agent.sendDwnRequest(agentRequest); } else { agentResponse = await this.agent.processDwnRequest(agentRequest); } const { reply: { record: responseRecord, status } } = agentResponse; let record: Record; if (200 <= status.code && status.code <= 299) { const recordOptions = { /** * Extract the `author` DID from the record since records may be signed by the * tenant owner or any other entity. */ author : RecordsWrite.getAuthor(responseRecord), /** * Set the `connectedDid` to currently connected DID so that subsequent calls to * {@link Record} instance methods, such as `record.update()` are executed on the * local DWN even if the record was read from a remote DWN. */ connectedDid : this.connectedDid, /** * If the record was returned by reading from a remote DWN, set the `remoteOrigin` to * the DID of the DWN that returned the record. The `remoteOrigin` property will be used * to determine which DWN to send subsequent read requests to in the event the data * payload must be read again (e.g., if the data stream is consumed). */ remoteOrigin : request.from, ...responseRecord, }; record = new Record(this.agent, recordOptions); } return { record, status }; }, /** * Writes a record to the DWN * * As a convenience, the Record instance returned will cache a copy of the data. This is done * to maintain consistency with other DWN methods, like RecordsQuery, that include relatively * small data payloads when returning RecordsWrite message properties. Regardless of data * size, methods such as `record.data.stream()` will return the data when called even if it * requires fetching from the DWN datastore. */ write: async (request: RecordsWriteRequest): Promise<RecordsWriteResponse> => { const messageOptions: Partial<RecordsWriteOptions> = { ...request.message }; const { dataBlob, dataFormat } = dataToBlob(request.data, messageOptions.dataFormat); messageOptions.dataFormat = dataFormat; const agentResponse = await this.agent.processDwnRequest({ author : this.connectedDid, dataStream : dataBlob, messageOptions, messageType : DwnInterfaceName.Records + DwnMethodName.Write, store : request.store, target : this.connectedDid }); const { message, reply: { status } } = agentResponse; const responseMessage = message as RecordsWriteMessage; let record: Record; if (200 <= status.code && status.code <= 299) { const recordOptions = { /** * Assume the author is the connected DID since the record was just written to the * local DWN. */ author : this.connectedDid, /** * Set the `connectedDid` to currently connected DID so that subsequent calls to * {@link Record} instance methods, such as `record.update()` are executed on the * local DWN. */ connectedDid : this.connectedDid, encodedData : dataBlob, ...responseMessage, }; record = new Record(this.agent, recordOptions); } return { record, status }; }, }; } }