3 Copyright 2012 Yahoo! Inc. All rights reserved.
4 Licensed under the BSD License.
5 http://yuilibrary.com/license/
7 YUI.add('model-sync-rest', function (Y, NAME) {
10 An extension which provides a RESTful XHR sync implementation that can be mixed
11 into a Model or ModelList subclass.
14 @submodule model-sync-rest
21 An extension which provides a RESTful XHR sync implementation that can be mixed
22 into a Model or ModelList subclass.
24 This makes it trivial for your Model or ModelList subclasses communicate and
25 transmit their data via RESTful XHRs. In most cases you'll only need to provide
26 a value for `root` when sub-classing `Y.Model`.
28 Y.User = Y.Base.create('user', Y.Model, [Y.ModelSync.REST], {
32 Y.Users = Y.Base.create('users', Y.ModelList, [Y.ModelSync.REST], {
33 // By convention `Y.User`'s `root` will be used for the lists' URL.
37 var users = new Y.Users();
39 // GET users list from: "/users"
40 users.load(function () {
41 var firstUser = users.item(0);
43 firstUser.get('id'); // => "1"
45 // PUT updated user data at: "/users/1"
46 firstUser.set('name', 'Eric').save();
51 @extensionfor ModelList
54 function RESTSync() {}
57 A request authenticity token to validate HTTP requests made by this extension
58 with the server when the request results in changing persistent state. This
59 allows you to protect your server from Cross-Site Request Forgery attacks.
61 A CSRF token provided by the server can be embedded in the HTML document and
62 assigned to `YUI.Env.CSRF_TOKEN` like this:
65 YUI.Env.CSRF_TOKEN = {{session.authenticityToken}};
68 The above should come after YUI seed file so that `YUI.Env` will be defined.
70 **Note:** This can be overridden on a per-request basis. See `sync()` method.
72 When a value for the CSRF token is provided, either statically or via `options`
73 passed to the `save()` and `destroy()` methods, the applicable HTTP requests
74 will have a `X-CSRF-Token` header added with the token value.
78 @default YUI.Env.CSRF_TOKEN
82 RESTSync.CSRF_TOKEN = YUI.Env.CSRF_TOKEN;
85 Static flag to use the HTTP POST method instead of PUT or DELETE.
87 If the server-side HTTP framework isn't RESTful, setting this flag to `true`
88 will cause all PUT and DELETE requests to instead use the POST HTTP method, and
89 add a `X-HTTP-Method-Override` HTTP header with the value of the method type
92 @property EMULATE_HTTP
98 RESTSync.EMULATE_HTTP = false;
101 Default headers used with all XHRs.
103 By default the `Accept` and `Content-Type` headers are set to
104 "application/json", this signals to the HTTP server to process the request
105 bodies as JSON and send JSON responses. If you're sending and receiving content
106 other than JSON, you can override these headers and the `parse()` and
107 `serialize()` methods.
109 **Note:** These headers will be merged with any request-specific headers, and
110 the request-specific headers will take precedence.
112 @property HTTP_HEADERS
116 "Accept" : "application/json",
117 "Content-Type": "application/json"
122 RESTSync.HTTP_HEADERS = {
123 'Accept' : 'application/json',
124 'Content-Type': 'application/json'
128 Static mapping of RESTful HTTP methods corresponding to CRUD actions.
130 @property HTTP_METHODS
142 RESTSync.HTTP_METHODS = {
150 The number of milliseconds before the XHRs will timeout/abort. This defaults to
153 **Note:** This can be overridden on a per-request basis. See `sync()` method.
155 @property HTTP_TIMEOUT
161 RESTSync.HTTP_TIMEOUT = 30000;
164 Properties that shouldn't be turned into ad-hoc attributes when passed to a
165 Model or ModelList constructor.
167 @property _NON_ATTRS_CFG
169 @default ["root", "url"]
174 RESTSync._NON_ATTRS_CFG = ['root', 'url'];
176 RESTSync.prototype = {
178 // -- Public Properties ----------------------------------------------------
181 A string which represents the root or collection part of the URL which
182 relates to a Model or ModelList. Usually this value should be same for all
183 instances of a specific Model/ModelList subclass.
185 When sub-classing `Y.Model`, usually you'll only need to override this
186 property, which lets the URLs for the XHRs be generated by convention. If
187 the `root` string ends with a trailing-slash, XHR URLs will also end with a
188 "/", and if the `root` does not end with a slash, neither will the XHR URLs.
191 Y.User = Y.Base.create('user', Y.Model, [Y.ModelSync.REST], {
195 var currentUser, newUser;
197 // GET the user data from: "/users/123"
198 currentUser = new Y.User({id: '123'}).load();
200 // POST the new user data to: "/users"
201 newUser = new Y.User({name: 'Eric Ferraiuolo'}).save();
203 When sub-classing `Y.ModelList`, usually you'll want to ignore configuring
204 the `root` and simply rely on the build-in convention of the list's
205 generated URLs defaulting to the `root` specified by the list's `model`.
215 A string which specifies the URL to use when making XHRs, if not value is
216 provided, the URLs used to make XHRs will be generated by convention.
218 While a `url` can be provided for each Model/ModelList instance, usually
219 you'll want to either rely on the default convention or provide a tokenized
220 string on the prototype which can be used for all instances.
222 When sub-classing `Y.Model`, you will probably be able to rely on the
223 default convention of generating URLs in conjunction with the `root`
224 property and whether the model is new or not (i.e. has an `id`). If the
225 `root` property ends with a trailing-slash, the generated URL for the
226 specific model will also end with a trailing-slash.
229 Y.User = Y.Base.create('user', Y.Model, [Y.ModelSync.REST], {
233 var currentUser, newUser;
235 // GET the user data from: "/users/123/"
236 currentUser = new Y.User({id: '123'}).load();
238 // POST the new user data to: "/users/"
239 newUser = new Y.User({name: 'Eric Ferraiuolo'}).save();
241 If a `url` is specified, it will be processed by `Y.Lang.sub()`, which is
242 useful when the URLs for a Model/ModelList subclass match a specific pattern
243 and can use simple replacement tokens; e.g.:
246 Y.User = Y.Base.create('user', Y.Model, [Y.ModelSync.REST], {
248 url : '/users/{username}'
251 **Note:** String subsitituion of the `url` only use string an number values
252 provided by this object's attribute and/or the `options` passed to the
253 `getURL()` method. Do not expect something fancy to happen with Object,
254 Array, or Boolean values, they will simply be ignored.
256 If your URLs have plural roots or collection URLs, while the specific item
257 resources are under a singular name, e.g. "/users" (plural) and "/user/123"
258 (singular), you'll probably want to configure the `root` and `url`
259 properties like this:
262 Y.User = Y.Base.create('user', Y.Model, [Y.ModelSync.REST], {
267 var currentUser, newUser;
269 // GET the user data from: "/user/123"
270 currentUser = new Y.User({id: '123'}).load();
272 // POST the new user data to: "/users"
273 newUser = new Y.User({name: 'Eric Ferraiuolo'}).save();
275 When sub-classing `Y.ModelList`, usually you'll be able to rely on the
276 associated `model` to supply its `root` to be used as the model list's URL.
277 If this needs to be customized, you can provide a simple string for the
281 Y.Users = Y.Base.create('users', Y.ModelList, [Y.ModelSync.REST], {
282 // Leverages `Y.User`'s `root`, which is "/users".
286 // Or specified explicitly...
288 Y.Users = Y.Base.create('users', Y.ModelList, [Y.ModelSync.REST], {
300 // -- Lifecycle Methods ----------------------------------------------------
302 initializer: function (config) {
303 config || (config = {});
305 // Overrides `root` at the instance level.
306 if ('root' in config) {
307 this.root = config.root || '';
310 // Overrides `url` at the instance level.
311 if ('url' in config) {
312 this.url = config.url || '';
316 // -- Public Methods -------------------------------------------------------
319 Returns the URL for this model or model list for the given `action` and
320 `options`, if specified.
322 This method correctly handles the variations of `root` and `url` values and
323 is called by the `sync()` method to get the URLs used to make the XHRs.
325 You can override this method if you need to provide a specific
326 implementation for how the URLs of your Model and ModelList subclasses need
330 @param {String} [action] Optional `sync()` action for which to generate the
332 @param {Object} [options] Optional options which may be used to help
334 @return {String} this model's or model list's URL for the the given
335 `action` and `options`.
338 getURL: function (action, options) {
339 var root = this.root,
342 // If this is a model list, use its `url` and substitute placeholders,
343 // but default to the `root` of its `model`. By convention a model's
344 // `root` is the location to a collection resource.
345 if (this._isYUIModelList) {
347 return this.model.prototype.root;
350 return this._substituteURL(url, Y.merge(this.getAttrs(), options));
353 // Assume `this` is a model.
355 // When a model is new, i.e. has no `id`, the `root` should be used. By
356 // convention a model's `root` is the location to a collection resource.
357 // The model's `url` will be used as a fallback if `root` isn't defined.
358 if (root && (action === 'create' || this.isNew())) {
362 // When a model's `url` is not provided, we'll generate a URL to use by
363 // convention. This will combine the model's `id` with its configured
364 // `root` and add a trailing-slash if the root ends with "/".
366 return this._joinURL(this.getAsURL('id') || '');
369 // Substitute placeholders in the `url` with URL-encoded values from the
370 // model's attribute values or the specified `options`.
371 return this._substituteURL(url, Y.merge(this.getAttrs(), options));
375 Called to parse the response object returned from `Y.io()`. This method
376 receives the full response object and is expected to "prep" a response which
377 is suitable to pass to the `parse()` method.
379 By default the response body is returned (`responseText`), because it
380 usually represents the entire entity of this model on the server.
382 If you need to parse data out of the response's headers you should do so by
383 overriding this method. If you'd like the entire response object from the
384 XHR to be passed to your `parse()` method, you can simply assign this
387 @method parseIOResponse
388 @param {Object} response Response object from `Y.io()`.
389 @return {Any} The modified response to pass along to the `parse()` method.
392 parseIOResponse: function (response) {
393 return response.responseText;
397 Serializes `this` model to be used as the HTTP request entity body.
399 By default this model will be serialized to a JSON string via its `toJSON()`
402 You can override this method when the HTTP server expects a different
403 representation of this model's data that is different from the default JSON
404 serialization. If you're sending and receive content other than JSON, be
405 sure change the `Accept` and `Content-Type` `HTTP_HEADERS` as well.
407 **Note:** A model's `toJSON()` method can also be overridden. If you only
408 need to modify which attributes are serialized to JSON, that's a better
412 @param {String} [action] Optional `sync()` action for which to generate the
413 the serialized representation of this model.
414 @return {String} serialized HTTP request entity body.
417 serialize: function (action) {
418 return Y.JSON.stringify(this);
422 Communicates with a RESTful HTTP server by sending and receiving data via
423 XHRs. This method is called internally by load(), save(), and destroy().
425 The URL used for each XHR will be retrieved by calling the `getURL()` method
426 and passing it the specified `action` and `options`.
428 This method relies heavily on standard RESTful HTTP conventions
431 @param {String} action Sync action to perform. May be one of the following:
433 * `create`: Store a newly-created model for the first time.
434 * `delete`: Delete an existing model.
435 * `read` : Load an existing model.
436 * `update`: Update an existing model.
438 @param {Object} [options] Sync options:
439 @param {String} [options.csrfToken] The authenticity token used by the
440 server to verify the validity of this request and protected against CSRF
441 attacks. This overrides the default value provided by the static
442 `CSRF_TOKEN` property.
443 @param {Object} [options.headers] The HTTP headers to mix with the default
444 headers specified by the static `HTTP_HEADERS` property.
445 @param {Number} [options.timeout] The number of milliseconds before the
446 request will timeout and be aborted. This overrides the default provided
447 by the static `HTTP_TIMEOUT` property.
448 @param {Function} [callback] Called when the sync operation finishes.
449 @param {Error|null} callback.err If an error occurred, this parameter will
450 contain the error. If the sync operation succeeded, _err_ will be
452 @param {Any} [callback.response] The server's response.
454 sync: function (action, options, callback) {
455 options || (options = {});
457 var url = this.getURL(action, options),
458 method = RESTSync.HTTP_METHODS[action],
459 headers = Y.merge(RESTSync.HTTP_HEADERS, options.headers),
460 timeout = options.timeout || RESTSync.HTTP_TIMEOUT,
461 csrfToken = options.csrfToken || RESTSync.CSRF_TOKEN,
464 // Prepare the content if we are sending data to the server.
465 if (method === 'POST' || method === 'PUT') {
466 entity = this.serialize(action);
468 // Remove header, no content is being sent.
469 delete headers['Content-Type'];
472 // Setup HTTP emulation for older servers if we need it.
473 if (RESTSync.EMULATE_HTTP &&
474 (method === 'PUT' || method === 'DELETE')) {
476 // Pass along original method type in the headers.
477 headers['X-HTTP-Method-Override'] = method;
479 // Fall-back to using POST method type.
483 // Add CSRF token to HTTP request headers if one is specified and the
484 // request will cause side effects on the server.
486 (method === 'POST' || method === 'PUT' || method === 'DELETE')) {
488 headers['X-CSRF-Token'] = csrfToken;
491 this._sendSyncIORequest({
502 // -- Protected Methods ----------------------------------------------------
505 Joins the `root` URL to the specified `url`, normalizing leading/trailing
510 model._joinURL('bar'); // => '/foo/bar'
511 model._joinURL('/bar'); // => '/foo/bar'
514 model._joinURL('bar'); // => '/foo/bar/'
515 model._joinURL('/bar'); // => '/foo/bar/'
518 @param {String} url URL to append to the `root` URL.
519 @return {String} Joined URL.
523 _joinURL: function (url) {
524 var root = this.root;
526 if (!(root || url)) {
530 if (url.charAt(0) === '/') {
531 url = url.substring(1);
534 // Combines the `root` with the `url` and adds a trailing-slash if the
535 // `root` has a trailing-slash.
536 return root && root.charAt(root.length - 1) === '/' ?
543 Calls both public, overrideable methods: `parseIOResponse()`, then `parse()`
544 and returns the result.
546 This will call into `parseIOResponse()`, if it's defined as a method,
547 passing it the full response object from the XHR and using its return value
548 to pass along to the `parse()`. This enables developers to easily parse data
549 out of the response headers which should be used by the `parse()` method.
552 @param {Object} response Response object from `Y.io()`.
553 @return {Object|Object[]} Attribute hash or Array of model attribute hashes.
557 _parse: function (response) {
558 // When `parseIOResponse` is defined as a method, it will be invoked and
559 // the result will become the new response object that the `parse()`
560 // will be invoked with.
561 if (typeof this.parseIOResponse === 'function') {
562 response = this.parseIOResponse(response);
565 return this.parse(response);
569 Performs the XHR and returns the resulting `Y.io()` request object.
571 This method is called by `sync()`.
573 @method _sendSyncIORequest
574 @param {Object} config An object with the following properties:
575 @param {String} config.action The `sync()` action being performed.
576 @param {Function} [config.callback] Called when the sync operation
578 @param {String} [config.entity] The HTTP request entity body.
579 @param {Object} config.headers The HTTP request headers.
580 @param {String} config.method The HTTP request method.
581 @param {Number} [config.timeout] Time until the HTTP request is aborted.
582 @param {String} config.url The URL of the HTTP resource.
583 @return {Object} The resulting `Y.io()` request object.
587 _sendSyncIORequest: function (config) {
588 return Y.io(config.url, {
590 action : config.action,
591 callback: config.callback,
596 data : config.entity,
597 headers: config.headers,
598 method : config.method,
599 timeout: config.timeout,
602 start : this._onSyncIOStart,
603 failure: this._onSyncIOFailure,
604 success: this._onSyncIOSuccess,
605 end : this._onSyncIOEnd
611 Utility which takes a tokenized `url` string and substitutes its
612 placeholders using a specified `data` object.
614 This method will property URL-encode any values before substituting them.
615 Also, only expect it to work with String and Number values.
618 var url = this._substituteURL('/users/{name}', {id: 'Eric F'});
619 // => "/users/Eric%20F"
621 @method _substituteURL
622 @param {String} url Tokenized URL string to substitute placeholder values.
623 @param {Object} data Set of data to fill in the `url`'s placeholders.
624 @return {String} Substituted URL.
628 _substituteURL: function (url, data) {
635 // Creates a hash of the string and number values only to be used to
636 // replace any placeholders in a tokenized `url`.
637 Y.Object.each(data, function (v, k) {
638 if (Lang.isString(v) || Lang.isNumber(v)) {
639 // URL-encode any string or number values.
640 values[k] = encodeURIComponent(v);
644 return Lang.sub(url, values);
647 // -- Event Handlers -------------------------------------------------------
650 Called when the `Y.io` request has finished, after "success" or "failure"
653 This is a no-op by default, but provides a hook for overriding.
656 @param {String} txId The `Y.io` transaction id.
657 @param {Object} details Extra details carried through from `sync()`:
658 @param {String} details.action The sync action performed.
659 @param {Function} [details.callback] The function to call after syncing.
660 @param {String} details.url The URL of the requested resource.
664 _onSyncIOEnd: function (txId, details) {},
667 Called when the `Y.io` request has finished unsuccessfully.
669 By default this calls the `details.callback` function passing it the HTTP
670 status code and message as an error object along with the response body.
672 @method _onSyncIOFailure
673 @param {String} txId The `Y.io` transaction id.
674 @param {Object} res The `Y.io` response object.
675 @param {Object} details Extra details carried through from `sync()`:
676 @param {String} details.action The sync action performed.
677 @param {Function} [details.callback] The function to call after syncing.
678 @param {String} details.url The URL of the requested resource.
682 _onSyncIOFailure: function (txId, res, details) {
683 var callback = details.callback;
694 Called when the `Y.io` request has finished successfully.
696 By default this calls the `details.callback` function passing it the
699 @method _onSyncIOSuccess
700 @param {String} txId The `Y.io` transaction id.
701 @param {Object} res The `Y.io` response object.
702 @param {Object} details Extra details carried through from `sync()`:
703 @param {String} details.action The sync action performed.
704 @param {Function} [details.callback] The function to call after syncing.
705 @param {String} details.url The URL of the requested resource.
709 _onSyncIOSuccess: function (txId, res, details) {
710 var callback = details.callback;
718 Called when the `Y.io` request is made.
720 This is a no-op by default, but provides a hook for overriding.
722 @method _onSyncIOStart
723 @param {String} txId The `Y.io` transaction id.
724 @param {Object} details Extra details carried through from `sync()`:
725 @param {String} detials.action The sync action performed.
726 @param {Function} [details.callback] The function to call after syncing.
727 @param {String} details.url The URL of the requested resource.
731 _onSyncIOStart: function (txId, details) {}
734 // -- Namespace ----------------------------------------------------------------
736 Y.namespace('ModelSync').REST = RESTSync;
739 }, '3.7.1', {"requires": ["model", "io-base", "json-stringify"]});