jsonld
Version:
A JSON-LD Processor and API implementation in JavaScript.
291 lines (263 loc) • 8.38 kB
JavaScript
/*
* Copyright (c) 2017 Digital Bazaar, Inc. All rights reserved.
*/
'use strict';
const {isKeyword} = require('./context');
const graphTypes = require('./graphTypes');
const types = require('./types');
const util = require('./util');
const JsonLdError = require('./JsonLdError');
const api = {};
module.exports = api;
/**
* Creates a merged JSON-LD node map (node ID => node).
*
* @param input the expanded JSON-LD to create a node map of.
* @param [options] the options to use:
* [issuer] a jsonld.IdentifierIssuer to use to label blank nodes.
*
* @return the node map.
*/
api.createMergedNodeMap = (input, options) => {
options = options || {};
// produce a map of all subjects and name each bnode
const issuer = options.issuer || new util.IdentifierIssuer('_:b');
const graphs = {'@default': {}};
api.createNodeMap(input, graphs, '@default', issuer);
// add all non-default graphs to default graph
return api.mergeNodeMaps(graphs);
};
/**
* Recursively flattens the subjects in the given JSON-LD expanded input
* into a node map.
*
* @param input the JSON-LD expanded input.
* @param graphs a map of graph name to subject map.
* @param graph the name of the current graph.
* @param issuer the blank node identifier issuer.
* @param name the name assigned to the current input if it is a bnode.
* @param list the list to append to, null for none.
*/
api.createNodeMap = (input, graphs, graph, issuer, name, list) => {
// recurse through array
if(types.isArray(input)) {
for(const node of input) {
api.createNodeMap(node, graphs, graph, issuer, undefined, list);
}
return;
}
// add non-object to list
if(!types.isObject(input)) {
if(list) {
list.push(input);
}
return;
}
// add values to list
if(graphTypes.isValue(input)) {
if('@type' in input) {
let type = input['@type'];
// rename @type blank node
if(type.indexOf('_:') === 0) {
input['@type'] = type = issuer.getId(type);
}
}
if(list) {
list.push(input);
}
return;
} else if(list && graphTypes.isList(input)) {
const _list = [];
api.createNodeMap(input['@list'], graphs, graph, issuer, name, _list);
list.push({'@list': _list});
return;
}
// Note: At this point, input must be a subject.
// spec requires @type to be named first, so assign names early
if('@type' in input) {
const types = input['@type'];
for(const type of types) {
if(type.indexOf('_:') === 0) {
issuer.getId(type);
}
}
}
// get name for subject
if(types.isUndefined(name)) {
name = graphTypes.isBlankNode(input) ?
issuer.getId(input['@id']) : input['@id'];
}
// add subject reference to list
if(list) {
list.push({'@id': name});
}
// create new subject or merge into existing one
const subjects = graphs[graph];
const subject = subjects[name] = subjects[name] || {};
subject['@id'] = name;
const properties = Object.keys(input).sort();
for(let property of properties) {
// skip @id
if(property === '@id') {
continue;
}
// handle reverse properties
if(property === '@reverse') {
const referencedNode = {'@id': name};
const reverseMap = input['@reverse'];
for(const reverseProperty in reverseMap) {
const items = reverseMap[reverseProperty];
for(const item of items) {
let itemName = item['@id'];
if(graphTypes.isBlankNode(item)) {
itemName = issuer.getId(itemName);
}
api.createNodeMap(item, graphs, graph, issuer, itemName);
util.addValue(
subjects[itemName], reverseProperty, referencedNode,
{propertyIsArray: true, allowDuplicate: false});
}
}
continue;
}
// recurse into graph
if(property === '@graph') {
// add graph subjects map entry
if(!(name in graphs)) {
graphs[name] = {};
}
api.createNodeMap(input[property], graphs, name, issuer);
continue;
}
// recurse into included
if(property === '@included') {
api.createNodeMap(input[property], graphs, graph, issuer);
continue;
}
// copy non-@type keywords
if(property !== '@type' && isKeyword(property)) {
if(property === '@index' && property in subject &&
(input[property] !== subject[property] ||
input[property]['@id'] !== subject[property]['@id'])) {
throw new JsonLdError(
'Invalid JSON-LD syntax; conflicting @index property detected.',
'jsonld.SyntaxError',
{code: 'conflicting indexes', subject});
}
subject[property] = input[property];
continue;
}
// iterate over objects
const objects = input[property];
// if property is a bnode, assign it a new id
if(property.indexOf('_:') === 0) {
property = issuer.getId(property);
}
// ensure property is added for empty arrays
if(objects.length === 0) {
util.addValue(subject, property, [], {propertyIsArray: true});
continue;
}
for(let o of objects) {
if(property === '@type') {
// rename @type blank nodes
o = (o.indexOf('_:') === 0) ? issuer.getId(o) : o;
}
// handle embedded subject or subject reference
if(graphTypes.isSubject(o) || graphTypes.isSubjectReference(o)) {
// skip null @id
if('@id' in o && !o['@id']) {
continue;
}
// relabel blank node @id
const id = graphTypes.isBlankNode(o) ?
issuer.getId(o['@id']) : o['@id'];
// add reference and recurse
util.addValue(
subject, property, {'@id': id},
{propertyIsArray: true, allowDuplicate: false});
api.createNodeMap(o, graphs, graph, issuer, id);
} else if(graphTypes.isValue(o)) {
util.addValue(
subject, property, o,
{propertyIsArray: true, allowDuplicate: false});
} else if(graphTypes.isList(o)) {
// handle @list
const _list = [];
api.createNodeMap(o['@list'], graphs, graph, issuer, name, _list);
o = {'@list': _list};
util.addValue(
subject, property, o,
{propertyIsArray: true, allowDuplicate: false});
} else {
// handle @value
api.createNodeMap(o, graphs, graph, issuer, name);
util.addValue(
subject, property, o, {propertyIsArray: true, allowDuplicate: false});
}
}
}
};
/**
* Merge separate named graphs into a single merged graph including
* all nodes from the default graph and named graphs.
*
* @param graphs a map of graph name to subject map.
*
* @return the merged graph map.
*/
api.mergeNodeMapGraphs = graphs => {
const merged = {};
for(const name of Object.keys(graphs).sort()) {
for(const id of Object.keys(graphs[name]).sort()) {
const node = graphs[name][id];
if(!(id in merged)) {
merged[id] = {'@id': id};
}
const mergedNode = merged[id];
for(const property of Object.keys(node).sort()) {
if(isKeyword(property) && property !== '@type') {
// copy keywords
mergedNode[property] = util.clone(node[property]);
} else {
// merge objects
for(const value of node[property]) {
util.addValue(
mergedNode, property, util.clone(value),
{propertyIsArray: true, allowDuplicate: false});
}
}
}
}
}
return merged;
};
api.mergeNodeMaps = graphs => {
// add all non-default graphs to default graph
const defaultGraph = graphs['@default'];
const graphNames = Object.keys(graphs).sort();
for(const graphName of graphNames) {
if(graphName === '@default') {
continue;
}
const nodeMap = graphs[graphName];
let subject = defaultGraph[graphName];
if(!subject) {
defaultGraph[graphName] = subject = {
'@id': graphName,
'@graph': []
};
} else if(!('@graph' in subject)) {
subject['@graph'] = [];
}
const graph = subject['@graph'];
for(const id of Object.keys(nodeMap).sort()) {
const node = nodeMap[id];
// only add full subjects
if(!graphTypes.isSubjectReference(node)) {
graph.push(node);
}
}
}
return defaultGraph;
};