Skip to content
This repository has been archived by the owner on Feb 12, 2024. It is now read-only.

Commit

Permalink
refactor: return promise from dag resolve (#3152)
Browse files Browse the repository at this point in the history
Makes `ipfs.dag.resolve` behave the same when calling into core and over the http api.

Adds documentation and interface tests for `ipfs.dag.resolve`.

Supersedes #3131
Fixes #2962

BREAKING CHANGES:

- `ipfs.dag.resolve` returns `Promise<{ cid, remainderPath }` instead of `AsyncIterator<{ value, remainderPath }>`
  - Previously the core api returned an async iterator and the http client returned a simple promise

Co-authored-by: Tarun Batra <[email protected]>
  • Loading branch information
achingbrain and tarunbatra authored Jul 9, 2020
1 parent 585a142 commit f20cdf1
Show file tree
Hide file tree
Showing 11 changed files with 269 additions and 184 deletions.
54 changes: 54 additions & 0 deletions docs/core-api/DAG.md
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,60 @@ console.log(result)

A great source of [examples][] can be found in the tests for this API.

## `ipfs.dag.resolve(ipfsPath, [options])`

> Returns the CID and remaining path of the node at the end of the passed IPFS path
### Parameters

| Name | Type | Description |
| ---- | ---- | ----------- |
| ipfsPath | `String` or [CID][] | An IPFS path, e.g. `/ipfs/bafy/dir/file.txt` or a [CID][] instance |

### Options

An optional object which may have the following keys:

| Name | Type | Default | Description |
| ---- | ---- | ------- | ----------- |
| path | `String` | `undefined` | If `ipfsPath` is a [CID][], you may pass a path here |
| timeout | `Number` | `undefined` | A timeout in ms |
| signal | [AbortSignal][] | `undefined` | Can be used to cancel any long running requests started as a result of this call |

### Returns

| Type | Description |
| -------- | -------- |
| `Promise<{ cid: CID, remainderPath: String }>` | The last CID encountered during the traversal and the path to the end of the IPFS path inside the node referenced by the CID |

### Example

```JavaScript
// example obj
const obj = {
a: 1,
b: [1, 2, 3],
c: {
ca: [5, 6, 7],
cb: 'foo'
}
}

const cid = await ipfs.dag.put(obj, { format: 'dag-cbor', hashAlg: 'sha2-256' })
console.log(cid.toString())
// bafyreicyer3d34cutdzlsbe2nqu5ye62mesuhwkcnl2ypdwpccrsecfmjq

const result = await ipfs.dag.resolve(`${cid}/c/cb`)
console.log(result)
// Logs:
// {
// cid: CID(bafyreicyer3d34cutdzlsbe2nqu5ye62mesuhwkcnl2ypdwpccrsecfmjq),
// remainderPath: 'c/cb'
// }
```

A great source of [examples][] can be found in the tests for this API.


[examples]: https://github.com/ipfs/js-ipfs/blob/master/packages/interface-ipfs-core/src/dag
[cid]: https://www.npmjs.com/package/cids
Expand Down
1 change: 1 addition & 0 deletions packages/interface-ipfs-core/src/dag/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ const { createSuite } = require('../utils/suite')
const tests = {
get: require('./get'),
put: require('./put'),
resolve: require('./resolve'),
tree: require('./tree')
}

Expand Down
130 changes: 130 additions & 0 deletions packages/interface-ipfs-core/src/dag/resolve.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
/* eslint-env mocha */
'use strict'

const { Buffer } = require('buffer')
const dagPB = require('ipld-dag-pb')
const DAGNode = dagPB.DAGNode
const { getDescribe, getIt, expect } = require('../utils/mocha')
const testTimeout = require('../utils/test-timeout')

/** @typedef { import("ipfsd-ctl/src/factory") } Factory */
/**
* @param {Factory} common
* @param {Object} options
*/
module.exports = (common, options) => {
const describe = getDescribe(options)
const it = getIt(options)

describe('.dag.resolve', () => {
let ipfs
before(async () => { ipfs = (await common.spawn()).api })

after(() => common.clean())

it('should respect timeout option when resolving a path within a DAG node', async () => {
const cid = await ipfs.dag.put({}, { format: 'dag-cbor', hashAlg: 'sha2-256' })

return testTimeout(() => ipfs.dag.resolve(cid, {
timeout: 1
}))
})

it('should resolve a path inside a cbor node', async () => {
const obj = {
a: 1,
b: [1, 2, 3],
c: {
ca: [5, 6, 7],
cb: 'foo'
}
}

const cid = await ipfs.dag.put(obj, { format: 'dag-cbor', hashAlg: 'sha2-256' })

const result = await ipfs.dag.resolve(`${cid}/c/cb`)
expect(result).to.have.deep.property('cid', cid)
expect(result).to.have.property('remainderPath', 'c/cb')
})

it('should resolve a path inside a cbor node by CID', async () => {
const obj = {
a: 1,
b: [1, 2, 3],
c: {
ca: [5, 6, 7],
cb: 'foo'
}
}

const cid = await ipfs.dag.put(obj, { format: 'dag-cbor', hashAlg: 'sha2-256' })

const result = await ipfs.dag.resolve(cid, { path: '/c/cb' })
expect(result).to.have.deep.property('cid', cid)
expect(result).to.have.property('remainderPath', 'c/cb')
})

it('should resolve a multi-node path inside a cbor node', async () => {
const obj0 = {
ca: [5, 6, 7],
cb: 'foo'
}
const cid0 = await ipfs.dag.put(obj0, { format: 'dag-cbor', hashAlg: 'sha2-256' })

const obj1 = {
a: 1,
b: [1, 2, 3],
c: cid0
}

const cid1 = await ipfs.dag.put(obj1, { format: 'dag-cbor', hashAlg: 'sha2-256' })

const result = await ipfs.dag.resolve(`/ipfs/${cid1}/c/cb`)
expect(result).to.have.deep.property('cid', cid0)
expect(result).to.have.property('remainderPath', 'cb')
})

it('should resolve a multi-node path inside a cbor node by CID', async () => {
const obj0 = {
ca: [5, 6, 7],
cb: 'foo'
}
const cid0 = await ipfs.dag.put(obj0, { format: 'dag-cbor', hashAlg: 'sha2-256' })

const obj1 = {
a: 1,
b: [1, 2, 3],
c: cid0
}

const cid1 = await ipfs.dag.put(obj1, { format: 'dag-cbor', hashAlg: 'sha2-256' })

const result = await ipfs.dag.resolve(cid1, { path: '/c/cb' })
expect(result).to.have.deep.property('cid', cid0)
expect(result).to.have.property('remainderPath', 'cb')
})

it('should resolve a raw node', async () => {
const node = Buffer.from(['hello world'])
const cid = await ipfs.dag.put(node, { format: 'raw', hashAlg: 'sha2-256' })

const result = await ipfs.dag.resolve(cid, { path: '/' })
expect(result).to.have.deep.property('cid', cid)
expect(result).to.have.property('remainderPath', '')
})

it('should resolve a path inside a dag-pb node linked to from another dag-pb node', async () => {
const someData = Buffer.from('some other data')
const childNode = new DAGNode(someData)
const childCid = await ipfs.dag.put(childNode, { format: 'dag-pb', hashAlg: 'sha2-256' })

const linkToChildNode = await childNode.toDAGLink({ name: 'foo', cidVersion: 0 })
const parentNode = new DAGNode(Buffer.from('derp'), [linkToChildNode])
const parentCid = await ipfs.dag.put(parentNode, { format: 'dag-pb', hashAlg: 'sha2-256' })

const result = await ipfs.dag.resolve(parentCid, { path: '/foo' })
expect(result).to.have.deep.property('cid', childCid)
expect(result).to.have.property('remainderPath', '')
})
})
}
4 changes: 2 additions & 2 deletions packages/ipfs-http-client/src/dag/get.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,9 @@ module.exports = configure((api, options) => {
}

if (block.cid.codec === 'raw' && !resolved.remPath) {
resolved.remPath = '/'
resolved.remainderPath = '/'
}

return dagResolver.resolve(block.data, resolved.remPath)
return dagResolver.resolve(block.data, resolved.remainderPath)
}
})
6 changes: 3 additions & 3 deletions packages/ipfs-http-client/src/dag/resolve.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,19 @@ const configure = require('../lib/configure')
const toUrlSearchParams = require('../lib/to-url-search-params')

