UNPKG

webgme-engine

Version:

WebGME server and Client API without a GUI

816 lines (720 loc) 29.5 kB
/*globals define*/ /*eslint-env node, browser*/ /** * This is a partial implementation of RFC 6902 * the generated patch is fully compliant though the * patch generation is specialized to the expected input form. * * @author kecso / https://github.com/kecso */ define([ 'common/util/canon', 'common/util/random', 'common/core/constants', 'common/regexp' ], function (CANON, RANDOM, CORE_CONSTANTS, REGEXP) { 'use strict'; var MIN_RELID_LENGTH_PATH = CORE_CONSTANTS.PATH_SEP + CORE_CONSTANTS.MINIMAL_RELID_LENGTH_PROPERTY; function _strEncode(str) { //we should replace the '/' in the patch paths return str.replace(/\//g, '%2f'); } function _strDecode(str) { return str.replace(/%2f/g, '/'); } // function _endsWith(str, pattern) { // var d = str.length - pattern.length; // return d >= 0 && str.lastIndexOf(pattern) === d; // } function _startsWith(str, pattern) { return str.indexOf(pattern) === 0; } function _isOvr(path) { return path.indexOf('/ovr') === 0; } function _isRelid(path) { return RANDOM.isValidRelid(path.substring(1)); } function _isGmePath(path) { if (typeof path !== 'string') { return false; } if (path === '') { return true; } var relIds = path.split('/'), result = false, i; for (i = 1; i < relIds.length; i += 1) { if (RANDOM.isValidRelid(relIds[i]) === false) { return false; } else { result = true; } } return result; } function diff(source, target, basePath, excludeList, noUpdate, innerPath, overlay, inOverlay) { var result = [], patchItem, path, i; //add for (i in target) { if (excludeList.indexOf(i) === -1 && Object.hasOwn(target, i)) { if (!Object.hasOwn(source, i)) { patchItem = { op: 'add', path: basePath + _strEncode(i), value: target[i] }; if (inOverlay || overlay) { patchItem.partialUpdates = []; patchItem.updates = []; if (inOverlay) { if (_isGmePath(innerPath)) { patchItem.updates.push(innerPath); } if (_isGmePath(target[i])) { patchItem.partialUpdates.push(target[i]); } } else { if (_isGmePath(i)) { patchItem.updates.push(i); } for (path in target[i]) { if (_isGmePath(target[i][path])) { patchItem.partialUpdates.push(target[i][path]); } } } } result.push(patchItem); } } } //replace if (!noUpdate) { for (i in target) { if (excludeList.indexOf(i) === -1 && Object.hasOwn(target, i)) { if (Object.hasOwn(source, i) && CANON.stringify(source[i]) !== CANON.stringify(target[i])) { patchItem = { op: 'replace', path: basePath + _strEncode(i), value: target[i] //oldValue: source[i] }; if (inOverlay) { patchItem.partialUpdates = []; patchItem.updates = []; if (_isGmePath(innerPath)) { patchItem.updates.push(innerPath); } if (_isGmePath(target[i])) { patchItem.partialUpdates.push(target[i]); } if (_isGmePath(source[i])) { patchItem.partialUpdates.push(source[i]); } } result.push(patchItem); } } } } //remove for (i in source) { if (excludeList.indexOf(i) === -1 && Object.hasOwn(source, i)) { if (!Object.hasOwn(target, i)) { patchItem = { op: 'remove', path: basePath + _strEncode(i) //oldValue: source[i] }; if (inOverlay || overlay) { patchItem.partialUpdates = []; patchItem.updates = []; if (inOverlay) { if (_isGmePath(innerPath)) { patchItem.updates.push(innerPath); } if (_isGmePath(source[i])) { patchItem.partialUpdates.push(source[i]); } } else { if (_isGmePath(i)) { patchItem.updates.push(i); } for (path in source[i]) { if (_isGmePath(source[i][path])) { patchItem.partialUpdates.push(source[i][path]); } } } } result.push(patchItem); } } } return result; } function _isEmptyObject(object) { for (var key in object) { return false; } return true; } function wholeShardDiff(items, isAddition) { var patchItem = { updates: [], partialUpdates: [] }, source, name; patchItem.op = 'replace'; patchItem.path = '/items'; if (isAddition) { patchItem.value = items; } else { patchItem.value = {}; } for (source in items) { patchItem.updates.push(source); for (name in items[source]) { if (_isGmePath(items[source][name])) { patchItem.partialUpdates.push(items[source][name]); } } } return patchItem; } function overlayShardDiff(sourceJson, targetJson) { var patch, key, sourceEmpty = _isEmptyObject(sourceJson.items || {}), targetEmpty = _isEmptyObject(targetJson.items || {}); patch = diff(sourceJson, targetJson, '/', ['_id', 'type', 'items'], false, '', false, false); if (sourceEmpty && targetEmpty) { // Do nothing as nothing have changed } else if (sourceEmpty) { patch.push(wholeShardDiff(targetJson.items, true)); } else if (targetEmpty) { patch.push(wholeShardDiff(sourceJson.items, false)); } else { patch = patch .concat(diff(sourceJson.items || {}, targetJson.items || {}, '/items/', [], true, '', true, false)); for (key in sourceJson.items) { if (Object.hasOwn(targetJson.items, key)) { patch = patch.concat(diff( sourceJson.items[key], targetJson.items[key], '/items/' + _strEncode(key) + '/', [], false, key, false, true)); } } } return patch; } function getShardingDiff(commonOverlay, shardedOverlay) { var patchItem = { op: 'replace', path: '/ovr', value: shardedOverlay, preShardRelations: commonOverlay }; return patchItem; } function create(sourceJson, targetJson) { var patch, patchItem, diffRes, i, key; //if it is an overlay shard, we make a more simple diff if (sourceJson.type === 'shard' && targetJson.type === 'shard') { return overlayShardDiff(sourceJson, targetJson); } //main level diff patch = diff(sourceJson, targetJson, '/', ['_id', 'ovr', 'atr', 'reg', '_sets'], false, '', false, false); //atr if (sourceJson.atr && targetJson.atr) { patch = patch.concat(diff(sourceJson.atr, targetJson.atr, '/atr/', [], false, '', false, false)); } else if (sourceJson.atr) { patch.push({ op: 'remove', path: '/atr' }); } else if (targetJson.atr) { patch.push({ op: 'add', path: '/atr', value: targetJson.atr }); } //reg if (sourceJson.reg && targetJson.reg) { patch = patch.concat(diff(sourceJson.reg, targetJson.reg, '/reg/', [], false, '', false, false)); } else if (sourceJson.reg) { patch.push({ op: 'remove', path: '/reg' }); } else if (targetJson.reg) { patch.push({ op: 'add', path: '/reg', value: targetJson.reg }); } //_sets if (sourceJson._sets && targetJson._sets) { patch = patch.concat(diff(sourceJson._sets, targetJson._sets, '/_sets/', [], true, '', false, false)); for (key in targetJson._sets) { if (sourceJson._sets[key]) { patch = patch.concat(diff( sourceJson._sets[key], targetJson._sets[key], '/_sets/' + _strEncode(key) + '/', [], false, '', false, false)); } } } else if (sourceJson._sets) { patch.push({ op: 'remove', path: '/_sets' }); } else if (targetJson._sets) { patch.push({ op: 'add', path: '/_sets', value: targetJson._sets }); } //ovr if (sourceJson.ovr && targetJson.ovr) { if (sourceJson.ovr.sharded !== true && targetJson.ovr.sharded === true) { // Transformation into sharded overlays means that we have to collect update information. patch.push(getShardingDiff(sourceJson.ovr, targetJson.ovr)); } else if (sourceJson.ovr.sharded === true && targetJson.ovr.sharded === true) { patch = patch.concat(diff(sourceJson.ovr, targetJson.ovr, '/ovr/', [], false, '', true, false)); } else { patch = patch.concat(diff(sourceJson.ovr, targetJson.ovr, '/ovr/', [], true, '', true, false)); for (key in targetJson.ovr) { if (sourceJson.ovr[key]) { patch = patch.concat(diff( sourceJson.ovr[key], targetJson.ovr[key], '/ovr/' + _strEncode(key) + '/', [], false, key, false, true)); } } } } else if (sourceJson.ovr || targetJson.ovr) { patchItem = { path: '/ovr', partialUpdates: [], updates: [] }; if (sourceJson.ovr) { patchItem.op = 'remove'; } else { patchItem.op = 'add'; patchItem.value = targetJson.ovr; } // For ovr removal/addition we need to compute updates/partialUpdates diffRes = diff(sourceJson.ovr || {}, targetJson.ovr || {}, '/ovr/', [], true, '', true, false); for (i = 0; i < diffRes.length; i += 1) { patchItem.partialUpdates = patchItem.partialUpdates.concat(diffRes[i].partialUpdates); patchItem.updates = patchItem.updates.concat(diffRes[i].updates); } patch.push(patchItem); } return patch; } function apply(sourceJson, patch) { var targetJson = JSON.parse(JSON.stringify(sourceJson)), i, j, badOperation = false, pathArray, key, parent, result = { status: 'success', faults: [], patch: patch, result: targetJson }; for (i = 0; i < patch.length; i += 1) { pathArray = (patch[i].path + '').split('/').slice(1); parent = targetJson; for (j = 0; j < pathArray.length; j += 1) { pathArray[j] = _strDecode(pathArray[j]); } key = pathArray.pop(); badOperation = false; switch (patch[i].op) { case 'remove': if (typeof patch[i].path === 'string') { for (j = 0; j < pathArray.length; j += 1) { if (!parent[pathArray[j]]) { badOperation = true; break; } parent = parent[pathArray[j]]; } if (!badOperation && parent[key] !== undefined) { delete parent[key]; } else { result.status = 'fail'; result.faults.push(patch[i]); } } else { result.status = 'fail'; result.faults.push(patch[i]); } break; case 'add': if (typeof patch[i].path === 'string' && patch[i].value !== undefined) { for (j = 0; j < pathArray.length; j += 1) { if (!parent[pathArray[j]]) { parent[pathArray[j]] = {}; } parent = parent[pathArray[j]]; } parent[key] = patch[i].value; } else { result.status = 'fail'; result.faults.push(patch[i]); } break; case 'replace': if (typeof patch[i].path === 'string' && patch[i].value !== undefined) { for (j = 0; j < pathArray.length; j += 1) { if (!parent[pathArray[j]]) { badOperation = true; break; } parent = parent[pathArray[j]]; } if (!badOperation && parent[key] !== undefined) { parent[key] = patch[i].value; } else { result.status = 'fail'; result.faults.push(patch[i]); } } else { result.status = 'fail'; result.faults.push(patch[i]); } break; default: result.status = 'fail'; result.faults.push(patch[i]); break; } } return result; } function _inLoadOrUnload(res, gmePath) { var pathPieces = gmePath.split('/'), parentPath; parentPath = gmePath; do { if (res.load[parentPath] || res.unload[parentPath]) { return true; } pathPieces.pop(); parentPath = pathPieces.join('/'); } while (pathPieces.length > 1); return false; } function _removeFromUpdates(res, gmePath) { var updatesPath, i; updatesPath = Object.keys(res.update); for (i = 0; i < updatesPath; i += 1) { if (_startsWith(updatesPath[i], gmePath)) { delete res.update[gmePath]; } } updatesPath = Object.keys(res.partialUpdate); for (i = 0; i < updatesPath; i += 1) { if (_startsWith(updatesPath[i], gmePath)) { delete res.partialUpdate[gmePath]; } } } function _isShardChange(patchItem) { if (!_isOvr(patchItem.path)) { return false; } if (patchItem.op === 'add' || patchItem.op === 'replace') { return REGEXP.HASH.test(patchItem.value); } } function _getChangedNodesFromShard(patch, res, hash, gmePath) { var shardPatch = patch[hash] && patch[hash].patch ? patch[hash] && patch[hash].patch : patch[hash], source, name, i, j, absGmePath; if (!shardPatch) { return; } if (shardPatch instanceof Array) { // patch objects for (i = 0; i < shardPatch.length; i += 1) { if (shardPatch[i].updates instanceof Array) { for (j = 0; j < shardPatch[i].updates.length; j += 1) { absGmePath = gmePath + shardPatch[i].updates[j]; if (_inLoadOrUnload(res, absGmePath) === false) { res.update[absGmePath] = true; } } } if (shardPatch[i].partialUpdates instanceof Array) { for (j = 0; j < shardPatch[i].partialUpdates.length; j += 1) { absGmePath = gmePath + shardPatch[i].partialUpdates[j]; if (_inLoadOrUnload(res, absGmePath) === false) { res.partialUpdate[absGmePath] = true; } } } } } else { // completely new shard for (source in shardPatch.items || {}) { for (name in shardPatch.items[source]) { absGmePath = gmePath + source; if (_inLoadOrUnload(res, absGmePath) === false) { res.update[absGmePath] = true; } absGmePath = gmePath + shardPatch.items[source][name]; if (_inLoadOrUnload(res, absGmePath) === false) { res.partialUpdate[absGmePath] = true; } } } } } function _getChangedNodesFromSharding(patch, patchItem, res, gmePath) { var shards = [], shardId, absGmePath, preShardRelations = patchItem.preShardRelations, foundSource, source, name, i; for (shardId in patchItem.value) { if (REGEXP.HASH.test(patchItem.value[shardId])) { if (patch[patchItem.value[shardId]]) { shards.push(patch[patchItem.value[shardId]]); } } } // First handle those relations where we have the source in both states for (source in preShardRelations) { foundSource = false; for (i = 0; i < shards.length; i += 1) { if (Object.hasOwn(shards[i].items, source)) { foundSource = true; // check the removal and updates for (name in preShardRelations[source]) { if (Object.hasOwn(shards[i].items[source], name)) { if (shards[i].items[source][name] !== preShardRelations[source][name]) { // update absGmePath = gmePath + source; if (_inLoadOrUnload(res, absGmePath) === false) { res.update[absGmePath] = true; } if (_isGmePath(preShardRelations[source][name])) { absGmePath = gmePath + preShardRelations[source][name]; if (_inLoadOrUnload(res, absGmePath) === false) { res.partialUpdate[absGmePath] = true; } } if (_isGmePath(shards[i].items[source][name])) { absGmePath = gmePath + shards[i].items[source][name]; if (_inLoadOrUnload(res, absGmePath) === false) { res.partialUpdate[absGmePath] = true; } } } } else { // remove absGmePath = gmePath + source; if (_inLoadOrUnload(res, absGmePath) === false) { res.update[absGmePath] = true; } if (_isGmePath(preShardRelations[source][name])) { absGmePath = gmePath + preShardRelations[source][name]; if (_inLoadOrUnload(res, absGmePath) === false) { res.partialUpdate[absGmePath] = true; } } } } // check additions for (name in shards[i].items[source]) { if (Object.hasOwn(preShardRelations[source], name) === false) { absGmePath = gmePath + source; if (_inLoadOrUnload(res, absGmePath) === false) { res.update[absGmePath] = true; } if (_isGmePath(shards[i].items[source][name])) { absGmePath = gmePath + shards[i].items[source][name]; if (_inLoadOrUnload(res, absGmePath) === false) { res.partialUpdate[absGmePath] = true; } } } } break; } } if (!foundSource) { // All relations from this source was removed absGmePath = gmePath + source; if (_inLoadOrUnload(res, absGmePath) === false) { res.update[absGmePath] = true; } for (name in preShardRelations[source]) { if (preShardRelations[source][name] instanceof Array) { // Sharding from v < 1 with inverse relations stored. continue; } absGmePath = gmePath + preShardRelations[source][name]; if (_inLoadOrUnload(res, absGmePath) === false) { res.partialUpdate[absGmePath] = true; } } } } // Finally check for completely new sources for (i = 0; i < shards.length; i += 1) { for (source in shards[i].items) { if (Object.hasOwn(preShardRelations, source) === false) { // All relations from this source was removed absGmePath = gmePath + source; if (_inLoadOrUnload(res, absGmePath) === false) { res.update[absGmePath] = true; } for (name in shards[i].items[source]) { absGmePath = gmePath + shards[i].items[source][name]; if (_inLoadOrUnload(res, absGmePath) === false) { res.partialUpdate[absGmePath] = true; } } } } } } function _getChangedNodesRec(patch, res, hash, gmePath) { var nodePatches = patch[hash] && patch[hash].patch, // Changes regarding node with hash i, j, ownChange = false, absGmePath, patchPath, subPath, pathPieces; if (!nodePatches) { // E.g. if the node was added the full data is given instead of a patch. return; } for (i = 0; i < nodePatches.length; i += 1) { patchPath = nodePatches[i].path; if (nodePatches[i].op === 'replace' && typeof nodePatches[i].preShardRelations === 'object') { //special case when the overlay is converted _getChangedNodesFromSharding(patch, nodePatches[i], res, gmePath); } else if (_isShardChange(nodePatches[i])) { _getChangedNodesFromShard(patch, res, nodePatches[i].value, gmePath); } else if (_isOvr(patchPath) === true) { // Now handle the updates for (j = 0; j < nodePatches[i].partialUpdates.length; j += 1) { absGmePath = gmePath + nodePatches[i].partialUpdates[j]; if (_inLoadOrUnload(res, absGmePath) === false) { res.partialUpdate[absGmePath] = true; } } for (j = 0; j < nodePatches[i].updates.length; j += 1) { absGmePath = gmePath + nodePatches[i].updates[j]; if (_inLoadOrUnload(res, absGmePath) === false) { res.update[absGmePath] = true; } } // #1438 This will capture set-owner updates. subPath = _strDecode(patchPath.substring('/ovr/'.length)); pathPieces = subPath.split('/'); if (pathPieces.length >= 3 && pathPieces[2] === CORE_CONSTANTS.ALL_SETS_PROPERTY) { // Original path looks something like /ovr/<nodePath>/_sets/... absGmePath = gmePath + '/' + pathPieces[1]; if (_isGmePath(absGmePath) && _inLoadOrUnload(res, absGmePath) === false) { res.update[absGmePath] = true; } } } else if (_isRelid(patchPath) === true) { // There was a change in one of the children.. switch (nodePatches[i].op) { case 'add': res.load[gmePath + patchPath] = true; _removeFromUpdates(res, gmePath + patchPath); break; case 'remove': res.unload[gmePath + patchPath] = true; _removeFromUpdates(res, gmePath + patchPath); break; case 'replace': _getChangedNodesRec(patch, res, nodePatches[i].value, gmePath + patchPath); break; default: throw new Error('Unexpected patch operation ' + nodePatches[i]); } } else if (patchPath !== MIN_RELID_LENGTH_PATH && patchPath !== '/__v') { ownChange = true; } } if (ownChange) { res.update[gmePath] = true; } } /** * * @param {object} patch * @returns {object} */ // TODO check if all event related information could be set during patch creation, // so this function would only collect those information. function getChangedNodes(patch, rootHash) { var res; if (patch[rootHash] && patch[rootHash].patch) { res = { load: {}, unload: {}, update: {}, partialUpdate: {} }; _getChangedNodesRec(patch, res, rootHash, ''); } else { res = null; } return res; } return { create: create, apply: apply, getChangedNodes: getChangedNodes }; });