Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Merge pull request #399 from Shopify/extra_rest_actions

Extra rest actions
  • Loading branch information...
commit 83667647185b9bd995e3d80601b9f13ad2cc8005 2 parents 9f8f3a7 + 3cf3e0b
@airhorns airhorns authored
View
353 lib/batman.js
@@ -1,5 +1,5 @@
(function() {
- var $addEventListener, $appendChild, $clearImmediate, $contains, $destroyNode, $escapeHTML, $extendsEnumerable, $forEach, $forgetParseExit, $functionName, $get, $getPath, $hasAddEventListener, $insertBefore, $isChildOf, $mixin, $objectHasKey, $onParseExit, $preventDefault, $propagateBindingEvent, $propagateBindingEvents, $redirect, $removeEventListener, $removeNode, $removeOrDestroyNode, $setImmediate, $setInnerHTML, $setStyleProperty, $stopPropagation, $trackBinding, $typeOf, $unmixin, Batman, BatmanObject, Validators, Yield, buntUndefined, camelize_rx, capitalize_rx, defaultAndOr, developer, helpers, t, underscore_rx1, underscore_rx2, _Batman, _implementImmediates, _objectToString,
+ var $addEventListener, $appendChild, $clearImmediate, $contains, $destroyNode, $escapeHTML, $extend, $forEach, $forgetParseExit, $functionName, $get, $getPath, $hasAddEventListener, $insertBefore, $isChildOf, $mixin, $objectHasKey, $onParseExit, $preventDefault, $propagateBindingEvent, $propagateBindingEvents, $redirect, $removeEventListener, $removeNode, $removeOrDestroyNode, $setImmediate, $setInnerHTML, $setStyleProperty, $stopPropagation, $trackBinding, $typeOf, $unmixin, Batman, BatmanObject, Validators, Yield, buntUndefined, camelize_rx, capitalize_rx, defaultAndOr, developer, helpers, t, underscore_rx1, underscore_rx2, _Batman, _implementImmediates, _objectToString,
__slice = [].slice,
__hasProp = {}.hasOwnProperty,
__indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; },
@@ -34,6 +34,19 @@
_objectToString = Object.prototype.toString;
+ Batman.extend = $extend = function() {
+ var key, object, objects, to, value, _i, _len;
+ to = arguments[0], objects = 2 <= arguments.length ? __slice.call(arguments, 1) : [];
+ for (_i = 0, _len = objects.length; _i < _len; _i++) {
+ object = objects[_i];
+ for (key in object) {
+ value = object[key];
+ to[key] = value;
+ }
+ }
+ return to;
+ };
+
Batman.mixin = $mixin = function() {
var hasSet, key, mixin, mixins, to, value, _i, _len;
to = arguments[0], mixins = 2 <= arguments.length ? __slice.call(arguments, 1) : [];
@@ -362,7 +375,7 @@
}
},
addFilters: function() {
- return $mixin(Batman.Filters, {
+ return $extend(Batman.Filters, {
log: function(value, key) {
if (typeof console !== "undefined" && console !== null) {
if (typeof console.log === "function") {
@@ -1431,7 +1444,7 @@
} : results.every(function(x) {
return typeof x === 'object';
}) ? (results.unshift({}), function(a, b) {
- return $mixin(a, b);
+ return $extend(a, b);
}) : void 0;
if (reduction) {
return results.reduceRight(reduction);
@@ -1827,17 +1840,6 @@
}
};
- Batman.extendsEnumerable = $extendsEnumerable = function(onto) {
- var k, v, _ref, _results;
- _ref = Batman.Enumerable;
- _results = [];
- for (k in _ref) {
- v = _ref[k];
- _results.push(onto[k] = v);
- }
- return _results;
- };
-
Batman.SimpleHash = (function() {
SimpleHash.name = 'SimpleHash';
@@ -1850,7 +1852,7 @@
}
}
- $extendsEnumerable(SimpleHash.prototype);
+ $extend(SimpleHash.prototype, Batman.Enumerable);
SimpleHash.prototype.propertyClass = Batman.Property;
@@ -2119,7 +2121,7 @@
Hash.__super__.constructor.apply(this, arguments);
}
- $extendsEnumerable(Hash.prototype);
+ $extend(Hash.prototype, Batman.Enumerable);
Hash.prototype.propertyClass = Batman.Property;
@@ -2249,7 +2251,7 @@
}
}
- $extendsEnumerable(SimpleSet.prototype);
+ $extend(SimpleSet.prototype, Batman.Enumerable);
SimpleSet.prototype.has = function(item) {
return !!(~this._storage.indexOf(item));
@@ -2400,7 +2402,7 @@
Batman.SimpleSet.apply(this, arguments);
}
- $extendsEnumerable(Set.prototype);
+ $extend(Set.prototype, Batman.Enumerable);
Set._applySetAccessors = function(klass) {
var accessor, accessors, key, _results;
@@ -2611,7 +2613,7 @@
});
}
- $extendsEnumerable(SetProxy.prototype);
+ $extend(SetProxy.prototype, Batman.Enumerable);
SetProxy.prototype.filter = function(f) {
var r;
@@ -2803,7 +2805,7 @@
return this.toArray();
});
- $extendsEnumerable(SetIndex.prototype);
+ $extend(SetIndex.prototype, Batman.Enumerable);
SetIndex.prototype.propertyClass = Batman.Property;
@@ -3106,7 +3108,7 @@
}
table[k] = object;
}
- this.prototype.transitionTable = $mixin({}, this.prototype.transitionTable, table);
+ this.prototype.transitionTable = $extend({}, this.prototype.transitionTable, table);
_ref = this.prototype.transitionTable;
for (k in _ref) {
transitions = _ref[k];
@@ -3406,7 +3408,7 @@
var index, key, match, matches, name, namedArguments, pair, params, query, value, _i, _j, _len, _len1, _ref, _ref1, _ref2;
_ref = path.split('?'), path = _ref[0], query = _ref[1];
namedArguments = this.get('namedArguments');
- params = $mixin({
+ params = $extend({
path: path
}, this.get('baseParams'));
matches = this.get('regexp').exec(path).slice(1);
@@ -3428,7 +3430,7 @@
Route.prototype.pathFromParams = function(argumentParams) {
var key, name, newPath, params, path, queryParams, regexp, value, _i, _j, _len, _len1, _ref, _ref1;
- params = $mixin({}, argumentParams);
+ params = $extend({}, argumentParams);
path = this.get('templatePath');
_ref = this.get('namedArguments');
for (_i = 0, _len = _ref.length; _i < _len; _i++) {
@@ -3994,7 +3996,7 @@
route = this.constructor.ROUTES[action];
as = route.name(resourceRoot);
path = route.path(resourceRoot);
- routeOptions = $mixin({
+ routeOptions = $extend({
controller: controller,
action: action,
path: path,
@@ -4057,13 +4059,13 @@
names.push(options);
options = {};
}
- options = $mixin({}, this.baseOptions, options);
+ options = $extend({}, this.baseOptions, options);
options[cardinality] = true;
route = this.constructor.ROUTES[cardinality];
resourceRoot = options.controller;
for (_j = 0, _len = names.length; _j < _len; _j++) {
name = names[_j];
- routeOptions = $mixin({
+ routeOptions = $extend({
action: name
}, options);
if (routeOptions.path == null) {
@@ -4662,7 +4664,7 @@
RenderCache.prototype.viewForOptions = function(options) {
var _this = this;
return this.getOrSet(options, function() {
- return _this._newViewFromOptions($mixin({}, options));
+ return _this._newViewFromOptions($extend({}, options));
});
};
@@ -4975,27 +4977,19 @@
Model.storageKey = null;
- Model.persist = function() {
- var mechanism, mechanisms, results, storage, _base;
- mechanisms = 1 <= arguments.length ? __slice.call(arguments, 0) : [];
+ Model.persist = function(mechanism, options) {
Batman.initializeObject(this.prototype);
- storage = (_base = this.prototype._batman).storage || (_base.storage = []);
- results = (function() {
- var _i, _len, _results;
- _results = [];
- for (_i = 0, _len = mechanisms.length; _i < _len; _i++) {
- mechanism = mechanisms[_i];
- mechanism = mechanism.isStorageAdapter ? mechanism : new mechanism(this);
- storage.push(mechanism);
- _results.push(mechanism);
- }
- return _results;
- }).call(this);
- if (results.length > 1) {
- return results;
- } else {
- return results[0];
+ mechanism = mechanism.isStorageAdapter ? mechanism : new mechanism(this);
+ if (options) {
+ $mixin(mechanism, options);
}
+ this.prototype._batman.storage = mechanism;
+ return mechanism;
+ };
+
+ Model.storageAdapter = function() {
+ Batman.initializeObject(this.prototype);
+ return this.prototype._batman.storage;
};
Model.encode = function() {
@@ -5020,7 +5014,7 @@
encoder.decode = encoderOrLastKey.decode;
}
}
- encoder = $mixin({}, this.defaultEncoder, encoder);
+ encoder = $extend({}, this.defaultEncoder, encoder);
_ref = ['encode', 'decode'];
for (_j = 0, _len = _ref.length; _j < _len; _j++) {
operation = _ref[_j];
@@ -5223,10 +5217,11 @@
callback = options;
options = {};
}
- developer.assert(this.prototype._batman.getAll('storage').length, "Can't load model " + ($functionName(this)) + " without any storage adapters!");
lifecycle = this.get('lifecycle');
if (lifecycle.load()) {
- return this.prototype._doStorageOperation('readAll', options, function(err, records, env) {
+ return this._doStorageOperation('readAll', {
+ data: options
+ }, function(err, records, env) {
var mappedRecords, record;
if (err != null) {
lifecycle.error();
@@ -5288,6 +5283,13 @@
}
};
+ Model._doStorageOperation = function(operation, options, callback) {
+ var adapter;
+ developer.assert(this.prototype.hasStorage(), "Can't " + operation + " model " + ($functionName(this.constructor)) + " without any storage adapters!");
+ adapter = this.prototype._batman.get('storage');
+ return adapter.perform(operation, this, options, callback);
+ };
+
Model.InstanceLifecycleStateMachine = (function(_super1) {
__extends(InstanceLifecycleStateMachine, _super1);
@@ -5485,7 +5487,7 @@
};
Model.prototype.hasStorage = function() {
- return (this._batman.get('storage') || []).length > 0;
+ return this._batman.get('storage') != null;
};
Model.prototype.load = function(options, callback) {
@@ -5509,7 +5511,9 @@
if (!hasOptions) {
this._currentLoad = callbackQueue;
}
- return this._doStorageOperation('read', options, function(err, record, env) {
+ return this._doStorageOperation('read', {
+ data: options
+ }, function(err, record, env) {
var callback, _i, _len;
if (!err) {
_this.get('lifecycle').loaded();
@@ -5570,7 +5574,9 @@
}
}
_this._pauseDirtyTracking = false;
- return _this._doStorageOperation(storageOperation, options, function(err, record, env) {
+ return _this._doStorageOperation(storageOperation, {
+ data: options
+ }, function(err, record, env) {
var _ref4, _ref5;
if (!err) {
_this.get('dirtyKeys').clear();
@@ -5606,7 +5612,9 @@
_ref = [{}, options], options = _ref[0], callback = _ref[1];
}
if (this.get('lifecycle').destroy()) {
- return this._doStorageOperation('destroy', options, function(err, record, env) {
+ return this._doStorageOperation('destroy', {
+ data: options
+ }, function(err, record, env) {
if (!err) {
_this.constructor.get('loaded').remove(_this);
_this.get('lifecycle').destroyed();
@@ -5682,21 +5690,15 @@
};
Model.prototype._doStorageOperation = function(operation, options, callback) {
- var adapter, adapters, _i, _len,
+ var adapter,
_this = this;
developer.assert(this.hasStorage(), "Can't " + operation + " model " + ($functionName(this.constructor)) + " without any storage adapters!");
- adapters = this._batman.get('storage');
- for (_i = 0, _len = adapters.length; _i < _len; _i++) {
- adapter = adapters[_i];
- this._pauseDirtyTracking = true;
- adapter.perform(operation, this, {
- data: options
- }, function() {
- callback.apply(null, arguments);
- return _this._pauseDirtyTracking = false;
- });
- }
- return true;
+ adapter = this._batman.get('storage');
+ this._pauseDirtyTracking = true;
+ return adapter.perform(operation, this, options, function() {
+ callback.apply(null, arguments);
+ return _this._pauseDirtyTracking = false;
+ });
};
return Model;
@@ -6114,7 +6116,7 @@
namespace: Batman.currentApp,
name: helpers.camelize(helpers.singularize(this.label))
};
- this.options = $mixin(defaultOptions, this.defaultOptions, options);
+ this.options = $extend(defaultOptions, this.defaultOptions, options);
this.model.encode(label, this.encoder());
self = this;
getAccessor = function() {
@@ -6954,7 +6956,7 @@
})(Batman.Validator)
];
- $mixin(Batman.translate.messages, {
+ $extend(Batman.translate.messages, {
errors: {
format: "%{attribute} %{message}",
messages: {
@@ -7023,9 +7025,17 @@
})(StorageAdapter.StorageError);
function StorageAdapter(model) {
+ var constructor;
StorageAdapter.__super__.constructor.call(this, {
model: model
});
+ constructor = this.constructor;
+ if (constructor.ModelMixin) {
+ $extend(model, constructor.ModelMixin);
+ }
+ if (constructor.RecordMixin) {
+ $extend(model.prototype, constructor.RecordMixin);
+ }
}
StorageAdapter.prototype.isStorageAdapter = true;
@@ -7110,7 +7120,7 @@
allFilters = this._batman.filters[position].all || [];
actionFilters = this._batman.filters[position][action] || [];
env.action = action;
- filters = actionFilters.concat(allFilters);
+ filters = position === 'before' ? actionFilters.concat(allFilters) : allFilters.concat(actionFilters);
next = function(newEnv) {
var nextFilter;
if (newEnv != null) {
@@ -7143,18 +7153,14 @@
return JSON.parse(json);
};
- StorageAdapter.prototype.perform = function(key, recordOrProto, options, callback) {
+ StorageAdapter.prototype.perform = function(key, subject, options, callback) {
var env, next,
_this = this;
options || (options = {});
env = {
- options: options
+ options: options,
+ subject: subject
};
- if (key === 'readAll') {
- env.proto = recordOrProto;
- } else {
- env.record = recordOrProto;
- }
next = function(newEnv) {
if (newEnv != null) {
env = newEnv;
@@ -7210,15 +7216,15 @@
return true;
};
- LocalStorage.prototype._storageEntriesMatching = function(proto, options) {
+ LocalStorage.prototype._storageEntriesMatching = function(constructor, options) {
var re, records;
- re = this.storageRegExpForRecord(proto);
+ re = this.storageRegExpForRecord(constructor.prototype);
records = [];
this._forAllStorageEntries(function(storageKey, storageString) {
var data, keyMatches;
if (keyMatches = re.exec(storageKey)) {
data = this._jsonToAttributes(storageString);
- data[proto.constructor.primaryKey] = keyMatches[1];
+ data[constructor.primaryKey] = keyMatches[1];
if (this._dataMatches(options, data)) {
return records.push(data);
}
@@ -7242,20 +7248,20 @@
LocalStorage.prototype.before('read', 'create', 'update', 'destroy', LocalStorage.skipIfError(function(env, next) {
if (env.action === 'create') {
- env.id = env.record.get('id') || env.record.set('id', this.nextIdForRecord(env.record));
+ env.id = env.subject.get('id') || env.subject.set('id', this.nextIdForRecord(env.subject));
} else {
- env.id = env.record.get('id');
+ env.id = env.subject.get('id');
}
if (env.id == null) {
env.error = new this.constructor.StorageError("Couldn't get/set record primary key on " + env.action + "!");
} else {
- env.key = this.storageKey(env.record) + env.id;
+ env.key = this.storageKey(env.subject) + env.id;
}
return next();
}));
LocalStorage.prototype.before('create', 'update', LocalStorage.skipIfError(function(env, next) {
- env.recordAttributes = JSON.stringify(env.record);
+ env.recordAttributes = JSON.stringify(env.subject);
return next();
}));
@@ -7268,12 +7274,12 @@
return next();
}
}
- env.record.fromJSON(env.recordAttributes);
+ env.subject.fromJSON(env.recordAttributes);
return next();
}));
LocalStorage.prototype.after('read', 'create', 'update', 'destroy', LocalStorage.skipIfError(function(env, next) {
- env.result = env.record;
+ env.result = env.subject;
return next();
}));
@@ -7285,18 +7291,13 @@
_results = [];
for (_i = 0, _len = _ref.length; _i < _len; _i++) {
recordAttributes = _ref[_i];
- _results.push(this.getRecordFromData(recordAttributes, env.proto.constructor));
+ _results.push(this.getRecordFromData(recordAttributes, env.subject));
}
return _results;
}).call(this);
return next();
}));
- LocalStorage.prototype.after('destroy', LocalStorage.skipIfError(function(env, next) {
- env.result = env.record;
- return next();
- }));
-
LocalStorage.prototype.read = LocalStorage.skipIfError(function(env, next) {
env.recordAttributes = this.storage.getItem(env.key);
if (!env.recordAttributes) {
@@ -7330,11 +7331,9 @@
return next();
});
- LocalStorage.prototype.readAll = LocalStorage.skipIfError(function(_arg, next) {
- var options, proto;
- proto = _arg.proto, options = _arg.options;
+ LocalStorage.prototype.readAll = LocalStorage.skipIfError(function(env, next) {
try {
- arguments[0].recordsAttributes = this._storageEntriesMatching(proto, options.data);
+ arguments[0].recordsAttributes = this._storageEntriesMatching(env.subject, env.options.data);
} catch (error) {
arguments[0].error = error;
}
@@ -7375,23 +7374,83 @@
RestStorage.PostBodyContentType = 'application/x-www-form-urlencoded';
+ RestStorage.BaseMixin = {
+ request: function(action, options, callback) {
+ if (!callback) {
+ callback = options;
+ options = {};
+ }
+ options.method || (options.method = 'GET');
+ options.action = action;
+ return this._doStorageOperation(options.method.toLowerCase(), options, callback);
+ }
+ };
+
+ RestStorage.ModelMixin = $extend({}, RestStorage.BaseMixin, {
+ urlNestsUnder: function() {
+ var children, key, keys, parents, _i, _len;
+ keys = 1 <= arguments.length ? __slice.call(arguments, 0) : [];
+ parents = {};
+ for (_i = 0, _len = keys.length; _i < _len; _i++) {
+ key = keys[_i];
+ parents[key + '_id'] = Batman.helpers.pluralize(key);
+ }
+ children = Batman.helpers.pluralize(Batman._functionName(this).toLowerCase());
+ this.url = function(options) {
+ var key, parentID, plural;
+ for (key in parents) {
+ plural = parents[key];
+ parentID = options.data[key];
+ if (parentID) {
+ delete options.data[key];
+ return "" + plural + "/" + parentID + "/" + children;
+ }
+ }
+ return children;
+ };
+ return this.prototype.url = function() {
+ var id, key, parentID, plural, url;
+ for (key in parents) {
+ plural = parents[key];
+ parentID = this.dirtyKeys.get(key);
+ if (parentID === void 0) {
+ parentID = this.get(key);
+ }
+ if (parentID) {
+ url = "" + plural + "/" + parentID + "/" + children;
+ break;
+ }
+ }
+ url || (url = children);
+ if (id = this.get('id')) {
+ url += '/' + id;
+ }
+ return url;
+ };
+ }
+ });
+
+ RestStorage.RecordMixin = $extend({}, RestStorage.BaseMixin);
+
RestStorage.prototype.defaultRequestOptions = {
type: 'json'
};
+ RestStorage.prototype._implicitActionNames = ['create', 'read', 'update', 'destroy', 'readAll'];
+
RestStorage.prototype.serializeAsForm = true;
function RestStorage() {
RestStorage.__super__.constructor.apply(this, arguments);
- this.defaultRequestOptions = $mixin({}, this.defaultRequestOptions);
+ this.defaultRequestOptions = $extend({}, this.defaultRequestOptions);
}
RestStorage.prototype.recordJsonNamespace = function(record) {
return helpers.singularize(this.storageKey(record));
};
- RestStorage.prototype.collectionJsonNamespace = function(proto) {
- return helpers.pluralize(this.storageKey(proto));
+ RestStorage.prototype.collectionJsonNamespace = function(constructor) {
+ return helpers.pluralize(this.storageKey(constructor.prototype));
};
RestStorage.prototype._execWithOptions = function(object, key, options) {
@@ -7402,8 +7461,16 @@
}
};
- RestStorage.prototype._defaultCollectionUrl = function(record) {
- return "/" + (this.storageKey(record));
+ RestStorage.prototype._defaultCollectionUrl = function(model) {
+ return "/" + (this.storageKey(model.prototype));
+ };
+
+ RestStorage.prototype._addParams = function(url, options) {
+ var _ref;
+ if (options && options.action && !(_ref = options.action, __indexOf.call(this._implicitActionNames, _ref) >= 0)) {
+ url += '/' + options.action.toLowerCase();
+ }
+ return url;
};
RestStorage.prototype.urlForRecord = function(record, env) {
@@ -7411,7 +7478,7 @@
if (record.url) {
url = this._execWithOptions(record, 'url', env.options);
} else {
- url = record.constructor.url ? this._execWithOptions(record.constructor, 'url', env.options) : this._defaultCollectionUrl(record);
+ url = record.constructor.url ? this._execWithOptions(record.constructor, 'url', env.options) : this._defaultCollectionUrl(record.constructor);
if (env.action !== 'create') {
if ((id = record.get('id')) != null) {
url = url + "/" + id;
@@ -7420,12 +7487,14 @@
}
}
}
+ url = this._addParams(url, env.options);
return this.urlPrefix(record, env) + url + this.urlSuffix(record, env);
};
RestStorage.prototype.urlForCollection = function(model, env) {
var url;
- url = model.url ? this._execWithOptions(model, 'url', env.options) : this._defaultCollectionUrl(model.prototype, env.options);
+ url = model.url ? this._execWithOptions(model, 'url', env.options) : this._defaultCollectionUrl(model, env.options);
+ url = this._addParams(url, env.options);
return this.urlPrefix(model, env) + url + this.urlSuffix(model, env);
};
@@ -7439,7 +7508,7 @@
RestStorage.prototype.request = function(env, next) {
var options;
- options = $mixin(env.options, {
+ options = $extend(env.options, {
success: function(data) {
return env.data = data;
},
@@ -7455,32 +7524,29 @@
};
RestStorage.prototype.perform = function(key, record, options, callback) {
- $mixin((options || (options = {})), this.defaultRequestOptions);
- return RestStorage.__super__.perform.apply(this, arguments);
+ options || (options = {});
+ $extend(options, this.defaultRequestOptions);
+ return RestStorage.__super__.perform.call(this, key, record, options, callback);
};
- RestStorage.prototype.before('create', 'read', 'update', 'destroy', RestStorage.skipIfError(function(env, next) {
+ RestStorage.prototype.before('all', RestStorage.skipIfError(function(env, next) {
try {
- env.options.url = this.urlForRecord(env.record, env);
+ env.options.url = env.subject.prototype ? this.urlForCollection(env.subject, env) : this.urlForRecord(env.subject, env);
} catch (error) {
env.error = error;
}
return next();
}));
- RestStorage.prototype.before('readAll', RestStorage.skipIfError(function(env, next) {
- try {
- env.options.url = this.urlForCollection(env.proto.constructor, env);
- } catch (error) {
- env.error = error;
- }
+ RestStorage.prototype.before('get', 'put', 'post', 'delete', RestStorage.skipIfError(function(env, next) {
+ env.options.method = env.action.toUpperCase();
return next();
}));
RestStorage.prototype.before('create', 'update', RestStorage.skipIfError(function(env, next) {
var data, json, namespace;
- json = env.record.toJSON();
- if (namespace = this.recordJsonNamespace(env.record)) {
+ json = env.subject.toJSON();
+ if (namespace = this.recordJsonNamespace(env.subject)) {
data = {};
data[namespace] = json;
} else {
@@ -7496,8 +7562,8 @@
return next();
}));
- RestStorage.prototype.after('create', 'read', 'update', RestStorage.skipIfError(function(env, next) {
- var json, namespace;
+ RestStorage.prototype.after('all', RestStorage.skipIfError(function(env, next) {
+ var json;
if (!(env.data != null)) {
return next();
}
@@ -7513,42 +7579,46 @@
} else {
json = env.data;
}
- if (json != null) {
- namespace = this.recordJsonNamespace(env.record);
- if (namespace && (json[namespace] != null)) {
- json = json[namespace];
- }
- env.record.fromJSON(json);
+ env.json = json;
+ return next();
+ }));
+
+ RestStorage.prototype.after('create', 'read', 'update', RestStorage.skipIfError(function(env, next) {
+ var json, namespace;
+ if (env.json != null) {
+ namespace = this.recordJsonNamespace(env.subject);
+ json = namespace && (env.json[namespace] != null) ? env.json[namespace] : env.json;
+ env.subject.fromJSON(json);
}
- env.result = env.record;
+ env.result = env.subject;
return next();
}));
RestStorage.prototype.after('readAll', RestStorage.skipIfError(function(env, next) {
var jsonRecordAttributes, namespace;
- if (typeof env.data === 'string') {
- try {
- env.data = JSON.parse(env.data);
- } catch (jsonError) {
- env.error = jsonError;
- return next();
- }
- }
- namespace = this.collectionJsonNamespace(env.proto);
- env.recordsAttributes = namespace && (env.data[namespace] != null) ? env.data[namespace] : env.data;
+ namespace = this.collectionJsonNamespace(env.subject);
+ env.recordsAttributes = namespace && (env.json[namespace] != null) ? env.json[namespace] : env.json;
env.result = env.records = (function() {
var _i, _len, _ref, _results;
_ref = env.recordsAttributes;
_results = [];
for (_i = 0, _len = _ref.length; _i < _len; _i++) {
jsonRecordAttributes = _ref[_i];
- _results.push(this.getRecordFromData(jsonRecordAttributes, env.proto.constructor));
+ _results.push(this.getRecordFromData(jsonRecordAttributes, env.subject));
}
return _results;
}).call(this);
return next();
}));
+ RestStorage.prototype.after('get', 'put', 'post', 'delete', RestStorage.skipIfError(function(env, next) {
+ var json, namespace;
+ json = env.json;
+ namespace = env.subject.prototype ? this.collectionJsonNamespace(env.subject) : this.recordJsonNamespace(env.subject);
+ env.result = namespace && (env.json[namespace] != null) ? env.json[namespace] : env.json;
+ return next();
+ }));
+
RestStorage.HTTPMethods = {
create: 'POST',
update: 'PUT',
@@ -7557,10 +7627,11 @@
destroy: 'DELETE'
};
- _ref = ['create', 'read', 'update', 'destroy', 'readAll'];
+ _ref = ['create', 'read', 'update', 'destroy', 'readAll', 'get', 'post', 'put', 'delete'];
_fn = function(key) {
return RestStorage.prototype[key] = RestStorage.skipIfError(function(env, next) {
- env.options.method = this.constructor.HTTPMethods[key];
+ var _base;
+ (_base = env.options).method || (_base.method = this.constructor.HTTPMethods[key]);
return this.request(env, next);
});
};
@@ -10501,7 +10572,7 @@
}
return true;
};
- return $mixin(Batman, {
+ return $extend(Batman, {
cache: {},
uuid: 0,
expando: "batman" + Math.random().toString().replace(/\D/g, ''),
@@ -10547,9 +10618,9 @@
}
if (typeof name === "object" || typeof name === "function") {
if (pvt) {
- cache[id][internalKey] = $mixin(cache[id][internalKey], name);
+ cache[id][internalKey] = $extend(cache[id][internalKey], name);
} else {
- cache[id] = $mixin(cache[id], name);
+ cache[id] = $extend(cache[id], name);
}
}
thisCache = cache[id];
@@ -10641,7 +10712,7 @@
Batman.exportHelpers = function(onto) {
var k, _i, _len, _ref;
- _ref = ['mixin', 'unmixin', 'route', 'redirect', 'typeOf', 'redirect', 'setImmediate'];
+ _ref = ['mixin', 'extend', 'unmixin', 'redirect', 'typeOf', 'redirect', 'setImmediate'];
for (_i = 0, _len = _ref.length; _i < _len; _i++) {
k = _ref[_i];
onto["$" + k] = Batman[k];
View
8 lib/extras/batman.rails.js
@@ -134,10 +134,10 @@
return next();
});
- RailsStorage.prototype.after('update', 'create', function(_arg, next) {
- var env, error, errorsArray, key, record, response, validationError, validationErrors, _i, _len, _ref;
- error = _arg.error, record = _arg.record, response = _arg.response;
- env = arguments[0];
+ RailsStorage.prototype.after('update', 'create', function(env, next) {
+ var error, errorsArray, key, record, response, validationError, validationErrors, _i, _len, _ref;
+ record = env.subject;
+ error = env.error, response = env.response;
if (error) {
if (((_ref = error.request) != null ? _ref.get('status') : void 0) === 422) {
try {
View
292 src/batman.coffee
@@ -36,6 +36,10 @@ Batman.typeOf = $typeOf = (object) ->
# Cache this function to skip property lookups.
_objectToString = Object.prototype.toString
+Batman.extend = $extend = (to, objects...) ->
+ to[key] = value for key, value of object for object in objects
+ to
+
# `$mixin` applies every key from every argument after the first to the
# first argument. If a mixin has an `initialize` method, it will be called in
# the context of the `to` object, and it's key/values won't be applied.
@@ -239,7 +243,7 @@ Batman.developer =
assert: (result, message) -> developer.error(message) unless result
do: (f) -> f() unless developer.suppressed
addFilters: ->
- $mixin Batman.Filters,
+ $extend Batman.Filters,
log: (value, key) ->
console?.log? arguments
value
@@ -832,7 +836,7 @@ Batman._Batman = class _Batman
(a, b) -> a.merge(b)
else if results.every((x) -> typeof x is 'object')
results.unshift({})
- (a, b) -> $mixin(a, b)
+ (a, b) -> $extend(a, b)
if reduction
results.reduceRight(reduction)
@@ -1039,17 +1043,12 @@ Batman.Enumerable =
current.push x
r
-# Provide this simple mixin ability so that during bootstrapping we don't have to use `$mixin`. `$mixin`
-# will correctly attempt to use `set` on the mixinee, which ends up requiring the definition of
-# `SimpleSet` to be complete during its definition.
-Batman.extendsEnumerable = $extendsEnumerable = (onto) -> onto[k] = v for k,v of Batman.Enumerable
-
class Batman.SimpleHash
constructor: (obj) ->
@_storage = {}
@length = 0
@update(obj) if obj?
- $extendsEnumerable(@prototype)
+ $extend @prototype, Batman.Enumerable
propertyClass: Batman.Property
hasKey: (key) ->
if @objectKey(key)
@@ -1167,7 +1166,7 @@ class Batman.Hash extends Batman.Object
Batman.SimpleHash.apply(@, arguments)
super
- $extendsEnumerable(@prototype)
+ $extend @prototype, Batman.Enumerable
propertyClass: Batman.Property
@defaultAccessor =
@@ -1236,7 +1235,7 @@ class Batman.SimpleSet
@length = 0
@add.apply @, arguments if arguments.length > 0
- $extendsEnumerable(@prototype)
+ $extend @prototype, Batman.Enumerable
has: (item) ->
!!(~@_storage.indexOf item)
@@ -1307,7 +1306,7 @@ class Batman.Set extends Batman.Object
constructor: ->
Batman.SimpleSet.apply @, arguments
- $extendsEnumerable(@prototype)
+ $extend @prototype, Batman.Enumerable
@_applySetAccessors = (klass) ->
accessors =
@@ -1385,7 +1384,7 @@ class Batman.SetProxy extends Batman.Object
@set 'length', @base.length
@fire('itemsWereRemoved', items...)
- $extendsEnumerable(@prototype)
+ $extend @prototype, Batman.Enumerable
filter: (f) ->
r = new Batman.Set()
@@ -1463,7 +1462,7 @@ class Batman.SetSort extends Batman.SetProxy
class Batman.SetIndex extends Batman.Object
@accessor 'toArray', -> @toArray()
- $extendsEnumerable(@prototype)
+ $extend @prototype, Batman.Enumerable
propertyClass: Batman.Property
constructor: (@base, @key) ->
super()
@@ -1569,7 +1568,7 @@ class Batman.StateMachine extends Batman.Object
object[v.from] = v.to
table[k] = object
- @::transitionTable = $mixin {}, @::transitionTable, table
+ @::transitionTable = $extend {}, @::transitionTable, table
for k, transitions of @::transitionTable when !@::[k]
do (k) =>
@::[k] = -> @startTransition(k)
@@ -1648,7 +1647,7 @@ class Batman.Request extends Batman.Object
formData.append(key, val)
formData
- @dataHasFileUploads = dataHasFileUploads = (data) ->
+ @dataHasFileUploads: dataHasFileUploads = (data) ->
return true if data instanceof File
type = $typeOf(data)
switch type
@@ -1732,7 +1731,7 @@ class Batman.Route extends Batman.Object
paramsFromPath: (path) ->
[path, query] = path.split '?'
namedArguments = @get('namedArguments')
- params = $mixin {path}, @get('baseParams')
+ params = $extend {path}, @get('baseParams')
matches = @get('regexp').exec(path).slice(1)
for match, index in matches
@@ -1747,7 +1746,7 @@ class Batman.Route extends Batman.Object
params
pathFromParams: (argumentParams) ->
- params = $mixin {}, argumentParams
+ params = $extend {}, argumentParams
path = @get('templatePath')
# Replace the names in the template with their values from params
@@ -2047,7 +2046,7 @@ class Batman.RouteMapBuilder
route = @constructor.ROUTES[action]
as = route.name(resourceRoot)
path = route.path(resourceRoot)
- routeOptions = $mixin {controller, action, path, as}, options
+ routeOptions = $extend {controller, action, path, as}, options
childBuilder[route.cardinality](action, routeOptions)
true
@@ -2083,12 +2082,12 @@ class Batman.RouteMapBuilder
if typeof options is 'string'
names.push options
options = {}
- options = $mixin {}, @baseOptions, options
+ options = $extend {}, @baseOptions, options
options[cardinality] = true
route = @constructor.ROUTES[cardinality]
resourceRoot = options.controller
for name in names
- routeOptions = $mixin {action: name}, options
+ routeOptions = $extend {action: name}, options
unless routeOptions.path?
routeOptions.path = route.path(resourceRoot, name)
unless routeOptions.as?
@@ -2403,7 +2402,7 @@ class Batman.RenderCache extends Batman.Hash
viewForOptions: (options) ->
@getOrSet options, =>
- @_newViewFromOptions($mixin {}, options)
+ @_newViewFromOptions($extend {}, options)
_newViewFromOptions: (options) -> new options.viewClass(options)
@@ -2610,17 +2609,16 @@ class Batman.Model extends Batman.Object
# Pick one or many mechanisms with which this model should be persisted. The mechanisms
# can be already instantiated or just the class defining them.
- @persist: (mechanisms...) ->
+ @persist: (mechanism, options) ->
Batman.initializeObject @prototype
- storage = @::_batman.storage ||= []
- results = for mechanism in mechanisms
- mechanism = if mechanism.isStorageAdapter then mechanism else new mechanism(@)
- storage.push mechanism
- mechanism
- if results.length > 1
- results
- else
- results[0]
+ mechanism = if mechanism.isStorageAdapter then mechanism else new mechanism(@)
+ $mixin mechanism, options if options
+ @::_batman.storage = mechanism
+ mechanism
+
+ @storageAdapter: ->
+ Batman.initializeObject @prototype
+ @::_batman.storage
# Encoders are the tiny bits of logic which manage marshalling Batman models to and from their
# storage representations. Encoders do things like stringifying dates and parsing them back out again,
@@ -2640,7 +2638,7 @@ class Batman.Model extends Batman.Object
encoder.encode = encoderOrLastKey.encode if encoderOrLastKey.encode?
encoder.decode = encoderOrLastKey.decode if encoderOrLastKey.decode?
- encoder = $mixin {}, @defaultEncoder, encoder
+ encoder = $extend {}, @defaultEncoder, encoder
for operation in ['encode', 'decode']
for key in keys
@@ -2756,11 +2754,9 @@ class Batman.Model extends Batman.Object
callback = options
options = {}
- developer.assert @::_batman.getAll('storage').length, "Can't load model #{$functionName(@)} without any storage adapters!"
-
lifecycle = @get('lifecycle')
if lifecycle.load()
- @::_doStorageOperation 'readAll', options, (err, records, env) =>
+ @_doStorageOperation 'readAll', {data: options}, (err, records, env) =>
if err?
lifecycle.error()
callback?(err, [])
@@ -2803,6 +2799,10 @@ class Batman.Model extends Batman.Object
@get('loaded').add(record)
return record
+ @_doStorageOperation: (operation, options, callback) ->
+ developer.assert @::hasStorage(), "Can't #{operation} model #{$functionName(@constructor)} without any storage adapters!"
+ adapter = @::_batman.get('storage')
+ adapter.perform(operation, @, options, callback)
# Each model instance (each record) can be in one of many states throughout its lifetime. Since various
# operations on the model are asynchronous, these states are used to indicate exactly what point the
@@ -2936,7 +2936,7 @@ class Batman.Model extends Batman.Object
# Mixin the buffer object to use optimized and event-preventing sets used by `mixin`.
@mixin obj
- hasStorage: -> (@_batman.get('storage') || []).length > 0
+ hasStorage: -> @_batman.get('storage')?
# `load` fetches the record from all sources possible
load: (options, callback) =>
@@ -2952,7 +2952,7 @@ class Batman.Model extends Batman.Object
callbackQueue.push callback if callback?
if !hasOptions
@_currentLoad = callbackQueue
- @_doStorageOperation 'read', options, (err, record, env) =>
+ @_doStorageOperation 'read', {data: options}, (err, record, env) =>
unless err
@get('lifecycle').loaded()
record = @constructor._mapIdentity(record)
@@ -2994,7 +2994,7 @@ class Batman.Model extends Batman.Object
associations?.getByType('belongsTo')?.forEach (association, label) => association.apply(@)
@_pauseDirtyTracking = false
- @_doStorageOperation storageOperation, options, (err, record, env) =>
+ @_doStorageOperation storageOperation, {data: options}, (err, record, env) =>
unless err
@get('dirtyKeys').clear()
if associations
@@ -3014,7 +3014,7 @@ class Batman.Model extends Batman.Object
[options, callback] = [{}, options]
if @get('lifecycle').destroy()
- @_doStorageOperation 'destroy', options, (err, record, env) =>
+ @_doStorageOperation 'destroy', {data: options}, (err, record, env) =>
unless err
@constructor.get('loaded').remove(@)
@get('lifecycle').destroyed()
@@ -3069,14 +3069,11 @@ class Batman.Model extends Batman.Object
_doStorageOperation: (operation, options, callback) ->
developer.assert @hasStorage(), "Can't #{operation} model #{$functionName(@constructor)} without any storage adapters!"
- adapters = @_batman.get('storage')
- for adapter in adapters
- @_pauseDirtyTracking = true
- adapter.perform operation, @, {data: options}, =>
- callback(arguments...)
- @_pauseDirtyTracking = false
-
- true
+ adapter = @_batman.get('storage')
+ @_pauseDirtyTracking = true
+ adapter.perform operation, @, options, =>
+ callback(arguments...)
+ @_pauseDirtyTracking = false
# ## Associations
class Batman.AssociationProxy extends Batman.Object
@@ -3249,7 +3246,7 @@ class Batman.Association
defaultOptions =
namespace: Batman.currentApp
name: helpers.camelize(helpers.singularize(@label))
- @options = $mixin defaultOptions, @defaultOptions, options
+ @options = $extend defaultOptions, @defaultOptions, options
# Setup encoders and accessors for this association.
@model.encode label, @encoder()
@@ -3720,7 +3717,7 @@ Validators = Batman.Validators = [
callback()
]
-$mixin Batman.translate.messages,
+$extend Batman.translate.messages,
errors:
format: "%{attribute} %{message}"
messages:
@@ -3748,7 +3745,11 @@ class Batman.StorageAdapter extends Batman.Object
constructor: (message) ->
super(message || "Record couldn't be found in storage!")
- constructor: (model) -> super(model: model)
+ constructor: (model) ->
+ super(model: model)
+ constructor = @constructor
+ $extend model, constructor.ModelMixin if constructor.ModelMixin
+ $extend model.prototype, constructor.RecordMixin if constructor.RecordMixin
isStorageAdapter: true
@@ -3793,8 +3794,13 @@ class Batman.StorageAdapter extends Batman.Object
actionFilters = @_batman.filters[position][action] || []
env.action = action
- # Action specific filters execute first, and then the `all` filters.
- filters = actionFilters.concat(allFilters)
+ filters = if position == 'before'
+ # Action specific filter execute first, and then the `all` filters.
+ actionFilters.concat(allFilters)
+ else
+ # `all` filters execute first, and then the action specific filters
+ allFilters.concat(actionFilters)
+
next = (newEnv) =>
env = newEnv if newEnv?
if (nextFilter = filters.shift())?
@@ -3810,13 +3816,9 @@ class Batman.StorageAdapter extends Batman.Object
_jsonToAttributes: (json) -> JSON.parse(json)
- perform: (key, recordOrProto, options, callback) ->
+ perform: (key, subject, options, callback) ->
options ||= {}
- env = {options}
- if key == 'readAll'
- env.proto = recordOrProto
- else
- env.record = recordOrProto
+ env = {options, subject}
next = (newEnv) =>
env = newEnv if newEnv?
@@ -3847,13 +3849,13 @@ class Batman.LocalStorage extends Batman.StorageAdapter
iterator.call(@, key, @storage.getItem(key))
true
- _storageEntriesMatching: (proto, options) ->
- re = @storageRegExpForRecord(proto)
+ _storageEntriesMatching: (constructor, options) ->
+ re = @storageRegExpForRecord(constructor.prototype)
records = []
@_forAllStorageEntries (storageKey, storageString) ->
if keyMatches = re.exec(storageKey)
data = @_jsonToAttributes(storageString)
- data[proto.constructor.primaryKey] = keyMatches[1]
+ data[constructor.primaryKey] = keyMatches[1]
records.push data if @_dataMatches(options, data)
records
@@ -3867,19 +3869,19 @@ class Batman.LocalStorage extends Batman.StorageAdapter
@::before 'read', 'create', 'update', 'destroy', @skipIfError (env, next) ->
if env.action == 'create'
- env.id = env.record.get('id') || env.record.set('id', @nextIdForRecord(env.record))
+ env.id = env.subject.get('id') || env.subject.set('id', @nextIdForRecord(env.subject))
else
- env.id = env.record.get('id')
+ env.id = env.subject.get('id')
unless env.id?
env.error = new @constructor.StorageError("Couldn't get/set record primary key on #{env.action}!")
else
- env.key = @storageKey(env.record) + env.id
+ env.key = @storageKey(env.subject) + env.id
next()
@::before 'create', 'update', @skipIfError (env, next) ->
- env.recordAttributes = JSON.stringify(env.record)
+ env.recordAttributes = JSON.stringify(env.subject)
next()
@::after 'read', @skipIfError (env, next) ->
@@ -3889,20 +3891,16 @@ class Batman.LocalStorage extends Batman.StorageAdapter
catch error
env.error = error
return next()
- env.record.fromJSON env.recordAttributes
+ env.subject.fromJSON env.recordAttributes
next()
@::after 'read', 'create', 'update', 'destroy', @skipIfError (env, next) ->
- env.result = env.record
+ env.result = env.subject
next()
@::after 'readAll', @skipIfError (env, next) ->
env.result = env.records = for recordAttributes in env.recordsAttributes
- @getRecordFromData(recordAttributes, env.proto.constructor)
- next()
-
- @::after 'destroy', @skipIfError (env, next) ->
- env.result = env.record
+ @getRecordFromData(recordAttributes, env.subject)
next()
read: @skipIfError (env, next) ->
@@ -3926,9 +3924,9 @@ class Batman.LocalStorage extends Batman.StorageAdapter
@storage.removeItem(key)
next()
- readAll: @skipIfError ({proto, options}, next) ->
+ readAll: @skipIfError (env, next) ->
try
- arguments[0].recordsAttributes = @_storageEntriesMatching(proto, options.data)
+ arguments[0].recordsAttributes = @_storageEntriesMatching(env.subject, env.options.data)
catch error
arguments[0].error = error
next()
@@ -3944,20 +3942,64 @@ class Batman.RestStorage extends Batman.StorageAdapter
@JSONContentType: 'application/json'
@PostBodyContentType: 'application/x-www-form-urlencoded'
+ @BaseMixin =
+ request: (action, options, callback) ->
+ if !callback
+ callback = options
+ options = {}
+ options.method ||= 'GET'
+ options.action = action
+ @_doStorageOperation options.method.toLowerCase(), options, callback
+
+ @ModelMixin: $extend({}, @BaseMixin,
+ urlNestsUnder: (keys...) ->
+ parents = {}
+ for key in keys
+ parents[key + '_id'] = Batman.helpers.pluralize(key)
+ children = Batman.helpers.pluralize(Batman._functionName(@).toLowerCase())
+
+ @url = (options) ->
+ for key, plural of parents
+ parentID = options.data[key]
+ if parentID
+ delete options.data[key]
+ return "#{plural}/#{parentID}/#{children}"
+ return children
+
+ @::url = ->
+ for key, plural of parents
+ parentID = @dirtyKeys.get(key)
+ if parentID is undefined
+ parentID = @get(key)
+ if parentID
+ url = "#{plural}/#{parentID}/#{children}"
+ break
+ url ||= children
+ if id = @get('id')
+ url += '/' + id
+ url
+ )
+
+ @RecordMixin: $extend({}, @BaseMixin)
+
defaultRequestOptions:
type: 'json'
-
+ _implicitActionNames: ['create', 'read', 'update', 'destroy', 'readAll']
serializeAsForm: true
constructor: ->
super
- @defaultRequestOptions = $mixin {}, @defaultRequestOptions
+ @defaultRequestOptions = $extend {}, @defaultRequestOptions
recordJsonNamespace: (record) -> helpers.singularize(@storageKey(record))
- collectionJsonNamespace: (proto) -> helpers.pluralize(@storageKey(proto))
+ collectionJsonNamespace: (constructor) -> helpers.pluralize(@storageKey(constructor.prototype))
_execWithOptions: (object, key, options) -> if typeof object[key] is 'function' then object[key](options) else object[key]
- _defaultCollectionUrl: (record) -> "/#{@storageKey(record)}"
+ _defaultCollectionUrl: (model) -> "/#{@storageKey(model.prototype)}"
+ _addParams: (url, options) ->
+ if options && options.action && !(options.action in @_implicitActionNames)
+ url += '/' + options.action.toLowerCase()
+ url
urlForRecord: (record, env) ->
if record.url
@@ -3966,7 +4008,7 @@ class Batman.RestStorage extends Batman.StorageAdapter
url = if record.constructor.url
@_execWithOptions(record.constructor, 'url', env.options)
else
- @_defaultCollectionUrl(record)
+ @_defaultCollectionUrl(record.constructor)
if env.action != 'create'
if (id = record.get('id'))?
@@ -3974,13 +4016,17 @@ class Batman.RestStorage extends Batman.StorageAdapter
else
throw new @constructor.StorageError("Couldn't get/set record primary key on #{env.action}!")
+ url = @_addParams(url, env.options)
+
@urlPrefix(record, env) + url + @urlSuffix(record, env)
urlForCollection: (model, env) ->
url = if model.url
@_execWithOptions(model, 'url', env.options)
else
- @_defaultCollectionUrl(model::, env.options)
+ @_defaultCollectionUrl(model, env.options)
+
+ url = @_addParams(url, env.options)
@urlPrefix(model, env) + url + @urlSuffix(model, env)
@@ -3991,7 +4037,7 @@ class Batman.RestStorage extends Batman.StorageAdapter
@_execWithOptions(object, 'urlSuffix', env.options) || ''
request: (env, next) ->
- options = $mixin env.options,
+ options = $extend env.options,
success: (data) -> env.data = data
error: (error) -> env.error = error
loaded: ->
@@ -4001,26 +4047,27 @@ class Batman.RestStorage extends Batman.StorageAdapter
env.request = new Batman.Request(options)
perform: (key, record, options, callback) ->
- $mixin (options ||= {}), @defaultRequestOptions
- super
+ options ||= {}
+ $extend options, @defaultRequestOptions
+ super(key, record, options, callback)
- @::before 'create', 'read', 'update', 'destroy', @skipIfError (env, next) ->
+ @::before 'all', @skipIfError (env, next) ->
try
- env.options.url = @urlForRecord(env.record, env)
+ env.options.url = if env.subject.prototype
+ @urlForCollection(env.subject, env)
+ else
+ @urlForRecord(env.subject, env)
catch error
env.error = error
next()
- @::before 'readAll', @skipIfError (env, next) ->
- try
- env.options.url = @urlForCollection(env.proto.constructor, env)
- catch error
- env.error = error
+ @::before 'get', 'put', 'post', 'delete', @skipIfError (env, next) ->
+ env.options.method = env.action.toUpperCase()
next()
@::before 'create', 'update', @skipIfError (env, next) ->
- json = env.record.toJSON()
- if namespace = @recordJsonNamespace(env.record)
+ json = env.subject.toJSON()
+ if namespace = @recordJsonNamespace(env.subject)
data = {}
data[namespace] = json
else
@@ -4036,7 +4083,7 @@ class Batman.RestStorage extends Batman.StorageAdapter
env.options.data = data
next()
- @::after 'create', 'read', 'update', @skipIfError (env, next) ->
+ @::after 'all', @skipIfError (env, next) ->
if !env.data?
return next()
@@ -4050,29 +4097,41 @@ class Batman.RestStorage extends Batman.StorageAdapter
else
json = env.data
- if json?
- namespace = @recordJsonNamespace(env.record)
- json = json[namespace] if namespace && json[namespace]?
- env.record.fromJSON(json)
- env.result = env.record
+ env.json = json
next()
- @::after 'readAll', @skipIfError (env, next) ->
- if typeof env.data is 'string'
- try
- env.data = JSON.parse(env.data)
- catch jsonError
- env.error = jsonError
- return next()
+ @::after 'create', 'read', 'update', @skipIfError (env, next) ->
+ if env.json?
+ namespace = @recordJsonNamespace(env.subject)
+ json = if namespace && env.json[namespace]?
+ env.json[namespace]
+ else
+ env.json
+ env.subject.fromJSON(json)
+ env.result = env.subject
+ next()
- namespace = @collectionJsonNamespace(env.proto)
- env.recordsAttributes = if namespace && env.data[namespace]?
- env.data[namespace]
+ @::after 'readAll', @skipIfError (env, next) ->
+ namespace = @collectionJsonNamespace(env.subject)
+ env.recordsAttributes = if namespace && env.json[namespace]?
+ env.json[namespace]
else
- env.data
+ env.json
env.result = env.records = for jsonRecordAttributes in env.recordsAttributes
- @getRecordFromData(jsonRecordAttributes, env.proto.constructor)
+ @getRecordFromData(jsonRecordAttributes, env.subject)
+ next()
+
+ @::after 'get', 'put', 'post', 'delete', @skipIfError (env, next) ->
+ json = env.json
+ namespace = if env.subject.prototype
+ @collectionJsonNamespace(env.subject)
+ else
+ @recordJsonNamespace(env.subject)
+ env.result = if namespace && env.json[namespace]?
+ env.json[namespace]
+ else
+ env.json
next()
@HTTPMethods =
@@ -4082,10 +4141,10 @@ class Batman.RestStorage extends Batman.StorageAdapter
readAll: 'GET'
destroy: 'DELETE'
- for key in ['create', 'read', 'update', 'destroy', 'readAll']
+ for key in ['create', 'read', 'update', 'destroy', 'readAll', 'get', 'post', 'put', 'delete']
do (key) =>
@::[key] = @skipIfError (env, next) ->
- env.options.method = @constructor.HTTPMethods[key]
+ env.options.method ||= @constructor.HTTPMethods[key]
@request(env, next)
# Views
@@ -5867,7 +5926,7 @@ do ->
return false
return true
- $mixin Batman,
+ $extend Batman,
cache: {}
uuid: 0
expando: "batman" + Math.random().toString().replace(/\D/g, '')
@@ -5916,9 +5975,9 @@ do ->
# shallow copied over onto the existing cache
if typeof name == "object" or typeof name == "function"
if pvt
- cache[id][internalKey] = $mixin(cache[id][internalKey], name)
+ cache[id][internalKey] = $extend(cache[id][internalKey], name)
else
- cache[id] = $mixin(cache[id], name)
+ cache[id] = $extend(cache[id], name)
thisCache = cache[id]
@@ -6018,9 +6077,8 @@ Batman.Encoders = new Batman.Object
if typeof define is 'function'
define 'batman', [], -> Batman
-# Optionally export global sugar. Not sure what to do with this.
Batman.exportHelpers = (onto) ->
- for k in ['mixin', 'unmixin', 'route', 'redirect', 'typeOf', 'redirect', 'setImmediate']
+ for k in ['mixin', 'extend', 'unmixin', 'redirect', 'typeOf', 'redirect', 'setImmediate']
onto["$#{k}"] = Batman[k]
onto
View
5 src/extras/batman.rails.coffee
@@ -108,8 +108,9 @@ applyExtra = (Batman) ->
env.options.data = @_serializeToFormData(env.options.data)
next()
- @::after 'update', 'create', ({error, record, response}, next) ->
- env = arguments[0]
+ @::after 'update', 'create', (env, next) ->
+ record = env.subject
+ {error, response} = env
if error
# Rails validation errors
if error.request?.get('status') == 422
View
2  tests/batman/model/associations/has_many_test.coffee
@@ -421,7 +421,7 @@ asyncTest "hasMany associations render", 4, ->
addedProduct.save (err, savedProduct) ->
delay ->
equal node.children().get(3)?.innerHTML, 'Product Four'
- , ASYNC_TEST_DELAY * 2
+ , ASYNC_TEST_DELAY * 3
asyncTest "hasMany adds new related model instances to its set", ->
@Store.find 1, (err, store) =>
View
25 tests/batman/model/model_test.coffee
@@ -76,16 +76,30 @@ test 'the instantiated storage adapter should be returned when persisting', ->
ok returned.isTestStorageAdapter
-test 'the array of instantiated storage adapters should be returned when persisting', ->
- [a, b, c] = [false, false, false]
+test 'the storage adapter should be returned after persisting with Model.storageAdapter()', ->
+ returned = false
class StorageAdapter extends Batman.StorageAdapter
isTestStorageAdapter: true
class Product extends Batman.Model
- [a,b,c] = @persist StorageAdapter, StorageAdapter, StorageAdapter
+ @persist StorageAdapter
+
+ equal Product.storageAdapter().constructor, StorageAdapter
+
+test 'options passed to persist should be mixed in to the storage adapter once instantiated', ->
+ returned = false
+ class StorageAdapter extends Batman.StorageAdapter
+ isTestStorageAdapter: true
+
+ class Product extends Batman.Model
+ @persist StorageAdapter, {foo: 'bar'}
+
+ equal Product.storageAdapter().foo, 'bar'
- for instance in [a,b,c]
- ok instance.isTestStorageAdapter
+ class Order extends Batman.Model
+ adapter = new StorageAdapter(Order)
+ Order.persist adapter, {baz: 'qux'}
+ equal adapter.baz, 'qux'
QUnit.module "Batman.Model class clearing"
setup: ->
@@ -118,6 +132,7 @@ asyncTest 'model will reload data from storage after clear', ->
QUnit.module 'Batman.Model.urlNestsUnder',
setup: ->
class @Product extends Batman.Model
+ @persist Batman.RestStorage
@urlNestsUnder 'shop', 'manufacturer'
test 'urlNestsUnder should nest collection URLs', 1, ->
View
4 tests/batman/storage_adapter/local_storage_test.coffee
@@ -21,11 +21,11 @@ if typeof window.localStorage isnt 'undefined'
throw err if err
@adapter.perform 'create', product2, {}, (err, createdRecord2) =>
throw err if err
- @adapter.perform 'readAll', product1.constructor::, {data: {cost: 10}}, (err, readProducts) =>
+ @adapter.perform 'readAll', product1.constructor, {data: {cost: 10}}, (err, readProducts) =>
throw err if err
equal readProducts.length, 1
deepEqual readProducts[0].get('name'), "testB"
- @adapter.perform 'readAll', product1.constructor::, {data: {cost: 20}}, (err, readProducts) ->
+ @adapter.perform 'readAll', product1.constructor, {data: {cost: 20}}, (err, readProducts) ->
throw err if err
equal readProducts.length, 1
deepEqual readProducts[0].get('name'), "testA"
View
94 tests/batman/storage_adapter/rest_storage_helper.coffee
@@ -71,14 +71,14 @@ restStorageTestSuite = ->
otherAdapter = new @adapter.constructor(@Product)
notEqual otherAdapter.defaultRequestOptions, @adapter.defaultRequestOptions
- asyncTest 'can readAll from JSON string', 3, ->
+ asyncTest 'can readAll from JSON string', 2, ->
MockRequest.expect
url: '/products'
method: 'GET'
, JSON.stringify products: [ name: "test", cost: 20 ]
- @adapter.perform 'readAll', @Product::, {}, (err, readProducts) ->
- ok !err
+ @adapter.perform 'readAll', @Product, {}, (err, readProducts) ->
+ throw err if err
ok readProducts
equal readProducts[0].get("name"), "test"
QUnit.start()
@@ -160,7 +160,7 @@ restStorageTestSuite = ->
cost: 10
]
- @adapter.perform 'readAll', @Product::, {}, (err, readProducts, env) ->
+ @adapter.perform 'readAll', @Product, {}, (err, readProducts, env) ->
throw err if err
equal env.data.someMetaData, "foo"
ok env.request instanceof Batman.Request
@@ -218,6 +218,71 @@ restStorageTestSuite = ->
sharedStorageTestSuite(restStorageTestSuite.sharedSuiteHooks)
+ asyncTest 'custom REST actions on storage: records should callback with the response', 4, ->
+ product = new @Product(name: "test", id: 10)
+ counter = 0
+ for method in ['GET', 'POST', 'PUT', 'DELETE']
+ MockRequest.expect
+ url: '/products/10/extra'
+ method: method
+ , foo: 'bar'
+
+ counter += 1
+ @adapter.perform method.toLowerCase(), product, {action: 'extra'}, (err, response) ->
+ throw err if err
+ deepEqual response, {foo: 'bar'}
+ QUnit.start() if --counter == 0
+
+ asyncTest 'custom REST actions on storage: records should callback with the error if given', 4, ->
+ product = new @Product(name: "test", id: 10)
+ counter = 0
+ for method in ['GET', 'POST', 'PUT', 'DELETE']
+ MockRequest.expect
+ url: '/products/10/extra'
+ method: method
+ , error: "Foo!"
+
+ counter += 1
+ @adapter.perform method.toLowerCase(), product, {action: 'extra'}, (err, response) ->
+ ok err
+ QUnit.start() if --counter == 0
+
+ asyncTest 'custom REST actions on storage: models should callback with the response', 4, ->
+ counter = 0
+ for method in ['GET', 'POST', 'PUT', 'DELETE']
+ MockRequest.expect
+ url: '/products/extra'
+ method: method
+ , foo: 'bar'
+
+ counter += 1
+ @adapter.perform method.toLowerCase(), @Product, {action: 'extra'}, (err, response) ->
+ throw err if err
+ deepEqual response, {foo: 'bar'}
+ QUnit.start() if --counter == 0
+
+ asyncTest 'custom REST actions on storage: models should callback with the error if given', 4, ->
+ counter = 0
+ for method in ['GET', 'POST', 'PUT', 'DELETE']
+ MockRequest.expect
+ url: '/products/extra'
+ method: method
+ , error: "Foo!"
+
+ counter += 1
+ @adapter.perform method.toLowerCase(), @Product, {action: 'extra'}, (err, response) ->
+ ok err
+ QUnit.start() if --counter == 0
+
+ test "persisting a model with this adapter should add helpers for making gets, puts, posts, and deletes", ->
+ @adapter.perform = perform = createSpy()
+
+ @product = new @Product(name: "test", id: 10)
+
+ @product.request 'duplicate', {method: 'GET'}, callback = (err, response) ->
+ deepEqual perform.lastCallArguments.slice(0,3), ['get', @product, {method: 'GET', action: 'duplicate'}]
+ equal typeof perform.lastCallArguments[3], 'function'
+
restStorageTestSuite.testOptionsGeneration = (urlSuffix = '') ->
test 'string record urls should be gotten in the options', 1, ->
product = new @Product
@@ -237,7 +302,6 @@ restStorageTestSuite.testOptionsGeneration = (urlSuffix = '') ->
product.url = (passedOpts) ->
equal passedOpts, opts
'/some/url'
-
@adapter.urlForRecord product, {options: opts}
test 'string model urls should be gotten in the options', 1, ->
@@ -283,6 +347,26 @@ restStorageTestSuite.testOptionsGeneration = (urlSuffix = '') ->
url = @adapter.urlForCollection @Product, {}
equal url, "/some/url.foo#{urlSuffix}"
+ test 'nonstandard actions can be passed to models without url functions defined', 1, ->
+ product = new @Product(id: 1)
+ url = @adapter.urlForRecord product, {options: {action: 'duplicate'}}
+ equal url, "/products/1/duplicate#{urlSuffix}"
+
+ test 'nonstandard actions can be passed to models with url functions defined', 1, ->
+ product = new @Product(id: 1)
+ product.url = '/some/url'
+ url = @adapter.urlForRecord product, {options: {action: 'duplicate'}}
+ equal url, "/some/url/duplicate#{urlSuffix}"
+
+ test 'nonstandard actions can be passed to records without url functions defined', 1, ->
+ url = @adapter.urlForCollection @Product, {options: {action: 'subset'}}
+ equal url, "/products/subset#{urlSuffix}"
+
+ test 'nonstandard actions can be passed to records with url functions defined', 1, ->
+ @Product.url = '/some/url'
+ url = @adapter.urlForCollection @Product, {options: {action: 'subset'}}
+ equal url, "/some/url/subset#{urlSuffix}"
+
restStorageTestSuite.sharedSuiteHooks =
'creating in storage: should succeed if the record doesn\'t already exist': ->
MockRequest.expect
View
8 tests/batman/storage_adapter/storage_adapter_helper.coffee
@@ -139,7 +139,7 @@ sharedStorageTestSuite = (hooks = {}) ->
throw err if err
@adapter.perform 'create', product2, {}, (err, createdRecord2) =>
throw err if err
- @adapter.perform 'readAll', product1.constructor.prototype, {}, (err, readProducts) ->
+ @adapter.perform 'readAll', product1.constructor, {}, (err, readProducts) ->
throw err if err
deepEqual t(readProducts), t([createdRecord1, createdRecord2])
QUnit.start()
@@ -165,7 +165,7 @@ sharedStorageTestSuite = (hooks = {}) ->
throw err if err
@adapter.perform 'create', product2, {}, (err, createdRecord2) =>
throw err if err
- @adapter.perform 'readAll', @Product::, {}, (err, readProducts) ->
+ @adapter.perform 'readAll', @Product, {}, (err, readProducts) ->
throw err if err
deepEqual t(readProducts), ['TESTA', 'TESTB']
QUnit.start()
@@ -177,13 +177,13 @@ sharedStorageTestSuite = (hooks = {}) ->
throw err if err
@adapter.perform 'create', product2, {}, (err, createdRecord2) =>
throw err if err
- @adapter.perform 'readAll', @Product::, {data: {cost: 10}}, (err, readProducts) ->
+ @adapter.perform 'readAll', @Product, {data: {cost: 10}}, (err, readProducts) ->
throw err if err
deepEqual t(readProducts), ['testA', 'testB']
QUnit.start()
asyncTestWithHooks 'reading many from storage: should callback with an empty array if no records exist', 1, ->
- @adapter.perform 'readAll', @Product::, {}, (err, readProducts) ->
+ @adapter.perform 'readAll', @Product, {}, (err, readProducts) ->
throw err if err
deepEqual readProducts, []
QUnit.start()
View
3  tests/batman/test.html
@@ -12,7 +12,7 @@
<script type="text/javascript" src="../../lib/es5-shim.js"></script>
<script type="text/javascript" src="../../lib/coffee-script.js"></script>
- <script type="text/coffeescript" nocopy src="test_helper.coffee"></script>
+ <script type="text/coffeescript" src="test_helper.coffee"></script>
<script type="text/coffeescript" src="../../src/batman.coffee"></script>
<script type="text/coffeescript" src="../../src/extras/batman.rails.coffee"></script>
<script type="text/coffeescript" src="../../src/extras/batman.i18n.coffee"></script>
@@ -85,6 +85,7 @@
<script type="text/coffeescript" src="utilities/inflector_test.coffee"></script>
<script type="text/coffeescript" src="utilities/interpolation_test.coffee"></script>
<script type="text/coffeescript" src="utilities/mixin_test.coffee"></script>
+ <script type="text/coffeescript" src="utilities/extend_test.coffee"></script>
<script type="text/coffeescript" src="utilities/polymorphic_object_access_test.coffee"></script>
<script type="text/coffeescript" src="utilities/set_immediate_test.coffee"></script>
<script type="text/coffeescript" src="utilities/state_machine_test.coffee"></script>
View
11 tests/batman/utilities/extend_test.coffee
@@ -0,0 +1,11 @@
+QUnit.module "$extend"
+ setup: ->
+ @base = {x: "x"}
+
+test "should copy properties from the source to the destination", ->
+ deepEqual {x: "z", y: "y"}, $extend(@base, {x: "y"}, {y: "y"}, {x: "z"})
+
+test "shouldn't affect the source objects", ->
+ more = x: "y"
+ $mixin @base, more
+ deepEqual more, x: "y"
View
2  tests/batman/utilities/get_test.coffee
@@ -1,5 +1,3 @@
-Batman.exportHelpers(this)
-
QUnit.module "Batman.get"
test "should invoke obj.get if it is a function", ->
View
2  tests/batman/utilities/mixin_test.coffee
@@ -1,5 +1,3 @@
-Batman.exportHelpers(this)
-
QUnit.module "$mixin"
setup: ->
@base = {x: "x"}
View
8 tests/batman/view/simple_rendering_test.coffee
@@ -364,15 +364,15 @@ asyncTest 'it should bind the value of textareas and inputs simulatenously', ->
unless IN_NODE # jsdom doesn't seem to like input type="file"
getMockModel = ->
- context = Batman
+ class Model extends Batman.Object
storageKey: 'one'
hasStorage: -> true
fileAttributes: ''
- adapter = new Batman.RestStorage(context)
- context._batman.storage = [adapter]
+ adapter = new Batman.RestStorage(Model)
+ Model::_batman.storage = adapter
- [context, adapter]
+ [new Model, adapter]
asyncTest 'it should bind the value of file type inputs', 2, ->
[context, adapter] = getMockModel()
Please sign in to comment.
Something went wrong with that request. Please try again.