2
YUI 3.10.3 (build 2fb5187)
3
Copyright 2013 Yahoo! Inc. All rights reserved.
4
Licensed under the BSD License.
5
http://yuilibrary.com/license/
8
YUI.add('model-sync-rest', function (Y, NAME) {
11
An extension which provides a RESTful XHR sync implementation that can be mixed
12
into a Model or ModelList subclass.
15
@submodule model-sync-rest
22
An extension which provides a RESTful XHR sync implementation that can be mixed
23
into a Model or ModelList subclass.
25
This makes it trivial for your Model or ModelList subclasses communicate and
26
transmit their data via RESTful XHRs. In most cases you'll only need to provide
27
a value for `root` when sub-classing `Y.Model`.
29
Y.User = Y.Base.create('user', Y.Model, [Y.ModelSync.REST], {
33
Y.Users = Y.Base.create('users', Y.ModelList, [Y.ModelSync.REST], {
34
// By convention `Y.User`'s `root` will be used for the lists' URL.
38
var users = new Y.Users();
40
// GET users list from: "/users"
41
users.load(function () {
42
var firstUser = users.item(0);
44
firstUser.get('id'); // => "1"
46
// PUT updated user data at: "/users/1"
47
firstUser.set('name', 'Eric').save();
52
@extensionfor ModelList
55
function RESTSync() {}
58
A request authenticity token to validate HTTP requests made by this extension
59
with the server when the request results in changing persistent state. This
60
allows you to protect your server from Cross-Site Request Forgery attacks.
62
A CSRF token provided by the server can be embedded in the HTML document and
63
assigned to `YUI.Env.CSRF_TOKEN` like this:
66
YUI.Env.CSRF_TOKEN = {{session.authenticityToken}};
69
The above should come after YUI seed file so that `YUI.Env` will be defined.
71
**Note:** This can be overridden on a per-request basis. See `sync()` method.
73
When a value for the CSRF token is provided, either statically or via `options`
74
passed to the `save()` and `destroy()` methods, the applicable HTTP requests
75
will have a `X-CSRF-Token` header added with the token value.
79
@default YUI.Env.CSRF_TOKEN
83
RESTSync.CSRF_TOKEN = YUI.Env.CSRF_TOKEN;
86
Static flag to use the HTTP POST method instead of PUT or DELETE.
88
If the server-side HTTP framework isn't RESTful, setting this flag to `true`
89
will cause all PUT and DELETE requests to instead use the POST HTTP method, and
90
add a `X-HTTP-Method-Override` HTTP header with the value of the method type
93
@property EMULATE_HTTP
99
RESTSync.EMULATE_HTTP = false;
102
Default headers used with all XHRs.
104
By default the `Accept` and `Content-Type` headers are set to
105
"application/json", this signals to the HTTP server to process the request
106
bodies as JSON and send JSON responses. If you're sending and receiving content
107
other than JSON, you can override these headers and the `parse()` and
108
`serialize()` methods.
110
**Note:** These headers will be merged with any request-specific headers, and
111
the request-specific headers will take precedence.
113
@property HTTP_HEADERS
117
"Accept" : "application/json",
118
"Content-Type": "application/json"
123
RESTSync.HTTP_HEADERS = {
124
'Accept' : 'application/json',
125
'Content-Type': 'application/json'
129
Static mapping of RESTful HTTP methods corresponding to CRUD actions.
131
@property HTTP_METHODS
143
RESTSync.HTTP_METHODS = {
151
The number of milliseconds before the XHRs will timeout/abort. This defaults to
154
**Note:** This can be overridden on a per-request basis. See `sync()` method.
156
@property HTTP_TIMEOUT
162
RESTSync.HTTP_TIMEOUT = 30000;
165
Properties that shouldn't be turned into ad-hoc attributes when passed to a
166
Model or ModelList constructor.
168
@property _NON_ATTRS_CFG
170
@default ["root", "url"]
175
RESTSync._NON_ATTRS_CFG = ['root', 'url'];
177
RESTSync.prototype = {
179
// -- Public Properties ----------------------------------------------------
182
A string which represents the root or collection part of the URL which
183
relates to a Model or ModelList. Usually this value should be same for all
184
instances of a specific Model/ModelList subclass.
186
When sub-classing `Y.Model`, usually you'll only need to override this
187
property, which lets the URLs for the XHRs be generated by convention. If
188
the `root` string ends with a trailing-slash, XHR URLs will also end with a
189
"/", and if the `root` does not end with a slash, neither will the XHR URLs.
192
Y.User = Y.Base.create('user', Y.Model, [Y.ModelSync.REST], {
196
var currentUser, newUser;
198
// GET the user data from: "/users/123"
199
currentUser = new Y.User({id: '123'}).load();
201
// POST the new user data to: "/users"
202
newUser = new Y.User({name: 'Eric Ferraiuolo'}).save();
204
When sub-classing `Y.ModelList`, usually you'll want to ignore configuring
205
the `root` and simply rely on the build-in convention of the list's
206
generated URLs defaulting to the `root` specified by the list's `model`.
216
A string which specifies the URL to use when making XHRs, if not value is
217
provided, the URLs used to make XHRs will be generated by convention.
219
While a `url` can be provided for each Model/ModelList instance, usually
220
you'll want to either rely on the default convention or provide a tokenized
221
string on the prototype which can be used for all instances.
223
When sub-classing `Y.Model`, you will probably be able to rely on the
224
default convention of generating URLs in conjunction with the `root`
225
property and whether the model is new or not (i.e. has an `id`). If the
226
`root` property ends with a trailing-slash, the generated URL for the
227
specific model will also end with a trailing-slash.
230
Y.User = Y.Base.create('user', Y.Model, [Y.ModelSync.REST], {
234
var currentUser, newUser;
236
// GET the user data from: "/users/123/"
237
currentUser = new Y.User({id: '123'}).load();
239
// POST the new user data to: "/users/"
240
newUser = new Y.User({name: 'Eric Ferraiuolo'}).save();
242
If a `url` is specified, it will be processed by `Y.Lang.sub()`, which is
243
useful when the URLs for a Model/ModelList subclass match a specific pattern
244
and can use simple replacement tokens; e.g.:
247
Y.User = Y.Base.create('user', Y.Model, [Y.ModelSync.REST], {
249
url : '/users/{username}'
252
**Note:** String subsitituion of the `url` only use string an number values
253
provided by this object's attribute and/or the `options` passed to the
254
`getURL()` method. Do not expect something fancy to happen with Object,
255
Array, or Boolean values, they will simply be ignored.
257
If your URLs have plural roots or collection URLs, while the specific item
258
resources are under a singular name, e.g. "/users" (plural) and "/user/123"
259
(singular), you'll probably want to configure the `root` and `url`
260
properties like this:
263
Y.User = Y.Base.create('user', Y.Model, [Y.ModelSync.REST], {
268
var currentUser, newUser;
270
// GET the user data from: "/user/123"
271
currentUser = new Y.User({id: '123'}).load();
273
// POST the new user data to: "/users"
274
newUser = new Y.User({name: 'Eric Ferraiuolo'}).save();
276
When sub-classing `Y.ModelList`, usually you'll be able to rely on the
277
associated `model` to supply its `root` to be used as the model list's URL.
278
If this needs to be customized, you can provide a simple string for the
282
Y.Users = Y.Base.create('users', Y.ModelList, [Y.ModelSync.REST], {
283
// Leverages `Y.User`'s `root`, which is "/users".
287
// Or specified explicitly...
289
Y.Users = Y.Base.create('users', Y.ModelList, [Y.ModelSync.REST], {
301
// -- Lifecycle Methods ----------------------------------------------------
303
initializer: function (config) {
304
config || (config = {});
306
// Overrides `root` at the instance level.
307
if ('root' in config) {
308
this.root = config.root || '';
311
// Overrides `url` at the instance level.
312
if ('url' in config) {
313
this.url = config.url || '';
317
// -- Public Methods -------------------------------------------------------
320
Returns the URL for this model or model list for the given `action` and
321
`options`, if specified.
323
This method correctly handles the variations of `root` and `url` values and
324
is called by the `sync()` method to get the URLs used to make the XHRs.
326
You can override this method if you need to provide a specific
327
implementation for how the URLs of your Model and ModelList subclasses need
331
@param {String} [action] Optional `sync()` action for which to generate the
333
@param {Object} [options] Optional options which may be used to help
335
@return {String} this model's or model list's URL for the the given
336
`action` and `options`.
339
getURL: function (action, options) {
340
var root = this.root,
343
// If this is a model list, use its `url` and substitute placeholders,
344
// but default to the `root` of its `model`. By convention a model's
345
// `root` is the location to a collection resource.
346
if (this._isYUIModelList) {
348
return this.model.prototype.root;
351
return this._substituteURL(url, Y.merge(this.getAttrs(), options));
354
// Assume `this` is a model.
356
// When a model is new, i.e. has no `id`, the `root` should be used. By
357
// convention a model's `root` is the location to a collection resource.
358
// The model's `url` will be used as a fallback if `root` isn't defined.
359
if (root && (action === 'create' || this.isNew())) {
363
// When a model's `url` is not provided, we'll generate a URL to use by
364
// convention. This will combine the model's `id` with its configured
365
// `root` and add a trailing-slash if the root ends with "/".
367
return this._joinURL(this.getAsURL('id') || '');
370
// Substitute placeholders in the `url` with URL-encoded values from the
371
// model's attribute values or the specified `options`.
372
return this._substituteURL(url, Y.merge(this.getAttrs(), options));
376
Called to parse the response object returned from `Y.io()`. This method
377
receives the full response object and is expected to "prep" a response which
378
is suitable to pass to the `parse()` method.
380
By default the response body is returned (`responseText`), because it
381
usually represents the entire entity of this model on the server.
383
If you need to parse data out of the response's headers you should do so by
384
overriding this method. If you'd like the entire response object from the
385
XHR to be passed to your `parse()` method, you can simply assign this
388
@method parseIOResponse
389
@param {Object} response Response object from `Y.io()`.
390
@return {Any} The modified response to pass along to the `parse()` method.
393
parseIOResponse: function (response) {
394
return response.responseText;
398
Serializes `this` model to be used as the HTTP request entity body.
400
By default this model will be serialized to a JSON string via its `toJSON()`
403
You can override this method when the HTTP server expects a different
404
representation of this model's data that is different from the default JSON
405
serialization. If you're sending and receive content other than JSON, be
406
sure change the `Accept` and `Content-Type` `HTTP_HEADERS` as well.
408
**Note:** A model's `toJSON()` method can also be overridden. If you only
409
need to modify which attributes are serialized to JSON, that's a better
413
@param {String} [action] Optional `sync()` action for which to generate the
414
the serialized representation of this model.
415
@return {String} serialized HTTP request entity body.
418
serialize: function (action) {
419
return Y.JSON.stringify(this);
423
Communicates with a RESTful HTTP server by sending and receiving data via
424
XHRs. This method is called internally by load(), save(), and destroy().
426
The URL used for each XHR will be retrieved by calling the `getURL()` method
427
and passing it the specified `action` and `options`.
429
This method relies heavily on standard RESTful HTTP conventions
432
@param {String} action Sync action to perform. May be one of the following:
434
* `create`: Store a newly-created model for the first time.
435
* `delete`: Delete an existing model.
436
* `read` : Load an existing model.
437
* `update`: Update an existing model.
439
@param {Object} [options] Sync options:
440
@param {String} [options.csrfToken] The authenticity token used by the
441
server to verify the validity of this request and protected against CSRF
442
attacks. This overrides the default value provided by the static
443
`CSRF_TOKEN` property.
444
@param {Object} [options.headers] The HTTP headers to mix with the default
445
headers specified by the static `HTTP_HEADERS` property.
446
@param {Number} [options.timeout] The number of milliseconds before the
447
request will timeout and be aborted. This overrides the default provided
448
by the static `HTTP_TIMEOUT` property.
449
@param {Function} [callback] Called when the sync operation finishes.
450
@param {Error|null} callback.err If an error occurred, this parameter will
451
contain the error. If the sync operation succeeded, _err_ will be
453
@param {Any} [callback.response] The server's response.
455
sync: function (action, options, callback) {
456
options || (options = {});
458
var url = this.getURL(action, options),
459
method = RESTSync.HTTP_METHODS[action],
460
headers = Y.merge(RESTSync.HTTP_HEADERS, options.headers),
461
timeout = options.timeout || RESTSync.HTTP_TIMEOUT,
462
csrfToken = options.csrfToken || RESTSync.CSRF_TOKEN,
465
// Prepare the content if we are sending data to the server.
466
if (method === 'POST' || method === 'PUT') {
467
entity = this.serialize(action);
469
// Remove header, no content is being sent.
470
delete headers['Content-Type'];
473
// Setup HTTP emulation for older servers if we need it.
474
if (RESTSync.EMULATE_HTTP &&
475
(method === 'PUT' || method === 'DELETE')) {
477
// Pass along original method type in the headers.
478
headers['X-HTTP-Method-Override'] = method;
480
// Fall-back to using POST method type.
484
// Add CSRF token to HTTP request headers if one is specified and the
485
// request will cause side effects on the server.
487
(method === 'POST' || method === 'PUT' || method === 'DELETE')) {
489
headers['X-CSRF-Token'] = csrfToken;
492
this._sendSyncIORequest({
503
// -- Protected Methods ----------------------------------------------------
506
Joins the `root` URL to the specified `url`, normalizing leading/trailing
511
model._joinURL('bar'); // => '/foo/bar'
512
model._joinURL('/bar'); // => '/foo/bar'
515
model._joinURL('bar'); // => '/foo/bar/'
516
model._joinURL('/bar'); // => '/foo/bar/'
519
@param {String} url URL to append to the `root` URL.
520
@return {String} Joined URL.
524
_joinURL: function (url) {
525
var root = this.root;
527
if (!(root || url)) {
531
if (url.charAt(0) === '/') {
532
url = url.substring(1);
535
// Combines the `root` with the `url` and adds a trailing-slash if the
536
// `root` has a trailing-slash.
537
return root && root.charAt(root.length - 1) === '/' ?
544
Calls both public, overrideable methods: `parseIOResponse()`, then `parse()`
545
and returns the result.
547
This will call into `parseIOResponse()`, if it's defined as a method,
548
passing it the full response object from the XHR and using its return value
549
to pass along to the `parse()`. This enables developers to easily parse data
550
out of the response headers which should be used by the `parse()` method.
553
@param {Object} response Response object from `Y.io()`.
554
@return {Object|Object[]} Attribute hash or Array of model attribute hashes.
558
_parse: function (response) {
559
// When `parseIOResponse` is defined as a method, it will be invoked and
560
// the result will become the new response object that the `parse()`
561
// will be invoked with.
562
if (typeof this.parseIOResponse === 'function') {
563
response = this.parseIOResponse(response);
566
return this.parse(response);
570
Performs the XHR and returns the resulting `Y.io()` request object.
572
This method is called by `sync()`.
574
@method _sendSyncIORequest
575
@param {Object} config An object with the following properties:
576
@param {String} config.action The `sync()` action being performed.
577
@param {Function} [config.callback] Called when the sync operation
579
@param {String} [config.entity] The HTTP request entity body.
580
@param {Object} config.headers The HTTP request headers.
581
@param {String} config.method The HTTP request method.
582
@param {Number} [config.timeout] Time until the HTTP request is aborted.
583
@param {String} config.url The URL of the HTTP resource.
584
@return {Object} The resulting `Y.io()` request object.
588
_sendSyncIORequest: function (config) {
589
return Y.io(config.url, {
591
action : config.action,
592
callback: config.callback,
597
data : config.entity,
598
headers: config.headers,
599
method : config.method,
600
timeout: config.timeout,
603
start : this._onSyncIOStart,
604
failure: this._onSyncIOFailure,
605
success: this._onSyncIOSuccess,
606
end : this._onSyncIOEnd
612
Utility which takes a tokenized `url` string and substitutes its
613
placeholders using a specified `data` object.
615
This method will property URL-encode any values before substituting them.
616
Also, only expect it to work with String and Number values.
619
var url = this._substituteURL('/users/{name}', {id: 'Eric F'});
620
// => "/users/Eric%20F"
622
@method _substituteURL
623
@param {String} url Tokenized URL string to substitute placeholder values.
624
@param {Object} data Set of data to fill in the `url`'s placeholders.
625
@return {String} Substituted URL.
629
_substituteURL: function (url, data) {
636
// Creates a hash of the string and number values only to be used to
637
// replace any placeholders in a tokenized `url`.
638
Y.Object.each(data, function (v, k) {
639
if (Lang.isString(v) || Lang.isNumber(v)) {
640
// URL-encode any string or number values.
641
values[k] = encodeURIComponent(v);
645
return Lang.sub(url, values);
648
// -- Event Handlers -------------------------------------------------------
651
Called when the `Y.io` request has finished, after "success" or "failure"
654
This is a no-op by default, but provides a hook for overriding.
657
@param {String} txId The `Y.io` transaction id.
658
@param {Object} details Extra details carried through from `sync()`:
659
@param {String} details.action The sync action performed.
660
@param {Function} [details.callback] The function to call after syncing.
661
@param {String} details.url The URL of the requested resource.
665
_onSyncIOEnd: function (txId, details) {},
668
Called when the `Y.io` request has finished unsuccessfully.
670
By default this calls the `details.callback` function passing it the HTTP
671
status code and message as an error object along with the response body.
673
@method _onSyncIOFailure
674
@param {String} txId The `Y.io` transaction id.
675
@param {Object} res The `Y.io` response object.
676
@param {Object} details Extra details carried through from `sync()`:
677
@param {String} details.action The sync action performed.
678
@param {Function} [details.callback] The function to call after syncing.
679
@param {String} details.url The URL of the requested resource.
683
_onSyncIOFailure: function (txId, res, details) {
684
var callback = details.callback;
695
Called when the `Y.io` request has finished successfully.
697
By default this calls the `details.callback` function passing it the
700
@method _onSyncIOSuccess
701
@param {String} txId The `Y.io` transaction id.
702
@param {Object} res The `Y.io` response object.
703
@param {Object} details Extra details carried through from `sync()`:
704
@param {String} details.action The sync action performed.
705
@param {Function} [details.callback] The function to call after syncing.
706
@param {String} details.url The URL of the requested resource.
710
_onSyncIOSuccess: function (txId, res, details) {
711
var callback = details.callback;
719
Called when the `Y.io` request is made.
721
This is a no-op by default, but provides a hook for overriding.
723
@method _onSyncIOStart
724
@param {String} txId The `Y.io` transaction id.
725
@param {Object} details Extra details carried through from `sync()`:
726
@param {String} detials.action The sync action performed.
727
@param {Function} [details.callback] The function to call after syncing.
728
@param {String} details.url The URL of the requested resource.
732
_onSyncIOStart: function (txId, details) {}
735
// -- Namespace ----------------------------------------------------------------
737
Y.namespace('ModelSync').REST = RESTSync;
740
}, '3.10.3', {"requires": ["model", "io-base", "json-stringify"]});