/* * Copyright (c) 2012-2019 Kaarle Ritvanen * See LICENSE file for license details */ angular.module("aconf").factory( "aconfTxn", function($http, $q, aconfExclusive, aconfPath, aconfType) { return function(token, saveRequired) { var txnMgr = {}; var txn, changed, invalid; function reset() { txn = null; changed = {}; invalid = {}; } reset(); function isValid() { return !(_.size(invalid)); } function request(url, options) { options = options || {}; options.url = url; options.headers = {"X-AConf-Auth-Token": token}; if (txn) options.headers["X-AConf-Transaction-ID"] = txn; options.data = options.data == null ? "" : JSON.stringify(options.data); return $http(options); } txnMgr.abort = function() { request("/transaction", {method: "DELETE"}); reset(); } function objRequest(path, options) { return request("/config" + encodeURI(path), options); } function exclusive(task) { function attempt() { return $q(function(resolve, reject) { function postpone() { attempt().then(resolve, reject); } var tasks = _.filter( _.pluck(_.values(invalid), 1), function(task) { return task; } ); if (tasks.length) tasks[0].then(postpone, postpone); else task().then(resolve, reject); }); } return aconfExclusive(attempt); } txnMgr.query = function(path) { return $q(function(resolve, reject) { objRequest(path).success(function(data) { data.txnMgr = txnMgr; if (aconfType.isTreeNode(data.meta) && !_.size(data.data)) data.data = aconfType.isList(data.meta) ? [] : {}; if (!_.isArray(data.meta.removable)) data.meta.removable = []; function index(name) { return _.isArray(data.data) ? name - 1 : name; } function ensureKeyPresence(name) { var key = index(name); if (!(key in data.data)) data.data[key] = null; if (data.data[key] == null) data.meta.removable.push(name); } function findSubordinateKeys(objmap, path) { return _.filter(_.keys(objmap), function(p) { return aconfPath.isSubordinate(p, path); }); } function invalidSubordinates() { return findSubordinateKeys(invalid, path); } var set = data.meta.type == "set"; if (aconfType.isCollection(data.meta) && !set) { var level = aconfPath.split(path).length; _.each(invalidSubordinates(), function(p) { ensureKeyPresence(aconfPath.split(p)[level]); }); } data.get = function(name, valid) { var p = aconfPath.join(path, name); if (!valid && p in invalid) return invalid[p][0]; if (data.meta.type == "set") return _.contains(data.data, name) ? name : null; var res = data.data[index(name)]; return res === undefined ? null : res; }; data.metaRequest = function(name) { return request("/meta" + aconfPath.join(path, name)); }; data.match = function(filter) { if (!filter) return true; return _.every( _.map(filter, function(values, key) { return _.contains(values, data.data[key]); }) ); }; data.status = function(name) { var p = aconfPath.join(path, name); function scan(objmap) { return _.size(findSubordinateKeys(objmap, p)); } if (scan(invalid)) return "invalid"; if (scan(changed)) return "changed"; return null; }; data.isNodeValid = function() { return !(path in invalid); }; data.isSubtreeValid = function() { return !_.size(invalidSubordinates(invalid, path)); }; data.validate = function() { var valid = true; if (data.meta.required) { valid = _.size(data.data); if (valid) delete invalid[path]; else invalid[path] = [path]; } return valid && isValid(); }; data.set = function(name, newValue) { var def = $q.defer(); function reject(resp) { if (mpath in invalid && invalid[mpath][1] == def.promise) invalid[mpath][1] = null; def.reject(resp); } var mpath = aconfPath.join(path, name); ensureKeyPresence(name); var value = data.get(name, true); var empty = newValue === null && aconfType.isCollection(data.meta); if (newValue === undefined) newValue = null; var tn = _.isObject(newValue); var npv = tn ? mpath : newValue; var prevTask = mpath in invalid && invalid[mpath][1] || $q.when(); invalid[mpath] = [npv, def.promise]; function ignore(path) { _.each(_.keys(invalid), function(p) { if (aconfPath.isSubordinate(p, path)) delete invalid[p]; }); } function resolve() { if (mpath in invalid && invalid[mpath][1] == def.promise) { var del = invalid[mpath][0] == null; delete invalid[mpath]; if (del) ignore(mpath); } def.resolve(isValid()); } function newTask() { if (empty) { reject("Value not set"); return; } var del = newValue == null; var options; if (!del) options = { method: set ? "POST" : "PUT", data: newValue }; else if (data.get(name, true) != null) options = {method: "DELETE"}; if (!options) { if (data.meta.type == "model" && _.findWhere( data.meta.fields, {name: name} ).required) reject("Required value not set"); else resolve(); return; } objRequest( set && !del ? path : mpath, options ).then(function() { if (!(mpath in changed)) changed[mpath] = value; if (!tn && newValue == changed[mpath]) delete changed[mpath]; if (del) _.each( _.keys(changed), function(p) { if (aconfPath.isSubordinate( p, mpath, true )) delete changed[p]; } ); if (!set) data.data[index(name)] = npv; else if (del) data.data = _.without(data.data, name); else data.data.push(name); data.validate(); if (data.meta.type == "model") _.each( data.meta.fields, function(field) { if (field.condition && field.condition[name] && !_.contains( field.condition[name], newValue )) ignore( aconfPath.join( path, field.name ) ); } ); if (tn && !set) txnMgr.query(mpath).then(function(data) { if (mpath in invalid) { if (data.meta.type == "model") _.each( data.meta.fields, function(field) { var mmpath = aconfPath.join( mpath, field.name ); if (field.required && data.match( field.condition ) && !(mmpath in invalid) && (aconfType.isCollection( field ) || data.get( field.name ) == null)) { invalid[mmpath] = [ null ]; } }); else if (aconfType.isCollection(data.meta) && data.meta.required) invalid[mpath] = [mpath]; } resolve(); }, reject); else resolve(); }, reject); } prevTask.then(newTask, newTask); return def.promise; }; data.add = function(name) { return data.set(name, name); }; function adjustListIndex(oldIndex, newIndex) { var opath = aconfPath.join(path, oldIndex); var npath = aconfPath.join(path, newIndex); _.each( [changed, invalid], function(map) { _.each( _.keys(map), function(p) { if (aconfPath.isSubordinate( p, opath )) { map[npath + p.substring( opath.length )] = map[p]; delete map[p]; } }); } ); } function adjustListIndices(start, end) { var offset = start < end ? 1 : -1; for (var i = start; i != end; i += offset) adjustListIndex(i + offset, i); } function _delete(name) { return $q(function(resolve, reject) { var length = data.data.length; data.set(name).then(function(txnValid) { if (aconfType.isTreeNode(data.meta) && data.meta.type != "set") { var mpath = aconfPath.join(path, name); var key = index(name); delete changed[mpath]; changed[path] = path; if (data.meta.type == "list") { if (aconfType.isTreeNode( data.meta.members )) { data.data[key] = mpath; data.data.pop(); } else data.data.splice(name - 1, 1); adjustListIndices(name, length); } else delete data.data[key]; } resolve(txnValid); }, reject); }); } data.delete = function(name) { return exclusive(function() { return _delete(name); }); }; data.move = function(oldIndex, newIndex) { if (oldIndex == newIndex) return $q.when(isValid()); var value = data.get(oldIndex); var length = data.data.length; return exclusive(function() { return $q(function(resolve, reject) { if (oldIndex > newIndex) oldIndex++; else newIndex++; objRequest(path, {method: "POST", data: { index: newIndex, data: aconfType.isTreeNode( data.meta.members ) ? aconfPath.join( path, oldIndex ) : value }}).then(function() { if (aconfType.isTreeNode( data.meta.members )) data.data.push(value); else data.data.splice( newIndex - 1, 0, value ); adjustListIndices(length + 1, newIndex); adjustListIndex(oldIndex, newIndex); _delete(oldIndex).then(resolve, reject); }, reject); }); }); }; data.invoke = function(name) { return objRequest( aconfPath.join(path, name), {method: "POST"} ); }; resolve(data); }).error(reject); }); }; txnMgr.isPristine = function() { return !(_.size(changed)); }; txnMgr.start = function() { return $q(function(resolve, reject) { if (txn && isValid() && txnMgr.isPristine()) txnMgr.abort(); if (txn) resolve(); else request("/transaction", {method: "POST"}) .success(function(data, status, headers) { txn = headers("X-AConf-Transaction-ID"); resolve(); }).error(reject); }); }; txnMgr.commit = function() { return $q(function(resolve, reject) { request("/transaction", {method: "PUT"}).then(function() { reset(); if (saveRequired) request("/save", {method: "POST"}) .then(resolve, reject); else resolve(); }, reject); }); }; txnMgr.logout = function() { return request("/login", {method: "DELETE"}); }; return txnMgr; }; } );