diff --git a/.travis.yml b/.travis.yml index d0cf281..525bd09 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,12 +1,17 @@ language: node_js cache: npm +branches: + only: + - master + - /^release\/.*$/ stages: - check - test - cov node_js: - - '10' + - '14' + - '15' os: - linux diff --git a/package.json b/package.json index beb5333..8e576fb 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,11 @@ "leadMaintainer": "Volker Mische ", "main": "src/index.js", "scripts": { + "prepare": "run-s prepare:*", + "prepare:proto": "pbjs -t static-module -w commonjs --force-number --no-verify --no-delimited --no-create --no-beautify --no-defaults --lint eslint-disable -o src/dag.js ./src/dag.proto", + "prepare:proto:types": "pbts -o src/dag.d.ts src/dag.js", + "prepare:types": "aegir build --no-bundle", + "prepare:copy": "cp ./src/*.d.ts ./dist/src", "test": "aegir test", "test:browser": "aegir test --target browser", "test:node": "aegir test --target node", @@ -13,7 +18,6 @@ "release": "aegir release", "release-minor": "aegir release --type minor", "release-major": "aegir release --type major", - "build": "aegir build", "coverage": "aegir coverage", "coverage-publish": "aegir coverage publish" }, @@ -66,18 +70,29 @@ }, "dependencies": { "cids": "^1.0.0", - "class-is": "^1.1.0", "multicodec": "^2.0.0", "multihashing-async": "^2.0.0", - "protons": "^2.0.0", + "protobufjs": "^6.10.2", "reset": "^0.1.0", "run": "^1.4.0", "stable": "^0.1.8", - "uint8arrays": "^1.0.0" + "uint8arrays": "^2.0.5" }, "devDependencies": { - "aegir": "^25.0.0", + "aegir": "^30.3.0", "multibase": "^3.0.0", - "multihashes": "^3.0.0" + "npm-run-all": "^4.1.5" + }, + "types": "dist/src/index.d.ts", + "typesVersions": { + "*": { + "src/*": [ + "dist/src/*", + "dist/src/*/index" + ], + "src/": [ + "dist/src/index" + ] + } } } diff --git a/src/dag-link/dagLink.js b/src/dag-link/dagLink.js index 6028c57..dd5767e 100644 --- a/src/dag-link/dagLink.js +++ b/src/dag-link/dagLink.js @@ -1,11 +1,17 @@ 'use strict' const CID = require('cids') -const withIs = require('class-is') const uint8ArrayFromString = require('uint8arrays/from-string') -// Link represents an IPFS Merkle DAG Link between Nodes. +/** + * Link represents an IPFS Merkle DAG Link between Nodes. + */ class DAGLink { + /** + * @param {string | undefined | null} name + * @param {number} size + * @param {CID | string | Uint8Array} cid + */ constructor (name, size, cid) { if (!cid) { throw new Error('A link requires a cid to point to') @@ -14,11 +20,11 @@ class DAGLink { // assert(size, 'A link requires a size') // note - links should include size, but this assert is disabled // for now to maintain consistency with go-ipfs pinset + this.Name = name || '' + this.Tsize = size + this.Hash = new CID(cid) Object.defineProperties(this, { - Name: { value: name || '', writable: false, enumerable: true }, - Tsize: { value: size, writable: false, enumerable: true }, - Hash: { value: new CID(cid), writable: false, enumerable: true }, _nameBuf: { value: null, writable: true, enumerable: false } }) } @@ -43,7 +49,7 @@ class DAGLink { // We need this to sort the links, otherwise // we will reallocate new Uint8Arrays every time get nameAsBuffer () { - if (this._nameBuf !== null) { + if (this._nameBuf != null) { return this._nameBuf } @@ -52,4 +58,4 @@ class DAGLink { } } -exports = module.exports = withIs(DAGLink, { className: 'DAGLink', symbolName: '@ipld/js-ipld-dag-pb/daglink' }) +module.exports = DAGLink diff --git a/src/dag-link/index.js b/src/dag-link/index.js index c4ea477..d72b170 100644 --- a/src/dag-link/index.js +++ b/src/dag-link/index.js @@ -1,4 +1,3 @@ 'use strict' exports = module.exports = require('./dagLink') -exports.util = require('./util') diff --git a/src/dag-link/util.js b/src/dag-link/util.js index 0f2e349..0fe0173 100644 --- a/src/dag-link/util.js +++ b/src/dag-link/util.js @@ -1,7 +1,10 @@ 'use strict' -const DAGLink = require('./dagLink') +const DAGLink = require('./') +/** + * @param {*} link + */ function createDagLinkFromB58EncodedHash (link) { return new DAGLink( link.Name || link.name || '', @@ -10,5 +13,6 @@ function createDagLinkFromB58EncodedHash (link) { ) } -exports = module.exports -exports.createDagLinkFromB58EncodedHash = createDagLinkFromB58EncodedHash +module.exports = { + createDagLinkFromB58EncodedHash +} diff --git a/src/dag-node/addLink.js b/src/dag-node/addLink.js index 032b216..4986dc4 100644 --- a/src/dag-node/addLink.js +++ b/src/dag-node/addLink.js @@ -1,10 +1,19 @@ 'use strict' const sortLinks = require('./sortLinks') -const DAGLink = require('../dag-link') +const DAGLink = require('../dag-link/dagLink') +/** + * @typedef {import('./dagNode')} DAGNode + * @typedef {import('../types')} DAGLinkLike + */ + +/** + * @param {*} link + * @returns {DAGLink} + */ const asDAGLink = (link) => { - if (DAGLink.isDAGLink(link)) { + if (link instanceof DAGLink) { // It's a DAGLink instance // no need to do anything return link @@ -21,9 +30,14 @@ const asDAGLink = (link) => { } // It's a Object with name, multihash/hash/cid and size + // @ts-ignore return new DAGLink(link.Name || link.name, link.Tsize || link.size, link.Hash || link.multihash || link.hash || link.cid) } +/** + * @param {DAGNode} node + * @param {DAGLink | DAGLinkLike} link + */ const addLink = (node, link) => { const dagLink = asDAGLink(link) node.Links.push(dagLink) diff --git a/src/dag-node/dagNode.js b/src/dag-node/dagNode.js index ae3fc10..8b144aa 100644 --- a/src/dag-node/dagNode.js +++ b/src/dag-node/dagNode.js @@ -1,16 +1,26 @@ 'use strict' -const withIs = require('class-is') const sortLinks = require('./sortLinks') const DAGLink = require('../dag-link/dagLink') -const { serializeDAGNode } = require('../serialize.js') +const { createDagLinkFromB58EncodedHash } = require('../dag-link/util') +const { serializeDAGNode } = require('../serialize') const toDAGLink = require('./toDagLink') const addLink = require('./addLink') const rmLink = require('./rmLink') const uint8ArrayFromString = require('uint8arrays/from-string') const uint8ArrayToString = require('uint8arrays/to-string') +/** + * @typedef {import('cids')} CID + * @typedef {import('../types').DAGLinkLike} DAGLinkLike + */ + class DAGNode { + /** + *@param {Uint8Array | string} [data] + * @param {(DAGLink | DAGLinkLike)[]} links + * @param {number | null} [serializedSize] + */ constructor (data, links = [], serializedSize = null) { if (!data) { data = new Uint8Array(0) @@ -27,16 +37,17 @@ class DAGNode { throw new Error('Passed \'serializedSize\' must be a number!') } - links = links.map((link) => { - return DAGLink.isDAGLink(link) + const sortedLinks = links.map((link) => { + return link instanceof DAGLink ? link - : DAGLink.util.createDagLinkFromB58EncodedHash(link) + : createDagLinkFromB58EncodedHash(link) }) - sortLinks(links) + sortLinks(sortedLinks) + + this.Data = data + this.Links = sortedLinks Object.defineProperties(this, { - Data: { value: data, writable: false, enumerable: true }, - Links: { value: links, writable: false, enumerable: true }, _serializedSize: { value: serializedSize, writable: true, enumerable: false }, _size: { value: null, writable: true, enumerable: false } }) @@ -63,31 +74,47 @@ class DAGNode { this._size = null } + /** + * @param {DAGLink | import('../types').DAGLinkLike} link + */ addLink (link) { this._invalidateCached() return addLink(this, link) } + /** + * @param {DAGLink | string | CID} link + */ rmLink (link) { this._invalidateCached() return rmLink(this, link) } - // @returns {Promise.} + /** + * @param {import('./toDagLink').ToDagLinkOptions} [options] + */ toDAGLink (options) { return toDAGLink(this, options) } serialize () { - return serializeDAGNode(this) + const buf = serializeDAGNode(this) + + this._serializedSize = buf.length + + return buf } get size () { - if (this._size === null) { - if (this._serializedSize === null) { + if (this._size == null) { + let serializedSize + + if (serializedSize == null) { this._serializedSize = this.serialize().length + serializedSize = this._serializedSize } - this._size = this.Links.reduce((sum, l) => sum + l.Tsize, this._serializedSize) + + this._size = this.Links.reduce((sum, l) => sum + l.Tsize, serializedSize) } return this._size @@ -98,4 +125,4 @@ class DAGNode { } } -exports = module.exports = withIs(DAGNode, { className: 'DAGNode', symbolName: '@ipld/js-ipld-dag-pb/dagnode' }) +module.exports = DAGNode diff --git a/src/dag-node/rmLink.js b/src/dag-node/rmLink.js index 4e9fe92..466592e 100644 --- a/src/dag-node/rmLink.js +++ b/src/dag-node/rmLink.js @@ -3,14 +3,25 @@ const CID = require('cids') const uint8ArrayEquals = require('uint8arrays/equals') +/** + * @typedef {import('../dag-link/dagLink')} DAGLink + */ + +/** + * + * @param {import('./dagNode')} dagNode + * @param {string | CID | Uint8Array | DAGLink} nameOrCid + */ const rmLink = (dagNode, nameOrCid) => { let predicate = null // It's a name if (typeof nameOrCid === 'string') { - predicate = (link) => link.Name === nameOrCid - } else if (nameOrCid instanceof Uint8Array || CID.isCID(nameOrCid)) { - predicate = (link) => uint8ArrayEquals(link.Hash, nameOrCid) + predicate = (/** @type {DAGLink} */ link) => link.Name === nameOrCid + } else if (nameOrCid instanceof Uint8Array) { + predicate = (/** @type {DAGLink} */ link) => uint8ArrayEquals(link.Hash.bytes, nameOrCid) + } else if (CID.isCID(nameOrCid)) { + predicate = (/** @type {DAGLink} */ link) => uint8ArrayEquals(link.Hash.bytes, nameOrCid.bytes) } if (predicate) { diff --git a/src/dag-node/sortLinks.js b/src/dag-node/sortLinks.js index 8b015d5..855d967 100644 --- a/src/dag-node/sortLinks.js +++ b/src/dag-node/sortLinks.js @@ -3,6 +3,15 @@ const sort = require('stable') const uint8ArrayCompare = require('uint8arrays/compare') +/** + * @typedef {import('../dag-link/dagLink')} DAGLink + */ + +/** + * + * @param {DAGLink} a + * @param {DAGLink} b + */ const linkSort = (a, b) => { const buf1 = a.nameAsBuffer const buf2 = b.nameAsBuffer @@ -12,7 +21,8 @@ const linkSort = (a, b) => { /** * Sorts links in place (mutating given array) - * @param {Array} links + * + * @param {DAGLink[]} links * @returns {void} */ const sortLinks = (links) => { diff --git a/src/dag-node/toDagLink.js b/src/dag-node/toDagLink.js index 6250877..dc57a8b 100644 --- a/src/dag-node/toDagLink.js +++ b/src/dag-node/toDagLink.js @@ -1,13 +1,24 @@ 'use strict' -const DAGLink = require('../dag-link/dagLink') +const DAGLink = require('../dag-link') const genCid = require('../genCid') -/* +/** * toDAGLink converts a DAGNode to a DAGLink + * + * @typedef {import('../genCid').GenCIDOptions} GenCIDOptions + * + * @typedef {object} ToDagLinkExtraOptions + * @property {string} [name] + * + * @typedef {GenCIDOptions & ToDagLinkExtraOptions} ToDagLinkOptions + * + * @param {import('./dagNode')} node + * @param {ToDagLinkOptions} options */ const toDAGLink = async (node, options = {}) => { - const nodeCid = await genCid.cid(node.serialize(), options) + const buf = node.serialize() + const nodeCid = await genCid.cid(buf, options) return new DAGLink(options.name || '', node.size, nodeCid) } diff --git a/src/dag.d.ts b/src/dag.d.ts new file mode 100644 index 0000000..8636d34 --- /dev/null +++ b/src/dag.d.ts @@ -0,0 +1,136 @@ +import * as $protobuf from "protobufjs"; +/** Properties of a PBLink. */ +export interface IPBLink { + + /** PBLink Hash */ + Hash?: (Uint8Array|null); + + /** PBLink Name */ + Name?: (string|null); + + /** PBLink Tsize */ + Tsize?: (number|null); +} + +/** Represents a PBLink. */ +export class PBLink implements IPBLink { + + /** + * Constructs a new PBLink. + * @param [p] Properties to set + */ + constructor(p?: IPBLink); + + /** PBLink Hash. */ + public Hash: Uint8Array; + + /** PBLink Name. */ + public Name: string; + + /** PBLink Tsize. */ + public Tsize: number; + + /** + * Encodes the specified PBLink message. Does not implicitly {@link PBLink.verify|verify} messages. + * @param m PBLink message or plain object to encode + * @param [w] Writer to encode to + * @returns Writer + */ + public static encode(m: IPBLink, w?: $protobuf.Writer): $protobuf.Writer; + + /** + * Decodes a PBLink message from the specified reader or buffer. + * @param r Reader or buffer to decode from + * @param [l] Message length if known beforehand + * @returns PBLink + * @throws {Error} If the payload is not a reader or valid buffer + * @throws {$protobuf.util.ProtocolError} If required fields are missing + */ + public static decode(r: ($protobuf.Reader|Uint8Array), l?: number): PBLink; + + /** + * Creates a PBLink message from a plain object. Also converts values to their respective internal types. + * @param d Plain object + * @returns PBLink + */ + public static fromObject(d: { [k: string]: any }): PBLink; + + /** + * Creates a plain object from a PBLink message. Also converts values to other types if specified. + * @param m PBLink + * @param [o] Conversion options + * @returns Plain object + */ + public static toObject(m: PBLink, o?: $protobuf.IConversionOptions): { [k: string]: any }; + + /** + * Converts this PBLink to JSON. + * @returns JSON object + */ + public toJSON(): { [k: string]: any }; +} + +/** Properties of a PBNode. */ +export interface IPBNode { + + /** PBNode Links */ + Links?: (IPBLink[]|null); + + /** PBNode Data */ + Data?: (Uint8Array|null); +} + +/** Represents a PBNode. */ +export class PBNode implements IPBNode { + + /** + * Constructs a new PBNode. + * @param [p] Properties to set + */ + constructor(p?: IPBNode); + + /** PBNode Links. */ + public Links: IPBLink[]; + + /** PBNode Data. */ + public Data: Uint8Array; + + /** + * Encodes the specified PBNode message. Does not implicitly {@link PBNode.verify|verify} messages. + * @param m PBNode message or plain object to encode + * @param [w] Writer to encode to + * @returns Writer + */ + public static encode(m: IPBNode, w?: $protobuf.Writer): $protobuf.Writer; + + /** + * Decodes a PBNode message from the specified reader or buffer. + * @param r Reader or buffer to decode from + * @param [l] Message length if known beforehand + * @returns PBNode + * @throws {Error} If the payload is not a reader or valid buffer + * @throws {$protobuf.util.ProtocolError} If required fields are missing + */ + public static decode(r: ($protobuf.Reader|Uint8Array), l?: number): PBNode; + + /** + * Creates a PBNode message from a plain object. Also converts values to their respective internal types. + * @param d Plain object + * @returns PBNode + */ + public static fromObject(d: { [k: string]: any }): PBNode; + + /** + * Creates a plain object from a PBNode message. Also converts values to other types if specified. + * @param m PBNode + * @param [o] Conversion options + * @returns Plain object + */ + public static toObject(m: PBNode, o?: $protobuf.IConversionOptions): { [k: string]: any }; + + /** + * Converts this PBNode to JSON. + * @returns JSON object + */ + public toJSON(): { [k: string]: any }; +} diff --git a/src/dag.js b/src/dag.js new file mode 100644 index 0000000..999e46a --- /dev/null +++ b/src/dag.js @@ -0,0 +1,388 @@ +/*eslint-disable*/ +"use strict"; + +var $protobuf = require("protobufjs/minimal"); + +// Common aliases +var $Reader = $protobuf.Reader, $Writer = $protobuf.Writer, $util = $protobuf.util; + +// Exported root namespace +var $root = $protobuf.roots["default"] || ($protobuf.roots["default"] = {}); + +$root.PBLink = (function() { + + /** + * Properties of a PBLink. + * @exports IPBLink + * @interface IPBLink + * @property {Uint8Array|null} [Hash] PBLink Hash + * @property {string|null} [Name] PBLink Name + * @property {number|null} [Tsize] PBLink Tsize + */ + + /** + * Constructs a new PBLink. + * @exports PBLink + * @classdesc Represents a PBLink. + * @implements IPBLink + * @constructor + * @param {IPBLink=} [p] Properties to set + */ + function PBLink(p) { + if (p) + for (var ks = Object.keys(p), i = 0; i < ks.length; ++i) + if (p[ks[i]] != null) + this[ks[i]] = p[ks[i]]; + } + + /** + * PBLink Hash. + * @member {Uint8Array} Hash + * @memberof PBLink + * @instance + */ + PBLink.prototype.Hash = $util.newBuffer([]); + + /** + * PBLink Name. + * @member {string} Name + * @memberof PBLink + * @instance + */ + PBLink.prototype.Name = ""; + + /** + * PBLink Tsize. + * @member {number} Tsize + * @memberof PBLink + * @instance + */ + PBLink.prototype.Tsize = $util.Long ? $util.Long.fromBits(0,0,true) : 0; + + /** + * Encodes the specified PBLink message. Does not implicitly {@link PBLink.verify|verify} messages. + * @function encode + * @memberof PBLink + * @static + * @param {IPBLink} m PBLink message or plain object to encode + * @param {$protobuf.Writer} [w] Writer to encode to + * @returns {$protobuf.Writer} Writer + */ + PBLink.encode = function encode(m, w) { + if (!w) + w = $Writer.create(); + if (m.Hash != null && Object.hasOwnProperty.call(m, "Hash")) + w.uint32(10).bytes(m.Hash); + if (m.Name != null && Object.hasOwnProperty.call(m, "Name")) + w.uint32(18).string(m.Name); + if (m.Tsize != null && Object.hasOwnProperty.call(m, "Tsize")) + w.uint32(24).uint64(m.Tsize); + return w; + }; + + /** + * Decodes a PBLink message from the specified reader or buffer. + * @function decode + * @memberof PBLink + * @static + * @param {$protobuf.Reader|Uint8Array} r Reader or buffer to decode from + * @param {number} [l] Message length if known beforehand + * @returns {PBLink} PBLink + * @throws {Error} If the payload is not a reader or valid buffer + * @throws {$protobuf.util.ProtocolError} If required fields are missing + */ + PBLink.decode = function decode(r, l) { + if (!(r instanceof $Reader)) + r = $Reader.create(r); + var c = l === undefined ? r.len : r.pos + l, m = new $root.PBLink(); + while (r.pos < c) { + var t = r.uint32(); + switch (t >>> 3) { + case 1: + m.Hash = r.bytes(); + break; + case 2: + m.Name = r.string(); + break; + case 3: + m.Tsize = r.uint64(); + break; + default: + r.skipType(t & 7); + break; + } + } + return m; + }; + + /** + * Creates a PBLink message from a plain object. Also converts values to their respective internal types. + * @function fromObject + * @memberof PBLink + * @static + * @param {Object.} d Plain object + * @returns {PBLink} PBLink + */ + PBLink.fromObject = function fromObject(d) { + if (d instanceof $root.PBLink) + return d; + var m = new $root.PBLink(); + if (d.Hash != null) { + if (typeof d.Hash === "string") + $util.base64.decode(d.Hash, m.Hash = $util.newBuffer($util.base64.length(d.Hash)), 0); + else if (d.Hash.length) + m.Hash = d.Hash; + } + if (d.Name != null) { + m.Name = String(d.Name); + } + if (d.Tsize != null) { + if ($util.Long) + (m.Tsize = $util.Long.fromValue(d.Tsize)).unsigned = true; + else if (typeof d.Tsize === "string") + m.Tsize = parseInt(d.Tsize, 10); + else if (typeof d.Tsize === "number") + m.Tsize = d.Tsize; + else if (typeof d.Tsize === "object") + m.Tsize = new $util.LongBits(d.Tsize.low >>> 0, d.Tsize.high >>> 0).toNumber(true); + } + return m; + }; + + /** + * Creates a plain object from a PBLink message. Also converts values to other types if specified. + * @function toObject + * @memberof PBLink + * @static + * @param {PBLink} m PBLink + * @param {$protobuf.IConversionOptions} [o] Conversion options + * @returns {Object.} Plain object + */ + PBLink.toObject = function toObject(m, o) { + if (!o) + o = {}; + var d = {}; + if (o.defaults) { + if (o.bytes === String) + d.Hash = ""; + else { + d.Hash = []; + if (o.bytes !== Array) + d.Hash = $util.newBuffer(d.Hash); + } + d.Name = ""; + if ($util.Long) { + var n = new $util.Long(0, 0, true); + d.Tsize = o.longs === String ? n.toString() : o.longs === Number ? n.toNumber() : n; + } else + d.Tsize = o.longs === String ? "0" : 0; + } + if (m.Hash != null && m.hasOwnProperty("Hash")) { + d.Hash = o.bytes === String ? $util.base64.encode(m.Hash, 0, m.Hash.length) : o.bytes === Array ? Array.prototype.slice.call(m.Hash) : m.Hash; + } + if (m.Name != null && m.hasOwnProperty("Name")) { + d.Name = m.Name; + } + if (m.Tsize != null && m.hasOwnProperty("Tsize")) { + if (typeof m.Tsize === "number") + d.Tsize = o.longs === String ? String(m.Tsize) : m.Tsize; + else + d.Tsize = o.longs === String ? $util.Long.prototype.toString.call(m.Tsize) : o.longs === Number ? new $util.LongBits(m.Tsize.low >>> 0, m.Tsize.high >>> 0).toNumber(true) : m.Tsize; + } + return d; + }; + + /** + * Converts this PBLink to JSON. + * @function toJSON + * @memberof PBLink + * @instance + * @returns {Object.} JSON object + */ + PBLink.prototype.toJSON = function toJSON() { + return this.constructor.toObject(this, $protobuf.util.toJSONOptions); + }; + + return PBLink; +})(); + +$root.PBNode = (function() { + + /** + * Properties of a PBNode. + * @exports IPBNode + * @interface IPBNode + * @property {Array.|null} [Links] PBNode Links + * @property {Uint8Array|null} [Data] PBNode Data + */ + + /** + * Constructs a new PBNode. + * @exports PBNode + * @classdesc Represents a PBNode. + * @implements IPBNode + * @constructor + * @param {IPBNode=} [p] Properties to set + */ + function PBNode(p) { + this.Links = []; + if (p) + for (var ks = Object.keys(p), i = 0; i < ks.length; ++i) + if (p[ks[i]] != null) + this[ks[i]] = p[ks[i]]; + } + + /** + * PBNode Links. + * @member {Array.} Links + * @memberof PBNode + * @instance + */ + PBNode.prototype.Links = $util.emptyArray; + + /** + * PBNode Data. + * @member {Uint8Array} Data + * @memberof PBNode + * @instance + */ + PBNode.prototype.Data = $util.newBuffer([]); + + /** + * Encodes the specified PBNode message. Does not implicitly {@link PBNode.verify|verify} messages. + * @function encode + * @memberof PBNode + * @static + * @param {IPBNode} m PBNode message or plain object to encode + * @param {$protobuf.Writer} [w] Writer to encode to + * @returns {$protobuf.Writer} Writer + */ + PBNode.encode = function encode(m, w) { + if (!w) + w = $Writer.create(); + if (m.Data != null && Object.hasOwnProperty.call(m, "Data")) + w.uint32(10).bytes(m.Data); + if (m.Links != null && m.Links.length) { + for (var i = 0; i < m.Links.length; ++i) + $root.PBLink.encode(m.Links[i], w.uint32(18).fork()).ldelim(); + } + return w; + }; + + /** + * Decodes a PBNode message from the specified reader or buffer. + * @function decode + * @memberof PBNode + * @static + * @param {$protobuf.Reader|Uint8Array} r Reader or buffer to decode from + * @param {number} [l] Message length if known beforehand + * @returns {PBNode} PBNode + * @throws {Error} If the payload is not a reader or valid buffer + * @throws {$protobuf.util.ProtocolError} If required fields are missing + */ + PBNode.decode = function decode(r, l) { + if (!(r instanceof $Reader)) + r = $Reader.create(r); + var c = l === undefined ? r.len : r.pos + l, m = new $root.PBNode(); + while (r.pos < c) { + var t = r.uint32(); + switch (t >>> 3) { + case 2: + if (!(m.Links && m.Links.length)) + m.Links = []; + m.Links.push($root.PBLink.decode(r, r.uint32())); + break; + case 1: + m.Data = r.bytes(); + break; + default: + r.skipType(t & 7); + break; + } + } + return m; + }; + + /** + * Creates a PBNode message from a plain object. Also converts values to their respective internal types. + * @function fromObject + * @memberof PBNode + * @static + * @param {Object.} d Plain object + * @returns {PBNode} PBNode + */ + PBNode.fromObject = function fromObject(d) { + if (d instanceof $root.PBNode) + return d; + var m = new $root.PBNode(); + if (d.Links) { + if (!Array.isArray(d.Links)) + throw TypeError(".PBNode.Links: array expected"); + m.Links = []; + for (var i = 0; i < d.Links.length; ++i) { + if (typeof d.Links[i] !== "object") + throw TypeError(".PBNode.Links: object expected"); + m.Links[i] = $root.PBLink.fromObject(d.Links[i]); + } + } + if (d.Data != null) { + if (typeof d.Data === "string") + $util.base64.decode(d.Data, m.Data = $util.newBuffer($util.base64.length(d.Data)), 0); + else if (d.Data.length) + m.Data = d.Data; + } + return m; + }; + + /** + * Creates a plain object from a PBNode message. Also converts values to other types if specified. + * @function toObject + * @memberof PBNode + * @static + * @param {PBNode} m PBNode + * @param {$protobuf.IConversionOptions} [o] Conversion options + * @returns {Object.} Plain object + */ + PBNode.toObject = function toObject(m, o) { + if (!o) + o = {}; + var d = {}; + if (o.arrays || o.defaults) { + d.Links = []; + } + if (o.defaults) { + if (o.bytes === String) + d.Data = ""; + else { + d.Data = []; + if (o.bytes !== Array) + d.Data = $util.newBuffer(d.Data); + } + } + if (m.Data != null && m.hasOwnProperty("Data")) { + d.Data = o.bytes === String ? $util.base64.encode(m.Data, 0, m.Data.length) : o.bytes === Array ? Array.prototype.slice.call(m.Data) : m.Data; + } + if (m.Links && m.Links.length) { + d.Links = []; + for (var j = 0; j < m.Links.length; ++j) { + d.Links[j] = $root.PBLink.toObject(m.Links[j], o); + } + } + return d; + }; + + /** + * Converts this PBNode to JSON. + * @function toJSON + * @memberof PBNode + * @instance + * @returns {Object.} JSON object + */ + PBNode.prototype.toJSON = function toJSON() { + return this.constructor.toObject(this, $protobuf.util.toJSONOptions); + }; + + return PBNode; +})(); + +module.exports = $root; diff --git a/src/dag.proto.js b/src/dag.proto similarity index 86% rename from src/dag.proto.js rename to src/dag.proto index 07d5bb4..ef4e08e 100644 --- a/src/dag.proto.js +++ b/src/dag.proto @@ -1,6 +1,4 @@ -'use strict' - -module.exports = `// An IPFS MerkleDAG Link +// An IPFS MerkleDAG Link message PBLink { // multihash of the target object @@ -21,4 +19,4 @@ message PBNode { // opaque user data optional bytes Data = 1; -}` +} diff --git a/src/genCid.js b/src/genCid.js index 0180e69..a46e3aa 100644 --- a/src/genCid.js +++ b/src/genCid.js @@ -3,30 +3,39 @@ const CID = require('cids') const multicodec = require('multicodec') const multihashing = require('multihashing-async') +const { multihash } = multihashing -exports = module.exports +const codec = multicodec.DAG_PB +const defaultHashAlg = multihash.names['sha2-256'] -exports.codec = multicodec.DAG_PB -exports.defaultHashAlg = multicodec.SHA2_256 +/** + * @typedef {object} GenCIDOptions - Options to create the CID + * @property {CID.CIDVersion} [cidVersion=1] - CID version number + * @property {multihashing.multihash.HashCode} [hashAlg=multihash.names['sha2-256']] - Defaults to the defaultHashAlg of the format + */ /** * Calculate the CID of the binary blob. * - * @param {Object} binaryBlob - Encoded IPLD Node - * @param {Object} [userOptions] - Options to create the CID - * @param {number} [userOptions.cidVersion=1] - CID version number - * @param {string} [UserOptions.hashAlg] - Defaults to the defaultHashAlg of the format - * @returns {Promise.} + * @param {Uint8Array} binaryBlob - Encoded IPLD Node + * @param {GenCIDOptions} [userOptions] - Options to create the CID */ -const cid = async (binaryBlob, userOptions) => { - const defaultOptions = { cidVersion: 1, hashAlg: exports.defaultHashAlg } - const options = Object.assign(defaultOptions, userOptions) +const cid = async (binaryBlob, userOptions = {}) => { + const options = { + cidVersion: userOptions.cidVersion == null ? 1 : userOptions.cidVersion, + hashAlg: userOptions.hashAlg == null ? defaultHashAlg : userOptions.hashAlg + } - const multihash = await multihashing(binaryBlob, options.hashAlg) - const codecName = multicodec.print[exports.codec] - const cid = new CID(options.cidVersion, codecName, multihash) + const hashName = multihash.codes[options.hashAlg] + const hash = await multihashing(binaryBlob, hashName) + const codecName = multicodec.print[codec] + const cid = new CID(options.cidVersion, codecName, hash) return cid } -exports.cid = cid +module.exports = { + codec, + defaultHashAlg, + cid +} diff --git a/src/index.js b/src/index.js index d08fd17..274533e 100644 --- a/src/index.js +++ b/src/index.js @@ -1,13 +1,24 @@ 'use strict' -exports.DAGNode = require('./dag-node') -exports.DAGLink = require('./dag-link') +const resolver = require('./resolver') +const util = require('./util') +const DAGNode = require('./dag-node/dagNode') +const DAGLink = require('./dag-link/dagLink') -/* - * Functions to fulfil IPLD Format interface - * https://github.com/ipld/interface-ipld-format +/** + * @typedef {import('./types').DAGLinkLike} DAGLinkLike */ -exports.resolver = require('./resolver') -exports.util = require('./util') -exports.codec = exports.util.codec -exports.defaultHashAlg = exports.util.defaultHashAlg + +module.exports = { + DAGNode, + DAGLink, + + /** + * Functions to fulfil IPLD Format interface + * https://github.com/ipld/interface-ipld-format + */ + resolver, + util, + codec: util.codec, + defaultHashAlg: util.defaultHashAlg +} diff --git a/src/resolver.js b/src/resolver.js index a65c568..be6393e 100644 --- a/src/resolver.js +++ b/src/resolver.js @@ -7,23 +7,22 @@ const util = require('./util') /** * Resolves a path within a PB block. * - * Returns the value or a link and the partial mising path. This way the + * If the path resolves half-way to a link, then the `remainderPath` is the part + * after the link that can be used for further resolving + * + * Returns the value or a link and the partial missing path. This way the * IPLD Resolver can fetch the link and continue to resolve. * * @param {Uint8Array} binaryBlob - Binary representation of a PB block * @param {string} [path='/'] - Path that should be resolved - * @returns {Object} result - Result of the path it it was resolved successfully - * @returns {*} result.value - Value the path resolves to - * @returns {string} result.remainderPath - If the path resolves half-way to a - * link, then the `remainderPath` is the part after the link that can be used - * for further resolving */ -exports.resolve = (binaryBlob, path) => { +exports.resolve = (binaryBlob, path = '/') => { let node = util.deserialize(binaryBlob) const parts = path.split('/').filter(Boolean) while (parts.length) { const key = parts.shift() + // @ts-ignore if (node[key] === undefined) { // There might be a matching named link for (const link of node.Links) { @@ -39,6 +38,7 @@ exports.resolve = (binaryBlob, path) => { throw new Error(`Object has no property '${key}'`) } + // @ts-ignore node = node[key] if (CID.isCID(node)) { return { diff --git a/src/serialize.js b/src/serialize.js index f651dac..f6edf56 100644 --- a/src/serialize.js +++ b/src/serialize.js @@ -1,11 +1,25 @@ 'use strict' -const protons = require('protons') -const proto = protons(require('./dag.proto.js')) -const DAGLink = require('./dag-link/dagLink') +const protobuf = require('protobufjs/minimal') +const { + PBLink +} = require('./dag') -exports = module.exports +const { + createDagLinkFromB58EncodedHash +} = require('./dag-link/util') +/** + * @typedef {import('./dag-link/dagLink')} DAGLink + * @typedef {import('./types').DAGLinkLike} DAGLinkLike + * @typedef {import('./types').SerializableDAGNode} SerializableDAGNode + * @typedef {import('cids')} CID + */ + +/** + * @param { { Data?: Uint8Array, Links: (DAGLink | DAGLinkLike)[] }} node + * @returns {SerializableDAGNode} + */ const toProtoBuf = (node) => { const pbn = {} @@ -34,31 +48,54 @@ const toProtoBuf = (node) => { /** * Serialize internal representation into a binary PB block. * - * @param {Object} node - Internal representation of a PB block - * @returns {Uint8Array} - The encoded binary representation + * @param {import('./dag-node/dagNode')} node - Internal representation of a PB block */ const serializeDAGNode = (node) => { - const data = node.Data - const links = node.Links || [] + return encode(toProtoBuf(node)) +} - const serialized = proto.PBNode.encode(toProtoBuf({ +/** + * Serialize an object where the `Links` might not be a `DAGLink` instance yet + * + * @param {Uint8Array} [data] + * @param {(DAGLink | string | DAGLinkLike)[]} [links] + */ +const serializeDAGNodeLike = (data, links = []) => { + const node = { Data: data, - Links: links - })) + Links: links.map((link) => { + return createDagLinkFromB58EncodedHash(link) + }) + } - return serialized + return encode(toProtoBuf(node)) } -// Serialize an object where the `Links` might not be a `DAGLink` instance yet -const serializeDAGNodeLike = (data, links = []) => { - const node = { Data: data } - node.Links = links.map((link) => { - return DAGLink.isDAGLink(link) - ? link - : DAGLink.util.createDagLinkFromB58EncodedHash(link) - }) - return serializeDAGNode(node) +module.exports = { + serializeDAGNode, + serializeDAGNodeLike } -exports.serializeDAGNode = serializeDAGNode -exports.serializeDAGNodeLike = serializeDAGNodeLike +/** + * The fields in PBNode are the wrong way round - `id: 2` comes before + * `id: 1`. protobufjs writes them out in id order but go-IPFS does not so + * we have to use the protobuf.Writer interface directly to get the same + * serialized form as go-IPFS + * + * @param {SerializableDAGNode} pbf + */ +function encode (pbf) { + const writer = protobuf.Writer.create() + + if (pbf.Links != null) { + for (let i = 0; i < pbf.Links.length; i++) { + PBLink.encode(pbf.Links[i], writer.uint32(18).fork()).ldelim() + } + } + + if (pbf.Data != null) { + writer.uint32(10).bytes(pbf.Data) + } + + return writer.finish() +} diff --git a/src/types.d.ts b/src/types.d.ts new file mode 100644 index 0000000..4f63b25 --- /dev/null +++ b/src/types.d.ts @@ -0,0 +1,18 @@ +import CID from 'cids' + +export interface DAGLinkLike { + Hash: CID + Name: string + Tsize: number +} + +export interface SerializableDAGLink { + Hash: Uint8Array + Name: string + Tsize: number +} + +export interface SerializableDAGNode { + Data?: Uint8Array | null + Links?: SerializableDAGLink[] | null +} diff --git a/src/util.js b/src/util.js index 53fcc9d..cd4eb48 100644 --- a/src/util.js +++ b/src/util.js @@ -1,39 +1,35 @@ 'use strict' -const protons = require('protons') -const proto = protons(require('./dag.proto')) +const { + PBNode +} = require('./dag') const DAGLink = require('./dag-link/dagLink') const DAGNode = require('./dag-node/dagNode') -const { serializeDAGNodeLike } = require('./serialize') +const { serializeDAGNode, serializeDAGNodeLike } = require('./serialize') const genCid = require('./genCid') -exports = module.exports - -exports.codec = genCid.codec -exports.defaultHashAlg = genCid.defaultHashAlg +/** + * @typedef {import('./types').DAGLinkLike} DAGLinkLike + */ /** - * Calculate the CID of the binary blob. + * Calculate the CID of the binary blob * - * @param {Object} binaryBlob - Encoded IPLD Node - * @param {Object} [userOptions] - Options to create the CID - * @param {number} [userOptions.cidVersion=1] - CID version number - * @param {string} [UserOptions.hashAlg] - Defaults to the defaultHashAlg of the format - * @returns {Promise.} + * @param {Uint8Array} binaryBlob - Encoded IPLD Node + * @param {import('./genCid').GenCIDOptions} [userOptions] - Options to create the CID */ const cid = (binaryBlob, userOptions) => { return genCid.cid(binaryBlob, userOptions) } /** - * Serialize internal representation into a binary PB block. + * Serialize internal representation into a binary PB block * - * @param {Object} node - Internal representation of a CBOR block - * @returns {Uint8Array} - The encoded binary representation + * @param {DAGNode | { Data?: Uint8Array, Links?: (DAGLink | DAGLinkLike)[]}} node */ const serialize = (node) => { - if (DAGNode.isDAGNode(node)) { - return node.serialize() + if (node instanceof DAGNode) { + return serializeDAGNode(node) } else { return serializeDAGNodeLike(node.Data, node.Links) } @@ -43,12 +39,19 @@ const serialize = (node) => { * Deserialize PB block into the internal representation. * * @param {Uint8Array} buffer - Binary representation of a PB block - * @returns {Object} - An object that conforms to the IPLD Data Model */ const deserialize = (buffer) => { - const pbn = proto.PBNode.decode(buffer) + const message = PBNode.decode(buffer) + const pbn = PBNode.toObject(message, { + defaults: false, + arrays: true, + longs: Number, + objects: false + }) - const links = pbn.Links.map((link) => { + /** @type {DAGLink[]} */ + const links = pbn.Links.map((/** @type {DAGLinkLike} */ link) => { + // @ts-ignore return new DAGLink(link.Name, link.Tsize, link.Hash) }) @@ -57,6 +60,10 @@ const deserialize = (buffer) => { return new DAGNode(data, links, buffer.byteLength) } -exports.serialize = serialize -exports.deserialize = deserialize -exports.cid = cid +module.exports = { + codec: genCid.codec, + defaultHashAlg: genCid.defaultHashAlg, + serialize, + deserialize, + cid +} diff --git a/test/dag-link-test.spec.js b/test/dag-link-test.spec.js index d989b63..3cb9f6b 100644 --- a/test/dag-link-test.spec.js +++ b/test/dag-link-test.spec.js @@ -31,6 +31,7 @@ describe('DAGLink', () => { it('fail to create without multihash', () => { expect(() => { + // @ts-expect-error cid is required const link = new DAGLink('hello', 3) expect(link).to.not.exist() }).to.throw() @@ -58,9 +59,4 @@ describe('DAGLink', () => { const link = new DAGLink('hello', 3, cid) expect(link.Hash.toBaseEncodedString()).to.equal(cid) }) - - it('has an immutable CID', () => { - const link = new DAGLink('hello', 3, 'QmXg9Pp2ytZ14xgmQjYEiHjVjMFXzCVVEcRTWJBmLgR39U') - expect(() => { link.Hash = 'foo' }).to.throw(/read.only/) - }) }) diff --git a/test/dag-node-test.spec.js b/test/dag-node-test.spec.js index 6136f80..9b2b567 100644 --- a/test/dag-node-test.spec.js +++ b/test/dag-node-test.spec.js @@ -5,14 +5,16 @@ const chai = require('aegir/utils/chai') const expect = chai.expect const dagPB = require('../src') -const DAGLink = dagPB.DAGLink -const DAGNode = dagPB.DAGNode -const multihash = require('multihashes') -const multicodec = require('multicodec') +const DAGLink = require('../src/dag-link/dagLink') +const DAGNode = require('../src/dag-node/dagNode') const multihashing = require('multihashing-async') +const { + multihash +} = multihashing const CID = require('cids') const multibase = require('multibase') +// @ts-ignore const loadFixture = require('aegir/fixtures') const uint8ArrayFromString = require('uint8arrays/from-string') @@ -153,6 +155,7 @@ describe('DAGNode', () => { const deserialized = dagPB.util.deserialize(serialized) for (const key of Object.keys(node)) { if (key !== '_serializedSize') { + // @ts-ignore expect(node[key]).to.deep.equal(deserialized[key]) } } @@ -171,10 +174,16 @@ describe('DAGNode', () => { }) it('fail to create a node with other data types', () => { - expect(() => new DAGNode({})).to.throw( + expect(() => { + // @ts-ignore invalid constructor args + return new DAGNode({}) + }).to.throw( 'Passed \'data\' is not a Uint8Array or a String!' ) - expect(() => new DAGNode([])).to.throw( + expect(() => { + // @ts-ignore invalid constructor args + return new DAGNode([]) + }).to.throw( 'Passed \'data\' is not a Uint8Array or a String!' ) }) @@ -203,6 +212,7 @@ describe('DAGNode', () => { const node2 = new DAGNode(uint8ArrayFromString('2')) const link = await node2.toDAGLink() const linkObject = link.toJSON() + // @ts-ignore node1.addLink(linkObject) expect(node1.Links.length).to.equal(1) expect(node1.Links[0].Tsize).to.eql(node2.size) @@ -241,7 +251,7 @@ describe('DAGNode', () => { expect(source.Links.length).to.equal(1) - const target = new DAGNode(null, [], 0) + const target = new DAGNode('', [], 0) target.addLink(source.Links[0]) expect(target.Links.length).to.equal(1) @@ -304,7 +314,7 @@ describe('DAGNode', () => { it('get node CID with hashAlg', async () => { const node = new DAGNode(uint8ArrayFromString('some data')) const serialized = dagPB.util.serialize(node) - const cid = await dagPB.util.cid(serialized, { hashAlg: multicodec.SHA2_512 }) + const cid = await dagPB.util.cid(serialized, { hashAlg: multihash.names['sha2-512'] }) expect(cid.multihash).to.exist() expect(cid.codec).to.equal('dag-pb') expect(cid.version).to.equal(1) @@ -315,12 +325,19 @@ describe('DAGNode', () => { it('node size updates with mutation', async () => { // see pbcross.go for the source of the sizes and CIDs here + /** + * @param {DAGNode} node + */ async function cid (node) { const serialized = dagPB.util.serialize(node) const cid = await dagPB.util.cid(serialized, { cidVersion: 0 }) return cid.toBaseEncodedString() } + /** + * + * @param {string} str + */ async function rawBlockCid (str) { const raw = uint8ArrayFromString(str) const rawHash = await multihashing(raw, 'sha2-256') @@ -350,7 +367,7 @@ describe('DAGNode', () => { // pnd1 // links by constructor and addLink should yield the same node - const pnd1ByConstructor = new DAGNode(null, [cat]) + const pnd1ByConstructor = new DAGNode(undefined, [cat]) expect(pnd1ByConstructor.size).to.equal(51) expect(await cid(pnd1ByConstructor)).to.equal('QmdwjhxpxzcMsR3qUuj7vUL8pbA7MgR3GAxWi2GLHjsKCT') @@ -360,7 +377,7 @@ describe('DAGNode', () => { // pnd2 const pnd1Link = await pnd1.toDAGLink({ name: 'first', cidVersion: 0 }) - const pnd2ByConstructor = new DAGNode(null, [pnd1Link, dog]) + const pnd2ByConstructor = new DAGNode(undefined, [pnd1Link, dog]) expect(pnd2ByConstructor.size).to.equal(149) expect(await cid(pnd2ByConstructor)).to.equal('QmWXZxVQ9yZfhQxLD35eDR8LiMRsYtHxYqTFCBbJoiJVys') @@ -371,7 +388,7 @@ describe('DAGNode', () => { // pnd3 const pnd2Link = await pnd2.toDAGLink({ name: 'second', cidVersion: 0 }) - const pnd3ByConstructor = new DAGNode(null, [pnd2Link, bear]) + const pnd3ByConstructor = new DAGNode(undefined, [pnd2Link, bear]) expect(pnd3ByConstructor.size).to.equal(250) expect(await cid(pnd3ByConstructor)).to.equal('QmNX6Tffavsya4xgBi2VJQnSuqy9GsxongxZZ9uZBqp16d') @@ -485,13 +502,13 @@ describe('DAGNode', () => { const l1 = { Name: '', Hash: 'QmbAmuwox51c91FmC2jEX5Ng4zS4HyVgpA5GNPBF5QsWMA', - Size: 57806 + Tsize: 57806 } const l2 = { Name: '', Hash: 'QmP7SrR76KHK9A916RbHG1ufy2TzNABZgiE23PjZDMzZXy', - Size: 262158 + Tsize: 262158 } const link1 = new DAGLink( l1.Name, @@ -538,10 +555,38 @@ describe('DAGNode', () => { it('creates links from objects with .Size properties', () => { const node = new DAGNode(uint8ArrayFromString('some data'), [{ + // @ts-ignore Hash: 'QmUxD5gZfKzm8UN4WaguAMAZjw2TzZ2ZUmcqm2qXPtais7', Size: 9001 }]) expect(node.Links[0].Tsize).to.eql(9001) }) + + it('serializing should set _serializedSize', () => { + const node = new DAGNode('hello') + + expect(node._serializedSize).to.be.null() + + node.serialize() + + expect(node._serializedSize).to.be.greaterThan(0) + }) + + it('removes dag link by uint8array CID', () => { + const cid = new CID('QmUxD5gZfKzm8UN4WaguAMAZjw2TzZ2ZUmcqm2qXPtais7') + + const node = new DAGNode('hello world', [{ + Hash: cid, + Tsize: 5, + Name: '' + }]) + + expect(node).to.have.property('Links').that.has.lengthOf(1) + + // @ts-ignore only supposed to support CIDs or strings or DAGLinks + node.rmLink(cid.bytes) + + expect(node).to.have.property('Links').that.has.lengthOf(0) + }) }) diff --git a/test/resolver.spec.js b/test/resolver.spec.js index a2ca6e6..5177f9f 100644 --- a/test/resolver.spec.js +++ b/test/resolver.spec.js @@ -10,6 +10,11 @@ const { DAGNode, resolver } = require('../src') const utils = require('../src/util') const uint8ArrayFromString = require('uint8arrays/from-string') +/** + * @typedef {import('../src/dag-link/dagLink')} DAGLink + * @typedef {import('../src/types').DAGLinkLike} DAGLinkLike + */ + describe('IPLD Format resolver (local)', () => { const links = [{ Name: '', @@ -21,11 +26,19 @@ describe('IPLD Format resolver (local)', () => { Tsize: 8 }] + /** + * @param {Uint8Array} data + * @param {(DAGLink | DAGLinkLike)[]} links + */ const create = (data, links) => { const node = new DAGNode(data, links) return utils.serialize(node) } + /** + * @param {Uint8Array} data + * @param {(DAGLink | DAGLinkLike)[]} links + */ const createPlain = (data, links) => { const node = { Data: data, @@ -35,53 +48,53 @@ describe('IPLD Format resolver (local)', () => { } const emptyNodeBlobs = [ - ['DAGNode', create(new Uint8Array(), [])], - ['{data:Uint8Array}', createPlain(new Uint8Array(), [])] + { kind: 'DAGNode', blob: create(new Uint8Array(), []) }, + { kind: '{data:Uint8Array}', blob: createPlain(new Uint8Array(), []) } ] const linksNodeBlobs = [ - ['DAGNode', create(new Uint8Array(), links)], - ['{data:Uint8Array}', createPlain(new Uint8Array(), links)] + { kind: 'DAGNode', blob: create(new Uint8Array(), links) }, + { kind: '{data:Uint8Array}', blob: createPlain(new Uint8Array(), links) } ] const dataLinksNodeBlobs = [ - ['DAGNode', create(uint8ArrayFromString('aaah the data'), links)], - ['{data:Uint8Array}', createPlain(uint8ArrayFromString('aaah the data'), links)] + { kind: 'DAGNode', blob: create(uint8ArrayFromString('aaah the data'), links) }, + { kind: '{data:Uint8Array}', blob: createPlain(uint8ArrayFromString('aaah the data'), links) } ] - for (const [kind, emptyNodeBlob] of emptyNodeBlobs) { + for (const { kind, blob } of emptyNodeBlobs) { describe(`empty node (${kind})`, () => { describe('resolver.resolve', () => { it('links path', () => { - const result = resolver.resolve(emptyNodeBlob, 'Links') - expect(result.value).to.eql([]) - expect(result.remainderPath).to.eql('') + const result = resolver.resolve(blob, 'Links') + expect(result).to.have.deep.property('value', []) + expect(result).to.have.property('remainderPath', '') }) it('data path', () => { - const result = resolver.resolve(emptyNodeBlob, 'Data') - expect(result.value).that.is.an.instanceOf(Uint8Array).with.lengthOf(0) - expect(result.remainderPath).to.eql('') + const result = resolver.resolve(blob, 'Data') + expect(result).to.have.property('value').that.is.an.instanceOf(Uint8Array).with.lengthOf(0) + expect(result).to.have.property('remainderPath', '') }) it('non existent path', () => { expect(() => - resolver.resolve(emptyNodeBlob, 'pathThatDoesNotExist') + resolver.resolve(blob, 'pathThatDoesNotExist') ).to.throw( "Object has no property 'pathThatDoesNotExist'" ) }) it('empty path', () => { - const result = resolver.resolve(emptyNodeBlob, '') - expect(result.value.Data).to.be.an.instanceOf(Uint8Array).with.lengthOf(0) - expect(result.value.Links).to.eql([]) - expect(result.remainderPath).to.eql('') + const result = resolver.resolve(blob, '') + expect(result).to.have.nested.property('value.Data').that.is.an.instanceOf(Uint8Array).with.lengthOf(0) + expect(result).to.have.deep.nested.property('value.Links', []) + expect(result).to.have.property('remainderPath', '') }) }) it('resolver.tree', () => { - const tree = resolver.tree(emptyNodeBlob) + const tree = resolver.tree(blob) const paths = [...tree] expect(paths).to.have.members([ 'Links', @@ -91,82 +104,74 @@ describe('IPLD Format resolver (local)', () => { }) } - for (const [kind, linksNodeBlob] of linksNodeBlobs) { + for (const { kind, blob } of linksNodeBlobs) { describe(`links node ${kind}`, () => { describe('resolver.resolve', () => { it('links path', () => { - const result = resolver.resolve(linksNodeBlob, 'Links') - expect(result.value).to.containSubset(links) - expect(result.remainderPath).to.eql('') + const result = resolver.resolve(blob, 'Links') + expect(result).to.have.property('value').that.containSubset(links) + expect(result).to.have.property('remainderPath', '') }) it('links position path Hash', () => { - const result = resolver.resolve(linksNodeBlob, 'Links/1/Hash') - expect(result.value).to.eql(links[1].Hash) - expect(result.remainderPath).to.eql('') + const result = resolver.resolve(blob, 'Links/1/Hash') + expect(result).to.have.deep.property('value', links[1].Hash) + expect(result).to.have.property('remainderPath', '') }) it('links position path Name', () => { - const result = resolver.resolve(linksNodeBlob, 'Links/1/Name') - expect(result.value).to.eql(links[1].Name) - expect(result.remainderPath).to.eql('') + const result = resolver.resolve(blob, 'Links/1/Name') + expect(result).to.have.property('value', links[1].Name) + expect(result).to.have.property('remainderPath', '') }) it('links position path Tsize', () => { - const result = resolver.resolve(linksNodeBlob, 'Links/1/Tsize') - expect(result.value).to.eql(links[1].Tsize) - expect(result.remainderPath).to.eql('') + const result = resolver.resolve(blob, 'Links/1/Tsize') + expect(result).to.have.property('value', links[1].Tsize) + expect(result).to.have.property('remainderPath', '') }) it('links by name', () => { - const result = resolver.resolve(linksNodeBlob, 'named link') - expect(result.value.equals(links[1].Hash)).to.be.true() - expect(result.remainderPath).to.eql('') + const result = resolver.resolve(blob, 'named link') + expect(result).to.have.deep.property('value', links[1].Hash) + expect(result).to.have.property('remainderPath', '') }) it('missing link by name', () => { expect(() => - resolver.resolve(linksNodeBlob, 'missing link') + resolver.resolve(blob, 'missing link') ).to.throw( "Object has no property 'missing link'" ) }) it('yield remainderPath if impossible to resolve through (a)', () => { - const result = resolver.resolve(linksNodeBlob, 'Links/1/Hash/Data') - expect(result.value.equals( - new CID('QmXg9Pp2ytZ14xgmQjYEiHjVjMFXzCVVEcRTWJBmLgR39V') - )).to.be.true() - expect(result.remainderPath).to.equal('Data') + const result = resolver.resolve(blob, 'Links/1/Hash/Data') + expect(result).to.have.deep.property('value', new CID('QmXg9Pp2ytZ14xgmQjYEiHjVjMFXzCVVEcRTWJBmLgR39V')) + expect(result).to.have.property('remainderPath', 'Data') }) it('yield remainderPath if impossible to resolve through (b)', () => { - const result = resolver.resolve(linksNodeBlob, 'Links/1/Hash/Links/0/Hash/Data') - expect(result.value.equals( - new CID('QmXg9Pp2ytZ14xgmQjYEiHjVjMFXzCVVEcRTWJBmLgR39V') - )).to.be.true() - expect(result.remainderPath).to.equal('Links/0/Hash/Data') + const result = resolver.resolve(blob, 'Links/1/Hash/Links/0/Hash/Data') + expect(result).to.have.deep.property('value', new CID('QmXg9Pp2ytZ14xgmQjYEiHjVjMFXzCVVEcRTWJBmLgR39V')) + expect(result).to.have.property('remainderPath', 'Links/0/Hash/Data') }) it('yield remainderPath if impossible to resolve through named link (a)', () => { - const result = resolver.resolve(linksNodeBlob, 'named link/Data') - expect(result.value.equals( - new CID('QmXg9Pp2ytZ14xgmQjYEiHjVjMFXzCVVEcRTWJBmLgR39V') - )).to.be.true() - expect(result.remainderPath).to.equal('Data') + const result = resolver.resolve(blob, 'named link/Data') + expect(result).to.have.deep.property('value', new CID('QmXg9Pp2ytZ14xgmQjYEiHjVjMFXzCVVEcRTWJBmLgR39V')) + expect(result).to.have.property('remainderPath', 'Data') }) it('yield remainderPath if impossible to resolve through named link (b)', () => { - const result = resolver.resolve(linksNodeBlob, 'named link/Links/0/Hash/Data') - expect(result.value.equals( - new CID('QmXg9Pp2ytZ14xgmQjYEiHjVjMFXzCVVEcRTWJBmLgR39V') - )).to.be.true() - expect(result.remainderPath).to.equal('Links/0/Hash/Data') + const result = resolver.resolve(blob, 'named link/Links/0/Hash/Data') + expect(result).to.have.deep.property('value', new CID('QmXg9Pp2ytZ14xgmQjYEiHjVjMFXzCVVEcRTWJBmLgR39V')) + expect(result).to.have.property('remainderPath', 'Links/0/Hash/Data') }) }) it('resolver.tree', () => { - const tree = resolver.tree(linksNodeBlob) + const tree = resolver.tree(blob) const paths = [...tree] expect(paths).to.have.members([ 'Links', @@ -184,39 +189,39 @@ describe('IPLD Format resolver (local)', () => { }) } - for (const [kind, dataLinksNodeBlob] of dataLinksNodeBlobs) { + for (const { kind, blob } of dataLinksNodeBlobs) { describe(`links and data node (${kind})`, () => { describe('resolver.resolve', () => { it('links path', () => { - const result = resolver.resolve(dataLinksNodeBlob, 'Links') + const result = resolver.resolve(blob, 'Links') expect(result.value).to.containSubset(links) expect(result.remainderPath).to.eql('') }) it('data path', () => { - const result = resolver.resolve(dataLinksNodeBlob, 'Data') + const result = resolver.resolve(blob, 'Data') expect(result.value).to.eql(uint8ArrayFromString('aaah the data')) expect(result.remainderPath).to.eql('') }) it('non existent path', () => { expect(() => - resolver.resolve(dataLinksNodeBlob, 'pathThatDoesNotExist') + resolver.resolve(blob, 'pathThatDoesNotExist') ).to.throw( "Object has no property 'pathThatDoesNotExist'" ) }) it('empty path', () => { - const result = resolver.resolve(dataLinksNodeBlob, '') - expect(result.value.Data).to.eql(uint8ArrayFromString('aaah the data')) - expect(result.value.Links).to.containSubset(links) + const result = resolver.resolve(blob, '') + expect(result).to.have.deep.nested.property('value.Data', uint8ArrayFromString('aaah the data')) + expect(result).to.have.nested.property('value.Links').that.containSubset(links) expect(result.remainderPath).to.eql('') }) }) it('resolver.tree', () => { - const tree = resolver.tree(dataLinksNodeBlob) + const tree = resolver.tree(blob) const paths = [...tree] expect(paths).to.have.members([ 'Links', diff --git a/test/util.spec.js b/test/util.spec.js index e2f3abd..7b9e23b 100644 --- a/test/util.spec.js +++ b/test/util.spec.js @@ -5,13 +5,17 @@ const CID = require('cids') const chai = require('aegir/utils/chai') const expect = chai.expect +const { + multihash +} = require('multihashing-async') const { DAGLink } = require('../src') const { serialize, - deserialize + deserialize, + cid } = require('../src/util') describe('util', () => { @@ -68,7 +72,21 @@ describe('util', () => { }) it('should ignore invalid properties when serializing', () => { + // @ts-ignore invalid properties const result = serialize({ foo: 'bar' }) expect(result).to.be.empty() }) + + describe('cid', () => { + it('should allow the identity hash', async () => { + const buffer = serialize({ Data: new Uint8Array(0), Links: [] }) + const id = await cid(buffer, { + hashAlg: multihash.names.identity + }) + + const result = multihash.decode(id.multihash) + + expect(result).to.have.property('name', 'identity') + }) + }) }) diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..720d4ec --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "aegir/src/config/tsconfig.aegir.json", + "compilerOptions": { + "outDir": "dist" + }, + "include": [ + "test", + "src" + ], + "exclude": [ + "src/dag.js" // exclude generated file + ] +}