-
Notifications
You must be signed in to change notification settings - Fork 0
/
APITransport.js
257 lines (231 loc) · 10.2 KB
/
APITransport.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
define(function (require) {
var $ = require('jquery')
, config = require('config').api
, Utils = require('./Utils')
, Hackbone = require('./Hackbone')
var isCollection = function (obj) { return obj instanceof Hackbone.Collection }
, isModel = function (obj) { return obj instanceof Hackbone.Model }
, slice = function (obj, index) {
return Array.prototype.slice.call(obj, index)
}
// Parses server attributes according to a model's schema
, parseAttributes = function (attrs, schema) {
var val, attrType;
for (var name in schema) {
attrType = schema[name]
val = attrs[name]
// Nothing to parse ..
if (val === null || typeof val == 'undefined') continue;
// Collection
if (attrType instanceof Array) {
var modelConfig = config.models[Hackbone.Collection.getModel(attrType[0])];
if (!modelConfig) error('Invalid model for collection', attrType[0]);
if (!(val instanceof Array)) {
attrs[name] = Hackbone.Collection.create(attrType[0]);
} else {
attrs[name] = Hackbone.Collection.create(attrType[0], val.map(function (obj) {
return parseAttributes(obj, modelConfig.schema)
}));
}
}
// Model
else if (typeof attrType == 'string') {
if (!config.models[attrType]) error('Invalid model for association', attrType);
// ObjectId
if (typeof val == 'string') {
// Fetch from registry
attrs[name] = Hackbone.ModelRegistry.fetch(attrType, val) || Hackbone.Model.create(attrType, {_id:val});
}
// Object
else if (typeof val == 'object') {
// Expand it to be a new model then
attrs[name] = Hackbone.Model.create(attrType, parseAttributes(val, config.models[attrType].schema));
}
// Apparently invalid value, let's remove it
else {
delete attrs[name];
}
}
// Function, which may be constructed, like Date
else if (typeof attrType == 'function') {
attrs[name] = new attrType(val);
}
}
return attrs;
}
// Parses the given set of models into the Hackbone.ModelRegistry
, parseModels = function (models) {
for (var name in models) {
models[name] = models[name].map(function (attrs) {
return Hackbone.Model.create(name, parseAttributes(attrs, config.models[name].schema))
})
}
}
, modelToJSON = function (model) {
return model.toJSON()
}
, error = Utils.scopedError('APITransport')
, log = Utils.scopedLog('APITransport')
// Map from CRUD to HTTP for our default `Backbone.sync` implementation.
, methodMap = {
'create':'POST',
'update':'PUT',
'delete':'DELETE',
'read':'GET'
}
// Throw an error when a URL is needed, and none is supplied.
, urlError = function () {
throw new Error('A "url" property or function must be specified');
}
// Resolves the URL for the object
, resolveUrl = function (obj) {
if (isModel(obj)) {
var spec = config.models[obj.name]
if (!spec) error('Missing configuration for model', obj.name, obj);
if (!spec.url && !spec.embedded) error('Missing url for model', obj.name, obj)
// Embedded indicates this model lives within another model and doesn't have a flat endpoint for itself
if (spec.embedded && !isCollection(obj.collection)) error('Missing collection for embedded document url', obj.name, obj);
return (spec.embedded ? resolveUrl(obj.collection) : spec.url) + (obj.isNew() ? '' : '/' + obj.id);
}
// Collection
if (obj.embedded && !isModel(obj.parent)) error('Missing parent for embedded collection', obj.name, obj.parent, obj)
if (!obj.url) error('Missing url for collection', obj.name, obj);
return obj.embedded ? resolveUrl(obj.parent) + obj.url : obj.url;
}
// Lets expand the results which have the form
// [ModelName, id, id2, ..., idN]
, expandResultModels = function (result) {
var name = result[0]
, models = []
for (var i = 1; i < result.length; i++) {
models.push(Hackbone.ModelRegistry.fetch(name, result[i]))
}
return models;
}
, isObjectId = function (str) {
return str.length == 24;
}
;
/**
*
* This is the only point of server interaction,
* where all the parsing occurs
*
*/
var APITransport = {
sync:function (method, obj, options) {
options = options || {};
var _isModel = isModel(obj)
, _isCollection = isCollection(obj)
if (_isCollection && method != 'read') error('You may only read from a collection');
if (!_isModel && !_isCollection) error('Invalid object, only a Model or a Collection may be sync\'d', obj);
// Default JSON-request options.
var params = {
type:methodMap[method],
dataType:'json',
url:options.url || config.basePath + resolveUrl(obj)
};
// GETing = filtering by query string
if (method == 'read' && options.data) {
params.data = options.data;
}
// These operations need the model's attribte as a body
else if (obj && (method == 'create' || method == 'update')) {
params.contentType = 'application/json';
params.data = JSON.stringify(modelToJSON(obj));
}
// Don't process data on a non-GET request.
if (params.type !== 'GET') {
params.processData = false;
}
// This is how the outer world will be aware of us
var promise = new Promise
// We need a model schema in order to parse it
, schema = _isModel && obj.name && config.models[obj.name] && (config.models[obj.name].schema || {})
;
if (_isModel && !schema) error('Invalid model schema!', obj.name, obj, config, schema);
// Actual API Call
promise.xhr = $.ajax(params)
.success(function (res) {
if (method == 'delete') {
return promise.fire('success', result)
}
// log('Incoming response', params.url, method, res)
var result = res.result;
// All models from the response should be parsed into our Registry -- they are fresh
parseModels(res.models)
// if (!res.status) error('Invalid response, expected a status', res)
if (!res.result) error('Invalid response, result expected', res)
// If the result is an array, it's a set of results from the
// packaged models, of the form ['ModelName', id, id2, ..., idN]
if (result instanceof Array) {
// Lets pull them from Registry
result = expandResultModels(result, schema);
}
// Attributes given -- this should only be given upon resource creation
else if (typeof result == 'object') {
result = parseAttributes(result, schema)
}
// The result is actually a ModelName, which maps to one of the array of models
// sent
else if (typeof result == 'string') {
result = res.models[result] || [];
}
promise.fire('success', result, res)
})
.complete(function (a, b, c) {
promise.fire('complete', a, b, c)
})
// Oops -- something went wrong
.error(function (xhr, code, data) {
if (code == 'abort') {
return
}
var res;
try {
res = $.parseJSON(xhr.responseText);
} catch (e) {
}
// Unexpected error!
if (xhr.status == 200 || !res || !res.errors) {
log('Invalid API response', xhr, code, data)
promise.fire('error', xhr, code, data)
} else {
promise.fire('error', res.errors)
}
})
return promise;
}
}
/**
* API Transport Promise
*/
var addCallback = function (name, cb) {
this._callbacks[name] ? this._callbacks[name].push(cb) : (this._callbacks[name] = [cb])
return this;
}
var Promise = function () {
this._callbacks = {}
}
Promise.prototype = {
fire:function (name, obj) {
var cbs = this._callbacks[name];
if (cbs) {
var cb;
while (cb = cbs.shift()) {
cb.apply(obj, Array.prototype.slice.call(arguments, 1));
}
}
},
success:function (cb) {
return addCallback.call(this, 'success', cb)
},
error:function (cb) {
return addCallback.call(this, 'error', cb)
},
complete:function (cb) {
return addCallback.call(this, 'complete', cb)
}
}
return APITransport;
})