import { camelize } from '@ember/string'; import Mixin from '@ember/object/mixin'; import { get } from '@ember/object'; import { pluralize } from 'ember-inflector'; /** WARNING: This interface is likely to change in order to accomodate https://github.com/emberjs/rfcs/pull/4 ## Using BuildURLMixin To use url building, include the mixin when extending an adapter, and call `buildURL` where needed. The default behaviour is designed for RESTAdapter. ### Example ```javascript export default DS.Adapter.extend(BuildURLMixin, { findRecord: function(store, type, id, snapshot) { var url = this.buildURL(type.modelName, id, snapshot, 'findRecord'); return this.ajax(url, 'GET'); } }); ``` ### Attributes The `host` and `namespace` attributes will be used if defined, and are optional. @class BuildURLMixin @namespace DS */ export default Mixin.create({ /** Builds a URL for a given type and optional ID. By default, it pluralizes the type's name (for example, 'post' becomes 'posts' and 'person' becomes 'people'). To override the pluralization see [pathForType](#method_pathForType). If an ID is specified, it adds the ID to the path generated for the type, separated by a `/`. When called by RESTAdapter.findMany() the `id` and `snapshot` parameters will be arrays of ids and snapshots. @method buildURL @param {String} modelName @param {(String|Array|Object)} id single id or array of ids or query @param {(DS.Snapshot|Array)} snapshot single snapshot or array of snapshots @param {String} requestType @param {Object} query object of query parameters to send for query requests. @return {String} url */ buildURL(modelName, id, snapshot, requestType, query) { switch (requestType) { case 'findRecord': return this.urlForFindRecord(id, modelName, snapshot); case 'findAll': return this.urlForFindAll(modelName, snapshot); case 'query': return this.urlForQuery(query, modelName); case 'queryRecord': return this.urlForQueryRecord(query, modelName); case 'findMany': return this.urlForFindMany(id, modelName, snapshot); case 'findHasMany': return this.urlForFindHasMany(id, modelName, snapshot); case 'findBelongsTo': return this.urlForFindBelongsTo(id, modelName, snapshot); case 'createRecord': return this.urlForCreateRecord(modelName, snapshot); case 'updateRecord': return this.urlForUpdateRecord(id, modelName, snapshot); case 'deleteRecord': return this.urlForDeleteRecord(id, modelName, snapshot); default: return this._buildURL(modelName, id); } }, /** @method _buildURL @private @param {String} modelName @param {String} id @return {String} url */ _buildURL(modelName, id) { let path; let url = []; let host = get(this, 'host'); let prefix = this.urlPrefix(); if (modelName) { path = this.pathForType(modelName); if (path) { url.push(path); } } if (id) { url.push(encodeURIComponent(id)); } if (prefix) { url.unshift(prefix); } url = url.join('/'); if (!host && url && url.charAt(0) !== '/') { url = '/' + url; } return url; }, /** Builds a URL for a `store.findRecord(type, id)` call. Example: ```app/adapters/user.js import DS from 'ember-data'; export default DS.JSONAPIAdapter.extend({ urlForFindRecord(id, modelName, snapshot) { let baseUrl = this.buildURL(modelName, id, snapshot); return `${baseUrl}/users/${snapshot.adapterOptions.user_id}/playlists/${id}`; } }); ``` @method urlForFindRecord @param {String} id @param {String} modelName @param {DS.Snapshot} snapshot @return {String} url */ urlForFindRecord(id, modelName, snapshot) { return this._buildURL(modelName, id); }, /** Builds a URL for a `store.findAll(type)` call. Example: ```app/adapters/comment.js import DS from 'ember-data'; export default DS.JSONAPIAdapter.extend({ urlForFindAll(modelName, snapshot) { return 'data/comments.json'; } }); ``` @method urlForFindAll @param {String} modelName @param {DS.SnapshotRecordArray} snapshot @return {String} url */ urlForFindAll(modelName, snapshot) { return this._buildURL(modelName); }, /** Builds a URL for a `store.query(type, query)` call. Example: ```app/adapters/application.js import DS from 'ember-data'; export default DS.RESTAdapter.extend({ host: 'https://api.github.com', urlForQuery (query, modelName) { switch(modelName) { case 'repo': return `https://api.github.com/orgs/${query.orgId}/repos`; default: return this._super(...arguments); } } }); ``` @method urlForQuery @param {Object} query @param {String} modelName @return {String} url */ urlForQuery(query, modelName) { return this._buildURL(modelName); }, /** Builds a URL for a `store.queryRecord(type, query)` call. Example: ```app/adapters/application.js import DS from 'ember-data'; export default DS.RESTAdapter.extend({ urlForQueryRecord({ slug }, modelName) { let baseUrl = this.buildURL(); return `${baseUrl}/${encodeURIComponent(slug)}`; } }); ``` @method urlForQueryRecord @param {Object} query @param {String} modelName @return {String} url */ urlForQueryRecord(query, modelName) { return this._buildURL(modelName); }, /** Builds a URL for coalesceing multiple `store.findRecord(type, id)` records into 1 request when the adapter's `coalesceFindRequests` property is true. Example: ```app/adapters/application.js import DS from 'ember-data'; export default DS.RESTAdapter.extend({ urlForFindMany(ids, modelName) { let baseUrl = this.buildURL(); return `${baseUrl}/coalesce`; } }); ``` @method urlForFindMany @param {Array} ids @param {String} modelName @param {Array} snapshots @return {String} url */ urlForFindMany(ids, modelName, snapshots) { return this._buildURL(modelName); }, /** Builds a URL for fetching a async hasMany relationship when a url is not provided by the server. Example: ```app/adapters/application.js import DS from 'ember-data'; export default DS.JSONAPIAdapter.extend({ urlForFindHasMany(id, modelName, snapshot) { let baseUrl = this.buildURL(id, modelName); return `${baseUrl}/relationships`; } }); ``` @method urlForFindHasMany @param {String} id @param {String} modelName @param {DS.Snapshot} snapshot @return {String} url */ urlForFindHasMany(id, modelName, snapshot) { return this._buildURL(modelName, id); }, /** Builds a URL for fetching a async belongsTo relationship when a url is not provided by the server. Example: ```app/adapters/application.js import DS from 'ember-data'; export default DS.JSONAPIAdapter.extend({ urlForFindBelongsTo(id, modelName, snapshot) { let baseUrl = this.buildURL(id, modelName); return `${baseUrl}/relationships`; } }); ``` @method urlForFindBelongsTo @param {String} id @param {String} modelName @param {DS.Snapshot} snapshot @return {String} url */ urlForFindBelongsTo(id, modelName, snapshot) { return this._buildURL(modelName, id); }, /** Builds a URL for a `record.save()` call when the record was created locally using `store.createRecord()`. Example: ```app/adapters/application.js import DS from 'ember-data'; export default DS.RESTAdapter.extend({ urlForCreateRecord(modelName, snapshot) { return this._super(...arguments) + '/new'; } }); ``` @method urlForCreateRecord @param {String} modelName @param {DS.Snapshot} snapshot @return {String} url */ urlForCreateRecord(modelName, snapshot) { return this._buildURL(modelName); }, /** Builds a URL for a `record.save()` call when the record has been update locally. Example: ```app/adapters/application.js import DS from 'ember-data'; export default DS.RESTAdapter.extend({ urlForUpdateRecord(id, modelName, snapshot) { return `/${id}/feed?access_token=${snapshot.adapterOptions.token}`; } }); ``` @method urlForUpdateRecord @param {String} id @param {String} modelName @param {DS.Snapshot} snapshot @return {String} url */ urlForUpdateRecord(id, modelName, snapshot) { return this._buildURL(modelName, id); }, /** Builds a URL for a `record.save()` call when the record has been deleted locally. Example: ```app/adapters/application.js import DS from 'ember-data'; export default DS.RESTAdapter.extend({ urlForDeleteRecord(id, modelName, snapshot) { return this._super(...arguments) + '/destroy'; } }); ``` @method urlForDeleteRecord @param {String} id @param {String} modelName @param {DS.Snapshot} snapshot @return {String} url */ urlForDeleteRecord(id, modelName, snapshot) { return this._buildURL(modelName, id); }, /** @method urlPrefix @private @param {String} path @param {String} parentURL @return {String} urlPrefix */ urlPrefix(path, parentURL) { let host = get(this, 'host'); let namespace = get(this, 'namespace'); if (!host || host === '/') { host = ''; } if (path) { // Protocol relative url if (/^\/\//.test(path) || /http(s)?:\/\//.test(path)) { // Do nothing, the full host is already included. return path; // Absolute path } else if (path.charAt(0) === '/') { return `${host}${path}`; // Relative path } else { return `${parentURL}/${path}`; } } // No path provided let url = []; if (host) { url.push(host); } if (namespace) { url.push(namespace); } return url.join('/'); }, /** Determines the pathname for a given type. By default, it pluralizes the type's name (for example, 'post' becomes 'posts' and 'person' becomes 'people'). ### Pathname customization For example if you have an object LineItem with an endpoint of "/line_items/". ```app/adapters/application.js import DS from 'ember-data'; import { decamelize } from '@ember/string'; import { pluralize } from 'ember-inflector'; export default DS.RESTAdapter.extend({ pathForType: function(modelName) { var decamelized = decamelize(modelName); return pluralize(decamelized); } }); ``` @method pathForType @param {String} modelName @return {String} path **/ pathForType(modelName) { let camelized = camelize(modelName); return pluralize(camelized); } });