diff --git a/packages/-ember-data/tests/integration/identifiers/scenarios-test.ts b/packages/-ember-data/tests/integration/identifiers/scenarios-test.ts index bc67004462e..089495ebf82 100644 --- a/packages/-ember-data/tests/integration/identifiers/scenarios-test.ts +++ b/packages/-ember-data/tests/integration/identifiers/scenarios-test.ts @@ -31,6 +31,7 @@ module('Integration | Identifiers - scenarios', function(hooks) { module('Secondary Cache based on an attribute', function(hooks) { let store; let calls; + let isQuery = false; let secondaryCache: { id: ConfidentDict; username: ConfidentDict; @@ -44,10 +45,11 @@ module('Integration | Identifiers - scenarios', function(hooks) { shouldBackgroundReloadRecord() { return false; } - findRecord(isQuery = false) { + findRecord() { if (isQuery !== true) { calls.findRecord++; } + isQuery = false; return resolve({ data: { id: '1', @@ -62,7 +64,8 @@ module('Integration | Identifiers - scenarios', function(hooks) { } queryRecord() { calls.queryRecord++; - return this.findRecord(true); + isQuery = true; + return this.findRecord(); } } @@ -234,6 +237,7 @@ module('Integration | Identifiers - scenarios', function(hooks) { module('Secondary Cache using an attribute as an alternate id', function(hooks) { let store; let calls; + let isQuery = false; let secondaryCache: ConfidentDict; class TestSerializer extends Serializer { normalizeResponse(_, __, payload) { @@ -244,10 +248,11 @@ module('Integration | Identifiers - scenarios', function(hooks) { shouldBackgroundReloadRecord() { return false; } - findRecord(isQuery = false) { + findRecord() { if (isQuery !== true) { calls.findRecord++; } + isQuery = false; return resolve({ data: { id: '1', @@ -262,7 +267,8 @@ module('Integration | Identifiers - scenarios', function(hooks) { } queryRecord() { calls.queryRecord++; - return this.findRecord(true); + isQuery = true; + return this.findRecord(); } } diff --git a/packages/adapter/addon/-private/build-url-mixin.js b/packages/adapter/addon/-private/build-url-mixin.ts similarity index 87% rename from packages/adapter/addon/-private/build-url-mixin.js rename to packages/adapter/addon/-private/build-url-mixin.ts index 2d7cb39d42a..04f645b586f 100644 --- a/packages/adapter/addon/-private/build-url-mixin.js +++ b/packages/adapter/addon/-private/build-url-mixin.ts @@ -4,6 +4,10 @@ import { camelize } from '@ember/string'; import { pluralize } from 'ember-inflector'; +type Dict = import('@ember-data/store/-private/ts-interfaces/utils').Dict; +type Snapshot = import('@ember-data/store/-private/system/snapshot').default; +type SnapshotRecordArray = import('@ember-data/store/-private/system/snapshot-record-array').default; + /** @module @ember-data/adapter */ @@ -55,7 +59,13 @@ export default Mixin.create({ @param {Object} query object of query parameters to send for query requests. @return {String} url */ - buildURL(modelName, id, snapshot, requestType, query) { + buildURL( + modelName: string, + id: string | string[] | Dict | null, + snapshot: Snapshot | Snapshot[] | SnapshotRecordArray | null, + requestType: string = '', + query = {} + ): string { switch (requestType) { case 'findRecord': return this.urlForFindRecord(id, modelName, snapshot); @@ -89,9 +99,9 @@ export default Mixin.create({ @param {String} id @return {String} url */ - _buildURL(modelName, id) { + _buildURL(modelName: string | null | undefined, id: string | null | undefined): string { let path; - let url = []; + let url: string[] = []; let host = get(this, 'host'); let prefix = this.urlPrefix(); @@ -109,12 +119,12 @@ export default Mixin.create({ url.unshift(prefix); } - url = url.join('/'); - if (!host && url && url.charAt(0) !== '/') { - url = '/' + url; + let urlString = url.join('/'); + if (!host && urlString && urlString.charAt(0) !== '/') { + urlString = '/' + urlString; } - return url; + return urlString; }, /** @@ -140,7 +150,7 @@ export default Mixin.create({ @return {String} url */ - urlForFindRecord(id, modelName, snapshot) { + urlForFindRecord(id: string, modelName: string, snapshot: Snapshot): string { return this._buildURL(modelName, id); }, @@ -165,7 +175,7 @@ export default Mixin.create({ @param {SnapshotRecordArray} snapshot @return {String} url */ - urlForFindAll(modelName, snapshot) { + urlForFindAll(modelName: string, snapshot: Snapshot): string { return this._buildURL(modelName); }, @@ -195,7 +205,7 @@ export default Mixin.create({ @param {String} modelName @return {String} url */ - urlForQuery(query, modelName) { + urlForQuery(query: Dict, modelName: string): string { return this._buildURL(modelName); }, @@ -220,7 +230,7 @@ export default Mixin.create({ @param {String} modelName @return {String} url */ - urlForQueryRecord(query, modelName) { + urlForQueryRecord(query: Dict, modelName: string): string { return this._buildURL(modelName); }, @@ -248,7 +258,7 @@ export default Mixin.create({ @param {Array} snapshots @return {String} url */ - urlForFindMany(ids, modelName, snapshots) { + urlForFindMany(ids: string[], modelName: string, snapshots: Snapshot[]) { return this._buildURL(modelName); }, @@ -275,7 +285,7 @@ export default Mixin.create({ @param {Snapshot} snapshot @return {String} url */ - urlForFindHasMany(id, modelName, snapshot) { + urlForFindHasMany(id: string, modelName: string, snapshot: Snapshot): string { return this._buildURL(modelName, id); }, @@ -302,7 +312,7 @@ export default Mixin.create({ @param {Snapshot} snapshot @return {String} url */ - urlForFindBelongsTo(id, modelName, snapshot) { + urlForFindBelongsTo(id: string, modelName: string, snapshot: Snapshot): string { return this._buildURL(modelName, id); }, @@ -327,7 +337,7 @@ export default Mixin.create({ @param {Snapshot} snapshot @return {String} url */ - urlForCreateRecord(modelName, snapshot) { + urlForCreateRecord(modelName: string, snapshot: Snapshot) { return this._buildURL(modelName); }, @@ -352,7 +362,7 @@ export default Mixin.create({ @param {Snapshot} snapshot @return {String} url */ - urlForUpdateRecord(id, modelName, snapshot) { + urlForUpdateRecord(id: string, modelName: string, snapshot: Snapshot): string { return this._buildURL(modelName, id); }, @@ -377,7 +387,7 @@ export default Mixin.create({ @param {Snapshot} snapshot @return {String} url */ - urlForDeleteRecord(id, modelName, snapshot) { + urlForDeleteRecord(id: string, modelName: string, snapshot: Snapshot): string { return this._buildURL(modelName, id); }, @@ -388,7 +398,7 @@ export default Mixin.create({ @param {String} parentURL @return {String} urlPrefix */ - urlPrefix(path, parentURL) { + urlPrefix(path: string | null | undefined, parentURL: string): string { let host = get(this, 'host'); let namespace = get(this, 'namespace'); @@ -412,7 +422,7 @@ export default Mixin.create({ } // No path provided - let url = []; + let url: string[] = []; if (host) { url.push(host); } @@ -450,7 +460,7 @@ export default Mixin.create({ @param {String} modelName @return {String} path **/ - pathForType(modelName) { + pathForType(modelName: string): string { let camelized = camelize(modelName); return pluralize(camelized); }, diff --git a/packages/adapter/addon/-private/fastboot-interface.ts b/packages/adapter/addon/-private/fastboot-interface.ts new file mode 100644 index 00000000000..a9277fb0556 --- /dev/null +++ b/packages/adapter/addon/-private/fastboot-interface.ts @@ -0,0 +1,9 @@ +export interface Request { + protocol: string; + host: string; +} + +export interface FastBoot { + isFastBoot: boolean; + request: Request; +} diff --git a/packages/adapter/addon/-private/utils/determine-body-promise.ts b/packages/adapter/addon/-private/utils/determine-body-promise.ts index 07b8309ce9a..a97f583b75e 100644 --- a/packages/adapter/addon/-private/utils/determine-body-promise.ts +++ b/packages/adapter/addon/-private/utils/determine-body-promise.ts @@ -3,6 +3,7 @@ import { DEBUG } from '@glimmer/env'; import continueOnReject from './continue-on-reject'; +type RequestData = import('../../rest').RequestData; type Payload = object | string | undefined; interface CustomSyntaxError extends SyntaxError { @@ -62,7 +63,7 @@ function _determineContent(response: Response, requestData: JQueryAjaxSettings, return ret; } -export function determineBodyPromise(response: Response, requestData: JQueryAjaxSettings): Promise { +export function determineBodyPromise(response: Response, requestData: RequestData): Promise { // response.text() may resolve or reject // it is a native promise, may not have finally return continueOnReject(response.text()).then(payload => _determineContent(response, requestData, payload)); diff --git a/packages/adapter/addon/index.js b/packages/adapter/addon/index.ts similarity index 87% rename from packages/adapter/addon/index.js rename to packages/adapter/addon/index.ts index 8ac492e4779..b24c34a3804 100644 --- a/packages/adapter/addon/index.js +++ b/packages/adapter/addon/index.ts @@ -1,4 +1,14 @@ import EmberObject from '@ember/object'; +import { DEBUG } from '@glimmer/env'; + +import { Promise as RSVPPromise } from 'rsvp'; + +type Dict = import('@ember-data/store/-private/ts-interfaces/utils').Dict; +type MinimumAdapterInterface = import('@ember-data/store/-private/ts-interfaces/minimum-adapter-interface').default; +type ShimModelClass = import('@ember-data/store/-private/system/model/shim-model-class').default; +type Store = import('@ember-data/store/-private/system/core-store').default; +type Snapshot = import('ember-data/-private').Snapshot; +type SnapshotRecordArray = import('@ember-data/store/-private/system/snapshot-record-array').default; /** An adapter is an object that receives requests from a store and @@ -55,7 +65,7 @@ import EmberObject from '@ember/object'; @class Adapter @extends EmberObject */ -export default class Adapter extends EmberObject { +export default class Adapter extends EmberObject implements MinimumAdapterInterface { /** If you would like your adapter to use a custom serializer you can set the `defaultSerializer` property to be the name of the custom @@ -113,6 +123,13 @@ export default class Adapter extends EmberObject { @param {Snapshot} snapshot @return {Promise} promise */ + findRecord(store: Store, type: ShimModelClass, id: string, snapshot: Snapshot): Promise { + if (DEBUG) { + throw new Error('You subclassed the Adapter class but missing a findRecord override'); + } + + return RSVPPromise.resolve(); + } /** The `findAll()` method is used to retrieve all records for a given type. @@ -144,6 +161,13 @@ export default class Adapter extends EmberObject { @param {SnapshotRecordArray} snapshotRecordArray @return {Promise} promise */ + findAll(store: Store, type: ShimModelClass, neverSet, snapshotRecordArray: SnapshotRecordArray): Promise { + if (DEBUG) { + throw new Error('You subclassed the Adapter class but missing a findAll override'); + } + + return RSVPPromise.resolve(); + } /** This method is called when you call `query` on the store. @@ -176,6 +200,13 @@ export default class Adapter extends EmberObject { @param {Object} adapterOptions @return {Promise} promise */ + query(store: Store, type: ShimModelClass, query): Promise { + if (DEBUG) { + throw new Error('You subclassed the Adapter class but missing a query override'); + } + + return RSVPPromise.resolve(); + } /** The `queryRecord()` method is invoked when the store is asked for a single @@ -214,6 +245,13 @@ export default class Adapter extends EmberObject { @param {Object} adapterOptions @return {Promise} promise */ + queryRecord(store: Store, type: ShimModelClass, query, adapterOptions): Promise { + if (DEBUG) { + throw new Error('You subclassed the Adapter class but missing a queryRecord override'); + } + + return RSVPPromise.resolve(); + } /** If the globally unique IDs for your records should be generated on the client, @@ -271,7 +309,7 @@ export default class Adapter extends EmberObject { @param {Object} options @return {Object} serialized snapshot */ - serialize(snapshot, options) { + serialize(snapshot, options): Dict { return snapshot.serialize(options); } @@ -316,6 +354,13 @@ export default class Adapter extends EmberObject { @param {Snapshot} snapshot @return {Promise} promise */ + createRecord(store: Store, type: ShimModelClass, snapshot: Snapshot): Promise { + if (DEBUG) { + throw new Error('You subclassed the Adapter class but missing a createRecord override'); + } + + return RSVPPromise.resolve(); + } /** Implement this method in a subclass to handle the updating of @@ -367,6 +412,13 @@ export default class Adapter extends EmberObject { @param {Snapshot} snapshot @return {Promise} promise */ + updateRecord(store: Store, type: ShimModelClass, snapshot: Snapshot): Promise { + if (DEBUG) { + throw new Error('You subclassed the Adapter class but missing a updateRecord override'); + } + + return RSVPPromise.resolve(); + } /** Implement this method in a subclass to handle the deletion of @@ -410,6 +462,13 @@ export default class Adapter extends EmberObject { @param {Snapshot} snapshot @return {Promise} promise */ + deleteRecord(store: Store, type: ShimModelClass, snapshot: Snapshot): Promise { + if (DEBUG) { + throw new Error('You subclassed the Adapter class but missing a deleteRecord override'); + } + + return RSVPPromise.resolve(); + } /** By default the store will try to coalesce all `fetchRecord` calls within the same runloop @@ -475,7 +534,7 @@ export default class Adapter extends EmberObject { @return {Array} an array of arrays of records, each of which is to be loaded separately by `findMany`. */ - groupRecordsForFindMany(store, snapshots) { + groupRecordsForFindMany(store: Store, snapshots: Snapshot[]): Snapshot[][] { return [snapshots]; } @@ -525,7 +584,7 @@ export default class Adapter extends EmberObject { @param {Snapshot} snapshot @return {Boolean} */ - shouldReloadRecord(store, snapshot) { + shouldReloadRecord(store: Store, snapshot: Snapshot): boolean { return false; } @@ -580,7 +639,7 @@ export default class Adapter extends EmberObject { @param {SnapshotRecordArray} snapshotRecordArray @return {Boolean} */ - shouldReloadAll(store, snapshotRecordArray) { + shouldReloadAll(store: Store, snapshotRecordArray: SnapshotRecordArray): boolean { return !snapshotRecordArray.length; } @@ -616,7 +675,7 @@ export default class Adapter extends EmberObject { @param {Snapshot} snapshot @return {Boolean} */ - shouldBackgroundReloadRecord(store, snapshot) { + shouldBackgroundReloadRecord(store: Store, Snapshot): boolean { return true; } @@ -652,7 +711,7 @@ export default class Adapter extends EmberObject { @param {SnapshotRecordArray} snapshotRecordArray @return {Boolean} */ - shouldBackgroundReloadAll(store, snapshotRecordArray) { + shouldBackgroundReloadAll(store: Store, snapshotRecordArray: SnapshotRecordArray): boolean { return true; } } diff --git a/packages/adapter/addon/json-api.js b/packages/adapter/addon/json-api.ts similarity index 86% rename from packages/adapter/addon/json-api.js rename to packages/adapter/addon/json-api.ts index 425884831ad..87a6e7b1a1e 100644 --- a/packages/adapter/addon/json-api.js +++ b/packages/adapter/addon/json-api.ts @@ -8,6 +8,14 @@ import { pluralize } from 'ember-inflector'; import { serializeIntoHash } from './-private'; import RESTAdapter from './rest'; +type FetchRequestInit = import('./rest').FetchRequestInit; +type JQueryRequestInit = import('./rest').JQueryRequestInit; + +type Dict = import('@ember-data/store/-private/ts-interfaces/utils').Dict; +type ShimModelClass = import('@ember-data/store/-private/system/model/shim-model-class').default; +type Store = import('@ember-data/store/-private/system/core-store').default; +type Snapshot = import('@ember-data/store/-private/system/snapshot').default; + /** The `JSONAPIAdapter` is the default adapter used by Ember Data. It is responsible for transforming the store's requests into HTTP @@ -156,7 +164,11 @@ class JSONAPIAdapter extends RESTAdapter { @param {Object} options @return {Object} */ - ajaxOptions(url, type, options = {}) { + ajaxOptions( + url: string, + type: string, + options: JQueryAjaxSettings | RequestInit = {} + ): JQueryRequestInit | FetchRequestInit { let hash = super.ajaxOptions(url, type, options); hash.headers['Accept'] = hash.headers['Accept'] || 'application/vnd.api+json'; @@ -219,19 +231,19 @@ class JSONAPIAdapter extends RESTAdapter { @property coalesceFindRequests @type {boolean} */ - coalesceFindRequests = false; + coalesceFindRequests: boolean = false; - findMany(store, type, ids, snapshots) { + findMany(store: Store, type: ShimModelClass, ids: string[], snapshots: Snapshot[]): Promise { let url = this.buildURL(type.modelName, ids, snapshots, 'findMany'); return this.ajax(url, 'GET', { data: { filter: { id: ids.join(',') } } }); } - pathForType(modelName) { + pathForType(modelName): string { let dasherized = dasherize(modelName); return pluralize(dasherized); } - updateRecord(store, type, snapshot) { + updateRecord(store: Store, type: ShimModelClass, snapshot: Snapshot): Promise { const data = serializeIntoHash(store, type, snapshot); let url = this.buildURL(type.modelName, snapshot.id, snapshot, 'updateRecord'); diff --git a/packages/adapter/addon/rest.js b/packages/adapter/addon/rest.ts similarity index 81% rename from packages/adapter/addon/rest.js rename to packages/adapter/addon/rest.ts index 7f5b7a6018f..f58e3493139 100644 --- a/packages/adapter/addon/rest.js +++ b/packages/adapter/addon/rest.ts @@ -1,18 +1,16 @@ -/* globals najax jQuery */ - /** @module @ember-data/adapter */ import { getOwner } from '@ember/application'; import { deprecate, warn } from '@ember/debug'; -import { computed, get } from '@ember/object'; +import { computed } from '@ember/object'; import { assign } from '@ember/polyfills'; import { run } from '@ember/runloop'; import { DEBUG } from '@glimmer/env'; import { has } from 'require'; -import { Promise } from 'rsvp'; +import { Promise as RSVPPromise } from 'rsvp'; import Adapter, { BuildURLMixin } from '@ember-data/adapter'; import AdapterError, { @@ -30,6 +28,52 @@ import { addSymbol, symbol } from '@ember-data/store/-private'; import { determineBodyPromise, fetch, parseResponseHeaders, serializeIntoHash, serializeQueryParams } from './-private'; +type Dict = import('@ember-data/store/-private/ts-interfaces/utils').Dict; +type FastBoot = import('./-private/fastboot-interface').FastBoot; +type Payload = Dict | string | undefined; +type ShimModelClass = import('@ember-data/store/-private/system/model/shim-model-class').default; +type Snapshot = import('@ember-data/store/-private/system/snapshot').default; +type SnapshotRecordArray = import('@ember-data/store/-private/system/snapshot-record-array').default; +type Store = import('@ember-data/store/-private/system/core-store').default; + +type QueryState = { + include?: unknown; + since?: unknown; +}; + +export interface FetchRequestInit extends RequestInit { + url: string; + method: string; + type: string; + contentType?: any; + body?: any; + data?: any; + cache?: any; + headers?: any; +} + +export interface JQueryRequestInit extends JQueryAjaxSettings { + url: string; + method: string; + type: string; +} + +export type RequestData = { + url: string; + method: string; + [key: string]: any; +}; + +type ResponseData = { + status: number; + textStatus: string; + headers: Dict; + errorThrown?: any; +}; + +declare const najax: Function | undefined; +declare const jQuery: JQueryStatic | undefined; + const UseFetch = symbol('useFetch'); const hasJQuery = typeof jQuery !== 'undefined'; @@ -294,30 +338,35 @@ const hasJQuery = typeof jQuery !== 'undefined'; @uses BuildURLMixin */ class RESTAdapter extends Adapter.extend(BuildURLMixin) { + /** + @property useFetch + @type {Boolean} + @public + */ + declare useFetch: boolean; + + declare _fastboot: FastBoot; + declare _najaxRequest: Function; + defaultSerializer = '-rest'; _defaultContentType = 'application/json; charset=utf-8'; - @computed + @computed() get fastboot() { // Avoid computed property override deprecation in fastboot as suggested by: // https://deprecations.emberjs.com/v3.x/#toc_computed-property-override - if (this._fastboot) { - return this._fastboot; + let fastboot = this._fastboot; + if (fastboot) { + return fastboot; } return (this._fastboot = getOwner(this).lookup('service:fastboot')); } - set fastboot(value) { - return (this._fastboot = value); + set fastboot(value: FastBoot) { + this._fastboot = value; } - /** - @property useFetch - @type {Boolean} - @public - */ - /** By default, the RESTAdapter will send the query params sorted alphabetically to the server. @@ -360,7 +409,7 @@ class RESTAdapter extends Adapter.extend(BuildURLMixin) { @param {Object} obj @return {Object} */ - sortQueryParams(obj) { + sortQueryParams(obj): Dict { let keys = Object.keys(obj); let len = keys.length; if (len < 2) { @@ -422,7 +471,7 @@ class RESTAdapter extends Adapter.extend(BuildURLMixin) { @property coalesceFindRequests @type {boolean} */ - coalesceFindRequests = false; + coalesceFindRequests: boolean = false; /** Endpoint paths can be prefixed with a `namespace` by setting the namespace @@ -483,6 +532,7 @@ class RESTAdapter extends Adapter.extend(BuildURLMixin) { @property headers @type {Object} */ + declare headers: Dict | undefined; /** Called by the store in order to fetch the JSON for a given @@ -501,9 +551,9 @@ class RESTAdapter extends Adapter.extend(BuildURLMixin) { @param {Snapshot} snapshot @return {Promise} promise */ - findRecord(store, type, id, snapshot) { + findRecord(store: Store, type: ShimModelClass, id: string, snapshot: Snapshot): Promise { let url = this.buildURL(type.modelName, id, snapshot, 'findRecord'); - let query = this.buildQuery(snapshot); + let query: QueryState = this.buildQuery(snapshot); return this.ajax(url, 'GET', { data: query }); } @@ -522,8 +572,8 @@ class RESTAdapter extends Adapter.extend(BuildURLMixin) { @param {SnapshotRecordArray} snapshotRecordArray @return {Promise} promise */ - findAll(store, type, sinceToken, snapshotRecordArray) { - let query = this.buildQuery(snapshotRecordArray); + findAll(store: Store, type: ShimModelClass, sinceToken, snapshotRecordArray: SnapshotRecordArray): Promise { + let query: QueryState = this.buildQuery(snapshotRecordArray); let url = this.buildURL(type.modelName, null, snapshotRecordArray, 'findAll'); if (sinceToken) { @@ -550,7 +600,7 @@ class RESTAdapter extends Adapter.extend(BuildURLMixin) { @param {Object} query @return {Promise} promise */ - query(store, type, query) { + query(store: Store, type: ShimModelClass, query): Promise { let url = this.buildURL(type.modelName, null, null, 'query', query); if (this.sortQueryParams) { @@ -578,7 +628,7 @@ class RESTAdapter extends Adapter.extend(BuildURLMixin) { @param {Object} query @return {Promise} promise */ - queryRecord(store, type, query) { + queryRecord(store: Store, type: ShimModelClass, query: Dict): Promise { let url = this.buildURL(type.modelName, null, null, 'queryRecord', query); if (this.sortQueryParams) { @@ -621,7 +671,7 @@ class RESTAdapter extends Adapter.extend(BuildURLMixin) { @param {Array} snapshots @return {Promise} promise */ - findMany(store, type, ids, snapshots) { + findMany(store: Store, type: ShimModelClass, ids: string[], snapshots: Snapshot[]): Promise { let url = this.buildURL(type.modelName, ids, snapshots, 'findMany'); return this.ajax(url, 'GET', { data: { ids: ids } }); } @@ -662,7 +712,7 @@ class RESTAdapter extends Adapter.extend(BuildURLMixin) { @param {Object} relationship meta object describing the relationship @return {Promise} promise */ - findHasMany(store, snapshot, url, relationship) { + findHasMany(store: Store, snapshot: Snapshot, url: string, relationship: Dict): Promise { let id = snapshot.id; let type = snapshot.modelName; @@ -707,7 +757,7 @@ class RESTAdapter extends Adapter.extend(BuildURLMixin) { @param {Object} relationship meta object describing the relationship @return {Promise} promise */ - findBelongsTo(store, snapshot, url, relationship) { + findBelongsTo(store: Store, snapshot: Snapshot, url: string, relationship): Promise { let id = snapshot.id; let type = snapshot.modelName; @@ -731,7 +781,7 @@ class RESTAdapter extends Adapter.extend(BuildURLMixin) { @param {Snapshot} snapshot @return {Promise} promise */ - createRecord(store, type, snapshot) { + createRecord(store: Store, type: ShimModelClass, snapshot: Snapshot): Promise { let url = this.buildURL(type.modelName, null, snapshot, 'createRecord'); const data = serializeIntoHash(store, type, snapshot); @@ -755,7 +805,7 @@ class RESTAdapter extends Adapter.extend(BuildURLMixin) { @param {Snapshot} snapshot @return {Promise} promise */ - updateRecord(store, type, snapshot) { + updateRecord(store: Store, type: ShimModelClass, snapshot: Snapshot): Promise { const data = serializeIntoHash(store, type, snapshot, {}); let id = snapshot.id; @@ -775,13 +825,13 @@ class RESTAdapter extends Adapter.extend(BuildURLMixin) { @param {Snapshot} snapshot @return {Promise} promise */ - deleteRecord(store, type, snapshot) { + deleteRecord(store: Store, type: ShimModelClass, snapshot: Snapshot): Promise { let id = snapshot.id; return this.ajax(this.buildURL(type.modelName, id, snapshot, 'deleteRecord'), 'DELETE'); } - _stripIDFromURL(store, snapshot) { + _stripIDFromURL(store: Store, snapshot: Snapshot): string { let url = this.buildURL(snapshot.modelName, snapshot.id, snapshot); let expandedURL = url.split('/'); @@ -790,11 +840,11 @@ class RESTAdapter extends Adapter.extend(BuildURLMixin) { // the id, it has been encodeURIComponent-ified within `buildURL`. If we // don't do this, then records with id having special characters are not // coalesced correctly (see GH #4190 for the reported bug) - let lastSegment = expandedURL[expandedURL.length - 1]; + let lastSegment: string = expandedURL[expandedURL.length - 1]; let id = snapshot.id; if (decodeURIComponent(lastSegment) === id) { expandedURL[expandedURL.length - 1] = ''; - } else if (endsWith(lastSegment, '?id=' + id)) { + } else if (id && endsWith(lastSegment, '?id=' + id)) { //Case when the url is of the format ...something?id=:id expandedURL[expandedURL.length - 1] = lastSegment.substring(0, lastSegment.length - id.length - 1); } @@ -827,7 +877,7 @@ class RESTAdapter extends Adapter.extend(BuildURLMixin) { @return {Array} an array of arrays of records, each of which is to be loaded separately by `findMany`. */ - groupRecordsForFindMany(store, snapshots) { + groupRecordsForFindMany(store: Store, snapshots: Snapshot[]): Snapshot[][] { let groups = new Map(); let adapter = this; let maxURLLength = this.maxURLLength; @@ -844,7 +894,7 @@ class RESTAdapter extends Adapter.extend(BuildURLMixin) { function splitGroupToFitInUrl(group, maxURLLength, paramNameLength) { let idsSize = 0; let baseUrl = adapter._stripIDFromURL(store, group[0]); - let splitGroups = [[]]; + let splitGroups: Snapshot[][] = [[]]; group.forEach(snapshot => { let additionalLength = encodeURIComponent(snapshot.id).length + paramNameLength; @@ -862,7 +912,7 @@ class RESTAdapter extends Adapter.extend(BuildURLMixin) { return splitGroups; } - let groupsArray = []; + let groupsArray: Snapshot[][] = []; groups.forEach((group, key) => { let paramNameLength = '&ids%5B%5D='.length; let splitGroups = splitGroupToFitInUrl(group, maxURLLength, paramNameLength); @@ -902,11 +952,16 @@ class RESTAdapter extends Adapter.extend(BuildURLMixin) { @param {Object} requestData - the original request information @return {Object | AdapterError} response */ - handleResponse(status, headers, payload, requestData) { + handleResponse( + status: number, + headers: Dict, + payload: Payload, + requestData: RequestData + ): Payload | AdapterError { if (this.isSuccess(status, headers, payload)) { return payload; } else if (this.isInvalid(status, headers, payload)) { - return new InvalidError(payload.errors); + return new InvalidError(typeof payload === 'object' ? payload.errors : undefined); } let errors = this.normalizeErrorResponse(status, headers, payload); @@ -941,7 +996,7 @@ class RESTAdapter extends Adapter.extend(BuildURLMixin) { @param {Object} payload @return {Boolean} */ - isSuccess(status, headers, payload) { + isSuccess(status: number, _headers: Dict, _payload: Payload): boolean { return (status >= 200 && status < 300) || status === 304; } @@ -956,7 +1011,7 @@ class RESTAdapter extends Adapter.extend(BuildURLMixin) { @param {Object} payload @return {Boolean} */ - isInvalid(status, headers, payload) { + isInvalid(status: number, _headers: Dict, _payload: Payload): boolean { return status === 422; } @@ -984,44 +1039,46 @@ class RESTAdapter extends Adapter.extend(BuildURLMixin) { @param {Object} options @return {Promise} promise */ - ajax(url, type, options) { + ajax(url: string, type: string, options: JQueryAjaxSettings | RequestInit = {}): Promise { let adapter = this; - let requestData = { + let requestData: RequestData = { url: url, method: type, }; - let hash = adapter.ajaxOptions(url, type, options); if (this.useFetch) { let _response; + let hash: FetchRequestInit = adapter.ajaxOptions(url, type, options); return this._fetchRequest(hash) - .then(response => { + .then((response: Response) => { _response = response; return determineBodyPromise(response, requestData); }) - .then(payload => { + .then((payload: Payload) => { if (_response.ok && !(payload instanceof Error)) { return fetchSuccessHandler(adapter, payload, _response, requestData); } else { throw fetchErrorHandler(adapter, payload, _response, null, requestData); } }); - } + } else { + let hash: JQueryRequestInit = adapter.ajaxOptions(url, type, options); - return new Promise(function(resolve, reject) { - hash.success = function(payload, textStatus, jqXHR) { - let response = ajaxSuccessHandler(adapter, payload, jqXHR, requestData); - run.join(null, resolve, response); - }; + return new RSVPPromise(function(resolve, reject) { + hash.success = function(payload, textStatus, jqXHR) { + let response = ajaxSuccessHandler(adapter, payload, jqXHR, requestData); + run.join(null, resolve, response); + }; - hash.error = function(jqXHR, textStatus, errorThrown) { - let error = ajaxErrorHandler(adapter, jqXHR, errorThrown, requestData); - run.join(null, reject, error); - }; + hash.error = function(jqXHR, textStatus, errorThrown) { + let error = ajaxErrorHandler(adapter, jqXHR, errorThrown, requestData); + run.join(null, reject, error); + }; - adapter._ajax(hash); - }, 'DS: RESTAdapter#ajax ' + type + ' to ' + url); + adapter._ajax(hash); + }, 'DS: RESTAdapter#ajax ' + type + ' to ' + url); + } } /** @@ -1029,11 +1086,12 @@ class RESTAdapter extends Adapter.extend(BuildURLMixin) { @private @param {Object} options jQuery ajax options to be used for the ajax request */ - _ajaxRequest(options) { - jQuery.ajax(options); + _ajaxRequest(options: JQueryRequestInit): void { + // TODO add assertion that jquery is there rather then equality check + typeof jQuery !== 'undefined' && jQuery.ajax(options); } - _fetchRequest(options) { + _fetchRequest(options: FetchRequestInit): Promise { let fetchFunction = fetch(); if (fetchFunction) { @@ -1045,13 +1103,13 @@ class RESTAdapter extends Adapter.extend(BuildURLMixin) { } } - _ajax(options) { + _ajax(options: FetchRequestInit | JQueryRequestInit): void { if (this.useFetch) { - this._fetchRequest(options); - } else if (DEPRECATE_NAJAX && get(this, 'fastboot.isFastBoot')) { + this._fetchRequest(options as FetchRequestInit); + } else if (DEPRECATE_NAJAX && this.fastboot && this.fastboot.isFastBoot) { this._najaxRequest(options); } else { - this._ajaxRequest(options); + this._ajaxRequest(options as JQueryRequestInit); } } @@ -1063,8 +1121,12 @@ class RESTAdapter extends Adapter.extend(BuildURLMixin) { @param {Object} options @return {Object} */ - ajaxOptions(url, method, options) { - options = assign( + ajaxOptions( + url: string, + method: string, + options: JQueryAjaxSettings | RequestInit + ): JQueryRequestInit | FetchRequestInit { + let reqOptions: JQueryRequestInit | FetchRequestInit = assign( { url, method, @@ -1073,42 +1135,41 @@ class RESTAdapter extends Adapter.extend(BuildURLMixin) { options ); - let headers = get(this, 'headers'); - if (headers !== undefined) { - options.headers = assign({}, headers, options.headers); + if (this.headers !== undefined) { + reqOptions.headers = assign({}, this.headers, reqOptions.headers); } else if (!options.headers) { - options.headers = {}; + reqOptions.headers = {}; } - let contentType = options.contentType || this._defaultContentType; + let contentType = reqOptions.contentType || this._defaultContentType; if (this.useFetch) { - if (options.data && options.type !== 'GET') { - if (!options.headers['Content-Type'] && !options.headers['content-type']) { - options.headers['content-type'] = contentType; + if (reqOptions.data && reqOptions.type !== 'GET' && reqOptions.headers) { + if (!reqOptions.headers['Content-Type'] && !reqOptions.headers['content-type']) { + reqOptions.headers['content-type'] = contentType; } } - options = fetchOptions(options, this); + reqOptions = fetchOptions(reqOptions, this); } else { // GET requests without a body should not have a content-type header // and may be unexpected by a server - if (options.data && options.type !== 'GET') { - options = assign(options, { contentType }); + if (reqOptions.data && reqOptions.type !== 'GET') { + reqOptions = assign(reqOptions, { contentType }); } - options = ajaxOptions(options, this); + reqOptions = ajaxOptions(reqOptions, this); } - options.url = this._ajaxURL(options.url); + reqOptions.url = this._ajaxURL(reqOptions.url); - return options; + return reqOptions; } - _ajaxURL(url) { - if (get(this, 'fastboot.isFastBoot')) { + _ajaxURL(url: string): string { + if (this.fastboot?.isFastBoot) { let httpRegex = /^https?:\/\//; let protocolRelativeRegex = /^\/\//; - let protocol = get(this, 'fastboot.request.protocol'); - let host = get(this, 'fastboot.request.host'); + let protocol = this.fastboot.request.protocol; + let host = this.fastboot.request.host; if (protocolRelativeRegex.test(url)) { return `${protocol}${url}`; @@ -1133,8 +1194,8 @@ class RESTAdapter extends Adapter.extend(BuildURLMixin) { @param {String} responseText @return {Object} */ - parseErrorResponse(responseText) { - let json = responseText; + parseErrorResponse(responseText: string): Dict | string { + let json: string = responseText; try { json = JSON.parse(responseText); @@ -1153,8 +1214,8 @@ class RESTAdapter extends Adapter.extend(BuildURLMixin) { @param {Object} payload @return {Array} errors payload */ - normalizeErrorResponse(status, headers, payload) { - if (payload && typeof payload === 'object' && payload.errors) { + normalizeErrorResponse(status: number, _headers: Dict, payload: Payload): Dict[] { + if (payload && typeof payload === 'object' && payload.errors instanceof Array) { return payload.errors; } else { return [ @@ -1179,11 +1240,11 @@ class RESTAdapter extends Adapter.extend(BuildURLMixin) { @param {Object} requestData @return {String} detailed error message */ - generatedDetailedMessage(status, headers, payload, requestData) { + generatedDetailedMessage(status: number, headers, payload: Payload, requestData: RequestData): string { let shortenedPayload; let payloadContentType = headers['content-type'] || 'Empty Content-Type'; - if (payloadContentType === 'text/html' && payload.length > 250) { + if (payloadContentType === 'text/html' && typeof payload === 'string' && payload.length > 250) { shortenedPayload = '[Omitted Lengthy HTML]'; } else { shortenedPayload = payload; @@ -1206,8 +1267,8 @@ class RESTAdapter extends Adapter.extend(BuildURLMixin) { @param {Snapshot} snapshot @return {Object} */ - buildQuery(snapshot) { - let query = {}; + buildQuery(snapshot: Snapshot | SnapshotRecordArray): QueryState { + let query: QueryState = {}; if (snapshot) { let { include } = snapshot; @@ -1221,22 +1282,32 @@ class RESTAdapter extends Adapter.extend(BuildURLMixin) { } } -function ajaxSuccess(adapter, payload, requestData, responseData) { +function ajaxSuccess( + adapter: RESTAdapter, + payload: Payload, + requestData: RequestData, + responseData: ResponseData +): Promise { let response; try { response = adapter.handleResponse(responseData.status, responseData.headers, payload, requestData); } catch (error) { - return Promise.reject(error); + return RSVPPromise.reject(error); } if (response && response.isAdapterError) { - return Promise.reject(response); + return RSVPPromise.reject(response); } else { return response; } } -function ajaxError(adapter, payload, requestData, responseData) { +function ajaxError( + adapter: RESTAdapter, + payload: Payload, + requestData: RequestData, + responseData: ResponseData +): Error | TimeoutError | AbortError | Dict { let error; if (responseData.errorThrown instanceof Error && payload !== '') { @@ -1262,7 +1333,7 @@ function ajaxError(adapter, payload, requestData, responseData) { } // Adapter abort error to include any relevent info, e.g. request/response: -function handleAbort(requestData, responseData) { +function handleAbort(requestData: RequestData, responseData: ResponseData): AbortError { let { method, url, errorThrown } = requestData; let { status } = responseData; let msg = `Request failed: ${method} ${url} ${errorThrown || ''}`; @@ -1271,7 +1342,7 @@ function handleAbort(requestData, responseData) { } //From http://stackoverflow.com/questions/280634/endswith-in-javascript -function endsWith(string, suffix) { +function endsWith(string: string, suffix: string): boolean { if (typeof String.prototype.endsWith !== 'function') { return string.indexOf(suffix, string.length - suffix.length) !== -1; } else { @@ -1279,12 +1350,23 @@ function endsWith(string, suffix) { } } -function fetchSuccessHandler(adapter, payload, response, requestData) { +function fetchSuccessHandler( + adapter: RESTAdapter, + payload: Payload, + response: Response, + requestData: RequestData +): Promise { let responseData = fetchResponseData(response); return ajaxSuccess(adapter, payload, requestData, responseData); } -function fetchErrorHandler(adapter, payload, response, errorThrown, requestData) { +function fetchErrorHandler( + adapter: RESTAdapter, + payload: Payload, + response: Response, + errorThrown, + requestData: RequestData +) { let responseData = fetchResponseData(response); if (responseData.status === 200 && payload instanceof Error) { @@ -1292,17 +1374,24 @@ function fetchErrorHandler(adapter, payload, response, errorThrown, requestData) payload = responseData.errorThrown.payload; } else { responseData.errorThrown = errorThrown; - payload = adapter.parseErrorResponse(payload); + if (typeof payload === 'string') { + payload = adapter.parseErrorResponse(payload); + } } return ajaxError(adapter, payload, requestData, responseData); } -function ajaxSuccessHandler(adapter, payload, jqXHR, requestData) { +function ajaxSuccessHandler( + adapter: RESTAdapter, + payload: Payload, + jqXHR: JQuery.jqXHR, + requestData: RequestData +): Promise { let responseData = ajaxResponseData(jqXHR); return ajaxSuccess(adapter, payload, requestData, responseData); } -function ajaxErrorHandler(adapter, jqXHR, errorThrown, requestData) { +function ajaxErrorHandler(adapter: RESTAdapter, jqXHR: JQuery.jqXHR, errorThrown: string, requestData: RequestData) { let responseData = ajaxResponseData(jqXHR); responseData.errorThrown = errorThrown; let payload = adapter.parseErrorResponse(jqXHR.responseText); @@ -1318,15 +1407,15 @@ function ajaxErrorHandler(adapter, jqXHR, errorThrown, requestData) { return ajaxError(adapter, payload, requestData, responseData); } -function fetchResponseData(response) { +function fetchResponseData(response: Response): ResponseData { return { status: response.status, - textStatus: response.textStatus, + textStatus: response.statusText, headers: headersToObject(response.headers), }; } -function ajaxResponseData(jqXHR) { +function ajaxResponseData(jqXHR: JQuery.jqXHR): ResponseData { return { status: jqXHR.status, textStatus: jqXHR.statusText, @@ -1334,7 +1423,7 @@ function ajaxResponseData(jqXHR) { }; } -function headersToObject(headers) { +function headersToObject(headers: Headers): Dict { let headersObject = {}; if (headers) { @@ -1350,14 +1439,17 @@ function headersToObject(headers) { * @param {Adapter} adapter * @returns {Object} */ -export function fetchOptions(options, adapter) { +export function fetchOptions( + options: JQueryRequestInit & Partial, + adapter: RESTAdapter +): FetchRequestInit { options.credentials = options.credentials || 'same-origin'; if (options.data) { // GET and HEAD requests can't have a `body` if (options.method === 'GET' || options.method === 'HEAD') { // If no options are passed, Ember Data sets `data` to an empty object, which we test for. - if (Object.keys(options.data).length) { + if (Object.keys(options.data).length && options.url) { // Test if there are already query params in the url (mimics jQuey.ajax). const queryParamDelimiter = options.url.indexOf('?') > -1 ? '&' : '?'; options.url += `${queryParamDelimiter}${serializeQueryParams(options.data)}`; @@ -1384,7 +1476,7 @@ export function fetchOptions(options, adapter) { return options; } -function ajaxOptions(options, adapter) { +function ajaxOptions(options: JQueryRequestInit, adapter: RESTAdapter): JQueryRequestInit { options.dataType = 'json'; options.context = adapter; @@ -1393,7 +1485,15 @@ function ajaxOptions(options, adapter) { } options.beforeSend = function(xhr) { - Object.keys(options.headers).forEach(key => xhr.setRequestHeader(key, options.headers[key])); + if (options.headers) { + Object.keys(options.headers).forEach(key => { + let headerValue = options.headers && options.headers[key]; + const isString = (value: unknown): value is string => typeof value === 'string'; + if (isString(headerValue)) { + xhr.setRequestHeader(key, headerValue); + } + }); + } }; return options; @@ -1405,7 +1505,7 @@ if (DEPRECATE_NAJAX) { @private @param {Object} options jQuery ajax options to be used for the najax request */ - RESTAdapter.prototype._najaxRequest = function(options) { + RESTAdapter.prototype._najaxRequest = function(options: JQueryAjaxSettings): void { if (typeof najax !== 'undefined') { najax(options); } else { diff --git a/packages/model/addon/-private/errors.js b/packages/model/addon/-private/errors.js index 5185962ed76..61d59c9b148 100644 --- a/packages/model/addon/-private/errors.js +++ b/packages/model/addon/-private/errors.js @@ -222,7 +222,7 @@ export default ArrayProxy.extend(DeprecatedEvented, { // add a single error errors.add('username', 'This field is required'); - errors.errorsFor('password'); + errors.errorsFor('username'); // => // [ // { attribute: 'username', message: 'This field is required' }, diff --git a/packages/private-build-infra/addon/current-deprecations.ts b/packages/private-build-infra/addon/current-deprecations.ts index 4e8f2de5358..dec3da9a66b 100644 --- a/packages/private-build-infra/addon/current-deprecations.ts +++ b/packages/private-build-infra/addon/current-deprecations.ts @@ -15,7 +15,7 @@ * An app can use a different version than what it specifies as it's compatibility * version. For instance, an App could be using `3.16` while specifying compatibility * with `3.12`. This would remove any deprecations that were present in or before `3.12` - * but keep support for anything deprecated in or abvoe `3.13`. + * but keep support for anything deprecated in or above `3.13`. * * ### Configuring Compatibility *