summaryrefslogtreecommitdiffstats
path: root/web/transaction.js
diff options
context:
space:
mode:
authorKaarle Ritvanen <kaarle.ritvanen@datakunkku.fi>2014-02-06 22:27:20 +0200
committerKaarle Ritvanen <kaarle.ritvanen@datakunkku.fi>2014-02-12 12:38:31 +0200
commit1dc9c3a59137b1cb9a38669938ba865154100c64 (patch)
tree28599445ef4a199ecde352afa495c4a918dd4641 /web/transaction.js
parent8857a1753ba94c490211b06531995b6ad88506a6 (diff)
downloadaconf-1dc9c3a59137b1cb9a38669938ba865154100c64.tar.bz2
aconf-1dc9c3a59137b1cb9a38669938ba865154100c64.tar.xz
web client: transaction module
Diffstat (limited to 'web/transaction.js')
-rw-r--r--web/transaction.js416
1 files changed, 416 insertions, 0 deletions
diff --git a/web/transaction.js b/web/transaction.js
new file mode 100644
index 0000000..9a9368b
--- /dev/null
+++ b/web/transaction.js
@@ -0,0 +1,416 @@
+/*
+ * Copyright (c) 2012-2014 Kaarle Ritvanen
+ * See LICENSE file for license details
+ */
+
+define(
+ ["acf2/path", "acf2/type", "jquery", "underscore", "jquery-blockui"],
+ function(pth, type, $, _) {
+ return function(token, saveRequired) {
+ var txn, changed, invalid;
+
+ function reset() {
+ txn = null;
+ changed = {};
+ invalid = {};
+ }
+ reset();
+
+ function isValid() { return !(_.size(invalid)); }
+
+ function request(url, options) {
+ options = options || {};
+ options.headers = {"X-ACF-Auth-Token": token};
+ if (txn) options.headers["X-ACF-Transaction-ID"] = txn;
+ if (options.data != undefined)
+ options.data = JSON.stringify(options.data);
+ return $.ajax(url, options);
+ }
+
+ function abort() {
+ request("/transaction", {type: "DELETE"});
+ reset();
+ }
+
+ function objRequest(path, options) {
+ return request("/config" + path, options);
+ }
+
+ function exclusive(task) {
+ $.blockUI();
+
+ var def = $.Deferred();
+ function resolve(txnValid) { def.resolve(txnValid); }
+ function reject() { def.reject(); }
+
+ var tasks = _.filter(
+ _.pluck(_.values(invalid), 1), function(d) {
+ return d && d.state() == "pending";
+ }
+ );
+
+ if (tasks.length)
+ tasks[0].always(function() {
+ exclusive(task).done(resolve).fail(reject);
+ });
+ else task().always($.unblockUI).done(resolve).fail(reject);
+
+ return def;
+ }
+
+ function query(path) {
+ var def = $.Deferred();
+
+ objRequest(path).done(function(data) {
+ if (type.isTreeNode(data.meta) && !_.size(data.data))
+ data.data = type.isList(data.meta) ? [] : {};
+
+ function index(name) {
+ return _.isArray(data.data) ? name - 1 : name;
+ }
+
+ data.get = function(name, valid) {
+ var p = pth.join(path, name);
+ if (!valid && p in invalid) return invalid[p][0];
+
+ if (data.meta.type == "set")
+ return _.contains(data.data, name) ? name : null;
+
+ return data.data[index(name)];
+ };
+
+ data.metaRequest = function(name) {
+ return request("/meta" + pth.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 = name ? pth.join(path, name) : path;
+ function scan(objs) {
+ return _.size(_.filter(
+ _.keys(objs), function(obj) {
+ return pth.isSubordinate(obj, p);
+ }
+ ));
+ }
+
+ if (scan(invalid)) return "invalid";
+ if (scan(changed)) return "changed";
+ return null;
+ };
+
+ 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 = $.Deferred();
+ function reject(xhr) { def.reject(xhr); }
+
+ var mpath = pth.join(path, name);
+
+ var value = data.get(name, true);
+ if (value == undefined) value = null;
+
+ var tn = _.isObject(newValue);
+ var npv = tn ? mpath : newValue;
+
+ function ignore(path) {
+ _.each(_.keys(invalid), function(p) {
+ if (pth.isSubordinate(p, path))
+ delete invalid[p];
+ });
+ }
+
+ function resolve() {
+ if (mpath in invalid &&
+ invalid[mpath][1] == def) {
+
+ var del = invalid[mpath][0] == null;
+
+ delete invalid[mpath];
+ if (del) ignore(mpath);
+ }
+
+ def.resolve(isValid());
+ }
+
+ function validate() {
+ var del = newValue == null;
+ var set = data.meta.type == "set";
+
+ var options;
+ if (!del)
+ options = {
+ type: set ? "POST" : "PUT", data: newValue
+ };
+ else if (data.get(name, true) != null)
+ options = {type: "DELETE"};
+
+ if (!options) {
+ if (data.meta.type == "model" &&
+ _.findWhere(
+ data.meta.fields, {name: name}
+ ).required)
+ def.reject("Required value not set");
+ else resolve();
+ return;
+ }
+
+ objRequest(
+ set && !del ? path : mpath, options
+ ).done(function() {
+ if (!(mpath in changed))
+ changed[mpath] = value;
+ if (!tn && newValue == changed[mpath])
+ delete changed[mpath];
+
+ if (npv == null)
+ _.each(
+ _.keys(changed),
+ function(p) {
+ if (pth.isSubordinate(
+ p, mpath, true
+ ))
+ delete changed[p];
+ }
+ );
+
+ if (data.meta.type == "list" && del)
+ data.data.splice(name - 1, 1);
+ else if (!set) data.data[index(name)] = npv;
+ else if (del)
+ data.data.splice(
+ data.data.indexOf(name), 1
+ );
+ 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(
+ pth.join(path, field.name)
+ );
+ }
+ );
+
+ if (tn && !set)
+ query(mpath).done(function(data) {
+
+ if (mpath in invalid) {
+ if (data.meta.type == "model")
+ _.each(
+ data.meta.fields,
+ function(field) {
+ var mmpath = pth.join(
+ mpath,
+ field.name
+ );
+ if (field.required &&
+ data.match(
+ field.condition
+ ) &&
+ !(mmpath in invalid) &&
+ (type.isCollection(
+ field
+ ) || data.get(
+ field.name
+ ) == null)) {
+ invalid[mmpath] = [
+ null
+ ];
+ }
+ });
+
+ else if (
+ type.isCollection(data.meta) &&
+ data.meta.required
+ )
+ invalid[mpath] = [mpath];
+ }
+
+ resolve();
+ }).fail(reject);
+
+ else resolve();
+
+ }).fail(reject);
+ }
+
+ var prevTask;
+ if (mpath in invalid) prevTask = invalid[mpath][1];
+
+ invalid[mpath] = [npv, def];
+
+ if (prevTask) prevTask.always(validate);
+ else validate();
+
+ return def;
+ };
+
+ data.add = function(name) {
+ return data.set(name, name);
+ };
+
+ function adjustListIndex(oldIndex, newIndex) {
+ var opath = pth.join(path, oldIndex);
+ var npath = pth.join(path, newIndex);
+ _.each(
+ [changed, invalid],
+ function(map) {
+ _.each(
+ _.keys(map),
+ function(p) {
+ if (pth.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) {
+ var def = $.Deferred();
+ var length = data.data.length;
+
+ data.set(name, null).done(function(txnValid) {
+ if (type.isTreeNode(data.meta) &&
+ data.meta.type != "set") {
+
+ delete changed[pth.join(path, name)];
+ changed[path] = path;
+
+ if (data.meta.type == "list")
+ adjustListIndices(name, length);
+ }
+ def.resolve(txnValid);
+
+ }).fail(function() { def.reject(); });
+
+ return def;
+ }
+
+ data.delete = function(name) {
+ return exclusive(function() { return _delete(name); });
+ };
+
+ data.move = function(oldIndex, newIndex) {
+ if (oldIndex == newIndex)
+ return $.Deferred().resolve(isValid());
+
+ var value = data.get(oldIndex);
+ var length = data.data.length;
+
+ return exclusive(function() {
+ var def = $.Deferred();
+ function reject() { def.reject(); }
+
+ if (oldIndex > newIndex) oldIndex++;
+ else newIndex++;
+
+ objRequest(path, {type: "POST", data: {
+ index: newIndex,
+ data: type.isTreeNode(data.meta.members) ?
+ pth.join(path, oldIndex) : value
+ }}).done(function() {
+
+ data.data.splice(newIndex - 1, 0, value);
+
+ adjustListIndices(length + 1, newIndex);
+ adjustListIndex(oldIndex, newIndex);
+
+ data.delete(oldIndex)
+ .done(function(txnValid) {
+ def.resolve(txnValid);
+ })
+ .fail(reject);
+
+ }).fail(reject);
+
+ return def;
+ });
+ };
+
+ data.invoke = function(name) {
+ return objRequest(pth.join(path, name), {type: "POST"});
+ };
+
+ def.resolve(data);
+ }).fail(function() { def.reject(); });
+
+ return def;
+ }
+
+ return {
+ start: function() {
+ var def = $.Deferred();
+ if (txn && isValid() && !(_.size(changed))) abort();
+
+ if (txn) def.resolve();
+
+ else request("/transaction", {type: "POST"})
+ .done(function(data, status, xhr) {
+ txn = xhr.getResponseHeader("X-ACF-Transaction-ID");
+ def.resolve();
+ })
+ .fail(function() { def.reject(); });
+
+ return def;
+ },
+
+ commit: function() {
+ var def = $.Deferred();
+ function reject(xhr) { def.reject(xhr); }
+ request("/transaction", {type: "PUT"}).done(function() {
+ reset();
+ if (saveRequired)
+ request("/save", {type: "POST"}).done(function() {
+ def.resolve();
+ }).fail(reject);
+ else def.resolve();
+ }).fail(reject);
+ return def;
+ },
+
+ abort: abort,
+
+ query: query,
+
+ logout: function() {
+ return request("/login", {type: "DELETE"});
+ }
+ };
+ }
+ }
+);