-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #1 from RangerMauve/initial
Initial version with tests
- Loading branch information
Showing
4 changed files
with
426 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1,46 @@ | ||
# js-ipld-url-resolve | ||
Resolver for IPLD URLs based on the js-IPFS DAG API. supports advanced features like schemas and escaping | ||
|
||
## Features: | ||
|
||
- Traverse IPLD URLs | ||
- Support special character escaping in path segments (e.g. '/') | ||
- Support for IPLD URL path segment parameter syntax `using ;` | ||
- Support IPLD Schemas as lenses during traversal | ||
- Resolve Links during traversal of schemas | ||
- [x] Struct fields | ||
- [x] Map values | ||
- [x] List values | ||
- [ ] Union types | ||
- [ ] Links deeply nested within structs/maps | ||
- ADL Registry for `schema` parameter to convert nodes | ||
|
||
## API | ||
|
||
```javascript | ||
import IPLDURLSystem from 'js-ipld-url-resolve' | ||
|
||
// You map provide an optional map of ADLs to use | ||
const adls = new Map() | ||
|
||
// This ADL will asynchronously stringify any data into a JSON string | ||
// It's kinda useless, but you can make ADLs that return any JS object | ||
// Whose properties can be getters that reuturn Promises that will get | ||
// Automatically awaited during traversal. | ||
// The `parameters` are the IPLDURL parameters for that node's segment | ||
// Parameters might also be coming from the querystring if it's the root | ||
adls.set('example', async (node, parameters, system) => JSON.stringify(node)) | ||
|
||
async function getNode(cid) { | ||
const {value} = ipfs.dag.get(cid) | ||
return value | ||
} | ||
|
||
const system = new IPLDURLSystem({ | ||
getNode, | ||
adls | ||
}) | ||
|
||
// Resolve some data from an IPLD URL | ||
const data = await system.resolve('ipld://some_cid/some_path;schema=schema_cid;type=SchemaTypeName/plainpath/?adl=example') | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,163 @@ | ||
import { IPLDURL } from 'js-ipld-url' | ||
import { create as createTyped } from '@ipld/schema/typed.js' | ||
import { toDSL } from '@ipld/schema/to-dsl.js' | ||
import { CID } from 'multiformats/cid' | ||
import printify from '@ipld/printify' | ||
import { base32 } from 'multiformats/bases/base32' | ||
import { base36 } from 'multiformats/bases/base36' | ||
|
||
export const DEFAULT_CID_BASES = base32.decoder.or(base36.decoder) | ||
|
||
export default class IPLDURLSystem { | ||
constructor ({ | ||
getNode, | ||
adls = new Map(), | ||
cidBases = DEFAULT_CID_BASES | ||
}) { | ||
if (!getNode) throw new TypeError('Must provide a getNode function') | ||
this.getNode = getNode | ||
this.adls = adls | ||
this.cidBases = cidBases | ||
} | ||
|
||
async resolve (url) { | ||
const { hostname: root, segments, searchParams } = new IPLDURL(url) | ||
|
||
const cid = CID.parse(root, this.cidBases).toV1() | ||
let data = await this.getNode(cid) | ||
|
||
const initialParameters = {} | ||
let shouldProcessRoot = false | ||
if (searchParams.has('schema')) { | ||
shouldProcessRoot = true | ||
initialParameters.schema = searchParams.get('schema') | ||
initialParameters.type = searchParams.get('type') | ||
} | ||
if (searchParams.has('adl')) { | ||
// TODO Should other parameters be passed to the ADL function? | ||
shouldProcessRoot = true | ||
initialParameters.adl = searchParams.get('adl') | ||
} | ||
if (shouldProcessRoot) { | ||
data = await this.#applyParameters(data, initialParameters) | ||
} | ||
|
||
for (const { name, parameters } of segments) { | ||
// This does enables ADLs to return promises for properties | ||
data = await data[name] | ||
const asCID = CID.asCID(data) | ||
if (asCID) { | ||
data = await this.getNode(asCID) | ||
} | ||
data = await this.#applyParameters(data, parameters) | ||
} | ||
|
||
return data | ||
} | ||
|
||
async #applyParameters (origin, { schema, adl, ...parameters }) { | ||
let data = origin | ||
|
||
const asCID = CID.asCID(data) | ||
if (asCID) { | ||
data = await this.getNode(asCID) | ||
} | ||
|
||
if (schema) { | ||
data = await SchemaADL(data, { schema, ...parameters }, this) | ||
} | ||
|
||
if (adl) { | ||
if (!this.adls.has(adl)) { | ||
const known = [...this.adls.keys()].join(', ') | ||
throw new Error(`Unknown ADL type ${adl}. Must be one of ${known}`) | ||
} | ||
data = await this.adls.get(adl)(data, parameters, this) | ||
} | ||
|
||
return data | ||
} | ||
} | ||
|
||
export async function SchemaADL (node, { schema, type }, system) { | ||
if (!schema || !type) { | ||
throw new TypeError('Must specify which type to use with the schema parameter') | ||
} | ||
|
||
const schemaCID = CID.parse(schema, system.cidBases) | ||
const schemaDMT = await system.getNode(schemaCID) | ||
const converted = makeTyped(node, schemaDMT, type, system) | ||
|
||
return converted | ||
} | ||
|
||
function makeTyped (node, schemaDMT, type, system) { | ||
const typedSchema = createTyped(schemaDMT, type) | ||
const converted = typedSchema.toTyped(node) | ||
|
||
if (!converted) { | ||
const dataView = printify(node) | ||
const schemaDSL = toDSL(schemaDMT) | ||
throw new Error(`Data did not match schema\nData: ${dataView}\nSchema: ${schemaDSL}`) | ||
} | ||
|
||
const typeDMT = schemaDMT.types[type] | ||
|
||
// TODO: Account for union types and deeply nested links in structs | ||
if (typeDMT.struct) { | ||
const trapped = new Proxy(converted, { | ||
get (target, property) { | ||
const value = target[property] | ||
const propertySchema = typeDMT.struct.fields[property] | ||
const expectedType = propertySchema?.type?.link?.expectedType | ||
if (!expectedType) return value | ||
const asCID = CID.asCID(value) | ||
if (!asCID) return value | ||
return system.getNode(asCID).then((resolved) => { | ||
return makeTyped(resolved, schemaDMT, expectedType, system) | ||
}) | ||
} | ||
}) | ||
return trapped | ||
} else if (typeDMT.map) { | ||
let valueType = typeDMT.map.valueType | ||
if (typeof valueType === 'string') { | ||
valueType = schemaDMT.types[valueType] | ||
} | ||
if (valueType?.link?.expectedType) { | ||
const expectedType = valueType.link.expectedType | ||
const trapped = new Proxy(converted, { | ||
get (target, property) { | ||
const value = target[property] | ||
const asCID = CID.asCID(value) | ||
if (!asCID) return value | ||
return system.getNode(asCID).then((resolved) => { | ||
return makeTyped(resolved, schemaDMT, expectedType, system) | ||
}) | ||
} | ||
}) | ||
return trapped | ||
} | ||
} else if (typeDMT.list) { | ||
let valueType = typeDMT.map.valueType | ||
if (typeof valueType === 'string') { | ||
valueType = schemaDMT.types[valueType] | ||
} | ||
if (valueType?.link?.expectedType) { | ||
const expectedType = valueType.link.expectedType | ||
const trapped = new Proxy(converted, { | ||
get (target, property) { | ||
const value = target[property] | ||
const asCID = CID.asCID(value) | ||
if (!asCID) return value | ||
return system.getNode(asCID).then((resolved) => { | ||
return makeTyped(resolved, schemaDMT, expectedType, system) | ||
}) | ||
} | ||
}) | ||
return trapped | ||
} | ||
} | ||
|
||
return converted | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
{ | ||
"name": "js-ipld-url-resolve", | ||
"version": "1.0.0", | ||
"description": "Resolver for IPLD URLs based on the js-IPFS DAG API. supports advanced features like schemas and escaping", | ||
"main": "index.js", | ||
"type": "module", | ||
"scripts": { | ||
"test": "node test.js", | ||
"lint": "standard --fix" | ||
}, | ||
"repository": { | ||
"type": "git", | ||
"url": "git+https://github.com/RangerMauve/js-ipld-url-resolve.git" | ||
}, | ||
"keywords": [ | ||
"ipld", | ||
"url", | ||
"resolve" | ||
], | ||
"author": "rangermauve <ranger@mauve.moe> (https://mauve.moe/)", | ||
"license": "MIT", | ||
"bugs": { | ||
"url": "https://github.com/RangerMauve/js-ipld-url-resolve/issues" | ||
}, | ||
"homepage": "https://github.com/RangerMauve/js-ipld-url-resolve#readme", | ||
"dependencies": { | ||
"@ipld/printify": "^0.1.0", | ||
"@ipld/schema": "^4.1.0", | ||
"js-ipld-url": "^1.0.2", | ||
"multiformats": "^9.7.1" | ||
}, | ||
"devDependencies": { | ||
"ipfs-core": "^0.16.0", | ||
"standard": "^17.0.0", | ||
"tape": "^5.6.0" | ||
} | ||
} |
Oops, something went wrong.