module.exports = configure(api => {
return async (cid, options = {}) => {
return async (ipfsPath, options = {}) => {
const res = await api.post('dag/resolve', {
timeout: options.timeout,
signal: options.signal,
searchParams: toUrlSearchParams({
arg: `${cid}${options.path ? `/${options.path}`.replace(/\/[/]+/g, '/') : ''}`,
arg: `${ipfsPath}${options.path ? `/${options.path}`.replace(/\/[/]+/g, '/') : ''}`,
...options
}),
headers: options.headers
})

const data = await res.json()

return { cid: new CID(data.Cid['/']), remPath: data.RemPath }
return { cid: new CID(data.Cid['/']), remainderPath: data.RemPath }
}
})
11 changes: 3 additions & 8 deletions packages/ipfs/src/cli/commands/dag/resolve.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
'use strict'

const CID = require('cids')
const parseDuration = require('parse-duration').default

module.exports = {
Expand All @@ -24,13 +23,9 @@ module.exports = {
}

try {
let lastCid

for await (const res of ipfs.dag.resolve(ref, options)) {
if (CID.isCID(res.value)) {
lastCid = res.value
}
}
let {
cid: lastCid
} = await ipfs.dag.resolve(ref, options)

if (!lastCid) {
if (ref.startsWith('/ipfs/')) {
Expand Down
41 changes: 37 additions & 4 deletions packages/ipfs/src/core/components/dag/resolve.js
Original file line number Diff line number Diff line change
@@ -1,23 +1,56 @@
'use strict'

const CID = require('cids')
const { withTimeoutOption } = require('../../utils')
const toCidAndPath = require('ipfs-core-utils/src/to-cid-and-path')

module.exports = ({ ipld, preload }) => {
return withTimeoutOption(async function * resolve (ipfsPath, options) { // eslint-disable-line require-await
return withTimeoutOption(async function resolve (ipfsPath, options = {}) {
const {
cid,
path
} = toCidAndPath(ipfsPath)

if (options.preload !== false) {
preload(cid)
}

if (path) {
options.path = path
}

if (options.preload !== false) {
preload(cid)
let lastCid = cid
let lastRemainderPath = options.path || ''

if (lastRemainderPath.startsWith('/')) {
lastRemainderPath = lastRemainderPath.substring(1)
}

yield * ipld.resolve(cid, options.path, { signal: options.signal })
if (options.path) {
try {
for await (const { value, remainderPath } of ipld.resolve(cid, options.path, {
signal: options.signal
})) {
if (!CID.isCID(value)) {
break
}

lastRemainderPath = remainderPath
lastCid = value
}
} catch (err) {
// TODO: add error codes to IPLD
if (err.message.startsWith('Object has no property')) {
err.message = `no link named "${lastRemainderPath.split('/')[0]}" under ${lastCid}`
err.code = 'ERR_NO_LINK'
}
throw err
}
}

return {
cid: lastCid,
remainderPath: lastRemainderPath || ''
}
})
}
Loading

0 comments on commit f20cdf1

Please sign in to comment.