import Block from '@ipld/block' import CID from 'cids' import { getName } from 'multicodec' import { BlockService } from './blockservice' /** * AddOptions represents the set of default parameters to use when adding an IPLD Node via the DAG service. */ export interface AddOptions { /** * hashAlg is the hashing algorithm that is used to calculate the CID. The default comes from the given codec. */ hashAlg?: string /** * onlyHash controls whether or not the serialized form of the IPLD Node will be passed to the underlying block store. */ onlyHash?: boolean /** * cidVersion specifies which version of CID to use. This option is currently ignored (and defaults to v1). */ cidVersion?: number } /** * DAGOptions represents the set of parameters to use when initializing a DAG service. */ export interface DAGOptions extends AddOptions { /** * The underlying block service for adding, deleting, and retrieving blocks. It is required. */ blockService: BlockService } /** * ResolveResult represents an IPLD Node that was traversed during path resolving. */ export interface ResolveResult { /** * remainderPath is the part of the path that wasn’t resolved yet. */ remainderPath?: string /** * value is what the resolved path points to. If further traversing is possible, then `value` is a CID object * linking to another IPLD node. If it was possible to fully resolve the path, `value` is the value the path points * to. So if you need the CID of the IPLD node you’re currently at, just take the `value` of the previously returned * IPLD node. */ value: CID | any } /** * DAGService is a content-addressable store for adding, removing, and getting Merkle directed acyclic graphs (DAGs). * A DAG service provides an API for creating and resolving IPLD nodes, making it possible for apps and services to * integrate with the IPFS IPLD Merkle-forest. * * It satisfies the default IPLD interface: https://github.com/ipld/js-ipld. */ export class DAGService { /** * A set of default parameters to use when adding an IPLD Node via the DAG service. */ public defaultOptions: AddOptions /** * The underlying block service for adding, deleting, and retrieving blocks. */ public blockService: BlockService /** * DAGService creates a new directed acyclic graph (DAG) service. * * @param {DAGOptions} options The set of parameters to use when initializing a DAG service. Must include a `blockService` entry. */ constructor(options: DAGOptions) { const { blockService, ...opts } = options this.blockService = blockService this.defaultOptions = opts } /** * resolve retrieves IPLD nodes along the path that is rooted at a given IPLD node. * * @param {CID} cid The content identifier at which to start resolving. * @param {string} path The path that should be resolved. */ async *resolve(cid: CID, path: string): AsyncIterableIterator { const data = await this.blockService.get(cid) const block = Block.create(data.data, data.cid) const reader = block.reader() const node = reader.get(path) yield { value: node.value, remainderPath: node.remaining || '' } if (CID.isCID(node.value)) { for await (const res of this.resolve(node.value, node.remaining || '')) { yield res } } } /** * tree returns all the paths that can be resolved into. * * @param {CID} cid The ID to get the paths from * @param {string} offsetPath the path to start to retrieve the other paths from. * @param {object} options Currently only whether to get the paths recursively or not. If `recursive` is * `false`, it will only resolve the paths of the given CID. */ async *tree(cid: CID, offsetPath = '', options = { recursive: false }): AsyncIterableIterator { const data = await this.blockService.get(cid) const block = Block.create(data.data, data.cid) const reader = block.reader() // Start with this block and its sub-paths for await (let path of reader.tree()) { // Return it if it matches the given offset path, but is not the offset path itself if (path.startsWith(offsetPath) && path.length > offsetPath.length) { if (offsetPath.length > 0) { path = path.slice(offsetPath.length + 1) } yield path } } // Finish with links if recursive if (options.recursive) { for await (const [path, sub] of reader.links()) { for await (const subPath of this.tree(sub, undefined, options)) { let fullPath = `${path}/${subPath}` if (fullPath.startsWith(offsetPath) && fullPath.length > offsetPath.length) { if (offsetPath.length > 0) { fullPath = fullPath.slice(offsetPath.length + 1) } yield fullPath } } } } } /** * get returns an IPLD node by its content identifier. * * @param {CID} cid The content identifier for the IPLD node that should be retrieved. */ async get(cid: CID) { const data = await this.blockService.get(cid) const block = Block.create(data.data, data.cid) return block.decode() } /** * getMany returns multiple IPLD nodes from an iterable of content identifiers. * * @param {Iterable} cids The content identifiers for the IPLD nodes that should be retrieved. */ async *getMany(cids: Iterable): AsyncIterableIterator { for await (const cid of cids) { yield this.get(cid) } } /** * put encodes and adds an IPLD node using the specified codec. * * @param {any} node The deserialized IPLD node that should be added. Can be any value, object, Buffer, JSON, etc. * @param {string | number} codec A string representing the multicodec with which the IPLD node should be encoded. * Can also be a numeric multicodec code for compatibility with js-ipld. * @param {AddOptions} options The specific parameters to use when encoding the input node. */ async put(node: any, codec: string | number = 'dag-cbor', options?: AddOptions) { const opts = options || this.defaultOptions || {} if (typeof codec === 'number') { // Compatibility with js-ipld codec = getName(codec) } const block = Block.encoder(node, codec, opts.hashAlg) const data = block.encode() let cid = await block.cid() if (opts.cidVersion === 0) { // Compatibility with js-ipld cid = cid.toV0() } if (!opts.onlyHash) { await this.blockService.put({ data, cid }) } return cid } /** * putMany encodes and adds multiple IPLD nodes using the specified codec. * * @param {Iterable} nodes The deserialized IPLD nodes that should be added. * @param {string | number} codec A string representing the multicodec with which the IPLD node should be encoded. * Can also be a numeric multicodec code for compatibility with js-ipld. * @param {AddOptions} options The specific parameters to use when encoding the input node. */ async *putMany( nodes: Iterable, codec: string | number = 'dag-cbor', options?: AddOptions, ): AsyncIterableIterator { const opts = options || this.defaultOptions || {} if (typeof codec === 'number') { codec = getName(codec) // Compatibility with js-ipld } for await (const node of nodes) { yield this.put(node, codec, opts) } } /** * remove deletes an IPLD node from the local store. * * @param {CID} cid The content identifier for the IPLD node to be removed. */ async remove(cid: CID) { return this.blockService.delete(cid) } /** * removeMany deletes multiple IPLD nodes from the local store. * * Throws an error if any of the Blocks can’t be removed. This operation is *not* atomic, some nodes might have * already been removed. * * @param {Iterable} cids The content identifiers for the IPLD nodes to be removed. */ async *removeMany(cids: Iterable): AsyncIterableIterator { for await (const cid of cids) { yield this.remove(cid) } } }