From 14551ec428a97bd37a33f76391b9dbc0f0292baa Mon Sep 17 00:00:00 2001 From: Chris Thoburn Date: Mon, 22 Aug 2022 14:38:19 -0700 Subject: [PATCH 1/2] feat: deprecate proxies --- .../workflows/enforce-pr-labels-release.yml | 2 +- .../array/-private/mutable-enumerable.d.ts | 4 +- @types/@ember/array/index.d.ts | 11 +- .../q/minimum-adapter-interface.ts | 6 +- ember-data-types/q/record-data.ts | 4 + packages/-ember-data/addon/-private/index.ts | 18 +- packages/-ember-data/addon/index.js | 4 - packages/-ember-data/index.js | 3 + .../node-tests/fixtures/expected.js | 4 - .../acceptance/relationships/has-many-test.js | 41 +- .../tracking-record-state-test.js | 2 +- .../tests/helpers}/watch-property.js | 46 +- .../adapter/client-side-delete-test.js | 14 +- .../integration/adapter/find-all-test.js | 26 +- .../adapter/json-api-adapter-test.js | 54 +- .../tests/integration/adapter/queries-test.js | 8 +- .../integration/adapter/rest-adapter-test.js | 39 +- .../rest-adapter/create-record-test.js | 12 +- .../integration/adapter/store-adapter-test.js | 6 +- .../non-dasherized-lookups-test.js | 2 +- .../identifiers/lid-reflection-test.ts | 4 +- .../identifiers/polymorphic-scenarios-test.ts | 2 +- .../integration/record-array-manager-test.js | 39 +- .../tests/integration/record-array-test.js | 53 +- .../adapter-populated-record-array-test.js | 28 +- .../record-arrays/peeked-records-test.js | 288 +++--- .../record-data/record-data-errors-test.ts | 14 +- .../record-data/record-data-state-test.ts | 4 + .../record-data/record-data-test.ts | 121 ++- .../record-data/unloading-record-data-test.js | 4 +- .../records/collection-save-test.js | 41 +- .../integration/records/create-record-test.js | 8 +- .../integration/records/delete-record-test.js | 7 +- .../integration/records/edit-record-test.js | 24 +- .../tests/integration/records/error-test.js | 10 +- .../records/relationship-changes-test.js | 5 +- .../tests/integration/records/reload-test.js | 19 +- .../integration/records/rematerialize-test.js | 2 +- .../tests/integration/records/save-test.js | 28 +- .../tests/integration/records/unload-test.js | 255 ++++-- .../references/autotracking-test.js | 2 +- .../integration/references/belongs-to-test.js | 35 +- .../integration/references/has-many-test.js | 169 ++-- .../relationships/belongs-to-test.js | 62 +- .../relationships/has-many-test.js | 256 +++--- .../inverse-relationship-load-test.js | 121 +-- .../inverse-relationships-test.js | 10 +- .../relationships/json-api-links-test.js | 8 +- .../relationships/many-to-many-test.js | 797 ++++++++--------- .../relationships/one-to-many-test.js | 168 ++-- .../relationships/one-to-one-test.js | 319 +++---- .../polymorphic-mixins-has-many-test.js | 14 +- .../relationships/promise-many-array-test.js | 3 + .../embedded-records-mixin-test.js | 12 +- .../serializers/json-api-serializer-test.js | 8 +- .../serializers/json-serializer-test.js | 4 +- .../serializers/rest-serializer-test.js | 4 +- .../tests/integration/snapshot-test.js | 77 +- .../tests/integration/store-test.js | 83 +- .../tests/integration/store/query-test.js | 41 +- packages/-ember-data/tests/test-helper.js | 2 +- .../-ember-data/tests/unit/many-array-test.js | 79 +- .../tests/unit/model/errors-test.js | 6 +- .../tests/unit/model/init-properties-test.js | 20 +- .../model/relationships/belongs-to-test.js | 26 +- .../unit/model/relationships/has-many-test.js | 406 ++++----- .../model/relationships/record-array-test.js | 70 +- .../unit/model/rollback-attributes-test.js | 4 +- .../adapter-populated-record-array-test.js | 48 +- .../unit/record-arrays/record-array-test.js | 189 ++-- .../tests/unit/store/adapter-interop-test.js | 14 +- .../tests/unit/store/create-record-test.js | 6 +- .../-ember-data/tests/unit/store/push-test.js | 8 +- .../polymorphic-relationship-payloads-test.js | 24 +- .../unit/system/snapshot-record-array-test.js | 54 +- packages/adapter/addon/index.ts | 2 +- packages/adapter/addon/rest.ts | 2 +- .../-private/deprecated-promise-proxy.ts | 54 ++ packages/model/addon/-private/has-many.js | 4 +- .../-private/legacy-relationships-support.ts | 133 ++- packages/model/addon/-private/many-array.ts | 458 +++++----- packages/model/addon/-private/model.js | 17 +- .../addon/-private/promise-belongs-to.ts | 2 +- .../addon/-private/promise-many-array.ts | 12 +- .../addon/-private/promise-proxy-base.d.ts | 32 + .../addon/-private/promise-proxy-base.js | 4 + .../addon/-private/references/belongs-to.ts | 23 +- .../addon/-private/references/has-many.ts | 22 +- packages/model/index.js | 2 + .../addon/current-deprecations.ts | 2 + .../private-build-infra/addon/deprecations.ts | 2 + .../addon/-private/graph/-operations.ts | 22 +- .../record-data/addon/-private/record-data.ts | 6 + .../integration/graph/edge-removal/helpers.ts | 4 +- packages/store/addon/-private/index.ts | 15 +- .../legacy-model-support/shim-model-class.ts | 1 + .../-private/managers/record-array-manager.ts | 197 +++-- .../-private/managers/record-data-manager.ts | 43 +- .../managers/record-data-store-wrapper.ts | 8 +- .../managers/record-notification-manager.ts | 1 + .../-private/network/snapshot-record-array.ts | 12 +- .../addon/-private/proxies/promise-proxies.ts | 83 +- .../adapter-populated-record-array.ts | 99 --- .../record-arrays/identifier-array.ts | 837 ++++++++++++++++++ .../-private/record-arrays/record-array.ts | 289 ------ .../store/addon/-private/store-service.ts | 70 +- .../addon/-private/utils/promise-record.ts | 5 +- .../tests/integration/coalescing-test.js | 4 +- .../tests/integration/has-many-test.js | 16 +- .../tests/integration/queries-test.js | 6 +- .../routes/basic-record-materialization.js | 4 +- .../app/routes/destroy.js | 2 +- .../relationship-materialization-complex.js | 4 +- .../app/routes/unload-all.js | 4 +- .../app/routes/unload.js | 2 +- .../tests/integration/relationships-test.js | 12 +- .../tests/integration/requests-test.js | 12 +- 117 files changed, 3944 insertions(+), 2996 deletions(-) rename packages/{unpublished-test-infra/addon-test-support => -ember-data/tests/helpers}/watch-property.js (81%) create mode 100644 packages/model/addon/-private/deprecated-promise-proxy.ts create mode 100644 packages/model/addon/-private/promise-proxy-base.d.ts create mode 100644 packages/model/addon/-private/promise-proxy-base.js delete mode 100644 packages/store/addon/-private/record-arrays/adapter-populated-record-array.ts create mode 100644 packages/store/addon/-private/record-arrays/identifier-array.ts delete mode 100644 packages/store/addon/-private/record-arrays/record-array.ts diff --git a/.github/workflows/enforce-pr-labels-release.yml b/.github/workflows/enforce-pr-labels-release.yml index f1cfb1d634c..5b2c2bcd547 100644 --- a/.github/workflows/enforce-pr-labels-release.yml +++ b/.github/workflows/enforce-pr-labels-release.yml @@ -31,7 +31,7 @@ jobs: - uses: yogevbd/enforce-label-action@2.2.2 with: BANNED_LABELS: 'backport-beta,backport-lts,backport-lts-prev,backport-old-release' - BANNED_LABEL/s_DESCRIPTION: "The following labels should only be applied to PRs targeting other release channel branches, remove them.['backport-beta', 'backport-lts', 'backport-lts-prev', 'backport-old-release']" + BANNED_LABELS_DESCRIPTION: "The following labels should only be applied to PRs targeting other release channel branches, remove them.['backport-beta', 'backport-lts', 'backport-lts-prev', 'backport-old-release']" triage: runs-on: ubuntu-latest steps: diff --git a/@types/@ember/array/-private/mutable-enumerable.d.ts b/@types/@ember/array/-private/mutable-enumerable.d.ts index 9ebdb729f10..f6f7406009c 100644 --- a/@types/@ember/array/-private/mutable-enumerable.d.ts +++ b/@types/@ember/array/-private/mutable-enumerable.d.ts @@ -10,7 +10,7 @@ interface MutableEnumerable extends Enumerable { /** * __Required.__ You must implement this method to apply this mixin. */ - addObject(object: T): T; + addObject(object: T): this; /** * Adds each object in the passed enumerable to the receiver. */ @@ -18,7 +18,7 @@ interface MutableEnumerable extends Enumerable { /** * __Required.__ You must implement this method to apply this mixin. */ - removeObject(object: T): T; + removeObject(object: T): this; /** * Removes each object in the passed enumerable from the receiver. */ diff --git a/@types/@ember/array/index.d.ts b/@types/@ember/array/index.d.ts index a42f0e51ffd..087cbe54ca3 100644 --- a/@types/@ember/array/index.d.ts +++ b/@types/@ember/array/index.d.ts @@ -10,13 +10,12 @@ import Enumerable from '@ember/array/-private/enumerable'; import type NativeArray from '@ember/array/-private/native-array'; import type ComputedProperty from '@ember/object/computed'; -import type Mixin from '@ember/object/mixin'; /** * This module implements Observer-friendly Array-like behavior. This mixin is picked up by the * Array class as well as other controllers, etc. that want to appear to be arrays. */ -interface Array extends Enumerable { +interface EmberArray extends Enumerable { /** * __Required.__ You must implement this method to apply this mixin. */ @@ -89,16 +88,10 @@ interface Array extends Enumerable { */ '@each': ComputedProperty; } -// Ember.Array rather than Array because the `array-type` lint rule doesn't realize the global is shadowed -// tslint:disable-next-line:array-type -// eslint-disable-next-line @typescript-eslint/no-unused-vars -declare class Array extends Mixin> { - static detect(arr: unknown): boolean; -} export const NativeArray; -export default Array; +export default EmberArray; /** * Creates an `Ember.NativeArray` from an Array like object. diff --git a/ember-data-types/q/minimum-adapter-interface.ts b/ember-data-types/q/minimum-adapter-interface.ts index 67e370ef0f6..8cbaee2d27d 100644 --- a/ember-data-types/q/minimum-adapter-interface.ts +++ b/ember-data-types/q/minimum-adapter-interface.ts @@ -1,7 +1,7 @@ import type Store from '@ember-data/store'; import type Snapshot from '@ember-data/store/-private/network/snapshot'; import type SnapshotRecordArray from '@ember-data/store/-private/network/snapshot-record-array'; -import type AdapterPopulatedRecordArray from '@ember-data/store/-private/record-arrays/adapter-populated-record-array'; +import type { Collection } from '@ember-data/store/-private/record-arrays/identifier-array'; import type { ModelSchema } from './ds-model'; import type { RelationshipSchema } from './record-data-schemas'; @@ -122,7 +122,7 @@ export interface MinimumAdapterInterface { * @param {ModelSchema} schema An object with methods for accessing information about * the type, attributes and relationships of the primary type associated with the request. * @param {object} query - * @param {AdapterPopulatedRecordArray} recordArray + * @param {Collection} recordArray * @param {object} options * @return {Promise} a promise resolving with resource data to feed to the associated serializer */ @@ -130,7 +130,7 @@ export interface MinimumAdapterInterface { store: Store, schema: ModelSchema, query: Dict, - recordArray: AdapterPopulatedRecordArray, + recordArray: Collection, options: { adapterOptions?: unknown } ): Promise; diff --git a/ember-data-types/q/record-data.ts b/ember-data-types/q/record-data.ts index 6f21852f9c1..3bcf84721a4 100644 --- a/ember-data-types/q/record-data.ts +++ b/ember-data-types/q/record-data.ts @@ -1,3 +1,5 @@ +import { LocalRelationshipOperation } from '@ember-data/record-data/-private/graph/-operations'; + import type { CollectionResourceRelationship, SingleResourceRelationship } from './ember-data-json-api'; import type { RecordIdentifier, StableRecordIdentifier } from './identifier'; import type { JsonApiResource, JsonApiValidationError } from './record-data-json-api'; @@ -73,6 +75,8 @@ export interface RecordData { unloadRecord(identifier: StableRecordIdentifier): void; + update(operation: LocalRelationshipOperation): void; + // Attrs // ===== diff --git a/packages/-ember-data/addon/-private/index.ts b/packages/-ember-data/addon/-private/index.ts index f291dd2a6b4..4fda72e1ba4 100644 --- a/packages/-ember-data/addon/-private/index.ts +++ b/packages/-ember-data/addon/-private/index.ts @@ -1,19 +1,17 @@ // public +import ArrayProxy from '@ember/array/proxy'; +import PromiseProxyMixin from '@ember/object/promise-proxy-mixin'; +import ObjectProxy from '@ember/object/proxy'; + export { default as Store } from '@ember-data/store'; export { default as DS } from './core'; export { Errors } from '@ember-data/model/-private'; export { Snapshot } from '@ember-data/store/-private'; // `ember-data-model-fragments' and `ember-data-change-tracker` rely on `normalizeModelName` -export { - AdapterPopulatedRecordArray, - PromiseArray, - PromiseObject, - RecordArray, - RecordArrayManager, - SnapshotRecordArray, - normalizeModelName, - coerceId, -} from '@ember-data/store/-private'; +export { RecordArrayManager, SnapshotRecordArray, normalizeModelName, coerceId } from '@ember-data/store/-private'; export { ManyArray, PromiseManyArray } from '@ember-data/model/-private'; export { RecordData } from '@ember-data/record-data/-private'; + +export const PromiseArray = ArrayProxy.extend(PromiseProxyMixin); +export const PromiseObject = ObjectProxy.extend(PromiseProxyMixin); diff --git a/packages/-ember-data/addon/index.js b/packages/-ember-data/addon/index.js index 34f72ed64b2..1ef63fb505a 100644 --- a/packages/-ember-data/addon/index.js +++ b/packages/-ember-data/addon/index.js @@ -124,14 +124,12 @@ import Transform from '@ember-data/serializer/transform'; import Store, { normalizeModelName } from '@ember-data/store'; import { - AdapterPopulatedRecordArray, DS, Errors, ManyArray, PromiseArray, PromiseManyArray, PromiseObject, - RecordArray, RecordArrayManager, Snapshot, } from './-private'; @@ -171,8 +169,6 @@ if (macroCondition(dependencySatisfies('@ember-data/debug', '*'))) { DS.DebugAdapter = importSync('@ember-data/debug').default; } -DS.RecordArray = RecordArray; -DS.AdapterPopulatedRecordArray = AdapterPopulatedRecordArray; DS.ManyArray = ManyArray; DS.RecordArrayManager = RecordArrayManager; diff --git a/packages/-ember-data/index.js b/packages/-ember-data/index.js index 2ab6f7fe116..806d76c6e72 100644 --- a/packages/-ember-data/index.js +++ b/packages/-ember-data/index.js @@ -17,6 +17,9 @@ module.exports = Object.assign({}, addonBaseConfig, { '@ember-data/store', '@ember-data/model', '@ember-data/model/-private', + '@ember/array/proxy', + '@ember/object/promise-proxy-mixin', + '@ember/object/proxy', ]; }, treeForAddon(tree) { diff --git a/packages/-ember-data/node-tests/fixtures/expected.js b/packages/-ember-data/node-tests/fixtures/expected.js index a40ae0ed460..eaed3848398 100644 --- a/packages/-ember-data/node-tests/fixtures/expected.js +++ b/packages/-ember-data/node-tests/fixtures/expected.js @@ -75,9 +75,6 @@ module.exports = { '(private) @ember-data/store IdentifierCache#_mergeRecordIdentifiers', '(private) @ember-data/store IdentifierCache#peekRecordIdentifier', '(private) @ember-data/store ManyArray#isPolymorphic', - '(private) @ember-data/store ManyArray#relationship', - '(private) @ember-data/store RecordArray#content', - '(private) @ember-data/store RecordArray#objectAtContent', '(private) @ember-data/store RecordArray#store', '(private) @ember-data/store Snapshot#constructor', '(private) @ember-data/store SnapshotRecordArray#_recordArray', @@ -313,7 +310,6 @@ module.exports = { '(public) @ember-data/store ManyArray#meta', '(public) @ember-data/store ManyArray#reload', '(public) @ember-data/store ManyArray#save', - '(public) @ember-data/store RecordArray#isLoaded', '(public) @ember-data/store RecordArray#isUpdating', '(public) @ember-data/store RecordArray#save', '(public) @ember-data/store RecordArray#type', diff --git a/packages/-ember-data/tests/acceptance/relationships/has-many-test.js b/packages/-ember-data/tests/acceptance/relationships/has-many-test.js index 5f63ba264af..2d0119508b4 100644 --- a/packages/-ember-data/tests/acceptance/relationships/has-many-test.js +++ b/packages/-ember-data/tests/acceptance/relationships/has-many-test.js @@ -16,6 +16,7 @@ import { ServerError } from '@ember-data/adapter/error'; import JSONAPIAdapter from '@ember-data/adapter/json-api'; import Model, { attr, belongsTo, hasMany } from '@ember-data/model'; import { LEGACY_SUPPORT } from '@ember-data/model/-private'; +import { DEPRECATE_ARRAY_LIKE } from '@ember-data/private-build-infra/deprecations'; import JSONAPISerializer from '@ember-data/serializer/json-api'; import Store from '@ember-data/store'; import { deprecatedTest } from '@ember-data/unpublished-test-infra/test-support/deprecated-test'; @@ -697,7 +698,7 @@ module('autotracking has-many', function (hooks) { store = owner.lookup('service:store'); }); - test('We can re-render a pojo', async function (assert) { + test('We can re-render a simple array', async function (assert) { class ChildrenList extends Component { @service store; @@ -706,7 +707,13 @@ module('autotracking has-many', function (hooks) { } get sortedChildren() { - return this.children.sortBy('name'); + if (DEPRECATE_ARRAY_LIKE) { + let result = this.children.sortBy('name'); + assert.expectDeprecation({ id: 'ember-data:deprecate-array-like' }); + return result; + } else { + return this.children.slice().sort((a, b) => (a.name > b.name ? 1 : -1)); + } } @action @@ -760,7 +767,9 @@ module('autotracking has-many', function (hooks) { @service store; get sortedChildren() { - return this.args.person.children.sortBy('name'); + let result = this.args.person.children.sortBy('name'); + assert.expectDeprecation({ id: 'ember-data:deprecate-array-like' }); + return result; } @action @@ -860,19 +869,28 @@ module('autotracking has-many', function (hooks) { deprecatedTest( 'We can re-render hasMany with PromiseManyArray.objectAt', - { id: 'ember-data:deprecate-promise-many-array-behaviors', until: '5.0', count: 6 }, + { id: 'ember-data:deprecate-promise-many-array-behaviors', until: '5.0', count: 12 }, async function (assert) { + let calls = 0; class ChildrenList extends Component { @service store; get firstChild() { - return this.args.person.children.objectAt(0); + const result = this.args.person.children.objectAt(0); + assert.expectDeprecation({ id: 'ember-data:deprecate-array-like' }); + return result; + } + + get lastChild() { + const result = this.args.person.children.objectAt(-1); + assert.expectDeprecation({ id: 'ember-data:deprecate-array-like' }); + return result; } @action createChild() { const parent = this.args.person; - const name = 'RGB'; + const name = 'RGB ' + calls++; this.store.createRecord('person', { name, parent }); } } @@ -881,6 +899,7 @@ module('autotracking has-many', function (hooks) {

{{this.firstChild.name}}

+

{{this.lastChild.name}}

`; this.owner.register('component:children-list', ChildrenList); this.owner.register('template:components/children-list', layout); @@ -894,11 +913,13 @@ module('autotracking has-many', function (hooks) { await click('#createChild'); - assert.dom('h2').hasText('RGB', 'renders first child'); + assert.dom('h2').hasText('RGB 0', 'renders first child'); + assert.dom('h3').hasText('RGB 0', 'renders last child'); await click('#createChild'); - assert.dom('h2').hasText('RGB', 'renders first child'); + assert.dom('h2').hasText('RGB 0', 'renders first child'); + assert.dom('h3').hasText('RGB 1', 'renders last child'); } ); @@ -1054,7 +1075,7 @@ module('autotracking has-many', function (hooks) { @service store; get firstChild() { - return this.args.children.objectAt(0); + return this.args.children.at(0); } @action @@ -1145,7 +1166,7 @@ module('autotracking has-many', function (hooks) { @service store; get children() { - return this.args.children.toArray(); + return this.args.children.slice(); } @action diff --git a/packages/-ember-data/tests/acceptance/relationships/tracking-record-state-test.js b/packages/-ember-data/tests/acceptance/relationships/tracking-record-state-test.js index 537f2b44187..cde3be0ab27 100644 --- a/packages/-ember-data/tests/acceptance/relationships/tracking-record-state-test.js +++ b/packages/-ember-data/tests/acceptance/relationships/tracking-record-state-test.js @@ -79,7 +79,7 @@ module('tracking state flags on a record', function (hooks) { // - access a prop on a single record (which is the prop // we will now change) if (this.children !== null) { - return this.children.toArray().filter((child) => { + return this.children.slice().filter((child) => { return child.isNew; }); } else { diff --git a/packages/unpublished-test-infra/addon-test-support/watch-property.js b/packages/-ember-data/tests/helpers/watch-property.js similarity index 81% rename from packages/unpublished-test-infra/addon-test-support/watch-property.js rename to packages/-ember-data/tests/helpers/watch-property.js index c0cc20c98e3..2a4bebd4792 100644 --- a/packages/unpublished-test-infra/addon-test-support/watch-property.js +++ b/packages/-ember-data/tests/helpers/watch-property.js @@ -1,5 +1,8 @@ +import { helper } from '@ember/component/helper'; import { addObserver, removeObserver } from '@ember/object/observers'; +import { render } from '@ember/test-helpers'; +import hbs from 'htmlbars-inline-precompile'; import QUnit from 'qunit'; function freeze(obj) { @@ -50,6 +53,23 @@ export function watchProperty(obj, propertyName) { return { counter, unwatch }; } +export async function startWatching() { + this.set( + 'observe', + helper(([obj, prop, value]) => { + obj.watchers[prop](); + }) + ); + this.set('__watchedObjects', this.__watchedObjects); + await render(hbs` + {{#each this.__watchedObjects key="@index" as |obj|}} + {{#each obj.properties key="@index" as |prop|}} + {{this.observe obj prop (get obj.context prop)}} + {{/each}} + {{/each}} +`); +} + export function watchProperties(obj, propertyNames) { let watched = {}; let counters = {}; @@ -68,17 +88,11 @@ export function watchProperties(obj, propertyNames) { let { counter, increment } = makeCounter(); watched[propertyName] = increment; counters[propertyName] = counter; - - addObserver(obj, propertyName, increment); - } - - function unwatch() { - Object.keys(watched).forEach((propertyName) => { - removeObserver(obj, propertyName, watched[propertyName]); - }); } - return { counters, unwatch }; + this.__watchedObjects = this.__watchedObjects || []; + this.__watchedObjects.push({ context: obj, counters, watchers: watched, properties: propertyNames }); + return { counters }; } QUnit.assert.watchedPropertyCounts = function assertWatchedPropertyCount(watchedObject, expectedCounts, label = '') { @@ -133,17 +147,3 @@ QUnit.assert.watchedPropertyCount = function assertWatchedPropertyCount(watcher, message: label, }); }; - -QUnit.assert.dirties = function assertDirties(options, updateMethodCallback, label) { - let { object: obj, property, count } = options; - count = typeof count === 'number' ? count : 1; - let { counter, unwatch } = watchProperty(obj, property); - updateMethodCallback(); - this.pushResult({ - result: counter.count === count, - actual: counter.count, - expected: count, - message: label, - }); - unwatch(); -}; diff --git a/packages/-ember-data/tests/integration/adapter/client-side-delete-test.js b/packages/-ember-data/tests/integration/adapter/client-side-delete-test.js index c44ef0912b9..ed43c66889b 100644 --- a/packages/-ember-data/tests/integration/adapter/client-side-delete-test.js +++ b/packages/-ember-data/tests/integration/adapter/client-side-delete-test.js @@ -69,14 +69,22 @@ module('integration/adapter/store-adapter - client-side delete', function (hooks ], }); - assert.deepEqual(bookstore.books.mapBy('id'), ['1', '2'], 'initial hasmany loaded'); + assert.deepEqual( + bookstore.books.map((book) => book.id), + ['1', '2'], + 'initial hasmany loaded' + ); let book2 = store.peekRecord('book', '2'); await book2.destroyRecord({ adapterOptions: { clientSideDelete: true } }); assert.strictEqual(store.peekRecord('book', '2'), null, 'book 2 unloaded'); - assert.deepEqual(bookstore.books.mapBy('id'), ['1'], 'one book client-side deleted'); + assert.deepEqual( + bookstore.books.map((book) => book.id), + ['1'], + 'one book client-side deleted' + ); store.push({ data: { @@ -94,7 +102,7 @@ module('integration/adapter/store-adapter - client-side delete', function (hooks }); assert.deepEqual( - bookstore.books.mapBy('id'), + bookstore.books.map((book) => book.id), ['1', '2'], 'the deleted book (with same id) is pushed back into the store' ); diff --git a/packages/-ember-data/tests/integration/adapter/find-all-test.js b/packages/-ember-data/tests/integration/adapter/find-all-test.js index 45b7ec65e4f..06bfb9a63d7 100644 --- a/packages/-ember-data/tests/integration/adapter/find-all-test.js +++ b/packages/-ember-data/tests/integration/adapter/find-all-test.js @@ -60,11 +60,7 @@ module('integration/adapter/find-all - Finding All Records of a Type', function let allRecords = await store.findAll('person'); assert.strictEqual(allRecords.length, 1, "the record array's length is 1 after a record is loaded into it"); - assert.strictEqual( - allRecords.objectAt(0).name, - 'Braaaahm Dale', - 'the first item in the record array is Braaaahm Dale' - ); + assert.strictEqual(allRecords[0].name, 'Braaaahm Dale', 'the first item in the record array is Braaaahm Dale'); let all = await store.findAll('person'); // Only one record array per type should ever be created (identity map) @@ -106,7 +102,7 @@ module('integration/adapter/find-all - Finding All Records of a Type', function return store.findAll('person'); }); assert.strictEqual(all.length, 1, "the record array's length is 1 after a record is loaded into it"); - assert.strictEqual(all.objectAt(0).name, 'Braaaahm Dale', 'the first item in the record array is Braaaahm Dale'); + assert.strictEqual(all[0].name, 'Braaaahm Dale', 'the first item in the record array is Braaaahm Dale'); }); test('When all records for a type are requested, records that are already loaded should be returned immediately.', async function (assert) { @@ -129,16 +125,8 @@ module('integration/adapter/find-all - Finding All Records of a Type', function let allRecords = store.peekAll('person'); assert.strictEqual(allRecords.length, 2, "the record array's length is 2"); - assert.strictEqual( - allRecords.objectAt(0).name, - 'Jeremy Ashkenas', - 'the first item in the record array is Jeremy Ashkenas' - ); - assert.strictEqual( - allRecords.objectAt(1).name, - 'Alex MacCaw', - 'the second item in the record array is Alex MacCaw' - ); + assert.strictEqual(allRecords[0].name, 'Jeremy Ashkenas', 'the first item in the record array is Jeremy Ashkenas'); + assert.strictEqual(allRecords[1].name, 'Alex MacCaw', 'the second item in the record array is Alex MacCaw'); }); test('When all records for a type are requested, records that are created on the client should be added to the record array.', async function (assert) { @@ -156,11 +144,7 @@ module('integration/adapter/find-all - Finding All Records of a Type', function // await settled(); assert.strictEqual(allRecords.length, 1, "the record array's length is 1"); - assert.strictEqual( - allRecords.objectAt(0).name, - 'Carsten Nielsen', - 'the first item in the record array is Carsten Nielsen' - ); + assert.strictEqual(allRecords[0].name, 'Carsten Nielsen', 'the first item in the record array is Carsten Nielsen'); }); testInDebug('When all records are requested, assert the payload is not blank', async function (assert) { diff --git a/packages/-ember-data/tests/integration/adapter/json-api-adapter-test.js b/packages/-ember-data/tests/integration/adapter/json-api-adapter-test.js index 5d289168d8e..a11318e6dbf 100644 --- a/packages/-ember-data/tests/integration/adapter/json-api-adapter-test.js +++ b/packages/-ember-data/tests/integration/adapter/json-api-adapter-test.js @@ -186,11 +186,11 @@ module('integration/adapter/json-api-adapter - JSONAPIAdapter', function (hooks) assert.strictEqual(passedUrl[0], '/posts'); assert.strictEqual(posts.length, 2, 'Returns two post records'); - assert.strictEqual(posts.firstObject.title, 'Ember.js rocks', 'The title for the first post is correct'); - assert.strictEqual(posts.lastObject.title, 'Tomster rules', 'The title for the second post is correct'); + assert.strictEqual(posts.at(0).title, 'Ember.js rocks', 'The title for the first post is correct'); + assert.strictEqual(posts.at(-1).title, 'Tomster rules', 'The title for the second post is correct'); - const firstPostAuthor = await posts.firstObject.author; - const lastPostAuthor = await posts.lastObject.author; + const firstPostAuthor = await posts.at(0).author; + const lastPostAuthor = await posts.at(-1).author; assert.strictEqual( firstPostAuthor.firstName, @@ -203,21 +203,13 @@ module('integration/adapter/json-api-adapter - JSONAPIAdapter', function (hooks) 'The author for the last post is loaded and has the correct last name' ); - const firstComments = await posts.firstObject.comments; - const lastComments = await posts.lastObject.comments; + const firstComments = await posts.at(0).comments; + const lastComments = await posts.at(-1).comments; assert.strictEqual(firstComments.length, 0, 'First post doesnt have comments'); - assert.strictEqual( - lastComments.firstObject.text, - 'This is the first comment', - 'Loads first comment for second post' - ); - assert.strictEqual( - lastComments.lastObject.text, - 'This is the second comment', - 'Loads second comment for second post' - ); + assert.strictEqual(lastComments.at(0).text, 'This is the first comment', 'Loads first comment for second post'); + assert.strictEqual(lastComments.at(-1).text, 'This is the second comment', 'Loads second comment for second post'); }); test('find many records', async function (assert) { @@ -243,7 +235,7 @@ module('integration/adapter/json-api-adapter - JSONAPIAdapter', function (hooks) assert.deepEqual(passedHash[0], { data: { filter: { id: '1' } } }, 'Sends correct params to adapter'); assert.strictEqual(posts.length, 1, 'Returns the correct number of records'); - assert.strictEqual(posts.firstObject.title, 'Ember.js rocks', 'Sets correct title to record'); + assert.strictEqual(posts.at(0).title, 'Ember.js rocks', 'Sets correct title to record'); }); test('queryRecord - primary data being a single record', async function (assert) { @@ -544,8 +536,8 @@ module('integration/adapter/json-api-adapter - JSONAPIAdapter', function (hooks) 'The related records comments using correct url' ); assert.strictEqual(comments.length, 2, 'Loads the correct number of comments from response'); - assert.strictEqual(comments.firstObject.text, 'This is the first comment', 'First comment text is correct'); - assert.strictEqual(comments.lastObject.text, 'This is the second comment', 'Second comment text is correct'); + assert.strictEqual(comments.at(0).text, 'This is the first comment', 'First comment text is correct'); + assert.strictEqual(comments.at(-1).text, 'This is the second comment', 'Second comment text is correct'); }); test('find a single record with hasMany link as object { data }', async function (assert) { @@ -600,8 +592,8 @@ module('integration/adapter/json-api-adapter - JSONAPIAdapter', function (hooks) assert.strictEqual(passedUrl[1], '/comments/2', 'Builds correct URL to fetch related record'); assert.strictEqual(passedUrl[2], '/comments/3', 'Builds correct URL to fetch related record'); assert.strictEqual(comments.length, 2); - assert.strictEqual(comments.firstObject.text, 'This is the first comment', 'First comment text is correct'); - assert.strictEqual(comments.lastObject.text, 'This is the second comment', 'Second comment text is correct'); + assert.strictEqual(comments.at(0).text, 'This is the first comment', 'First comment text is correct'); + assert.strictEqual(comments.at(-1).text, 'This is the second comment', 'Second comment text is correct'); }); test('find a single record with hasMany link as object { data } (polymorphic)', async function (assert) { @@ -660,8 +652,8 @@ module('integration/adapter/json-api-adapter - JSONAPIAdapter', function (hooks) assert.strictEqual(passedUrl[2], '/twitter-handles/3', 'Builds correct URL to fetch related record'); assert.strictEqual(handles.length, 2); - assert.strictEqual(handles.firstObject.username, 'wycats', 'First handle username is correct'); - assert.strictEqual(handles.lastObject.nickname, '@wycats', 'Second handle nickname is correct'); + assert.strictEqual(handles.at(0).username, 'wycats', 'First handle username is correct'); + assert.strictEqual(handles.at(-1).nickname, '@wycats', 'Second handle nickname is correct'); }); test('find a single record with sideloaded hasMany link as object { data }', async function (assert) { @@ -714,8 +706,8 @@ module('integration/adapter/json-api-adapter - JSONAPIAdapter', function (hooks) assert.strictEqual(passedUrl.length, 1, 'Do not call extra end points because related records are included'); assert.strictEqual(comments.length, 2, 'Loads related records'); - assert.strictEqual(comments.firstObject.text, 'This is the first comment', 'First comment text is correct'); - assert.strictEqual(comments.lastObject.text, 'This is the second comment', 'Second comment text is correct'); + assert.strictEqual(comments.at(0).text, 'This is the first comment', 'First comment text is correct'); + assert.strictEqual(comments.at(-1).text, 'This is the second comment', 'Second comment text is correct'); }); test('find a single record with sideloaded hasMany link as object { data } (polymorphic)', async function (assert) { @@ -771,8 +763,8 @@ module('integration/adapter/json-api-adapter - JSONAPIAdapter', function (hooks) assert.strictEqual(passedUrl.length, 1, 'Do not call extra end points because related records are included'); assert.strictEqual(handles.length, 2); - assert.strictEqual(handles.firstObject.username, 'wycats'); - assert.strictEqual(handles.lastObject.nickname, '@wycats'); + assert.strictEqual(handles.at(0).username, 'wycats'); + assert.strictEqual(handles.at(-1).nickname, '@wycats'); }); test('create record', async function (assert) { @@ -815,7 +807,7 @@ module('integration/adapter/json-api-adapter - JSONAPIAdapter', function (hooks) let handles = await user.handles; - handles.addObject(githubHandle); + handles.push(githubHandle); await user.save(); @@ -889,7 +881,7 @@ module('integration/adapter/json-api-adapter - JSONAPIAdapter', function (hooks) let handles = await user.handles; - handles.addObject(githubHandle); + handles.push(githubHandle); await user.save(); @@ -971,8 +963,8 @@ module('integration/adapter/json-api-adapter - JSONAPIAdapter', function (hooks) let handles = await user.handles; - handles.addObject(githubHandle); - handles.addObject(twitterHandle); + handles.push(githubHandle); + handles.push(twitterHandle); await user.save(); diff --git a/packages/-ember-data/tests/integration/adapter/queries-test.js b/packages/-ember-data/tests/integration/adapter/queries-test.js index a93a527059d..7a332cbbecc 100644 --- a/packages/-ember-data/tests/integration/adapter/queries-test.js +++ b/packages/-ember-data/tests/integration/adapter/queries-test.js @@ -80,8 +80,8 @@ module('integration/adapter/queries - Queries', function (hooks) { assert.strictEqual(queryResults.length, 2, 'the record array has a length of 2 after the results are loaded'); assert.true(queryResults.isLoaded, "the record array's `isLoaded` property should be true"); - assert.strictEqual(queryResults.objectAt(0).name, 'Peter Wagenet', "the first record is 'Peter Wagenet'"); - assert.strictEqual(queryResults.objectAt(1).name, 'Brohuda Katz', "the second record is 'Brohuda Katz'"); + assert.strictEqual(queryResults.at(0).name, 'Peter Wagenet', "the first record is 'Peter Wagenet'"); + assert.strictEqual(queryResults.at(1).name, 'Brohuda Katz', "the second record is 'Brohuda Katz'"); }); test('a query can be updated via `update()`', async function (assert) { @@ -101,7 +101,7 @@ module('integration/adapter/queries - Queries', function (hooks) { let personsQuery = await store.query('person', {}); assert.strictEqual(personsQuery.length, 1, 'There is one person'); - assert.strictEqual(personsQuery.firstObject.id, 'first', 'the right person is present'); + assert.strictEqual(personsQuery.at(0).id, 'first', 'the right person is present'); assert.false(personsQuery.isUpdating, 'RecordArray is not updating'); let resolveQueryPromise; @@ -128,7 +128,7 @@ module('integration/adapter/queries - Queries', function (hooks) { assert.false(personsQuery.isUpdating, 'RecordArray is not updating anymore'); assert.strictEqual(personsQuery.length, 1, 'There is still one person after update resolves'); - assert.strictEqual(personsQuery.firstObject.id, 'second', 'Now it is a different person'); + assert.strictEqual(personsQuery.at(0).id, 'second', 'Now it is a different person'); }); testInDebug( diff --git a/packages/-ember-data/tests/integration/adapter/rest-adapter-test.js b/packages/-ember-data/tests/integration/adapter/rest-adapter-test.js index 6694aa242a7..de809aca9b9 100644 --- a/packages/-ember-data/tests/integration/adapter/rest-adapter-test.js +++ b/packages/-ember-data/tests/integration/adapter/rest-adapter-test.js @@ -432,12 +432,12 @@ module('integration/adapter/rest_adapter - REST Adapter', function (hooks) { let comments = post.comments; // Replace the comment with a new one - comments.popObject(); - comments.pushObject(newComment); + comments.pop(); + comments.push(newComment); await post.save(); assert.strictEqual(post.comments.length, 1, 'the post has the correct number of comments'); - assert.strictEqual(post.get('comments.firstObject.name'), 'Yes. Yes it is.', 'the post has the correct comment'); + assert.strictEqual(post.comments.at(0).name, 'Yes. Yes it is.', 'the post has the correct comment'); }); test('updateRecord - hasMany relationships faithfully reflect removal from response', async function (assert) { @@ -530,7 +530,7 @@ module('integration/adapter/rest_adapter - REST Adapter', function (hooks) { let post = await store.peekRecord('post', 1); let comment = await store.peekRecord('comment', 1); let comments = post.comments; - comments.pushObject(comment); + comments.push(comment); assert.strictEqual(post.comments.length, 1, 'the post has one comment'); post = await post.save(); @@ -752,7 +752,7 @@ module('integration/adapter/rest_adapter - REST Adapter', function (hooks) { assert.strictEqual(posts.length, 2, 'The posts are in the array'); assert.true(posts.isLoaded, 'The RecordArray is loaded'); - assert.deepEqual(posts.toArray(), [post1, post2], 'The correct records are in the array'); + assert.deepEqual(posts.slice(), [post1, post2], 'The correct records are in the array'); }); test('findAll - passes buildURL the requestType and snapshot', async function (assert) { @@ -867,7 +867,7 @@ module('integration/adapter/rest_adapter - REST Adapter', function (hooks) { assert.strictEqual(posts.length, 2, 'The posts are in the array'); assert.true(posts.isLoaded, 'The RecordArray is loaded'); - assert.deepEqual(posts.toArray(), [post1, post2], 'The correct records are in the array'); + assert.deepEqual(posts.slice(), [post1, post2], 'The correct records are in the array'); }); test('query - if `sortQueryParams` option is not provided, query params are sorted alphabetically', async function (assert) { @@ -1061,7 +1061,7 @@ module('integration/adapter/rest_adapter - REST Adapter', function (hooks) { assert.strictEqual(posts.length, 2, 'The posts are in the array'); assert.true(posts.isLoaded, 'The RecordArray is loaded'); - assert.deepEqual(posts.toArray(), [post1, post2], 'The correct records are in the array'); + assert.deepEqual(posts.slice(), [post1, post2], 'The correct records are in the array'); }); test('query - returning sideloaded data loads the data', async function (assert) { @@ -1127,7 +1127,7 @@ module('integration/adapter/rest_adapter - REST Adapter', function (hooks) { assert.strictEqual(posts.length, 2, 'The posts are in the array'); assert.true(posts.isLoaded, 'The RecordArray is loaded'); - assert.deepEqual(posts.toArray(), [post1, post2], 'The correct records are in the array'); + assert.deepEqual(posts.slice(), [post1, post2], 'The correct records are in the array'); }); test('queryRecord - empty response', async function (assert) { @@ -1492,7 +1492,7 @@ module('integration/adapter/rest_adapter - REST Adapter', function (hooks) { name: 'What is omakase?', }); - assert.deepEqual(comments.toArray(), [comment1, comment2, comment3], 'The correct records are in the array'); + assert.deepEqual(comments.slice(), [comment1, comment2, comment3], 'The correct records are in the array'); }); test('findMany - returning sideloaded data loads the data', async function (assert) { @@ -1546,7 +1546,7 @@ module('integration/adapter/rest_adapter - REST Adapter', function (hooks) { let comment4 = store.peekRecord('comment', '4'); let post2 = store.peekRecord('post', '2'); - assert.deepEqual(comments.toArray(), [comment1, comment2, comment3], 'The correct records are in the array'); + assert.deepEqual(comments.slice(), [comment1, comment2, comment3], 'The correct records are in the array'); assert.deepEqual(comment4.getProperties('id', 'name'), { id: '4', @@ -1627,7 +1627,7 @@ module('integration/adapter/rest_adapter - REST Adapter', function (hooks) { name: 'What is omakase?', }); - assert.deepEqual(comments.toArray(), [comment1, comment2, comment3], 'The correct records are in the array'); + assert.deepEqual(comments.slice(), [comment1, comment2, comment3], 'The correct records are in the array'); }); test('findHasMany - returning an array populates the array', async function (assert) { @@ -1689,7 +1689,7 @@ module('integration/adapter/rest_adapter - REST Adapter', function (hooks) { name: 'What is omakase?', }); - assert.deepEqual(comments.toArray(), [comment1, comment2, comment3], 'The correct records are in the array'); + assert.deepEqual(comments.slice(), [comment1, comment2, comment3], 'The correct records are in the array'); }); test('findHasMany - passes buildURL the requestType', async function (assert) { @@ -1790,7 +1790,7 @@ module('integration/adapter/rest_adapter - REST Adapter', function (hooks) { let comment3 = store.peekRecord('comment', 3); let post2 = store.peekRecord('post', 2); - assert.deepEqual(comments.toArray(), [comment1, comment2, comment3], 'The correct records are in the array'); + assert.deepEqual(comments.slice(), [comment1, comment2, comment3], 'The correct records are in the array'); assert.deepEqual(post2.getProperties('id', 'name'), { id: '2', name: 'The Parley Letter' }); }); @@ -1865,7 +1865,7 @@ module('integration/adapter/rest_adapter - REST Adapter', function (hooks) { name: 'What is omakase?', }); - assert.deepEqual(comments.toArray(), [comment1, comment2, comment3], 'The correct records are in the array'); + assert.deepEqual(comments.slice(), [comment1, comment2, comment3], 'The correct records are in the array'); }); }); @@ -2140,7 +2140,10 @@ module('integration/adapter/rest_adapter - REST Adapter', function (hooks) { let post = await store.findRecord('post', 1); assert.strictEqual(post.authorName, '@d2h'); assert.strictEqual(post.author.name, 'D2H'); - assert.deepEqual(post.comments.mapBy('body'), ['Rails is unagi', 'What is omakase?']); + assert.deepEqual( + post.comments.map((r) => r.body), + ['Rails is unagi', 'What is omakase?'] + ); }); test('groupRecordsForFindMany splits up calls for large ids', async function (assert) { @@ -2711,9 +2714,9 @@ module('integration/adapter/rest_adapter - REST Adapter', function (hooks) { let comments = store.peekAll('comment'); - assert.strictEqual(get(comments, 'length'), 2, 'comments.length is correct'); - assert.strictEqual(get(comments, 'firstObject.name'), 'First comment', 'comments.firstObject.name is correct'); - assert.strictEqual(get(comments, 'lastObject.name'), 'Second comment', 'comments.lastObject.name is correct'); + assert.strictEqual(comments.length, 2, 'comments.length is correct'); + assert.strictEqual(comments[0].name, 'First comment', 'comments.at(0).name is correct'); + assert.strictEqual(comments.at(-1).name, 'Second comment', 'comments.at(-1).name is correct'); }); testInDebug( diff --git a/packages/-ember-data/tests/integration/adapter/rest-adapter/create-record-test.js b/packages/-ember-data/tests/integration/adapter/rest-adapter/create-record-test.js index 3a1a48773c4..c38243ab30a 100644 --- a/packages/-ember-data/tests/integration/adapter/rest-adapter/create-record-test.js +++ b/packages/-ember-data/tests/integration/adapter/rest-adapter/create-record-test.js @@ -186,7 +186,7 @@ module('integration/adapter/rest_adapter - REST Adapter - createRecord', functio const comment = store.createRecord('comment', { name: 'The Parley Letter' }); const comments = await post.comments; - comments.pushObject(comment); + comments.push(comment); assert.strictEqual(comment.post, post, 'the post has been set correctly'); @@ -560,14 +560,10 @@ module('integration/adapter/rest_adapter - REST Adapter - createRecord', functio await post.save(); - assert.strictEqual( - post.get('comments.firstObject.post'), - post, - 'the comments are related to the correct post model' - ); + assert.strictEqual(post.comments.at(0).post, post, 'the comments are related to the correct post model'); assert.strictEqual( - post.get('comments.firstObject.post'), + post.comments.at(0).post, store.peekRecord('post', '1'), 'The record object in the store is the same object related to the comment' ); @@ -601,7 +597,7 @@ module('integration/adapter/rest_adapter - REST Adapter - createRecord', functio assert.strictEqual(post.comments.length, 0, 'post has 0 comments'); - post.comments.pushObject(comment); + post.comments.push(comment); assert.strictEqual(post.comments.length, 1, 'post has 1 comment'); diff --git a/packages/-ember-data/tests/integration/adapter/store-adapter-test.js b/packages/-ember-data/tests/integration/adapter/store-adapter-test.js index cc4847c5ae2..02add3f4d8c 100644 --- a/packages/-ember-data/tests/integration/adapter/store-adapter-test.js +++ b/packages/-ember-data/tests/integration/adapter/store-adapter-test.js @@ -76,7 +76,7 @@ module('integration/adapter/store-adapter - DS.Store and DS.Adapter integration assert.strictEqual(people2.length, 2, 'return the elements'); assert.ok(people2.isLoaded, 'array is loaded'); - const person = people.objectAt(0); + const person = people.at(0); assert.ok(person.isLoaded, 'record is loaded'); // delete record will not throw exception @@ -1357,7 +1357,7 @@ module('integration/adapter/store-adapter - DS.Store and DS.Adapter integration store.createRecord('person', { name: 'Tom' }).save({ adapterOptions: { subscribe: true } }); }); - test('record.save should pass adapterOptions to the deleteRecord method', function (assert) { + test('record.save should pass adapterOptions to the deleteRecord method', async function (assert) { assert.expect(1); let store = this.owner.lookup('service:store'); @@ -1378,7 +1378,7 @@ module('integration/adapter/store-adapter - DS.Store and DS.Adapter integration }, }); let person = store.peekRecord('person', '1'); - person.destroyRecord({ adapterOptions: { subscribe: true } }); + await person.destroyRecord({ adapterOptions: { subscribe: true } }); }); test('store.findRecord should pass adapterOptions to adapter.findRecord', function (assert) { diff --git a/packages/-ember-data/tests/integration/backwards-compat/non-dasherized-lookups-test.js b/packages/-ember-data/tests/integration/backwards-compat/non-dasherized-lookups-test.js index f134e24aeef..053d3f3b78e 100644 --- a/packages/-ember-data/tests/integration/backwards-compat/non-dasherized-lookups-test.js +++ b/packages/-ember-data/tests/integration/backwards-compat/non-dasherized-lookups-test.js @@ -183,7 +183,7 @@ module( const longModel = await store.findRecord('long_model_name', '1'); const postNotesRel = await longModel.postNotes; - const postNotes = postNotesRel.toArray(); + const postNotes = postNotesRel.slice(); assert.deepEqual(postNotes, [store.peekRecord('postNote', 1)], 'inverse records found'); }); diff --git a/packages/-ember-data/tests/integration/identifiers/lid-reflection-test.ts b/packages/-ember-data/tests/integration/identifiers/lid-reflection-test.ts index 1766c32228f..dec2deb7e79 100644 --- a/packages/-ember-data/tests/integration/identifiers/lid-reflection-test.ts +++ b/packages/-ember-data/tests/integration/identifiers/lid-reflection-test.ts @@ -165,7 +165,7 @@ module('Integration | Identifiers - lid reflection', function (hooks) { class TestAdapter extends Adapter { createRecord(store, ModelClass, snapshot) { const cakeLid = recordIdentifierFor(snapshot.record).lid; - const ingredientLid = recordIdentifierFor(snapshot.record.ingredients.firstObject).lid; + const ingredientLid = recordIdentifierFor(snapshot.record.ingredients.at(0)).lid; return resolve({ data: { type: 'cake', @@ -221,7 +221,7 @@ module('Integration | Identifiers - lid reflection', function (hooks) { await cake.save(); assert.deepEqual(cake.hasMany('ingredients').ids(), ['2']); - assert.strictEqual(cake.ingredients.objectAt(0).name, 'Cheese'); + assert.strictEqual(cake.ingredients.at(0).name, 'Cheese'); assert.strictEqual(cake.id, '1', 'cake has the correct id'); assert.strictEqual(cheese.id, '2', 'cheese has the correct id'); diff --git a/packages/-ember-data/tests/integration/identifiers/polymorphic-scenarios-test.ts b/packages/-ember-data/tests/integration/identifiers/polymorphic-scenarios-test.ts index aecdb048020..0b8bc19a8d0 100644 --- a/packages/-ember-data/tests/integration/identifiers/polymorphic-scenarios-test.ts +++ b/packages/-ember-data/tests/integration/identifiers/polymorphic-scenarios-test.ts @@ -164,7 +164,7 @@ module('Integration | Identifiers - single-table-inheritance polymorphic scenari ], 'We fetched all the right cars' ); - const bmw = allCars.objectAt(1); + const bmw = allCars.at(1); const foundBmw = await store.findRecord('car', '2'); assert.strictEqual(foundBmw, bmw, 'We found the bmw by finding car 2'); diff --git a/packages/-ember-data/tests/integration/record-array-manager-test.js b/packages/-ember-data/tests/integration/record-array-manager-test.js index 5b2081fecec..836c0f89431 100644 --- a/packages/-ember-data/tests/integration/record-array-manager-test.js +++ b/packages/-ember-data/tests/integration/record-array-manager-test.js @@ -8,6 +8,7 @@ import { setupTest } from 'ember-qunit'; import RESTAdapter from '@ember-data/adapter/rest'; import Model, { attr, belongsTo, hasMany } from '@ember-data/model'; import { recordIdentifierFor } from '@ember-data/store'; +import { SOURCE } from '@ember-data/store/-private'; let store, manager; @@ -42,23 +43,6 @@ module('integration/record_array_manager', function (hooks) { manager = store.recordArrayManager; }); - function tap(obj, methodName, callback) { - let old = obj[methodName]; - - let summary = { called: [] }; - - obj[methodName] = function () { - let result = old.apply(obj, arguments); - if (callback) { - callback.apply(obj, arguments); - } - summary.called.push(arguments); - return result; - }; - - return summary; - } - test('destroying the store correctly cleans everything up', async function (assert) { store.push({ data: { @@ -94,12 +78,10 @@ module('integration/record_array_manager', function (hooks) { let all = store.peekAll('person'); let query = {}; let adapterPopulated = manager.createArray({ type: 'person', query }); - let allSummary = tap(all, 'willDestroy'); - let adapterPopulatedSummary = tap(adapterPopulated, 'willDestroy'); let identifier = recordIdentifierFor(person); - assert.strictEqual(allSummary.called.length, 0, 'initial: no calls to all.willDestroy'); - assert.strictEqual(adapterPopulatedSummary.called.length, 0, 'initial: no calls to adapterPopulated.willDestroy'); + assert.false(all.isDestroyed, 'initial: no calls to all.willDestroy'); + assert.false(adapterPopulated.isDestroyed, 'initial: no calls to adapterPopulated.willDestroy'); assert.strictEqual( manager._identifiers.get(identifier)?.size, undefined, @@ -110,15 +92,14 @@ module('integration/record_array_manager', function (hooks) { manager.destroy(); await settled(); - assert.strictEqual(allSummary.called.length, 1, 'all.willDestroy called once'); + assert.true(all.isDestroyed, 'all.willDestroy called once'); assert.false(manager._live.has('person'), 'no longer have a live array for person'); assert.strictEqual( manager._identifiers.get(identifier)?.size, undefined, 'expected the person to be a member of no recordArrays' ); - assert.strictEqual(allSummary.called.length, 1, 'all.willDestroy still only called once'); - assert.strictEqual(adapterPopulatedSummary.called.length, 1, 'adapterPopulated.willDestroy called once'); + assert.true(adapterPopulated.isDestroyed, 'adapterPopulated.willDestroy called once'); }); if (!gte('4.0.0')) { @@ -133,7 +114,7 @@ module('integration/record_array_manager', function (hooks) { assert.strictEqual(addedCount, 2, 'expected 2 added'); }; - assert.deepEqual(cars.toArray(), []); + assert.deepEqual(cars.slice(), []); assert.strictEqual(arrayContentWillChangeCount, 0, 'expected NO arrayChangeEvents yet'); store.push({ @@ -160,7 +141,7 @@ module('integration/record_array_manager', function (hooks) { assert.strictEqual(arrayContentWillChangeCount, 1, 'expected ONE array change event'); - assert.deepEqual(cars.toArray(), [store.peekRecord('car', 1), store.peekRecord('car', 2)]); + assert.deepEqual(cars.slice(), [store.peekRecord('car', 1), store.peekRecord('car', 2)]); store.peekRecord('car', 1).set('model', 'Mini'); @@ -238,9 +219,9 @@ module('integration/record_array_manager', function (hooks) { assert.strictEqual(recordArray.modelName, 'foo'); assert.true(recordArray.isLoaded); - assert.strictEqual(recordArray.manager, manager); - assert.deepEqual(recordArray.content, []); - assert.deepEqual(recordArray.toArray(), []); + assert.strictEqual(recordArray._manager, manager, 'the manager is set correctly'); + assert.deepEqual(recordArray[SOURCE], []); + assert.deepEqual(recordArray.slice(), []); }); test('liveArrayFor always return the same array for a given type', function (assert) { diff --git a/packages/-ember-data/tests/integration/record-array-test.js b/packages/-ember-data/tests/integration/record-array-test.js index 1b9863d1c34..9e513e4a930 100644 --- a/packages/-ember-data/tests/integration/record-array-test.js +++ b/packages/-ember-data/tests/integration/record-array-test.js @@ -59,7 +59,7 @@ module('unit/record-array - RecordArray', function (hooks) { await settled(); - assert.strictEqual(get(recordArray, 'lastObject.name'), 'wycats'); + assert.strictEqual(recordArray.at(-1).name, 'wycats'); store.push({ data: { @@ -73,7 +73,7 @@ module('unit/record-array - RecordArray', function (hooks) { await settled(); - assert.strictEqual(get(recordArray, 'lastObject.name'), 'brohuda'); + assert.strictEqual(recordArray.at(-1).name, 'brohuda'); }); test('acts as a live query (normalized names)', async function (assert) { @@ -96,7 +96,10 @@ module('unit/record-array - RecordArray', function (hooks) { await settled(); - assert.deepEqual(recordArray.mapBy('name'), ['John Churchill']); + assert.deepEqual( + recordArray.map((v) => v.name), + ['John Churchill'] + ); store.push({ data: { @@ -110,7 +113,10 @@ module('unit/record-array - RecordArray', function (hooks) { await settled(); - assert.deepEqual(recordArray.mapBy('name'), ['John Churchill', 'Winston Churchill']); + assert.deepEqual( + recordArray.map((v) => v.name), + ['John Churchill', 'Winston Churchill'] + ); }); test('a loaded record is removed from a record array when it is deleted', async function (assert) { @@ -158,26 +164,26 @@ module('unit/record-array - RecordArray', function (hooks) { ], }); - let scumbag = await store.findRecord('person', 1); - let tag = await store.findRecord('tag', 1); + let scumbag = await store.findRecord('person', '1'); + let tag = await store.findRecord('tag', '1'); let recordArray = tag.people; - recordArray.addObject(scumbag); + recordArray.push(scumbag); assert.strictEqual(scumbag.tag, tag, "precond - the scumbag's tag has been set"); - assert.strictEqual(get(recordArray, 'length'), 1, 'precond - record array has one item'); - assert.strictEqual(get(recordArray.objectAt(0), 'name'), 'Scumbag Dale', 'item at index 0 is record with id 1'); + assert.strictEqual(recordArray.length, 1, 'precond - record array has one item'); + assert.strictEqual(recordArray.at(0)?.name, 'Scumbag Dale', 'item at index 0 is record with id 1'); scumbag.deleteRecord(); - assert.strictEqual(get(recordArray, 'length'), 1, 'record is still in the record array until it is saved'); + assert.strictEqual(recordArray.length, 1, 'record is still in the record array until it is saved'); await scumbag.save(); - assert.strictEqual(get(recordArray, 'length'), 0, 'record is removed from the array when it is saved'); + assert.strictEqual(recordArray.length, 0, 'record is removed from the array when it is saved'); }); - test("a loaded record is not removed from a record array when it is deleted even if the belongsTo side isn't defined", async function (assert) { + test("a loaded record is not removed from a relationship ManyArray when it is deleted even if the belongsTo side isn't defined", async function (assert) { class Person extends Model { @attr() name; @@ -227,8 +233,8 @@ module('unit/record-array - RecordArray', function (hooks) { scumbag.deleteRecord(); - assert.strictEqual(tag.people.length, 1, 'record is not removed from the record array'); - assert.strictEqual(tag.people.objectAt(0), scumbag, 'tag still has the scumbag'); + assert.strictEqual(tag.people.length, 1, 'record is not removed from the ManyArray'); + assert.strictEqual(tag.people.at(0), scumbag, 'tag still has the scumbag'); }); test("a loaded record is not removed from both the record array and from the belongs to, even if the belongsTo side isn't defined", async function (assert) { @@ -300,12 +306,12 @@ module('unit/record-array - RecordArray', function (hooks) { await settled(); - assert.strictEqual(get(recordArray, 'length'), 4, 'precond - record array has the created item'); - assert.strictEqual(recordArray.objectAt(0), scumbag, 'item at index 0 is record with id 1'); + assert.strictEqual(recordArray.length, 4, 'precond - record array has the created item'); + assert.strictEqual(recordArray.at(0), scumbag, 'item at index 0 is record with id 1'); scumbag.deleteRecord(); - assert.strictEqual(get(recordArray, 'length'), 3, 'record array no longer has the created item'); + assert.strictEqual(recordArray.length, 3, 'record array no longer has the created item'); }); test("a record array returns undefined when asking for a member outside of its content Array's range", async function (assert) { @@ -337,7 +343,7 @@ module('unit/record-array - RecordArray', function (hooks) { let recordArray = store.peekAll('person'); - assert.strictEqual(recordArray.objectAt(20), undefined, 'objects outside of the range just return undefined'); + assert.strictEqual(recordArray.at(20), undefined, 'objects outside of the range just return undefined'); }); // This tests for a bug in the recordCache, where the records were being cached in the incorrect order. @@ -370,16 +376,17 @@ module('unit/record-array - RecordArray', function (hooks) { let recordArray = store.peekAll('person'); - assert.strictEqual(get(recordArray.objectAt(2), 'id'), '3', 'should retrieve correct record at index 2'); - assert.strictEqual(get(recordArray.objectAt(1), 'id'), '2', 'should retrieve correct record at index 1'); - assert.strictEqual(get(recordArray.objectAt(0), 'id'), '1', 'should retrieve correct record at index 0'); + assert.strictEqual(recordArray.at(2).id, '3', 'should retrieve correct record at index 2'); + assert.strictEqual(recordArray.at(1).id, '2', 'should retrieve correct record at index 1'); + assert.strictEqual(recordArray.at(0).id, '1', 'should retrieve correct record at index 0'); }); test("an AdapterPopulatedRecordArray knows if it's loaded or not", async function (assert) { - assert.expect(1); + assert.expect(2); let adapter = store.adapterFor('person'); adapter.query = function (store, type, query, recordArray) { + assert.false(recordArray.isLoaded, 'not loaded yet'); return resolve({ data: [ { id: '1', type: 'person', attributes: { name: 'Scumbag Dale' } }, @@ -391,6 +398,6 @@ module('unit/record-array - RecordArray', function (hooks) { let people = await store.query('person', { page: 1 }); - assert.true(get(people, 'isLoaded'), 'The array is now loaded'); + assert.true(people.isLoaded, 'The array is now loaded'); }); }); diff --git a/packages/-ember-data/tests/integration/record-arrays/adapter-populated-record-array-test.js b/packages/-ember-data/tests/integration/record-arrays/adapter-populated-record-array-test.js index cab456b5daf..8693513c3a4 100644 --- a/packages/-ember-data/tests/integration/record-arrays/adapter-populated-record-array-test.js +++ b/packages/-ember-data/tests/integration/record-arrays/adapter-populated-record-array-test.js @@ -70,9 +70,7 @@ module('integration/record-arrays/adapter_populated_record_array - AdapterPopula assert.strictEqual(recordArray.length, 3, 'expected recordArray to contain exactly 3 records'); - recordArray.firstObject.destroyRecord(); - - await settled(); + await recordArray.at(0).destroyRecord(); assert.strictEqual(recordArray.length, 2, 'expected recordArray to contain exactly 2 records'); }); @@ -161,11 +159,27 @@ module('integration/record-arrays/adapter_populated_record_array - AdapterPopula await settled(); - assert.throws( + assert.expectAssertion( () => { recordArray.replace(); }, - Error('The result of a server query (on person) is immutable.'), + 'Assertion Failed: Mutating this array of records via splice is not allowed.', + 'throws error' + ); + assert.expectDeprecation({ id: 'ember-data:deprecate-array-like' }); + }); + + test('recordArray mutation throws error', async function (assert) { + let store = this.owner.lookup('service:store'); + let recordArray = store.recordArrayManager.createArray({ type: 'person', query: null }); + + await settled(); + + assert.expectAssertion( + () => { + recordArray.splice(0, 1); + }, + 'Assertion Failed: Mutating this array of records via splice is not allowed.', 'throws error' ); }); @@ -220,7 +234,7 @@ module('integration/record-arrays/adapter_populated_record_array - AdapterPopula recordArray = await recordArray.update(); assert.deepEqual( - recordArray.getEach('name'), + recordArray.map((r) => r.name), ['Scumbag Dale', 'Scumbag Katz'], 'expected query to contain specific records' ); @@ -235,7 +249,7 @@ module('integration/record-arrays/adapter_populated_record_array - AdapterPopula recordArray = await recordArray.update(); assert.deepEqual( - recordArray.getEach('name'), + recordArray.map((r) => r.name), ['Scumbag Dale', 'Scumbag Penner'], 'expected query to still contain specific records' ); diff --git a/packages/-ember-data/tests/integration/record-arrays/peeked-records-test.js b/packages/-ember-data/tests/integration/record-arrays/peeked-records-test.js index 87f0c2e32bf..932e537c3d1 100644 --- a/packages/-ember-data/tests/integration/record-arrays/peeked-records-test.js +++ b/packages/-ember-data/tests/integration/record-arrays/peeked-records-test.js @@ -1,12 +1,15 @@ import { get } from '@ember/object'; import { run } from '@ember/runloop'; +import { settled } from '@ember/test-helpers'; import { module, test } from 'qunit'; -import { setupTest } from 'ember-qunit'; +import { setupRenderingTest } from 'ember-qunit'; import Model, { attr } from '@ember-data/model'; -import { watchProperties } from '@ember-data/unpublished-test-infra/test-support/watch-property'; +import { SOURCE } from '@ember-data/store/-private'; + +import { startWatching, watchProperties } from '../../helpers/watch-property'; let store; @@ -18,163 +21,184 @@ const Person = Model.extend({ }); module('integration/peeked-records', function (hooks) { - setupTest(hooks); + setupRenderingTest(hooks); hooks.beforeEach(function () { this.owner.register('model:person', Person); store = this.owner.lookup('service:store'); }); - test('repeated calls to peekAll in separate run-loops works as expected', function (assert) { + test('repeated calls to peekAll in separate run-loops works as expected', async function (assert) { let peekedRecordArray = run(() => store.peekAll('person')); - let watcher = watchProperties(peekedRecordArray, ['length', '[]']); - - run(() => - store.push({ - data: [ - { - type: 'person', - id: '1', - attributes: { - name: 'John', - }, + let watcher = watchProperties.call(this, peekedRecordArray, ['length', '[]']); + await startWatching.call(this); + + assert.watchedPropertyCounts(watcher, { length: 1, '[]': 1 }, 'RecordArray state initial access'); + + store.push({ + data: [ + { + type: 'person', + id: '1', + attributes: { + name: 'John', }, - { - type: 'person', - id: '2', - attributes: { - name: 'Joe', - }, + }, + { + type: 'person', + id: '2', + attributes: { + name: 'Joe', }, - ], - }) - ); + }, + ], + }); + await settled(); assert.watchedPropertyCounts( watcher, - { length: 1, '[]': 1 }, + { length: 2, '[]': 2 }, 'RecordArray state after a single push with multiple records to add' ); - run(() => store.peekAll('person')); + store.peekAll('person'); assert.watchedPropertyCounts( watcher, - { length: 1, '[]': 1 }, + { length: 2, '[]': 2 }, 'RecordArray state has not changed after another call to peek' ); }); - test('peekAll in the same run-loop as push works as expected', function (assert) { + test('peekAll in the same run-loop as push works as expected', async function (assert) { let peekedRecordArray = run(() => store.peekAll('person')); - let watcher = watchProperties(peekedRecordArray, ['length', '[]']); - - run(() => { - store.push({ - data: [ - { - type: 'person', - id: '1', - attributes: { - name: 'John', - }, + let watcher = watchProperties.call(this, peekedRecordArray, ['length', '[]']); + await startWatching.call(this); + + assert.watchedPropertyCounts(watcher, { length: 1, '[]': 1 }, 'RecordArray state initial'); + + store.push({ + data: [ + { + type: 'person', + id: '1', + attributes: { + name: 'John', }, - { - type: 'person', - id: '2', - attributes: { - name: 'Joe', - }, + }, + { + type: 'person', + id: '2', + attributes: { + name: 'Joe', }, - ], - }); - store.peekAll('person'); + }, + ], }); + store.peekAll('person'); + await settled(); assert.watchedPropertyCounts( watcher, - { length: 1, '[]': 1 }, + { length: 2, '[]': 2 }, 'RecordArray state after a single push with multiple records to add' ); - run(() => store.peekAll('person')); + store.peekAll('person'); assert.watchedPropertyCounts( watcher, - { length: 1, '[]': 1 }, + { length: 2, '[]': 2 }, 'RecordArray state has not changed after another call to peek' ); }); - test('newly created records notify the array as expected', function (assert) { + test('newly created records notify the array as expected', async function (assert) { let peekedRecordArray = run(() => store.peekAll('person')); - let watcher = watchProperties(peekedRecordArray, ['length', '[]']); + let watcher = watchProperties.call(this, peekedRecordArray, ['length', '[]']); + await startWatching.call(this); + + assert.watchedPropertyCounts(watcher, { length: 1, '[]': 1 }, 'RecordArray state initial'); + let aNewlyCreatedRecord = store.createRecord('person', { name: 'James', }); - assert.watchedPropertyCounts(watcher, { length: 1, '[]': 1 }, 'RecordArray state when a new record is created'); + assert.watchedPropertyCounts(watcher, { length: 2, '[]': 2 }, 'RecordArray state when a new record is created'); - run(() => { - aNewlyCreatedRecord.unloadRecord(); - }); + aNewlyCreatedRecord.unloadRecord(); + await settled(); - assert.watchedPropertyCounts(watcher, { length: 2, '[]': 2 }, 'RecordArray state when a new record is unloaded'); + assert.watchedPropertyCounts(watcher, { length: 3, '[]': 3 }, 'RecordArray state when a new record is unloaded'); }); - test('immediately peeking newly created records works as expected', function (assert) { - let peekedRecordArray = run(() => store.peekAll('person')); - let watcher = watchProperties(peekedRecordArray, ['length', '[]']); + test('immediately peeking newly created records works as expected', async function (assert) { + let peekedRecordArray = store.peekAll('person'); + let watcher = watchProperties.call(this, peekedRecordArray, ['length', '[]']); + await startWatching.call(this); + assert.strictEqual(peekedRecordArray.length, 0); + assert.watchedPropertyCounts(watcher, { length: 1, '[]': 1 }, 'RecordArray state initial'); + let aNewlyCreatedRecord = store.createRecord('person', { name: 'James', }); - assert.watchedPropertyCounts(watcher, { length: 1, '[]': 1 }, 'RecordArray state when a new record is created'); + let records = store.peekAll('person'); + assert.strictEqual(records.length, 1); + assert.watchedPropertyCounts(watcher, { length: 2, '[]': 2 }, 'RecordArray state when a new record is created'); - run(() => { - aNewlyCreatedRecord.unloadRecord(); - store.peekAll('person'); - }); + aNewlyCreatedRecord.unloadRecord(); + records = store.peekAll('person'); + assert.strictEqual(records.length, 0); - assert.watchedPropertyCounts(watcher, { length: 2, '[]': 2 }, 'RecordArray state when a new record is unloaded'); + await settled(); + + assert.watchedPropertyCounts(watcher, { length: 3, '[]': 3 }, 'RecordArray state when a new record is unloaded'); }); - test('unloading newly created records notify the array as expected', function (assert) { + test('unloading newly created records notify the array as expected', async function (assert) { let peekedRecordArray = run(() => store.peekAll('person')); - let watcher = watchProperties(peekedRecordArray, ['length', '[]']); + let watcher = watchProperties.call(this, peekedRecordArray, ['length', '[]']); + await startWatching.call(this); + assert.watchedPropertyCounts(watcher, { length: 1, '[]': 1 }, 'RecordArray state init'); let aNewlyCreatedRecord = store.createRecord('person', { name: 'James', }); - assert.watchedPropertyCounts(watcher, { length: 1, '[]': 1 }, 'RecordArray state when a new record is created'); + assert.watchedPropertyCounts(watcher, { length: 2, '[]': 2 }, 'RecordArray state when a new record is created'); run(() => { aNewlyCreatedRecord.unloadRecord(); }); - assert.watchedPropertyCounts(watcher, { length: 2, '[]': 2 }, 'RecordArray state when a new record is unloaded'); + assert.watchedPropertyCounts(watcher, { length: 3, '[]': 3 }, 'RecordArray state when a new record is unloaded'); }); - test('immediately peeking after unloading newly created records works as expected', function (assert) { + test('immediately peeking after unloading newly created records works as expected', async function (assert) { let peekedRecordArray = run(() => store.peekAll('person')); - let watcher = watchProperties(peekedRecordArray, ['length', '[]']); + let watcher = watchProperties.call(this, peekedRecordArray, ['length', '[]']); + await startWatching.call(this); + assert.watchedPropertyCounts(watcher, { length: 1, '[]': 1 }, 'RecordArray state init'); + let aNewlyCreatedRecord = store.createRecord('person', { name: 'James', }); - assert.watchedPropertyCounts(watcher, { length: 1, '[]': 1 }, 'RecordArray state when a new record is created'); + assert.watchedPropertyCounts(watcher, { length: 2, '[]': 2 }, 'RecordArray state when a new record is created'); run(() => { aNewlyCreatedRecord.unloadRecord(); store.peekAll('person'); }); - assert.watchedPropertyCounts(watcher, { length: 2, '[]': 2 }, 'RecordArray state when a new record is unloaded'); + assert.watchedPropertyCounts(watcher, { length: 3, '[]': 3 }, 'RecordArray state when a new record is unloaded'); }); - test('unloadAll followed by peekAll in the same run-loop works as expected', function (assert) { + test('unloadAll followed by peekAll in the same run-loop works as expected', async function (assert) { let peekedRecordArray = run(() => store.peekAll('person')); - let watcher = watchProperties(peekedRecordArray, ['length', '[]']); + let watcher = watchProperties.call(this, peekedRecordArray, ['length', '[]']); + await startWatching.call(this); + assert.watchedPropertyCounts(watcher, { length: 1, '[]': 1 }, 'RecordArray state init'); run(() => { store.push({ @@ -197,41 +221,37 @@ module('integration/peeked-records', function (hooks) { }); }); - run(() => { - store.peekAll('person'); - - assert.watchedPropertyCounts( - watcher, - { length: 1, '[]': 1 }, - 'RecordArray state after a single push with multiple records to add' - ); + store.peekAll('person'); - store.unloadAll('person'); - - assert.watchedPropertyCounts( - watcher, - { length: 1, '[]': 1 }, - 'RecordArray state after unloadAll has not changed yet' - ); + assert.watchedPropertyCounts( + watcher, + { length: 2, '[]': 2 }, + 'RecordArray state after a single push with multiple records to add' + ); - assert.strictEqual(get(peekedRecordArray, 'length'), 2, 'Array length is unchanged before the next peek'); + store.unloadAll('person'); - store.peekAll('person'); + assert.watchedPropertyCounts( + watcher, + { length: 2, '[]': 2 }, + 'RecordArray state after unloadAll has not changed yet' + ); - assert.strictEqual(get(peekedRecordArray, 'length'), 0, 'We no longer have any array content'); + assert.strictEqual(peekedRecordArray[SOURCE].length, 2, 'Array length is unchanged before the next peek'); - assert.watchedPropertyCounts( - watcher, - { length: 2, '[]': 2 }, - 'RecordArray state after a follow up peekAll reflects unload changes' - ); - }); + store.peekAll('person'); - assert.watchedPropertyCounts(watcher, { length: 2, '[]': 2 }, 'RecordArray state has not changed any further'); + assert.strictEqual(get(peekedRecordArray, 'length'), 0, 'We no longer have any array content'); + await settled(); + assert.watchedPropertyCounts( + watcher, + { length: 3, '[]': 3 }, + 'RecordArray state after a follow up peekAll reflects unload changes' + ); }); - test('push+materialize => unloadAll => push+materialize works as expected', function (assert) { - function push() { + test('push+materialize => unloadAll => push+materialize works as expected', async function (assert) { + async function push() { run(() => { store.push({ data: [ @@ -252,35 +272,41 @@ module('integration/peeked-records', function (hooks) { ], }); }); + await settled(); } - function unload() { + async function unload() { run(() => store.unloadAll('person')); + await settled(); } - function peek() { - return run(() => store.peekAll('person')); + async function peek() { + const result = run(() => store.peekAll('person')); + await settled(); + return result; } - let peekedRecordArray = peek(); - let watcher = watchProperties(peekedRecordArray, ['length', '[]']); + let peekedRecordArray = await peek(); + let watcher = watchProperties.call(this, peekedRecordArray, ['length', '[]']); + await startWatching.call(this); + assert.watchedPropertyCounts(watcher, { length: 1, '[]': 1 }, 'RecordArray state init'); - push(); + await push(); assert.watchedPropertyCounts( watcher, - { length: 1, '[]': 1 }, + { length: 2, '[]': 2 }, 'RecordArray state after a single push with multiple records to add' ); - unload(); + await unload(); assert.strictEqual(get(peekedRecordArray, 'length'), 0, 'We no longer have any array content'); - assert.watchedPropertyCounts(watcher, { length: 2, '[]': 2 }, 'RecordArray state has signaled the unload'); + assert.watchedPropertyCounts(watcher, { length: 3, '[]': 3 }, 'RecordArray state has signaled the unload'); - push(); + await push(); assert.strictEqual(get(peekedRecordArray, 'length'), 2, 'We have array content'); - assert.watchedPropertyCounts(watcher, { length: 3, '[]': 3 }, 'RecordArray state now has records again'); + assert.watchedPropertyCounts(watcher, { length: 4, '[]': 4 }, 'RecordArray state now has records again'); }); - test('push-without-materialize => unloadAll => push-without-materialize works as expected', function (assert) { - function _push() { + test('push-without-materialize => unloadAll => push-without-materialize works as expected', async function (assert) { + async function _push() { run(() => { store._push({ data: [ @@ -301,30 +327,36 @@ module('integration/peeked-records', function (hooks) { ], }); }); + await settled(); } - function unload() { + async function unload() { run(() => store.unloadAll('person')); + await settled(); } - function peek() { - return run(() => store.peekAll('person')); + async function peek() { + const result = run(() => store.peekAll('person')); + await settled(); + return result; } - let peekedRecordArray = peek(); - let watcher = watchProperties(peekedRecordArray, ['length', '[]']); + let peekedRecordArray = await peek(); + let watcher = watchProperties.call(this, peekedRecordArray, ['length', '[]']); + await startWatching.call(this); + assert.watchedPropertyCounts(watcher, { length: 1, '[]': 1 }, 'RecordArray state init'); - _push(); + await _push(); assert.watchedPropertyCounts( watcher, - { length: 1, '[]': 1 }, + { length: 2, '[]': 2 }, 'RecordArray state after a single push with multiple records to add' ); - unload(); + await unload(); assert.strictEqual(get(peekedRecordArray, 'length'), 0, 'We no longer have any array content'); - assert.watchedPropertyCounts(watcher, { length: 2, '[]': 2 }, 'RecordArray state has signaled the unload'); + assert.watchedPropertyCounts(watcher, { length: 3, '[]': 3 }, 'RecordArray state has signaled the unload'); - _push(); + await _push(); assert.strictEqual(get(peekedRecordArray, 'length'), 2, 'We have array content'); - assert.watchedPropertyCounts(watcher, { length: 3, '[]': 3 }, 'RecordArray state now has records again'); + assert.watchedPropertyCounts(watcher, { length: 4, '[]': 4 }, 'RecordArray state now has records again'); }); }); diff --git a/packages/-ember-data/tests/integration/record-data/record-data-errors-test.ts b/packages/-ember-data/tests/integration/record-data/record-data-errors-test.ts index 3464246ddea..25234e5abf6 100644 --- a/packages/-ember-data/tests/integration/record-data/record-data-errors-test.ts +++ b/packages/-ember-data/tests/integration/record-data/record-data-errors-test.ts @@ -8,6 +8,7 @@ import { setupTest } from 'ember-qunit'; import { InvalidError } from '@ember-data/adapter/error'; import { V2CACHE_SINGLETON_MANAGER } from '@ember-data/canary-features'; import Model, { attr } from '@ember-data/model'; +import { LocalRelationshipOperation } from '@ember-data/record-data/-private/graph/-operations'; import JSONAPISerializer from '@ember-data/serializer/json-api'; import Store, { recordIdentifierFor } from '@ember-data/store'; import { DSModel } from '@ember-data/types/q/ds-model'; @@ -25,6 +26,9 @@ if (V2CACHE_SINGLETON_MANAGER) { } class TestRecordData implements RecordData { + update(operation: LocalRelationshipOperation): void { + throw new Error('Method not implemented.'); + } version: '2' = '2'; _errors?: JsonApiValidationError[]; @@ -253,7 +257,7 @@ if (V2CACHE_SINGLETON_MANAGER) { }) as DSModel; const identifier = recordIdentifierFor(person); - let nameError = person.errors.errorsFor('firstName').firstObject; + let nameError = person.errors.errorsFor('firstName').at(0); assert.strictEqual(nameError, undefined, 'no error shows up on firstName initially'); assert.true(person.isValid, 'person is initially valid'); @@ -268,7 +272,7 @@ if (V2CACHE_SINGLETON_MANAGER) { ]; storeWrapper.notifyChange(identifier, 'errors'); - nameError = person.errors.errorsFor('firstName').firstObject; + nameError = person.errors.errorsFor('firstName').at(0); assert.strictEqual(nameError?.attribute, 'firstName', 'error shows up on name'); assert.false(person.isValid, 'person is not valid'); @@ -291,7 +295,7 @@ if (V2CACHE_SINGLETON_MANAGER) { assert.false(person.isValid, 'person is not valid'); assert.strictEqual(person.errors.errorsFor('firstName').length, 0, 'no errors on firstName'); - let lastNameError = person.errors.errorsFor('lastName').firstObject; + let lastNameError = person.errors.errorsFor('lastName').at(0); assert.strictEqual(lastNameError?.attribute, 'lastName', 'error shows up on lastName'); }); }); @@ -568,7 +572,7 @@ if (V2CACHE_SINGLETON_MANAGER) { }); let person = store.peekRecord('person', '1'); const identifier = recordIdentifierFor(person); - let nameError = person.errors.errorsFor('name').firstObject; + let nameError = person.errors.errorsFor('name').at(0); assert.strictEqual(nameError.attribute, 'name', 'error shows up on name'); assert.false(person.isValid, 'person is not valid'); errorsToReturn = []; @@ -587,7 +591,7 @@ if (V2CACHE_SINGLETON_MANAGER) { storeWrapper.notifyChange(identifier, 'errors'); assert.false(person.isValid, 'person is valid'); assert.strictEqual(person.errors.errorsFor('name').length, 0, 'no errors on name'); - let lastNameError = person.errors.errorsFor('lastName').firstObject; + let lastNameError = person.errors.errorsFor('lastName').at(0); assert.strictEqual(lastNameError.attribute, 'lastName', 'error shows up on lastName'); }); }); diff --git a/packages/-ember-data/tests/integration/record-data/record-data-state-test.ts b/packages/-ember-data/tests/integration/record-data/record-data-state-test.ts index 18f31993013..8f0b3122b42 100644 --- a/packages/-ember-data/tests/integration/record-data/record-data-state-test.ts +++ b/packages/-ember-data/tests/integration/record-data/record-data-state-test.ts @@ -8,6 +8,7 @@ import { setupTest } from 'ember-qunit'; import { V2CACHE_SINGLETON_MANAGER } from '@ember-data/canary-features'; import Model, { attr } from '@ember-data/model'; +import { LocalRelationshipOperation } from '@ember-data/record-data/-private/graph/-operations'; import JSONAPISerializer from '@ember-data/serializer/json-api'; import Store, { recordIdentifierFor } from '@ember-data/store'; import { CollectionResourceRelationship, SingleResourceRelationship } from '@ember-data/types/q/ember-data-json-api'; @@ -114,6 +115,9 @@ class V1TestRecordData implements RecordDataV1 { } } class V2TestRecordData implements RecordData { + update(operation: LocalRelationshipOperation): void { + throw new Error('Method not implemented.'); + } version: '2' = '2'; _errors?: JsonApiValidationError[]; diff --git a/packages/-ember-data/tests/integration/record-data/record-data-test.ts b/packages/-ember-data/tests/integration/record-data/record-data-test.ts index 5bf67b1ac23..cc7034b531b 100644 --- a/packages/-ember-data/tests/integration/record-data/record-data-test.ts +++ b/packages/-ember-data/tests/integration/record-data/record-data-test.ts @@ -1,14 +1,15 @@ -import EmberObject, { get } from '@ember/object'; +import EmberObject from '@ember/object'; import { settled } from '@ember/test-helpers'; import { module, test } from 'qunit'; -import { Promise } from 'rsvp'; import { setupTest } from 'ember-qunit'; import JSONAPIAdapter from '@ember-data/adapter/json-api'; import { V2CACHE_SINGLETON_MANAGER } from '@ember-data/canary-features'; import Model, { attr, belongsTo, hasMany } from '@ember-data/model'; +import { DEPRECATE_V1_RECORD_DATA } from '@ember-data/private-build-infra/deprecations'; +import type { LocalRelationshipOperation } from '@ember-data/record-data/-private/graph/-operations'; import JSONAPISerializer from '@ember-data/serializer/json-api'; import Store from '@ember-data/store'; import type { @@ -119,6 +120,9 @@ class V2TestRecordData implements RecordData { this._storeWrapper = wrapper; this._identifier = identifier; } + update(operation: LocalRelationshipOperation): void { + throw new Error('Method not implemented.'); + } pushData( identifier: StableRecordIdentifier, @@ -194,9 +198,8 @@ class V2TestRecordData implements RecordData { } } -const TestRecordData: typeof V2TestRecordData | typeof V1TestRecordData = V2CACHE_SINGLETON_MANAGER - ? V2TestRecordData - : V1TestRecordData; +const TestRecordData: typeof V2TestRecordData | typeof V1TestRecordData = + !DEPRECATE_V1_RECORD_DATA || V2CACHE_SINGLETON_MANAGER ? V2TestRecordData : V1TestRecordData; const CustomStore = Store.extend({ createRecordDataFor(identifier: StableRecordIdentifier, storeWrapper: RecordDataStoreWrapper) { @@ -278,7 +281,7 @@ module('integration/record-data - Custom RecordData Implementations', function ( }); let all = store.peekAll('person'); - assert.strictEqual(get(all, 'length'), 2); + assert.strictEqual(all.length, 2, 'we have 2 records'); store.push({ data: [ @@ -294,7 +297,7 @@ module('integration/record-data - Custom RecordData Implementations', function ( await settled(); - assert.strictEqual(get(all, 'length'), 3); + assert.strictEqual(all.length, 3, 'we have 3 records'); }); test('Record Data push, create and save lifecycle', async function (assert) { @@ -488,7 +491,7 @@ module('integration/record-data - Custom RecordData Implementations', function ( getAttr(identifier: StableRecordIdentifier, key: string): string { calledGet++; - if (V2CACHE_SINGLETON_MANAGER) { + if (!DEPRECATE_V1_RECORD_DATA || V2CACHE_SINGLETON_MANAGER) { assert.strictEqual(key, 'name', 'key passed to getAttr'); } else { assert.strictEqual(identifier as unknown as string, 'name', 'key passed to getAttr'); @@ -595,7 +598,7 @@ module('integration/record-data - Custom RecordData Implementations', function ( let belongsToReturnValue; let RelationshipRecordData; - if (V2CACHE_SINGLETON_MANAGER) { + if (!DEPRECATE_V1_RECORD_DATA || V2CACHE_SINGLETON_MANAGER) { RelationshipRecordData = class extends TestRecordData { getRelationship(identifier: StableRecordIdentifier, key: string) { assert.strictEqual(key, 'landlord', 'Passed correct key to getBelongsTo'); @@ -664,11 +667,12 @@ module('integration/record-data - Custom RecordData Implementations', function ( }); test('Record Data controls hasMany notifications', async function (assert) { - assert.expect(10); + assert.expect(11); let { owner } = this; let hasManyReturnValue; + let notifier, houseIdentifier; class RelationshipRecordData extends TestRecordData { getHasMany(key: string) { return hasManyReturnValue; @@ -677,7 +681,7 @@ module('integration/record-data - Custom RecordData Implementations', function ( return hasManyReturnValue; } addToHasMany(key: string, recordDatas: any[], idx?: number) { - if (V2CACHE_SINGLETON_MANAGER) { + if (!DEPRECATE_V1_RECORD_DATA || V2CACHE_SINGLETON_MANAGER) { const key: string = arguments[1]; const identifiers: StableRecordIdentifier[] = arguments[2]; assert.strictEqual(key, 'tenants', 'Passed correct key to addToHasMany'); @@ -688,7 +692,7 @@ module('integration/record-data - Custom RecordData Implementations', function ( } } removeFromHasMany(key: string, recordDatas: any[]) { - if (V2CACHE_SINGLETON_MANAGER) { + if (!DEPRECATE_V1_RECORD_DATA || V2CACHE_SINGLETON_MANAGER) { const key: string = arguments[1]; const identifiers: StableRecordIdentifier[] = arguments[2]; assert.strictEqual(key, 'tenants', 'Passed correct key to removeFromHasMany'); @@ -706,11 +710,33 @@ module('integration/record-data - Custom RecordData Implementations', function ( assert.strictEqual(key, 'tenants', 'Passed correct key to addToHasMany'); assert.strictEqual(values[0].id, '3', 'Passed correct RD to addToHasMany'); } + update(operation: LocalRelationshipOperation) { + if (operation.op === 'addToRelatedRecords') { + assert.strictEqual(operation.field, 'tenants'); + assert.deepEqual( + (operation.value as StableRecordIdentifier[]).map((r) => r.id), + ['2'] + ); + } else if (operation.op === 'removeFromRelatedRecords') { + assert.strictEqual(operation.field, 'tenants'); + assert.deepEqual( + (operation.value as StableRecordIdentifier[]).map((r) => r.id), + ['1'] + ); + } else if (operation.op === 'replaceRelatedRecords') { + assert.strictEqual(operation.field, 'tenants', 'Passed correct key to addToHasMany'); + assert.strictEqual(operation.value[0].id, '3', 'Passed correct RD to addToHasMany'); + } else { + throw new Error(`operation ${operation.op} not implemented in test yet`); + } + } } let TestStore = Store.extend({ createRecordDataFor(identifier: StableRecordIdentifier, storeWrapper: RecordDataStoreWrapper) { if (identifier.type === 'house') { + notifier = storeWrapper; + houseIdentifier = identifier; return new RelationshipRecordData(storeWrapper, identifier); } else { return this._super(identifier, storeWrapper); @@ -737,16 +763,20 @@ module('integration/record-data - Custom RecordData Implementations', function ( let runspired = store.peekRecord('person', '2'); let igor = store.peekRecord('person', '3'); - assert.deepEqual(people.toArray(), [david], 'getHasMany correctly looked up'); + assert.deepEqual(people.slice(), [david], 'initial lookup is correct'); - people.pushObject(runspired); - assert.deepEqual(people.toArray(), [david], 'has many doesnt change if RD did not notify'); + people.push(runspired); + assert.deepEqual(people.slice(), [david, runspired], 'push applies the change locally'); - people.removeObject(david); - assert.deepEqual(people.toArray(), [david], 'hasMany removal doesnt apply the change unless notified'); + people.splice(people.indexOf(david), 1); + assert.deepEqual(people.slice(), [runspired], 'splice applies the change locally'); - house.set('tenants', [igor]); - assert.deepEqual(people.toArray(), [david], 'setDirtyHasMany doesnt apply unless notified'); + house.tenants = [igor]; + assert.deepEqual(people.slice(), [igor], 'replace applies the change locally'); + + notifier.notifyChange(houseIdentifier, 'relationships', 'tenants'); + await settled(); + assert.deepEqual(people.slice(), [david], 'final lookup is correct once notified'); }); test('Record Data supports custom hasMany handling', async function (assert) { @@ -763,7 +793,7 @@ module('integration/record-data - Custom RecordData Implementations', function ( } addToHasMany(this: V1TestRecordData, key: string, recordDatas: any[], idx?: number) { - if (V2CACHE_SINGLETON_MANAGER) { + if (!DEPRECATE_V1_RECORD_DATA || V2CACHE_SINGLETON_MANAGER) { const key: string = arguments[1]; const identifiers: StableRecordIdentifier[] = arguments[2]; assert.strictEqual(key, 'tenants', 'Passed correct key to addToHasMany'); @@ -783,7 +813,7 @@ module('integration/record-data - Custom RecordData Implementations', function ( } removeFromHasMany(this: V1TestRecordData, key: string, recordDatas: any[]) { - if (V2CACHE_SINGLETON_MANAGER) { + if (!DEPRECATE_V1_RECORD_DATA || V2CACHE_SINGLETON_MANAGER) { const key: string = arguments[1]; const identifiers: StableRecordIdentifier[] = arguments[2]; assert.strictEqual(key, 'tenants', 'Passed correct key to removeFromHasMany'); @@ -824,6 +854,43 @@ module('integration/record-data - Custom RecordData Implementations', function ( }; this._storeWrapper.notifyChange(this._identifier, 'relationships', 'tenants'); } + + update(this: V2TestRecordData, operation: LocalRelationshipOperation) { + if (operation.op === 'addToRelatedRecords') { + hasManyReturnValue = { + data: [ + { id: '3', type: 'person' }, + { id: '2', type: 'person' }, + ], + }; + assert.strictEqual(operation.field, 'tenants', 'correct field for addToRelatedRecords'); + assert.deepEqual( + (operation.value as StableRecordIdentifier[]).map((r) => r.id), + ['2'], + 'correct ids passed' + ); + } else if (operation.op === 'removeFromRelatedRecords') { + assert.strictEqual(operation.field, 'tenants', 'correct field for removeFromRelatedRecords'); + assert.deepEqual( + (operation.value as StableRecordIdentifier[]).map((r) => r.id), + ['2'], + 'correct ids passed' + ); + hasManyReturnValue = { data: [{ id: '1', type: 'person' }] }; + } else if (operation.op === 'replaceRelatedRecords') { + assert.strictEqual(operation.field, 'tenants', 'Passed correct key to addToHasMany'); + assert.strictEqual(operation.value[0].id, '3', 'Passed correct RD to addToHasMany'); + hasManyReturnValue = { + data: [ + { id: '1', type: 'person' }, + { id: '2', type: 'person' }, + ], + }; + } else { + throw new Error(`operation ${operation.op} not implemented in test yet`); + } + this._storeWrapper.notifyChange(this._identifier, 'relationships', 'tenants'); + } } let TestStore = Store.extend({ @@ -855,18 +922,18 @@ module('integration/record-data - Custom RecordData Implementations', function ( let runspired = store.peekRecord('person', '2'); let igor = store.peekRecord('person', '3'); - assert.deepEqual(people.toArray(), [david], 'getHasMany correctly looked up'); - people.pushObject(runspired); + assert.deepEqual(people.slice(), [david], 'getHasMany correctly looked up'); + people.push(runspired); // This is intentionally !== [david, runspired] to test the custom RD implementation - assert.deepEqual(people.toArray(), [igor, runspired], 'hasMany changes after notifying'); + assert.deepEqual(people.slice(), [igor, runspired], 'hasMany changes after notifying'); - people.removeObject(runspired); + people.splice(people.indexOf(runspired), 1); // This is intentionally !== [igor] to test the custom RD implementation - assert.deepEqual(people.toArray(), [david], 'hasMany removal applies the change when notified'); + assert.deepEqual(people.slice(), [david], 'hasMany removal applies the change when notified'); house.set('tenants', [igor]); // This is intentionally !== [igor] to test the custom RD implementation - assert.deepEqual(people.toArray(), [david, runspired], 'setDirtyHasMany applies changes'); + assert.deepEqual(people.slice(), [david, runspired], 'setDirtyHasMany applies changes'); }); }); diff --git a/packages/-ember-data/tests/integration/record-data/unloading-record-data-test.js b/packages/-ember-data/tests/integration/record-data/unloading-record-data-test.js index 44c193405f3..6c3d314979b 100644 --- a/packages/-ember-data/tests/integration/record-data/unloading-record-data-test.js +++ b/packages/-ember-data/tests/integration/record-data/unloading-record-data-test.js @@ -230,7 +230,7 @@ module('RecordData Compatibility', function (hooks) { ], }); let pets = chris.pets; - let shen = pets.objectAt(0); + let shen = pets.at(0); assert.strictEqual(shen.name, 'Shen', 'We found Shen'); assert.strictEqual(customCalled, 2, 'we used the custom record-data for pet'); @@ -305,7 +305,7 @@ module('RecordData Compatibility', function (hooks) { ], }); let pets = chris.pets; - let shen = pets.objectAt(0); + let shen = pets.at(0); assert.strictEqual(shen.name, 'Shen', 'We found Shen'); diff --git a/packages/-ember-data/tests/integration/records/collection-save-test.js b/packages/-ember-data/tests/integration/records/collection-save-test.js index d665106c9eb..f17176d5af5 100644 --- a/packages/-ember-data/tests/integration/records/collection-save-test.js +++ b/packages/-ember-data/tests/integration/records/collection-save-test.js @@ -46,7 +46,7 @@ module('integration/records/collection_save - Save Collection of Records', funct }); }); - test('Collection will reject save on error', function (assert) { + test('Collection will reject save on error', async function (assert) { let store = this.owner.lookup('service:store'); let adapter = store.adapterFor('application'); @@ -59,14 +59,15 @@ module('integration/records/collection_save - Save Collection of Records', funct return reject(); }; - return run(() => { - return posts.save().catch(() => { - assert.ok(true, 'save operation was rejected'); - }); - }); + try { + await posts.save(); + assert.ok(false, 'should error'); + } catch { + assert.ok(true, 'save operation was rejected'); + } }); - test('Retry is allowed in a failure handler', function (assert) { + test('Retry is allowed in a failure handler', async function (assert) { let store = this.owner.lookup('service:store'); let adapter = store.adapterFor('application'); @@ -90,19 +91,17 @@ module('integration/records/collection_save - Save Collection of Records', funct return resolve({ data: { id: snapshot.id, type: 'post' } }); }; - return run(() => { - return posts - .save() - .catch(() => posts.save()) - .then((post) => { - // the ID here is '2' because the second post saves on the first attempt, - // while the first post saves on the second attempt - assert.strictEqual(posts.firstObject.id, '2', 'The post ID made it through'); - }); - }); + await posts + .save() + .catch(() => posts.save()) + .then((post) => { + // the ID here is '2' because the second post saves on the first attempt, + // while the first post saves on the second attempt + assert.strictEqual(posts.at(0).id, '2', 'The post ID made it through'); + }); }); - test('Collection will reject save on invalid', function (assert) { + test('Collection will reject save on invalid', async function (assert) { assert.expect(1); let store = this.owner.lookup('service:store'); @@ -117,10 +116,8 @@ module('integration/records/collection_save - Save Collection of Records', funct return reject({ title: 'invalid' }); }; - return run(() => { - return posts.save().catch(() => { - assert.ok(true, 'save operation was rejected'); - }); + await posts.save().catch(() => { + assert.ok(true, 'save operation was rejected'); }); }); }); diff --git a/packages/-ember-data/tests/integration/records/create-record-test.js b/packages/-ember-data/tests/integration/records/create-record-test.js index 0bcbad611ce..4ff92011d0f 100644 --- a/packages/-ember-data/tests/integration/records/create-record-test.js +++ b/packages/-ember-data/tests/integration/records/create-record-test.js @@ -83,14 +83,14 @@ module('Store.createRecord() coverage', function (hooks) { // check that we are properly configured assert.strictEqual(pet.owner, chris, 'Precondition: Our owner is Chris'); - let pets = chris.pets.toArray().map((pet) => pet.name); + let pets = chris.pets.slice().map((pet) => pet.name); assert.deepEqual(pets, ['Shen'], 'Precondition: Chris has Shen as a pet'); pet.unloadRecord(); await settled(); assert.strictEqual(pet.owner, null, 'Shen no longer has an owner'); // check that the relationship has been dissolved - pets = chris.pets.toArray().map((pet) => pet.name); + pets = chris.pets.slice().map((pet) => pet.name); assert.deepEqual(pets, [], 'Chris no longer has any pets'); }); @@ -118,13 +118,13 @@ module('Store.createRecord() coverage', function (hooks) { // check that we are properly configured assert.strictEqual(pet.owner, chris, 'Precondition: Our owner is Chris'); - let pets = chris.pets.toArray().map((pet) => pet.name); + let pets = chris.pets.slice().map((pet) => pet.name); assert.deepEqual(pets, ['Shen'], 'Precondition: Chris has Shen as a pet'); chris.unloadRecord(); assert.strictEqual(pet.owner, null, 'Shen no longer has an owner'); // check that the relationship has been dissolved - pets = chris.pets.toArray().map((pet) => pet.name); + pets = chris.pets.slice().map((pet) => pet.name); assert.deepEqual(pets, [], 'Chris no longer has any pets'); }); diff --git a/packages/-ember-data/tests/integration/records/delete-record-test.js b/packages/-ember-data/tests/integration/records/delete-record-test.js index 63ae0d3e49e..3df1a0980bd 100644 --- a/packages/-ember-data/tests/integration/records/delete-record-test.js +++ b/packages/-ember-data/tests/integration/records/delete-record-test.js @@ -162,15 +162,18 @@ module('integration/deletedRecord - Deleting Records', function (hooks) { // pre-condition assert.strictEqual(all.length, 2, 'expected 2 records'); + let destroys = 0; run(function () { all.forEach(function (record) { + destroys++; record.destroyRecord(); }); }); + assert.strictEqual(destroys, 2, 'we destroyed 2 records'); assert.strictEqual(all.length, 0, 'expected 0 records'); - assert.strictEqual(all.objectAt(0), undefined, "can't get any records"); + assert.strictEqual(all.at(0), undefined, "can't get any records"); }); test('Deleting an invalid newly created record should remove it from the store', async function (assert) { @@ -440,7 +443,7 @@ module('integration/deletedRecord - Deleting Records', function (hooks) { assert.strictEqual(groupCompany.name, 'Inc.', 'group belongs to our company'); assert.strictEqual(group.employees.length, 1, 'expected 1 related record before delete'); const employees = await group.employees; - employee = employees.objectAt(0); + employee = employees.at(0); assert.strictEqual(employee.name, 'Adam Sunderland', 'expected related records to be loaded'); await group.destroyRecord(); diff --git a/packages/-ember-data/tests/integration/records/edit-record-test.js b/packages/-ember-data/tests/integration/records/edit-record-test.js index 69e2777f5bf..ae66287640b 100644 --- a/packages/-ember-data/tests/integration/records/edit-record-test.js +++ b/packages/-ember-data/tests/integration/records/edit-record-test.js @@ -67,7 +67,7 @@ module('Editing a Record', function (hooks) { // check that we are properly configured assert.strictEqual(pet.owner, null, 'Precondition: Our owner is null'); - let pets = chris.pets.toArray().map((pet) => pet.name); + let pets = chris.pets.slice().map((pet) => pet.name); assert.deepEqual(pets, [], 'Precondition: Chris has no pets'); pet.set('owner', chris); @@ -75,7 +75,7 @@ module('Editing a Record', function (hooks) { assert.strictEqual(pet.owner, chris, 'Shen has Chris as an owner'); // check that the relationship has been established - pets = chris.pets.toArray().map((pet) => pet.name); + pets = chris.pets.slice().map((pet) => pet.name); assert.deepEqual(pets, ['Shen'], 'Chris has Shen as a pet'); }); @@ -101,7 +101,7 @@ module('Editing a Record', function (hooks) { // check that we are properly configured assert.strictEqual(pet.owner, null, 'Precondition: Our owner is null'); - let pets = chris.pets.toArray().map((pet) => pet.name); + let pets = chris.pets.slice().map((pet) => pet.name); assert.deepEqual(pets, [], 'Precondition: Chris has no pets'); pet.set('owner', chris); @@ -109,7 +109,7 @@ module('Editing a Record', function (hooks) { assert.strictEqual(pet.owner, chris, 'Shen has Chris as an owner'); // check that the relationship has been established - pets = chris.pets.toArray().map((pet) => pet.name); + pets = chris.pets.slice().map((pet) => pet.name); assert.deepEqual(pets, ['Shen'], 'Chris has Shen as a pet'); }); @@ -127,7 +127,7 @@ module('Editing a Record', function (hooks) { // check that we are properly configured assert.strictEqual(pet.owner, null, 'Precondition: Our owner is null'); - let pets = chris.pets.toArray().map((pet) => pet.name); + let pets = chris.pets.slice().map((pet) => pet.name); assert.deepEqual(pets, [], 'Precondition: Chris has no pets'); pet.set('owner', chris); @@ -135,7 +135,7 @@ module('Editing a Record', function (hooks) { assert.strictEqual(pet.owner, chris, 'Shen has Chris as an owner'); // check that the relationship has been established - pets = chris.pets.toArray().map((pet) => pet.name); + pets = chris.pets.slice().map((pet) => pet.name); assert.deepEqual(pets, ['Shen'], 'Chris has Shen as a pet'); }); @@ -161,7 +161,7 @@ module('Editing a Record', function (hooks) { // check that we are properly configured assert.strictEqual(pet.owner, null, 'Precondition: Our owner is null'); - let pets = chris.pets.toArray().map((pet) => pet.name); + let pets = chris.pets.slice().map((pet) => pet.name); assert.deepEqual(pets, [], 'Precondition: Chris has no pets'); pet.set('owner', chris); @@ -169,7 +169,7 @@ module('Editing a Record', function (hooks) { assert.strictEqual(pet.owner, chris, 'Shen has Chris as an owner'); // check that the relationship has been established - pets = chris.pets.toArray().map((pet) => pet.name); + pets = chris.pets.slice().map((pet) => pet.name); assert.deepEqual(pets, ['Shen'], 'Chris has Shen as a pet'); }); @@ -238,16 +238,16 @@ module('Editing a Record', function (hooks) { assert.strictEqual(shen.owner, chris, 'Precondition: Chris is the current owner'); assert.strictEqual(rocky.owner, chris, 'Precondition: Chris is the current owner'); - let pets = chris.pets.toArray().map((pet) => pet.name); + let pets = chris.pets.slice().map((pet) => pet.name); assert.deepEqual(pets, ['Shen', 'Rocky'], 'Precondition: Chris has Shen and Rocky as pets'); shen.set('owner', john); assert.strictEqual(shen.owner, john, 'After Update: John is the new owner of Shen'); - pets = chris.pets.toArray().map((pet) => pet.name); + pets = chris.pets.slice().map((pet) => pet.name); assert.deepEqual(pets, ['Rocky'], 'After Update: Chris has Rocky as a pet'); - pets = john.pets.toArray().map((pet) => pet.name); + pets = john.pets.slice().map((pet) => pet.name); assert.deepEqual(pets, ['Shen'], 'After Update: John has Shen as a pet'); chris.unloadRecord(); @@ -255,7 +255,7 @@ module('Editing a Record', function (hooks) { assert.strictEqual(rocky.owner, null, 'After Unload: Rocky has no owner'); assert.strictEqual(shen.owner, john, 'After Unload: John should still be the owner of Shen'); - pets = john.pets.toArray().map((pet) => pet.name); + pets = john.pets.slice().map((pet) => pet.name); assert.deepEqual(pets, ['Shen'], 'After Unload: John still has Shen as a pet'); }); }); diff --git a/packages/-ember-data/tests/integration/records/error-test.js b/packages/-ember-data/tests/integration/records/error-test.js index 430fcc86fa8..6b5494bfdc7 100644 --- a/packages/-ember-data/tests/integration/records/error-test.js +++ b/packages/-ember-data/tests/integration/records/error-test.js @@ -59,7 +59,7 @@ module('integration/records/error', function (hooks) { person.errors.add('lastName', 'is invalid'); assert.deepEqual( - person.errors.toArray(), + person.errors.slice(), [ { attribute: 'firstName', message: 'is invalid' }, { attribute: 'lastName', message: 'is invalid' }, @@ -108,7 +108,7 @@ module('integration/records/error', function (hooks) { person.errors.add('lastName', 'is invalid'); assert.deepEqual( - person.errors.toArray(), + person.errors.slice(), [ { attribute: 'firstName', message: 'is invalid' }, { attribute: 'lastName', message: 'is invalid' }, @@ -144,11 +144,11 @@ module('integration/records/error', function (hooks) { person.errors.remove('firstName'); - assert.deepEqual(person.errors.toArray(), []); + assert.deepEqual(person.errors.slice(), []); person.errors.add('firstName', 'is invalid'); - assert.deepEqual(person.errors.toArray(), [{ attribute: 'firstName', message: 'is invalid' }]); + assert.deepEqual(person.errors.slice(), [{ attribute: 'firstName', message: 'is invalid' }]); }); testInDebug('adding errors root.loaded.created.invalid works add + (remove, add)', function (assert) { @@ -181,7 +181,7 @@ module('integration/records/error', function (hooks) { assert.strictEqual(person.currentState.stateName, 'root.loaded.created.invalid'); - assert.deepEqual(person.errors.toArray(), [{ attribute: 'firstName', message: 'is invalid' }]); + assert.deepEqual(person.errors.slice(), [{ attribute: 'firstName', message: 'is invalid' }]); }); test('using setProperties to clear errors', async function (assert) { diff --git a/packages/-ember-data/tests/integration/records/relationship-changes-test.js b/packages/-ember-data/tests/integration/records/relationship-changes-test.js index f7a3c4af849..d3e5e992a8d 100644 --- a/packages/-ember-data/tests/integration/records/relationship-changes-test.js +++ b/packages/-ember-data/tests/integration/records/relationship-changes-test.js @@ -208,7 +208,7 @@ module('integration/records/relationship-changes - Relationship changes', functi }); run(() => { - let cpResult = get(obj, 'siblings').toArray(); + let cpResult = get(obj, 'siblings').slice(); assert.strictEqual(cpResult.length, 1, 'siblings cp should have recalculated'); obj.destroy(); }); @@ -219,7 +219,7 @@ module('integration/records/relationship-changes - Relationship changes', functi 'Calling push with relationship recalculates computed alias property to firstObject if the relationship was empty and is added to', { id: 'ember-data:deprecate-promise-many-array-behaviors', until: '5.0', count: 1 }, function (assert) { - assert.expect(1); + assert.expect(2); let store = this.owner.lookup('service:store'); @@ -270,6 +270,7 @@ module('integration/records/relationship-changes - Relationship changes', functi assert.strictEqual(get(cpResult, 'id'), '1', 'siblings cp should have recalculated'); obj.destroy(); }); + assert.expectDeprecation({ id: 'ember-data:deprecate-array-like' }); } ); diff --git a/packages/-ember-data/tests/integration/records/reload-test.js b/packages/-ember-data/tests/integration/records/reload-test.js index c7768421a6c..abd10984adb 100644 --- a/packages/-ember-data/tests/integration/records/reload-test.js +++ b/packages/-ember-data/tests/integration/records/reload-test.js @@ -249,14 +249,21 @@ module('integration/reload - Reloading Records', function (hooks) { let tags = await person.tags; - assert.deepEqual(tags.mapBy('name'), ['hipster', 'hair']); + assert.deepEqual( + tags.map((r) => r.name), + ['hipster', 'hair'] + ); person = await tom.reload(); assert.strictEqual(person.name, 'Tom', 'precond'); tags = await person.tags; - assert.deepEqual(tags.mapBy('name'), ['hipster', 'hair'], 'The tags are still there'); + assert.deepEqual( + tags.map((r) => r.name), + ['hipster', 'hair'], + 'The tags are still there' + ); }); module('Reloading via relationship reference and { type, id }', function () { @@ -419,7 +426,7 @@ module('integration/reload - Reloading Records', function (hooks) { let owners = shen.owners; let ownersViaRef = await ownersRef.reload(); - assert.strictEqual(owners.objectAt(0), ownersViaRef.objectAt(0), 'We received the same reference via reload'); + assert.strictEqual(owners.at(0), ownersViaRef.at(0), 'We received the same reference via reload'); }); test('When a sync hasMany relationship has not been loaded, it can still be reloaded via the reference', async function (assert) { @@ -467,7 +474,7 @@ module('integration/reload - Reloading Records', function (hooks) { let ownersViaRef = await ownersRef.reload(); let owners = shen.owners; - assert.strictEqual(owners.objectAt(0), ownersViaRef.objectAt(0), 'We received the same reference via reload'); + assert.strictEqual(owners.at(0), ownersViaRef.at(0), 'We received the same reference via reload'); }); }); @@ -642,7 +649,7 @@ module('integration/reload - Reloading Records', function (hooks) { let owners = shen.owners; let ownersViaRef = await ownersRef.reload(); - assert.strictEqual(owners.objectAt(0), ownersViaRef.objectAt(0), 'We received the same reference via reload'); + assert.strictEqual(owners.at(0), ownersViaRef.at(0), 'We received the same reference via reload'); }); test('When a sync hasMany relationship has not been loaded, it can still be reloaded via the reference', async function (assert) { @@ -695,7 +702,7 @@ module('integration/reload - Reloading Records', function (hooks) { let ownersViaRef = await ownersRef.reload(); let owners = shen.owners; - assert.strictEqual(owners.objectAt(0), ownersViaRef.objectAt(0), 'We received the same reference via reload'); + assert.strictEqual(owners.at(0), ownersViaRef.at(0), 'We received the same reference via reload'); }); }); }); diff --git a/packages/-ember-data/tests/integration/records/rematerialize-test.js b/packages/-ember-data/tests/integration/records/rematerialize-test.js index 97d2a51bd4e..db2c5cadf77 100644 --- a/packages/-ember-data/tests/integration/records/rematerialize-test.js +++ b/packages/-ember-data/tests/integration/records/rematerialize-test.js @@ -236,7 +236,7 @@ module('integration/unload - Rematerializing Unloaded Records', function (hooks) // cause a rematerialization, this should also cause us to fetch boat '1' again boats = await adam.boats; - let rematerializedBoaty = boats.objectAt(1); + let rematerializedBoaty = boats.at(1); assert.ok(!!rematerializedBoaty, 'We have a boat!'); assert.strictEqual(adam.boats.length, 2, 'boats.length correct after rematerialization'); diff --git a/packages/-ember-data/tests/integration/records/save-test.js b/packages/-ember-data/tests/integration/records/save-test.js index d4f2959250e..bdba482e653 100644 --- a/packages/-ember-data/tests/integration/records/save-test.js +++ b/packages/-ember-data/tests/integration/records/save-test.js @@ -39,7 +39,6 @@ module('integration/records/save - Save Record', function (hooks) { if (DEPRECATE_SAVE_PROMISE_ACCESS) { // `save` returns a PromiseObject which allows to call get on it assert.strictEqual(saved.get('id'), undefined); - assert.strictEqual(saved.id, undefined); } deferred.resolve({ data: { id: '123', type: 'post' } }); @@ -47,7 +46,12 @@ module('integration/records/save - Save Record', function (hooks) { assert.ok(true, 'save operation was resolved'); if (DEPRECATE_SAVE_PROMISE_ACCESS) { assert.strictEqual(saved.get('id'), '123'); - assert.strictEqual(saved.id, undefined); + try { + saved.id; + assert.ok(false, 'access should error with .get assertion'); + } catch { + assert.ok(true, 'access errored correctly'); + } assert.strictEqual(model.id, '123'); } else { assert.strictEqual(saved.id, undefined); @@ -57,13 +61,15 @@ module('integration/records/save - Save Record', function (hooks) { if (DEPRECATE_SAVE_PROMISE_ACCESS) { // We don't care about the exact value of the property, but accessing it // should not throw an error and only show a deprecation. + saved.__ec_cancel__ = true; assert.strictEqual(saved.__ec_cancel__, undefined); + assert.strictEqual(model.__ec_cancel__, undefined); - assert.expectDeprecation({ id: 'ember-data:model-save-promise', count: 5 }); + assert.expectDeprecation({ id: 'ember-data:model-save-promise', count: 4 }); } }); - test('Will reject save on error', function (assert) { + test('Will reject save on error', async function (assert) { let store = this.owner.lookup('service:store'); let adapter = store.adapterFor('application'); let post = store.createRecord('post', { title: 'toto' }); @@ -73,14 +79,12 @@ module('integration/records/save - Save Record', function (hooks) { return reject(error); }; - run(function () { - post.save().then( - function () {}, - function () { - assert.ok(true, 'save operation was rejected'); - } - ); - }); + try { + await post.save(); + assert.ok(false, 'we should err'); + } catch (error) { + assert.ok(true, 'we errored during save'); + } }); test('Retry is allowed in a failure handler', function (assert) { diff --git a/packages/-ember-data/tests/integration/records/unload-test.js b/packages/-ember-data/tests/integration/records/unload-test.js index 0623f7c68e1..02a1ca36be1 100644 --- a/packages/-ember-data/tests/integration/records/unload-test.js +++ b/packages/-ember-data/tests/integration/records/unload-test.js @@ -429,7 +429,7 @@ module('integration/unload - Unloading Records', function (hooks) { assert.strictEqual(relationshipState.remoteState.length, 1, 'remoteMembers size should be 1'); assert.strictEqual(relationshipState.localMembers.size, 1, 'localMembers size should be 1'); assert.strictEqual(get(peopleBoats, 'length'), 1, 'Our person has a boat'); - assert.strictEqual(peopleBoats.objectAt(0), boat, 'Our person has the right boat'); + assert.strictEqual(peopleBoats.at(0), boat, 'Our person has the right boat'); assert.strictEqual(boatPerson, person, 'Our boat has the right person'); run(() => { @@ -498,7 +498,7 @@ module('integration/unload - Unloading Records', function (hooks) { assert.strictEqual(relationshipState.remoteState.length, 1, 'remoteMembers size should be 1'); assert.strictEqual(relationshipState.localMembers.size, 1, 'localMembers size should be 1'); assert.strictEqual(get(peopleBoats, 'length'), 1, 'Our person has a boat'); - assert.strictEqual(peopleBoats.objectAt(0), boat, 'Our person has the right boat'); + assert.strictEqual(peopleBoats.at(0), boat, 'Our person has the right boat'); assert.strictEqual(boatPerson, person, 'Our boat has the right person'); run(() => { @@ -552,7 +552,7 @@ module('integration/unload - Unloading Records', function (hooks) { assert.deepEqual(idsFromArr(relationshipState.remoteState), ['1'], 'remoteMembers size should be 1'); assert.deepEqual(idsFromArr(relationshipState.localState), ['1'], 'localMembers size should be 1'); assert.strictEqual(get(peopleBoats, 'length'), 1, 'Our person has a boat'); - assert.strictEqual(peopleBoats.objectAt(0), boat, 'Our person has the right boat'); + assert.strictEqual(peopleBoats.at(0), boat, 'Our person has the right boat'); assert.strictEqual(boatPerson, person, 'Our boat has the right person'); run(() => boat.unloadRecord()); @@ -743,7 +743,7 @@ module('integration/unload - Unloading Records', function (hooks) { assert.strictEqual(record.currentState.stateName, 'root.loaded.saved', 'We are loaded initially'); // we test that we can sync call unloadRecord followed by findRecord - assert.strictEqual(record.get('cars.firstObject.make'), 'jeep'); + assert.strictEqual(record.cars.at(0).make, 'jeep'); store.unloadRecord(record); assert.true(record.isDestroying, 'the record is destroying'); assert.true(recordData.isEmpty(identifier), 'Expected the previous data to be unloaded'); @@ -938,7 +938,7 @@ module('integration/unload - Unloading Records', function (hooks) { run(() => pushCar()); assert.strictEqual(adam.cars.length, 1, 'pushing car setups inverse relationship'); - run(() => adam.cars.firstObject.unloadRecord()); + run(() => adam.cars.at(0).unloadRecord()); assert.strictEqual(adam.cars.length, 0, 'unloading car cleaned up hasMany'); run(() => pushCar()); @@ -1054,7 +1054,11 @@ module('integration/unload - Unloading Records', function (hooks) { let cars = person.cars; assert.false(cars.isDestroyed, 'ManyArray not destroyed'); - assert.deepEqual(person.cars.mapBy('id'), ['2', '3'], 'initialy relationship established lhs'); + assert.deepEqual( + person.cars.map((r) => r.id), + ['2', '3'], + 'initialy relationship established lhs' + ); assert.strictEqual(car2.person.id, '1', 'initially relationship established rhs'); assert.strictEqual(car3.person.id, '1', 'initially relationship established rhs'); @@ -1076,7 +1080,11 @@ module('integration/unload - Unloading Records', function (hooks) { ); assert.notStrictEqual(store.peekRecord('person', '1'), null, 'unloaded record can be restored'); - assert.deepEqual(person.cars.mapBy('id'), [], 'restoring unloaded record does not restore relationship'); + assert.deepEqual( + person.cars.map((r) => r.id), + [], + 'restoring unloaded record does not restore relationship' + ); assert.strictEqual(car2.person, null, 'restoring unloaded record does not restore relationship'); assert.strictEqual(car3.person, null, 'restoring unloaded record does not restore relationship'); @@ -1105,7 +1113,11 @@ module('integration/unload - Unloading Records', function (hooks) { assert.strictEqual(car2.person.id, '1', 'after unloading, relationship can be restored'); assert.strictEqual(car3.person.id, '1', 'after unloading, relationship can be restored'); - assert.deepEqual(person.cars.mapBy('id'), ['2', '3'], 'after unloading, relationship can be restored'); + assert.deepEqual( + person.cars.map((r) => r.id), + ['2', '3'], + 'after unloading, relationship can be restored' + ); }); test('1:many sync unload many side', function (assert) { @@ -1148,7 +1160,11 @@ module('integration/unload - Unloading Records', function (hooks) { let cars = person.cars; assert.false(cars.isDestroyed, 'ManyArray not destroyed'); - assert.deepEqual(person.cars.mapBy('id'), ['2', '3'], 'initialy relationship established lhs'); + assert.deepEqual( + person.cars.map((r) => r.id), + ['2', '3'], + 'initialy relationship established lhs' + ); assert.strictEqual(car2.person.id, '1', 'initially relationship established rhs'); assert.strictEqual(car3.person.id, '1', 'initially relationship established rhs'); @@ -1157,7 +1173,11 @@ module('integration/unload - Unloading Records', function (hooks) { assert.strictEqual(store.peekRecord('car', '2'), null, 'unloaded record gone from store'); assert.false(cars.isDestroyed, 'ManyArray not destroyed'); - assert.deepEqual(person.cars.mapBy('id'), ['3'], 'unload sync relationship acts as delete'); + assert.deepEqual( + person.cars.map((r) => r.id), + ['3'], + 'unload sync relationship acts as delete' + ); assert.strictEqual(car3.person.id, '1', 'unloading one of a sync hasMany does not affect the rest'); car2 = run(() => @@ -1170,7 +1190,11 @@ module('integration/unload - Unloading Records', function (hooks) { ); assert.notStrictEqual(store.peekRecord('car', '2'), null, 'unloaded record can be restored'); - assert.deepEqual(person.cars.mapBy('id'), ['3'], 'restoring unloaded record does not restore relationship'); + assert.deepEqual( + person.cars.map((r) => r.id), + ['3'], + 'restoring unloaded record does not restore relationship' + ); assert.strictEqual(car2.person, null, 'restoring unloaded record does not restore relationship'); run(() => @@ -1197,7 +1221,11 @@ module('integration/unload - Unloading Records', function (hooks) { ); assert.strictEqual(car2.person.id, '1', 'after unloading, relationship can be restored'); - assert.deepEqual(person.cars.mapBy('id'), ['2', '3'], 'after unloading, relationship can be restored'); + assert.deepEqual( + person.cars.map((r) => r.id), + ['2', '3'], + 'after unloading, relationship can be restored' + ); }); test('many:many sync unload', function (assert) { @@ -1261,10 +1289,26 @@ module('integration/unload - Unloading Records', function (hooks) { let p2groups = person2.groups; let g3people = group3.people; - assert.deepEqual(person1.groups.mapBy('id'), ['3', '4'], 'initially established relationship lhs'); - assert.deepEqual(person2.groups.mapBy('id'), ['3', '4'], 'initially established relationship lhs'); - assert.deepEqual(group3.people.mapBy('id'), ['1', '2'], 'initially established relationship lhs'); - assert.deepEqual(group4.people.mapBy('id'), ['1', '2'], 'initially established relationship lhs'); + assert.deepEqual( + person1.groups.map((r) => r.id), + ['3', '4'], + 'initially established relationship lhs' + ); + assert.deepEqual( + person2.groups.map((r) => r.id), + ['3', '4'], + 'initially established relationship lhs' + ); + assert.deepEqual( + group3.people.map((r) => r.id), + ['1', '2'], + 'initially established relationship lhs' + ); + assert.deepEqual( + group4.people.map((r) => r.id), + ['1', '2'], + 'initially established relationship lhs' + ); assert.false(p2groups.isDestroyed, 'groups is not destroyed'); assert.false(g3people.isDestroyed, 'people is not destroyed'); @@ -1275,12 +1319,20 @@ module('integration/unload - Unloading Records', function (hooks) { assert.false(g3people.isDestroyed, 'people (inverse) is not destroyed'); assert.deepEqual( - person1.groups.mapBy('id'), + person1.groups.map((r) => r.id), ['3', '4'], 'unloaded record in many:many does not affect inverse of inverse' ); - assert.deepEqual(group3.people.mapBy('id'), ['1'], 'unloading acts as delete for sync relationships'); - assert.deepEqual(group4.people.mapBy('id'), ['1'], 'unloading acts as delete for sync relationships'); + assert.deepEqual( + group3.people.map((r) => r.id), + ['1'], + 'unloading acts as delete for sync relationships' + ); + assert.deepEqual( + group4.people.map((r) => r.id), + ['1'], + 'unloading acts as delete for sync relationships' + ); assert.strictEqual(store.peekRecord('person', '2'), null, 'unloading removes record from store'); @@ -1294,9 +1346,21 @@ module('integration/unload - Unloading Records', function (hooks) { ); assert.notStrictEqual(store.peekRecord('person', '2'), null, 'unloaded record can be restored'); - assert.deepEqual(person2.groups.mapBy('id'), [], 'restoring unloaded record does not restore relationship'); - assert.deepEqual(group3.people.mapBy('id'), ['1'], 'restoring unloaded record does not restore relationship'); - assert.deepEqual(group4.people.mapBy('id'), ['1'], 'restoring unloaded record does not restore relationship'); + assert.deepEqual( + person2.groups.map((r) => r.id), + [], + 'restoring unloaded record does not restore relationship' + ); + assert.deepEqual( + group3.people.map((r) => r.id), + ['1'], + 'restoring unloaded record does not restore relationship' + ); + assert.deepEqual( + group4.people.map((r) => r.id), + ['1'], + 'restoring unloaded record does not restore relationship' + ); run(() => store.push({ @@ -1321,9 +1385,21 @@ module('integration/unload - Unloading Records', function (hooks) { }) ); - assert.deepEqual(person2.groups.mapBy('id'), ['3', '4'], 'after unloading, relationship can be restored'); - assert.deepEqual(group3.people.mapBy('id'), ['1', '2'], 'after unloading, relationship can be restored'); - assert.deepEqual(group4.people.mapBy('id'), ['1', '2'], 'after unloading, relationship can be restored'); + assert.deepEqual( + person2.groups.map((r) => r.id), + ['3', '4'], + 'after unloading, relationship can be restored' + ); + assert.deepEqual( + group3.people.map((r) => r.id), + ['1', '2'], + 'after unloading, relationship can be restored' + ); + assert.deepEqual( + group4.people.map((r) => r.id), + ['1', '2'], + 'after unloading, relationship can be restored' + ); }); test('1:1 async unload', function (assert) { @@ -1448,7 +1524,7 @@ module('integration/unload - Unloading Records', function (hooks) { const asyncRecords = await person.boats; boats = asyncRecords; - [boat2, boat3] = boats.toArray(); + [boat2, boat3] = boats.slice(); await Promise.all([boat2, boat3].map((b) => b.person)); assert.deepEqual(person.hasMany('boats').ids(), ['2', '3'], 'initially relationship established lhs'); @@ -1521,21 +1597,34 @@ module('integration/unload - Unloading Records', function (hooks) { }); const boats = await person.boats; - let [boat2, boat3] = boats.toArray(); + + let [boat2, boat3] = boats.slice(); await Promise.all([boat2.person, boat3.person]); assert.deepEqual(person.hasMany('boats').ids(), ['2', '3'], 'initially relationship established lhs'); assert.strictEqual(boat2.belongsTo('person').id(), '1', 'initially relationship established rhs'); assert.strictEqual(boat3.belongsTo('person').id(), '1', 'initially relationship established rhs'); - assert.deepEqual(boats.mapBy('id'), ['2', '3'], 'many array is initially set up correctly'); + assert.deepEqual( + boats.map((r) => r.id), + ['2', '3'], + 'many array is initially set up correctly' + ); boat2.unloadRecord(); - assert.deepEqual(boats.mapBy('id'), ['3'], 'unload async removes from previous many array'); + assert.deepEqual( + boats.map((r) => r.id), + ['3'], + 'unload async removes from previous many array' + ); boat3.unloadRecord(); - assert.deepEqual(boats.mapBy('id'), [], 'unload async removes from previous many array'); + assert.deepEqual( + boats.map((r) => r.id), + [], + 'unload async removes from previous many array' + ); assert.deepEqual(person.hasMany('boats').ids(), ['2', '3'], 'unload async is not treated as delete'); boat3 = store.push({ @@ -1553,7 +1642,11 @@ module('integration/unload - Unloading Records', function (hooks) { boat3 = store.peekRecord('boat', '3'); assert.strictEqual(refetchedBoats, boats, 'we have the same ManyArray'); - assert.deepEqual(refetchedBoats.mapBy('id'), ['2', '3'], 'boats refetched'); + assert.deepEqual( + refetchedBoats.map((r) => r.id), + ['2', '3'], + 'boats refetched' + ); assert.deepEqual(person.hasMany('boats').ids(), ['2', '3'], 'unload async is not treated as delete'); assert.strictEqual(boat3.belongsTo('person').id(), '1', 'unload async is not treated as delete'); @@ -1626,7 +1719,7 @@ module('integration/unload - Unloading Records', function (hooks) { }); const person1Friends = await person1.friends; - const [person3, person4] = person1Friends.toArray(); + const [person3, person4] = person1Friends.slice(); await all([person2.friends, person3.friends, person4.friends]); @@ -1637,17 +1730,29 @@ module('integration/unload - Unloading Records', function (hooks) { person3.unloadRecord(); - assert.deepEqual(person1Friends.mapBy('id'), ['4'], 'unload async removes from previous many array'); + assert.deepEqual( + person1Friends.map((r) => r.id), + ['4'], + 'unload async removes from previous many array' + ); person4.unloadRecord(); - assert.deepEqual(person1Friends.mapBy('id'), [], 'unload async removes from previous many array'); + assert.deepEqual( + person1Friends.map((r) => r.id), + [], + 'unload async removes from previous many array' + ); assert.deepEqual(person1.hasMany('friends').ids(), ['3', '4'], 'unload async is not treated as delete'); const refetchedFriends = await person1.friends; assert.strictEqual(person1Friends, refetchedFriends, 'we have the same ManyArray'); - assert.deepEqual(refetchedFriends.mapBy('id'), ['3', '4'], 'friends refetched'); + assert.deepEqual( + refetchedFriends.map((r) => r.id), + ['3', '4'], + 'friends refetched' + ); assert.deepEqual(person1.hasMany('friends').ids(), ['3', '4'], 'unload async is not treated as delete'); assert.deepEqual( @@ -1832,7 +1937,11 @@ module('integration/unload - Unloading Records', function (hooks) { let spoons = person.favoriteSpoons; assert.false(spoons.isDestroyed, 'ManyArray not destroyed'); - assert.deepEqual(person.favoriteSpoons.mapBy('id'), ['2', '3'], 'initialy relationship established lhs'); + assert.deepEqual( + person.favoriteSpoons.map((r) => r.id), + ['2', '3'], + 'initialy relationship established lhs' + ); assert.strictEqual(spoon2.belongsTo('person').id(), '1', 'initially relationship established rhs'); assert.strictEqual(spoon3.belongsTo('person').id(), '1', 'initially relationship established rhs'); @@ -1841,7 +1950,11 @@ module('integration/unload - Unloading Records', function (hooks) { assert.strictEqual(store.peekRecord('spoon', '2'), null, 'unloaded record gone from store'); assert.false(spoons.isDestroyed, 'ManyArray not destroyed'); - assert.deepEqual(person.favoriteSpoons.mapBy('id'), ['3'], 'unload sync relationship acts as delete'); + assert.deepEqual( + person.favoriteSpoons.map((r) => r.id), + ['3'], + 'unload sync relationship acts as delete' + ); assert.strictEqual( spoon3.belongsTo('person').id(), '1', @@ -1859,7 +1972,7 @@ module('integration/unload - Unloading Records', function (hooks) { assert.notStrictEqual(store.peekRecord('spoon', '2'), null, 'unloaded record can be restored'); assert.deepEqual( - person.favoriteSpoons.mapBy('id'), + person.favoriteSpoons.map((r) => r.id), ['3'], 'restoring unloaded record does not restore relationship' ); @@ -1893,7 +2006,11 @@ module('integration/unload - Unloading Records', function (hooks) { ); assert.strictEqual(spoon2.belongsTo('person').id(), '1', 'after unloading, relationship can be restored'); - assert.deepEqual(person.favoriteSpoons.mapBy('id'), ['2', '3'], 'after unloading, relationship can be restored'); + assert.deepEqual( + person.favoriteSpoons.map((r) => r.id), + ['2', '3'], + 'after unloading, relationship can be restored' + ); }); test('1 async : many sync unload async side', async function (assert) { @@ -1948,7 +2065,11 @@ module('integration/unload - Unloading Records', function (hooks) { let spoon3 = store.peekRecord('spoon', '3'); let spoons = person.favoriteSpoons; - assert.deepEqual(person.favoriteSpoons.mapBy('id'), ['2', '3'], 'initially relationship established lhs'); + assert.deepEqual( + person.favoriteSpoons.map((r) => r.id), + ['2', '3'], + 'initially relationship established lhs' + ); assert.strictEqual(spoon2.belongsTo('person').id(), '1', 'initially relationship established rhs'); assert.strictEqual(spoon3.belongsTo('person').id(), '1', 'initially relationship established rhs'); @@ -1965,7 +2086,11 @@ module('integration/unload - Unloading Records', function (hooks) { assert.notEqual(person, refetchedPerson, 'the previously loaded record is not reused'); - assert.deepEqual(refetchedPerson.favoriteSpoons.mapBy('id'), ['2', '3'], 'unload async is not treated as delete'); + assert.deepEqual( + refetchedPerson.favoriteSpoons.map((r) => r.id), + ['2', '3'], + 'unload async is not treated as delete' + ); assert.strictEqual(spoon2.belongsTo('person').id(), '1', 'unload async is not treated as delete'); assert.strictEqual(spoon3.belongsTo('person').id(), '1', 'unload async is not treated as delete'); @@ -2018,26 +2143,42 @@ module('integration/unload - Unloading Records', function (hooks) { }); const shows = await person.favoriteShows; - const [show2, show3] = shows.toArray(); + const [show2, show3] = shows.slice(); assert.deepEqual(person.hasMany('favoriteShows').ids(), ['2', '3'], 'initially relationship established lhs'); assert.strictEqual(show2.person.id, '1', 'initially relationship established rhs'); assert.strictEqual(show3.person.id, '1', 'initially relationship established rhs'); - assert.deepEqual(shows.mapBy('id'), ['2', '3'], 'many array is initially set up correctly'); + assert.deepEqual( + shows.map((r) => r.id), + ['2', '3'], + 'many array is initially set up correctly' + ); show2.unloadRecord(); - assert.deepEqual(shows.mapBy('id'), ['3'], 'unload async removes from inverse many array'); + assert.deepEqual( + shows.map((r) => r.id), + ['3'], + 'unload async removes from inverse many array' + ); show3.unloadRecord(); - assert.deepEqual(shows.mapBy('id'), [], 'unload async removes from inverse many array'); + assert.deepEqual( + shows.map((r) => r.id), + [], + 'unload async removes from inverse many array' + ); assert.deepEqual(person.hasMany('favoriteShows').ids(), ['2', '3'], 'unload async is not treated as delete'); const refetchedShows = await person.favoriteShows; assert.strictEqual(shows, refetchedShows, 'we have the same ManyArray'); - assert.deepEqual(refetchedShows.mapBy('id'), ['2', '3'], 'shows refetched'); + assert.deepEqual( + refetchedShows.map((r) => r.id), + ['2', '3'], + 'shows refetched' + ); assert.deepEqual(person.hasMany('favoriteShows').ids(), ['2', '3'], 'unload async is not treated as delete'); assert.strictEqual(findManyCalls, 2, 'findMany called as expected'); @@ -2091,12 +2232,16 @@ module('integration/unload - Unloading Records', function (hooks) { const asyncRecords = await person.favoriteShows; shows = asyncRecords; - [show2, show3] = shows.toArray(); + [show2, show3] = shows.slice(); assert.deepEqual(person.hasMany('favoriteShows').ids(), ['2', '3'], 'initially relationship established lhs'); assert.strictEqual(show2.person.id, '1', 'initially relationship established rhs'); assert.strictEqual(show3.person.id, '1', 'initially relationship established rhs'); - assert.deepEqual(shows.mapBy('id'), ['2', '3'], 'many array is initially set up correctly'); + assert.deepEqual( + shows.map((r) => r.id), + ['2', '3'], + 'many array is initially set up correctly' + ); person.unloadRecord(); await settled(); @@ -2149,7 +2294,11 @@ module('integration/unload - Unloading Records', function (hooks) { const refetchedShows = await person.favoriteShows; assert.notEqual(refetchedShows, shows, 'ManyArray not reused'); - assert.deepEqual(refetchedShows.mapBy('id'), ['2', '3'], 'unload async not treated as a delete'); + assert.deepEqual( + refetchedShows.map((r) => r.id), + ['2', '3'], + 'unload async not treated as a delete' + ); assert.strictEqual(findManyCalls, 1, 'findMany calls as expected'); }); @@ -2215,7 +2364,7 @@ module('integration/unload - Unloading Records', function (hooks) { person.boats .then((asyncRecords) => { boats = asyncRecords; - [boat2, boat3] = boats.toArray(); + [boat2, boat3] = boats.slice(); }) .then(() => { assert.deepEqual(person.hasMany('boats').ids(), ['2', '3'], 'initially relationship established rhs'); @@ -2228,7 +2377,11 @@ module('integration/unload - Unloading Records', function (hooks) { person.boats; }); - assert.deepEqual(boats.mapBy('id'), ['3'], 'unloaded boat is removed from ManyArray'); + assert.deepEqual( + boats.map((r) => r.id), + ['3'], + 'unloaded boat is removed from ManyArray' + ); }) .then(() => { return run(() => person.boats); diff --git a/packages/-ember-data/tests/integration/references/autotracking-test.js b/packages/-ember-data/tests/integration/references/autotracking-test.js index 6ce8fcb6502..74d5328e61e 100644 --- a/packages/-ember-data/tests/integration/references/autotracking-test.js +++ b/packages/-ember-data/tests/integration/references/autotracking-test.js @@ -144,7 +144,7 @@ module('integration/references/autotracking', function (hooks) { assert.strictEqual(getRootElement().textContent, 'id: 2, ', 'the ids are initially correct'); assert.deepEqual(testContext.friendIds, ['2'], 'the ids are initially correct'); const bill = store.createRecord('user', { name: 'Bill' }); - user.friends.pushObject(bill); + user.friends.push(bill); await settled(); assert.strictEqual(getRootElement().textContent, 'id: 2, id: null, ', 'the id is added for the new record'); assert.deepEqual(testContext.friendIds, ['2', null], 'the ids are correct when we add a new record'); diff --git a/packages/-ember-data/tests/integration/references/belongs-to-test.js b/packages/-ember-data/tests/integration/references/belongs-to-test.js index 20084979c82..18d05862b4e 100644 --- a/packages/-ember-data/tests/integration/references/belongs-to-test.js +++ b/packages/-ember-data/tests/integration/references/belongs-to-test.js @@ -9,6 +9,7 @@ import { setupTest } from 'ember-qunit'; import JSONAPIAdapter from '@ember-data/adapter/json-api'; import Model, { attr, belongsTo, hasMany } from '@ember-data/model'; import JSONAPISerializer from '@ember-data/serializer/json-api'; +import { deprecatedTest } from '@ember-data/unpublished-test-infra/test-support/deprecated-test'; import testInDebug from '@ember-data/unpublished-test-infra/test-support/test-in-debug'; module('integration/references/belongs-to', function (hooks) { @@ -194,16 +195,16 @@ module('integration/references/belongs-to', function (hooks) { }); }); - test('push(promise)', function (assert) { - var done = assert.async(); - - let store = this.owner.lookup('service:store'); - let Family = store.modelFor('family'); + deprecatedTest( + 'push(promise)', + { id: 'ember-data:deprecate-promise-proxies', until: '5.0', count: 1 }, + async function (assert) { + let store = this.owner.lookup('service:store'); + let Family = store.modelFor('family'); - var push; - var deferred = defer(); + var push; + var deferred = defer(); - run(function () { var person = store.push({ data: { type: 'person', @@ -217,11 +218,9 @@ module('integration/references/belongs-to', function (hooks) { }); var familyReference = person.belongsTo('family'); push = familyReference.push(deferred.promise); - }); - assert.ok(push.then, 'BelongsToReference.push returns a promise'); + assert.ok(push.then, 'BelongsToReference.push returns a promise'); - run(function () { deferred.resolve({ data: { type: 'family', @@ -231,17 +230,13 @@ module('integration/references/belongs-to', function (hooks) { }, }, }); - }); - - run(function () { - push.then(function (record) { - assert.ok(Family.detectInstance(record), 'push resolves with the record'); - assert.strictEqual(get(record, 'name'), 'Coreleone', 'name is updated'); - done(); + await push.then(function (record) { + assert.ok(record instanceof Family, 'push resolves with the record'); + assert.strictEqual(record.name, 'Coreleone', 'name is updated'); }); - }); - }); + } + ); testInDebug('push(object) asserts for invalid modelClass', async function (assert) { let store = this.owner.lookup('service:store'); diff --git a/packages/-ember-data/tests/integration/references/has-many-test.js b/packages/-ember-data/tests/integration/references/has-many-test.js index 91f8ed9d539..b2ff6e6b614 100755 --- a/packages/-ember-data/tests/integration/references/has-many-test.js +++ b/packages/-ember-data/tests/integration/references/has-many-test.js @@ -5,11 +5,12 @@ import { module, test } from 'qunit'; import { defer, resolve } from 'rsvp'; import { gte } from 'ember-compatibility-helpers'; -import DS from 'ember-data'; import { setupRenderingTest } from 'ember-qunit'; +import Adapter from '@ember-data/adapter'; import Model, { attr, belongsTo, hasMany } from '@ember-data/model'; import JSONAPISerializer from '@ember-data/serializer/json-api'; +import { deprecatedTest } from '@ember-data/unpublished-test-infra/test-support/deprecated-test'; import testInDebug from '@ember-data/unpublished-test-infra/test-support/test-in-debug'; import createTrackingContext from '../../helpers/create-tracking-context'; @@ -36,7 +37,7 @@ module('integration/references/has-many', function (hooks) { this.owner.register('model:person', Person); this.owner.register('model:pet', Pet); - this.owner.register('adapter:application', DS.Adapter.extend()); + this.owner.register('adapter:application', Adapter.extend()); this.owner.register('serializer:application', class extends JSONAPISerializer {}); }); @@ -218,7 +219,7 @@ module('integration/references/has-many', function (hooks) { await context.render(); assert.strictEqual(renderedValue.length, 1, 'We have a value'); - assert.strictEqual(renderedValue.objectAt(0), person1, 'We have the right value'); + assert.strictEqual(renderedValue.at(0), person1, 'We have the right value'); store.push({ data: { @@ -254,51 +255,38 @@ module('integration/references/has-many', function (hooks) { let person2 = store.peekRecord('person', '2'); assert.notStrictEqual(person2, null, 'we have a person'); assert.strictEqual(renderedValue.length, 2, 'We have two values'); - assert.strictEqual(renderedValue.objectAt(0), person1, 'We have the right value[0]'); - assert.strictEqual(renderedValue.objectAt(1), person2, 'We have the right value[1]'); + assert.strictEqual(renderedValue.at(0), person1, 'We have the right value[0]'); + assert.strictEqual(renderedValue.at(1), person2, 'We have the right value[1]'); }); } - testInDebug('push(array)', function (assert) { - var done = assert.async(); - - let store = this.owner.lookup('service:store'); - - var family; - run(function () { - family = store.push({ - data: { - type: 'family', - id: '1', - relationships: { - persons: { - data: [ - { type: 'person', id: '1' }, - { type: 'person', id: '2' }, - ], - }, + testInDebug('push(array)', async function (assert) { + const store = this.owner.lookup('service:store'); + const family = store.push({ + data: { + type: 'family', + id: '1', + relationships: { + persons: { + data: [ + { type: 'person', id: '1' }, + { type: 'person', id: '2' }, + ], }, }, - }); + }, }); - var personsReference = family.hasMany('persons'); - - run(function () { - var data = [ - { data: { type: 'person', id: '1', attributes: { name: 'Vito' } } }, - { data: { type: 'person', id: '2', attributes: { name: 'Michael' } } }, - ]; - - personsReference.push(data).then(function (records) { - assert.ok(records instanceof DS.ManyArray, 'push resolves with the referenced records'); - assert.strictEqual(get(records, 'length'), 2); - assert.strictEqual(records.objectAt(0).name, 'Vito'); - assert.strictEqual(records.objectAt(1).name, 'Michael'); + const personsReference = family.hasMany('persons'); + const data = [ + { data: { type: 'person', id: '1', attributes: { name: 'Vito' } } }, + { data: { type: 'person', id: '2', attributes: { name: 'Michael' } } }, + ]; - done(); - }); - }); + const records = await personsReference.push(data); + assert.strictEqual(get(records, 'length'), 2); + assert.strictEqual(records.at(0).name, 'Vito'); + assert.strictEqual(records.at(1).name, 'Michael'); }); testInDebug('push(array) works with polymorphic type', function (assert) { @@ -325,9 +313,8 @@ module('integration/references/has-many', function (hooks) { var data = [{ data: { type: 'mafia-boss', id: '1', attributes: { name: 'Vito' } } }]; personsReference.push(data).then(function (records) { - assert.ok(records instanceof DS.ManyArray, 'push resolves with the referenced records'); assert.strictEqual(get(records, 'length'), 1); - assert.strictEqual(records.objectAt(0).name, 'Vito'); + assert.strictEqual(records.at(0).name, 'Vito'); done(); }); @@ -383,54 +370,56 @@ module('integration/references/has-many', function (hooks) { }; personsReference.push(payload).then(function (records) { - assert.ok(records instanceof DS.ManyArray, 'push resolves with the referenced records'); assert.strictEqual(get(records, 'length'), 2); - assert.strictEqual(records.objectAt(0).name, 'Vito'); - assert.strictEqual(records.objectAt(1).name, 'Michael'); + assert.strictEqual(records.at(0).name, 'Vito'); + assert.strictEqual(records.at(1).name, 'Michael'); done(); }); }); }); - test('push(promise)', async function (assert) { - const store = this.owner.lookup('service:store'); - const deferred = defer(); + deprecatedTest( + 'push(promise)', + { id: 'ember-data:deprecate-promise-proxies', until: '5.0', count: 1 }, + async function (assert) { + const store = this.owner.lookup('service:store'); + const deferred = defer(); - const family = store.push({ - data: { - type: 'family', - id: '1', - relationships: { - persons: { - data: [ - { type: 'person', id: '1' }, - { type: 'person', id: '2' }, - ], + const family = store.push({ + data: { + type: 'family', + id: '1', + relationships: { + persons: { + data: [ + { type: 'person', id: '1' }, + { type: 'person', id: '2' }, + ], + }, }, }, - }, - }); - const personsReference = family.hasMany('persons'); - let pushResult = personsReference.push(deferred.promise); + }); + const personsReference = family.hasMany('persons'); + let pushResult = personsReference.push(deferred.promise); - assert.ok(pushResult.then, 'HasManyReference.push returns a promise'); + assert.ok(pushResult.then, 'HasManyReference.push returns a promise'); - const payload = { - data: [ - { data: { type: 'person', id: '1', attributes: { name: 'Vito' } } }, - { data: { type: 'person', id: '2', attributes: { name: 'Michael' } } }, - ], - }; + const payload = { + data: [ + { data: { type: 'person', id: '1', attributes: { name: 'Vito' } } }, + { data: { type: 'person', id: '2', attributes: { name: 'Michael' } } }, + ], + }; - deferred.resolve(payload); + deferred.resolve(payload); - const records = await pushResult; - assert.ok(records instanceof DS.ManyArray, 'push resolves with the referenced records'); - assert.strictEqual(get(records, 'length'), 2); - assert.strictEqual(records.objectAt(0).name, 'Vito'); - assert.strictEqual(records.objectAt(1).name, 'Michael'); - }); + const records = await pushResult; + assert.strictEqual(get(records, 'length'), 2); + assert.strictEqual(records.at(0).name, 'Vito'); + assert.strictEqual(records.at(1).name, 'Michael'); + } + ); test('push valid json:api', async function (assert) { const store = this.owner.lookup('service:store'); @@ -460,10 +449,9 @@ module('integration/references/has-many', function (hooks) { assert.ok(pushResult.then, 'HasManyReference.push returns a promise'); const records = await pushResult; - assert.ok(records instanceof DS.ManyArray, 'push resolves with the referenced records'); assert.strictEqual(get(records, 'length'), 2); - assert.strictEqual(records.objectAt(0).name, 'Vito'); - assert.strictEqual(records.objectAt(1).name, 'Michael'); + assert.strictEqual(records.at(0).name, 'Vito'); + assert.strictEqual(records.at(1).name, 'Michael'); }); test('value() returns null when reference is not yet loaded', function (assert) { @@ -518,7 +506,7 @@ module('integration/references/has-many', function (hooks) { var personsReference = family.hasMany('persons'); var records = personsReference.value(); assert.strictEqual(get(records, 'length'), 2); - assert.true(records.isEvery('isLoaded')); + assert.true(records.every((v) => v.isLoaded)); }); }); @@ -612,10 +600,9 @@ module('integration/references/has-many', function (hooks) { run(function () { personsReference.load({ adapterOptions }).then(function (records) { - assert.ok(records instanceof DS.ManyArray, 'push resolves with the referenced records'); assert.strictEqual(get(records, 'length'), 2); - assert.strictEqual(records.objectAt(0).name, 'Vito'); - assert.strictEqual(records.objectAt(1).name, 'Michael'); + assert.strictEqual(records.at(0).name, 'Vito'); + assert.strictEqual(records.at(1).name, 'Michael'); done(); }); @@ -662,10 +649,9 @@ module('integration/references/has-many', function (hooks) { run(function () { personsReference.load({ adapterOptions }).then(function (records) { - assert.ok(records instanceof DS.ManyArray, 'push resolves with the referenced records'); assert.strictEqual(get(records, 'length'), 2); - assert.strictEqual(records.objectAt(0).name, 'Vito'); - assert.strictEqual(records.objectAt(1).name, 'Michael'); + assert.strictEqual(records.at(0).name, 'Vito'); + assert.strictEqual(records.at(1).name, 'Michael'); done(); }); @@ -705,7 +691,6 @@ module('integration/references/has-many', function (hooks) { return run(() => { return personsReference.load({ adapterOptions }).then((records) => { - assert.ok(records instanceof DS.ManyArray, 'push resolves with the referenced records'); assert.strictEqual(get(records, 'length'), 0); assert.strictEqual(get(personsReference.value(), 'length'), 0); }); @@ -805,10 +790,9 @@ module('integration/references/has-many', function (hooks) { run(function () { personsReference.reload({ adapterOptions }).then(function (records) { - assert.ok(records instanceof DS.ManyArray, 'push resolves with the referenced records'); assert.strictEqual(get(records, 'length'), 2); - assert.strictEqual(records.objectAt(0).name, 'Vito Coreleone'); - assert.strictEqual(records.objectAt(1).name, 'Michael Coreleone'); + assert.strictEqual(records.at(0).name, 'Vito Coreleone'); + assert.strictEqual(records.at(1).name, 'Michael Coreleone'); done(); }); @@ -871,10 +855,9 @@ module('integration/references/has-many', function (hooks) { return personsReference.reload({ adapterOptions }); }) .then(function (records) { - assert.ok(records instanceof DS.ManyArray, 'push resolves with the referenced records'); assert.strictEqual(get(records, 'length'), 2); - assert.strictEqual(records.objectAt(0).name, 'Vito Coreleone'); - assert.strictEqual(records.objectAt(1).name, 'Michael Coreleone'); + assert.strictEqual(records.at(0).name, 'Vito Coreleone'); + assert.strictEqual(records.at(1).name, 'Michael Coreleone'); done(); }); diff --git a/packages/-ember-data/tests/integration/relationships/belongs-to-test.js b/packages/-ember-data/tests/integration/relationships/belongs-to-test.js index 167a0577d1b..8662c8713ad 100644 --- a/packages/-ember-data/tests/integration/relationships/belongs-to-test.js +++ b/packages/-ember-data/tests/integration/relationships/belongs-to-test.js @@ -9,6 +9,7 @@ import JSONAPIAdapter from '@ember-data/adapter/json-api'; import Model, { attr, belongsTo, hasMany } from '@ember-data/model'; import JSONAPISerializer from '@ember-data/serializer/json-api'; import Store from '@ember-data/store'; +import { deprecatedTest } from '@ember-data/unpublished-test-infra/test-support/deprecated-test'; import testInDebug from '@ember-data/unpublished-test-infra/test-support/test-in-debug'; import { getRelationshipStateForRecord, hasRelationshipForRecord } from '../../helpers/accessors'; @@ -390,7 +391,11 @@ module('integration/relationship/belongs_to Belongs-To Relationships', function let appTeam = await app.team; assert.strictEqual(appTeam.id, team.id, 'sets team correctly on app'); const apps = await team.apps; - assert.deepEqual(apps.toArray().mapBy('id'), ['1'], 'sets apps correctly on team'); + assert.deepEqual( + apps.slice().map((r) => r.id), + ['1'], + 'sets apps correctly on team' + ); adapter.shouldBackgroundReloadRecord = () => false; adapter.updateRecord = (store, type, snapshot) => { @@ -414,7 +419,11 @@ module('integration/relationship/belongs_to Belongs-To Relationships', function await app.save(); appTeam = await app.team; assert.strictEqual(appTeam?.id, undefined, 'team removed from app relationship'); - assert.deepEqual(apps.toArray().mapBy('id'), [], 'app removed from team apps relationship'); + assert.deepEqual( + apps.slice().map((r) => r.id), + [], + 'app removed from team apps relationship' + ); }); test('The store can materialize a non loaded monomorphic belongsTo association', function (assert) { @@ -879,42 +888,51 @@ module('integration/relationship/belongs_to Belongs-To Relationships', function }); }); - test('A record can be created with a resolved belongsTo promise', function (assert) { - assert.expect(1); + deprecatedTest( + 'A record can be created with a resolved belongsTo promise', + { id: 'ember-data:deprecate-promise-proxies', until: '5.0' }, + async function (assert) { + assert.expect(1); - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + let store = this.owner.lookup('service:store'); + let adapter = store.adapterFor('application'); - adapter.shouldBackgroundReloadRecord = () => false; + adapter.shouldBackgroundReloadRecord = () => false; - let Group = Model.extend({ - people: hasMany('person', { async: false, inverse: 'group' }), - }); + let Group = Model.extend({ + people: hasMany('person', { async: false, inverse: 'group' }), + }); - let Person = Model.extend({ - group: belongsTo('group', { async: true, inverse: 'people' }), - }); + let Person = Model.extend({ + group: belongsTo('group', { async: true, inverse: 'people' }), + }); - this.owner.register('model:group', Group); - this.owner.register('model:person', Person); + this.owner.register('model:group', Group); + this.owner.register('model:person', Person); - run(() => { store.push({ data: { id: '1', type: 'group', }, }); - }); + const originalOwner = store.push({ + data: { + id: '1', + type: 'person', + group: { data: { type: 'group', id: '1' } }, + }, + }); - let groupPromise = store.findRecord('group', 1); - return groupPromise.then((group) => { + let groupPromise = originalOwner.group; + const group = await groupPromise; let person = store.createRecord('person', { group: groupPromise, }); - assert.strictEqual(person.group.content, group); - }); - }); + const personGroup = await person.group; + assert.strictEqual(personGroup, group, 'the group matches'); + } + ); test('polymorphic belongsTo class-checks check the superclass', function (assert) { assert.expect(1); diff --git a/packages/-ember-data/tests/integration/relationships/has-many-test.js b/packages/-ember-data/tests/integration/relationships/has-many-test.js index f83143a8268..fefd0a388ff 100644 --- a/packages/-ember-data/tests/integration/relationships/has-many-test.js +++ b/packages/-ember-data/tests/integration/relationships/has-many-test.js @@ -13,6 +13,7 @@ import Adapter from '@ember-data/adapter'; import JSONAPIAdapter from '@ember-data/adapter/json-api'; import RESTAdapter from '@ember-data/adapter/rest'; import Model, { attr, belongsTo, hasMany } from '@ember-data/model'; +import { DEPRECATE_ARRAY_LIKE } from '@ember-data/private-build-infra/deprecations'; import JSONAPISerializer from '@ember-data/serializer/json-api'; import RESTSerializer from '@ember-data/serializer/rest'; import { deprecatedTest } from '@ember-data/unpublished-test-infra/test-support/deprecated-test'; @@ -296,9 +297,9 @@ module('integration/relationships/has_many - Has-Many Relationships', function ( 'user should have expected contacts' ); - contacts.addObject(store.createRecord('user', { id: '5', name: 'chris' })); - contacts.addObject(store.createRecord('user', { id: '6' })); - contacts.addObject(store.createRecord('user', { id: '7' })); + contacts.push(store.createRecord('user', { id: '5', name: 'chris' })); + contacts.push(store.createRecord('user', { id: '6' })); + contacts.push(store.createRecord('user', { id: '7' })); assert.deepEqual( contacts.map((c) => c.id), @@ -319,7 +320,7 @@ module('integration/relationships/has_many - Has-Many Relationships', function ( assert.ok(!user.contacts.initialState || !user.contacts.initialState.find((model) => model.id === '2')); run(() => { - contacts.addObject(store.createRecord('user', { id: '8' })); + contacts.push(store.createRecord('user', { id: '8' })); }); assert.deepEqual( @@ -391,9 +392,9 @@ module('integration/relationships/has_many - Has-Many Relationships', function ( ); run(() => { - contacts.addObject(store.createRecord('user', { id: '5' })); - contacts.addObject(store.createRecord('user', { id: '6' })); - contacts.addObject(store.createRecord('user', { id: '7' })); + contacts.push(store.createRecord('user', { id: '5' })); + contacts.push(store.createRecord('user', { id: '6' })); + contacts.push(store.createRecord('user', { id: '7' })); }); assert.deepEqual( @@ -412,7 +413,7 @@ module('integration/relationships/has_many - Has-Many Relationships', function ( ); assert.strictEqual(contacts, user.contacts); - contacts.addObject(store.createRecord('user', { id: '8' })); + contacts.push(store.createRecord('user', { id: '8' })); assert.deepEqual( contacts.map((c) => c.id), ['3', '4', '5', '7', '8'], @@ -542,7 +543,7 @@ module('integration/relationships/has_many - Has-Many Relationships', function ( .then((comments) => { assert.true(comments.isLoaded, 'comments are loaded'); assert.strictEqual(comments.length, 2, 'comments have 2 length'); - assert.strictEqual(comments.objectAt(0).body, 'First', 'comment loaded successfully'); + assert.strictEqual(comments.at(0).body, 'First', 'comment loaded successfully'); }); }); }); @@ -1019,7 +1020,7 @@ module('integration/relationships/has_many - Has-Many Relationships', function ( return comments.reload(); }) .then(function (newComments) { - assert.strictEqual(newComments.firstObject.body, 'FirstUpdated', 'Record body was correctly updated'); + assert.strictEqual(newComments.at(0).body, 'FirstUpdated', 'Record body was correctly updated'); }); }); }); @@ -1097,7 +1098,7 @@ module('integration/relationships/has_many - Has-Many Relationships', function ( return comments.reload(); }) .then(function (newComments) { - assert.strictEqual(newComments.firstObject.body, 'FirstUpdated', 'Record body was correctly updated'); + assert.strictEqual(newComments.at(0).body, 'FirstUpdated', 'Record body was correctly updated'); }); }); }); @@ -1239,7 +1240,7 @@ module('integration/relationships/has_many - Has-Many Relationships', function ( return post.comments.reload().then(function (comments) { assert.true(comments.isLoaded, 'comments are loaded'); assert.strictEqual(comments.length, 2, 'comments have 2 length'); - assert.strictEqual(comments.firstObject.body, 'FirstUpdated', 'Record body was correctly updated'); + assert.strictEqual(comments.at(0).body, 'FirstUpdated', 'Record body was correctly updated'); }); }); }); @@ -1366,7 +1367,7 @@ module('integration/relationships/has_many - Has-Many Relationships', function ( return post.comments.reload().then(function (comments) { assert.true(comments.isLoaded, 'comments are loaded'); assert.strictEqual(comments.length, 2, 'comments have 2 length'); - assert.strictEqual(comments.firstObject.body, 'FirstUpdated', 'Record body was correctly updated'); + assert.strictEqual(comments.at(0).body, 'FirstUpdated', 'Record body was correctly updated'); }); }); }); @@ -1572,7 +1573,7 @@ module('integration/relationships/has_many - Has-Many Relationships', function ( const comments = await post.comments; assert.true(comments.isLoaded, 'comments are loaded'); assert.strictEqual(comments.length, 2, 'comments have 2 length'); - assert.strictEqual(comments.objectAt(0).body, 'First', 'comment 1 successfully loaded'); + assert.strictEqual(comments.at(0).body, 'First', 'comment 1 successfully loaded'); store.push({ data: { type: 'post', @@ -1589,7 +1590,7 @@ module('integration/relationships/has_many - Has-Many Relationships', function ( const newComments = await post.comments; assert.strictEqual(comments, newComments, 'hasMany array was kept the same'); assert.strictEqual(newComments.length, 3, 'comments updated successfully'); - assert.strictEqual(newComments.objectAt(0).body, 'Third', 'third comment loaded successfully'); + assert.strictEqual(newComments.at(0).body, 'Third', 'third comment loaded successfully'); }); test("When a polymorphic hasMany relationship is accessed, the adapter's findMany method should not be called if all the records in the relationship are already loaded", function (assert) { @@ -1697,16 +1698,14 @@ module('integration/relationships/has_many - Has-Many Relationships', function ( let store = this.owner.lookup('service:store'); - run(function () { - let igor = store.createRecord('user', { name: 'Igor' }); - let comment = store.createRecord('comment', { - body: 'Well I thought the title was fine', - }); + let igor = store.createRecord('user', { name: 'Igor' }); + let comment = store.createRecord('comment', { + body: 'Well I thought the title was fine', + }); - igor.messages.addObject(comment); + igor.messages.push(comment); - assert.strictEqual(igor.get('messages.firstObject.body'), 'Well I thought the title was fine'); - }); + assert.strictEqual(igor.messages.at(0)?.body, 'Well I thought the title was fine'); }); deprecatedTest( @@ -1955,7 +1954,7 @@ module('integration/relationships/has_many - Has-Many Relationships', function ( run(function () { all([store.findRecord('post', 1), store.findRecord('post', 2)]).then(function (records) { assert.expectAssertion(function () { - records[0].comments.pushObject(records[1]); + records[0].comments.push(records[1]); }, /The 'post' type does not implement 'comment' and thus cannot be assigned to the 'comments' relationship in 'post'/); }); }); @@ -2026,12 +2025,12 @@ module('integration/relationships/has_many - Has-Many Relationships', function ( return hash(records); }) .then(function (records) { - records.messages.pushObject(records.post); - records.messages.pushObject(records.comment); + records.messages.push(records.post); + records.messages.push(records.comment); assert.strictEqual(records.messages.length, 2, 'The messages are correctly added'); assert.expectAssertion(function () { - records.messages.pushObject(records.anotherUser); + records.messages.push(records.anotherUser); }, /The 'user' type does not implement 'message' and thus cannot be assigned to the 'messages' relationship in 'user'. Make it a descendant of 'message'/); }); }); @@ -2069,11 +2068,11 @@ module('integration/relationships/has_many - Has-Many Relationships', function ( assert.strictEqual(messages.length, 1, 'The user has 1 message'); - let removedObject = messages.popObject(); + let removedObject = messages.pop(); assert.strictEqual(removedObject, comment, 'The message is correctly removed'); assert.strictEqual(messages.length, 0, 'The user does not have any messages'); - assert.strictEqual(messages.objectAt(0), undefined, "Null messages can't be fetched"); + assert.strictEqual(messages.at(0), undefined, "Null messages can't be fetched"); }); test('When a record is created on the client, its hasMany arrays should be in a loaded state', function (assert) { @@ -2152,7 +2151,7 @@ module('integration/relationships/has_many - Has-Many Relationships', function ( }, ], }); - post.set('comments', store.peekAll('comment').toArray()); + post.set('comments', store.peekAll('comment').slice()); }); assert.strictEqual(get(post, 'comments.length'), 2, 'we can set HM relationship'); @@ -2200,7 +2199,7 @@ module('integration/relationships/has_many - Has-Many Relationships', function ( }, ], }); - post.set('comments', store.peekAll('comment').toArray()); + post.set('comments', store.peekAll('comment').slice()); }); return post.comments.then((comments) => { @@ -2222,7 +2221,7 @@ module('integration/relationships/has_many - Has-Many Relationships', function ( return run(() => { post = store.createRecord('post'); comment = store.createRecord('comment'); - post.comments.pushObject(comment); + post.comments.push(comment); return post.save(); }).then(() => { assert.strictEqual(get(post, 'comments.length'), 1, "The unsaved comment should be in the post's comments array"); @@ -2354,7 +2353,7 @@ module('integration/relationships/has_many - Has-Many Relationships', function ( let fetchedComments = await post.comments; assert.strictEqual(fetchedComments.length, 2, 'comments fetched successfully'); - assert.strictEqual(fetchedComments.objectAt(0).body, 'first', 'first comment loaded successfully'); + assert.strictEqual(fetchedComments.at(0).body, 'first', 'first comment loaded successfully'); store.push({ data: { @@ -2375,7 +2374,7 @@ module('integration/relationships/has_many - Has-Many Relationships', function ( let newlyFetchedComments = await post.comments; assert.strictEqual(newlyFetchedComments.length, 3, 'all three comments fetched successfully'); - assert.strictEqual(newlyFetchedComments.objectAt(2).body, 'third', 'third comment loaded successfully'); + assert.strictEqual(newlyFetchedComments.at(2).body, 'third', 'third comment loaded successfully'); }); testInDebug('A sync hasMany errors out if there are unloaded records in it', function (assert) { @@ -2433,7 +2432,7 @@ module('integration/relationships/has_many - Has-Many Relationships', function ( }); const comments = await post.comments; - comments.pushObject(comment); + comments.push(comment); assert.ok(post.comments.length, 1, 'expected length for comments'); }); @@ -2455,8 +2454,8 @@ module('integration/relationships/has_many - Has-Many Relationships', function ( }); const post = store.peekRecord('post', 1); const comments = post.comments; - const comment = comments.objectAt(0); - comments.removeObject(comment); + const comment = comments.at(0); + comments.splice(0, 1); store.unloadRecord(comment); assert.strictEqual(comments.length, 0); return post; @@ -2517,7 +2516,7 @@ module('integration/relationships/has_many - Has-Many Relationships', function ( }); post = store.peekRecord('post', 1); - assert.deepEqual(post.comments.toArray(), [comment1, comment2], 'Initial ordering is correct'); + assert.deepEqual(post.comments.slice(), [comment1, comment2], 'Initial ordering is correct'); }); run(() => { @@ -2536,7 +2535,7 @@ module('integration/relationships/has_many - Has-Many Relationships', function ( }, }); }); - assert.deepEqual(post.comments.toArray(), [comment2, comment1], 'Updated ordering is correct'); + assert.deepEqual(post.comments.slice(), [comment2, comment1], 'Updated ordering is correct'); run(() => { store.push({ @@ -2551,7 +2550,7 @@ module('integration/relationships/has_many - Has-Many Relationships', function ( }, }); }); - assert.deepEqual(post.comments.toArray(), [comment2], 'Updated ordering is correct'); + assert.deepEqual(post.comments.slice(), [comment2], 'Updated ordering is correct'); run(() => { store.push({ @@ -2571,7 +2570,7 @@ module('integration/relationships/has_many - Has-Many Relationships', function ( }, }); }); - assert.deepEqual(post.comments.toArray(), [comment1, comment2, comment3, comment4], 'Updated ordering is correct'); + assert.deepEqual(post.comments.slice(), [comment1, comment2, comment3, comment4], 'Updated ordering is correct'); run(() => { store.push({ @@ -2589,7 +2588,7 @@ module('integration/relationships/has_many - Has-Many Relationships', function ( }, }); }); - assert.deepEqual(post.comments.toArray(), [comment4, comment3], 'Updated ordering is correct'); + assert.deepEqual(post.comments.slice(), [comment4, comment3], 'Updated ordering is correct'); run(() => { store.push({ @@ -2610,7 +2609,7 @@ module('integration/relationships/has_many - Has-Many Relationships', function ( }); }); - assert.deepEqual(post.comments.toArray(), [comment4, comment2, comment3, comment1], 'Updated ordering is correct'); + assert.deepEqual(post.comments.slice(), [comment4, comment2, comment3, comment1], 'Updated ordering is correct'); }); test('Rollbacking attributes for deleted record restores implicit relationship correctly when the hasMany side has been deleted - async', async function (assert) { @@ -2644,7 +2643,7 @@ module('integration/relationships/has_many - Has-Many Relationships', function ( chapter.rollbackAttributes(); const fetchedChapters = await book.chapters; - assert.strictEqual(fetchedChapters.objectAt(0), chapter, 'Book has a chapter after rollback attributes'); + assert.strictEqual(fetchedChapters.at(0), chapter, 'Book has a chapter after rollback attributes'); }); test('Rollbacking attributes for deleted record restores implicit relationship correctly when the hasMany side has been deleted - sync', async function (assert) { @@ -2677,7 +2676,7 @@ module('integration/relationships/has_many - Has-Many Relationships', function ( page.deleteRecord(); page.rollbackAttributes(); - assert.strictEqual(chapter.pages.firstObject, page, 'Chapter has a page after rollback attributes'); + assert.strictEqual(chapter.pages.at(0), page, 'Chapter has a page after rollback attributes'); }); test('Rollbacking attributes for deleted record restores implicit relationship correctly when the belongsTo side has been deleted - async', function (assert) { @@ -2824,7 +2823,7 @@ module('integration/relationships/has_many - Has-Many Relationships', function ( chapter.pages.addArrayObserver(this, { willChange(pages, index, removeCount, addCount) { if (observe) { - assert.strictEqual(pages.objectAt(index), page2, 'page2 is passed to willChange'); + assert.strictEqual(pages.at(index), page2, 'page2 is passed to willChange'); } }, didChange(pages, index, removeCount, addCount) { @@ -2897,7 +2896,7 @@ module('integration/relationships/has_many - Has-Many Relationships', function ( }, didChange(pages, index, removeCount, addCount) { if (observe) { - assert.strictEqual(pages.objectAt(index), page2, 'page2 is passed to didChange'); + assert.strictEqual(pages.at(index), page2, 'page2 is passed to didChange'); } }, }); @@ -2998,12 +2997,24 @@ module('integration/relationships/has_many - Has-Many Relationships', function ( }); const comments = store.peekAll('comment'); - assert.deepEqual(comments.mapBy('post.id'), ['2', '2', '2']); + assert.deepEqual( + comments.map((comment) => comment.post.id), + ['2', '2', '2'] + ); const postComments = await post.comments; - postComments.clear(); - assert.deepEqual(comments.mapBy('post'), [null, null, null]); + if (DEPRECATE_ARRAY_LIKE) { + postComments.clear(); + assert.expectDeprecation({ id: 'ember-data:deprecate-array-like' }); + } else { + postComments.length = 0; + } + + assert.deepEqual( + comments.map((comment) => comment.post), + [null, null, null] + ); }); test('unloading a record with associated records does not prevent the store from tearing down', function (assert) { @@ -3165,21 +3176,21 @@ module('integration/relationships/has_many - Has-Many Relationships', function ( // Add comment #4 let comment = store.createRecord('comment'); - comments.addObject(comment); + comments.push(comment); await comment.save(); commentsPromiseArray = post.comments; assert.strictEqual(commentsPromiseArray.length, 4, 'Comments count after first add'); // Delete comment #4 - await comments.lastObject.destroyRecord(); + await comments.at(-1).destroyRecord(); commentsPromiseArray = post.comments; assert.strictEqual(commentsPromiseArray.length, 3, 'Comments count after destroy'); // Add another comment #4 comment = store.createRecord('comment'); - comments.addObject(comment); + comments.push(comment); await comment.save(); commentsPromiseArray = post.comments; @@ -3428,7 +3439,7 @@ module('integration/relationships/has_many - Has-Many Relationships', function ( }); }); - test('metadata is accessible when return from a fetchLink', function (assert) { + test('metadata is accessible when return from a fetchLink', async function (assert) { assert.expect(1); this.owner.register('serializer:application', RESTSerializer); @@ -3445,34 +3456,26 @@ module('integration/relationships/has_many - Has-Many Relationships', function ( }); }; - let book; - - run(() => { - store.push({ - data: { - type: 'book', - id: '1', - attributes: { - title: 'Sailing the Seven Seas', - }, - relationships: { - chapters: { - links: { - related: '/chapters', - }, + const book = store.push({ + data: { + type: 'book', + id: '1', + attributes: { + title: 'Sailing the Seven Seas', + }, + relationships: { + chapters: { + links: { + related: '/chapters', }, }, }, - }); - book = store.peekRecord('book', 1); + }, }); + const chapters = await book.chapters; - return run(() => { - return book.chapters.then((chapters) => { - let meta = chapters.meta; - assert.strictEqual(get(meta, 'foo'), 'bar', 'metadata is available'); - }); - }); + let meta = chapters.meta; + assert.strictEqual(meta?.foo, 'bar', 'metadata is available'); }); test('metadata should be reset between requests', function (assert) { @@ -3619,7 +3622,7 @@ module('integration/relationships/has_many - Has-Many Relationships', function ( }); return post.comments.then((comments) => { - assert.strictEqual(comments.firstObject.body, 'This is comment', 'comment body is correct'); + assert.strictEqual(comments.at(0).body, 'This is comment', 'comment body is correct'); }); }); }); @@ -3690,7 +3693,7 @@ module('integration/relationships/has_many - Has-Many Relationships', function ( }); return post.comments.then((comments) => { - assert.strictEqual(comments.firstObject.body, 'This is comment', 'comment body is correct'); + assert.strictEqual(comments.at(0).body, 'This is comment', 'comment body is correct'); }); }); }); @@ -3758,7 +3761,7 @@ module('integration/relationships/has_many - Has-Many Relationships', function ( }); return post.comments.then((comments) => { - assert.strictEqual(comments.firstObject.body, 'This is comment', 'comment body is correct'); + assert.strictEqual(comments.at(0).body, 'This is comment', 'comment body is correct'); }); }); }); @@ -3845,7 +3848,7 @@ module('integration/relationships/has_many - Has-Many Relationships', function ( }); return post.comments.then((comments) => { - assert.strictEqual(comments.firstObject.body, 'This is comment fetched by link', 'comment body is correct'); + assert.strictEqual(comments.at(0).body, 'This is comment fetched by link', 'comment body is correct'); }); }); }); @@ -3916,7 +3919,7 @@ module('integration/relationships/has_many - Has-Many Relationships', function ( }); return post.comments.then((comments) => { - assert.strictEqual(comments.firstObject.body, 'This is updated comment', 'comment body is correct'); + assert.strictEqual(comments.at(0).body, 'This is updated comment', 'comment body is correct'); }); }); }); @@ -4074,66 +4077,59 @@ module('integration/relationships/has_many - Has-Many Relationships', function ( test('unloading and reloading a record with hasMany relationship - #3084', function (assert) { let store = this.owner.lookup('service:store'); - let user; - let message; - - run(() => { - store.push({ - data: [ - { - type: 'user', - id: 'user-1', - attributes: { - name: 'Adolfo Builes', - }, - relationships: { - messages: { - data: [{ type: 'message', id: 'message-1' }], - }, - }, + store.push({ + data: [ + { + type: 'user', + id: 'user-1', + attributes: { + name: 'Adolfo Builes', }, - { - type: 'message', - id: 'message-1', + relationships: { + messages: { + data: [{ type: 'message', id: 'message-1' }], + }, }, - ], - }); + }, + { + type: 'message', + id: 'message-1', + }, + ], + }); - user = store.peekRecord('user', 'user-1'); - message = store.peekRecord('message', 'message-1'); + let user = store.peekRecord('user', 'user-1'); + let message = store.peekRecord('message', 'message-1'); - assert.strictEqual(get(user, 'messages.firstObject.id'), 'message-1'); - assert.strictEqual(get(message, 'user.id'), 'user-1'); - }); + assert.strictEqual(user.messages.at(0).id, 'message-1'); + assert.strictEqual(message.user.id, 'user-1'); run(() => { store.unloadRecord(user); }); - run(() => { - // The record is resurrected for some reason. - store.push({ - data: [ - { - type: 'user', - id: 'user-1', - attributes: { - name: 'Adolfo Builes', - }, - relationships: { - messages: { - data: [{ type: 'message', id: 'message-1' }], - }, + // The record is resurrected for some reason. + store.push({ + data: [ + { + type: 'user', + id: 'user-1', + attributes: { + name: 'Adolfo Builes', + }, + relationships: { + messages: { + data: [{ type: 'message', id: 'message-1' }], }, }, - ], - }); + }, + ], + }); - user = store.peekRecord('user', 'user-1'); + user = store.peekRecord('user', 'user-1'); - assert.strictEqual(get(user, 'messages.firstObject.id'), 'message-1', 'user points to message'); - assert.strictEqual(get(message, 'user.id'), 'user-1', 'message points to user'); - }); + assert.strictEqual(user.messages.at(0).id, 'message-1', 'user points to message'); + assert.strictEqual(message.user.id, 'user-1', 'message points to user'); }); test('deleted records should stay deleted', function (assert) { @@ -4201,7 +4197,7 @@ module('integration/relationships/has_many - Has-Many Relationships', function ( }); assert.deepEqual( - get(user, 'messages').mapBy('id'), + get(user, 'messages').map((r) => r.id), ['message-2', 'message-3'], 'user should have 2 message since 1 was deleted' ); diff --git a/packages/-ember-data/tests/integration/relationships/inverse-relationship-load-test.js b/packages/-ember-data/tests/integration/relationships/inverse-relationship-load-test.js index 4ce0fdae8ec..26a2535845b 100644 --- a/packages/-ember-data/tests/integration/relationships/inverse-relationship-load-test.js +++ b/packages/-ember-data/tests/integration/relationships/inverse-relationship-load-test.js @@ -97,14 +97,14 @@ module('inverse relationship load test', function (hooks) { assert.false(person.hasMany('dogs').hasManyRelationship.state.isEmpty, 'relationship state was set up correctly'); assert.strictEqual(dogs.length, 2, 'hasMany relationship has correct number of records'); - let dog1 = dogs.firstObject; + let dog1 = dogs.at(0); let dogPerson1 = await dog1.person; assert.strictEqual( dogPerson1.id, '1', 'dog.person inverse relationship is set up correctly when adapter does not include parent relationships in data.relationships' ); - let dogPerson2 = await dogs.objectAt(1).person; + let dogPerson2 = await dogs.at(1).person; assert.strictEqual( dogPerson2.id, '1', @@ -113,7 +113,7 @@ module('inverse relationship load test', function (hooks) { await dog1.destroyRecord(); assert.strictEqual(dogs.length, 1, 'record removed from hasMany relationship after deletion'); - assert.strictEqual(dogs.firstObject.id, '2', 'hasMany relationship has correct records'); + assert.strictEqual(dogs.at(0).id, '2', 'hasMany relationship has correct records'); } ); @@ -187,14 +187,14 @@ module('inverse relationship load test', function (hooks) { assert.false(person.hasMany('dogs').hasManyRelationship.state.isEmpty, 'relationship state was set up correctly'); assert.strictEqual(dogs.length, 2, 'hasMany relationship has correct number of records'); - let dog1 = dogs.firstObject; + let dog1 = dogs.at(0); let dogPerson1 = await dog1.person; assert.strictEqual( dogPerson1.id, '1', 'dog.person inverse relationship is set up correctly when adapter does not include parent relationships in data.relationships' ); - let dogPerson2 = await dogs.objectAt(1).person; + let dogPerson2 = await dogs.at(1).person; assert.strictEqual( dogPerson2.id, '1', @@ -203,7 +203,7 @@ module('inverse relationship load test', function (hooks) { await dog1.destroyRecord(); assert.strictEqual(dogs.length, 1, 'record removed from hasMany relationship after deletion'); - assert.strictEqual(dogs.firstObject.id, '2', 'hasMany relationship has correct records'); + assert.strictEqual(dogs.at(0).id, '2', 'hasMany relationship has correct records'); } ); @@ -276,14 +276,14 @@ module('inverse relationship load test', function (hooks) { assert.false(person.hasMany('dogs').hasManyRelationship.state.isEmpty, 'relationship state was set up correctly'); assert.strictEqual(dogs.length, 2, 'hasMany relationship has correct number of records'); - let dog1 = dogs.firstObject; + let dog1 = dogs.at(0); let dogPerson1 = await dog1.pal; assert.strictEqual( dogPerson1.id, '1', 'dog.person inverse relationship is set up correctly when adapter does not include parent relationships in data.relationships' ); - let dogPerson2 = await dogs.objectAt(1).pal; + let dogPerson2 = await dogs.at(1).pal; assert.strictEqual( dogPerson2.id, '1', @@ -292,7 +292,7 @@ module('inverse relationship load test', function (hooks) { await dog1.destroyRecord(); assert.strictEqual(dogs.length, 1, 'record removed from hasMany relationship after deletion'); - assert.strictEqual(dogs.firstObject.id, '2', 'hasMany relationship has correct records'); + assert.strictEqual(dogs.at(0).id, '2', 'hasMany relationship has correct records'); }); test('one-to-many (left hand async, right hand sync) - findHasMany/explicit inverse - adds parent relationship information to the payload if it is not included/added by the serializer', async function (assert) { @@ -364,14 +364,14 @@ module('inverse relationship load test', function (hooks) { assert.false(person.hasMany('dogs').hasManyRelationship.state.isEmpty, 'relationship state was set up correctly'); assert.strictEqual(dogs.length, 2, 'hasMany relationship has correct number of records'); - let dog1 = dogs.firstObject; + let dog1 = dogs.at(0); let dogPerson1 = await dog1.pal; assert.strictEqual( dogPerson1.id, '1', 'dog.person inverse relationship is set up correctly when adapter does not include parent relationships in data.relationships' ); - let dogPerson2 = await dogs.objectAt(1).pal; + let dogPerson2 = await dogs.at(1).pal; assert.strictEqual( dogPerson2.id, '1', @@ -380,7 +380,7 @@ module('inverse relationship load test', function (hooks) { await dog1.destroyRecord(); assert.strictEqual(dogs.length, 1, 'record removed from hasMany relationship after deletion'); - assert.strictEqual(dogs.firstObject.id, '2', 'hasMany relationship has correct records'); + assert.strictEqual(dogs.at(0).id, '2', 'hasMany relationship has correct records'); }); test('one-to-many - findHasMany/null inverse - adds parent relationship information to the payload if it is not included/added by the serializer', async function (assert) { @@ -455,12 +455,15 @@ module('inverse relationship load test', function (hooks) { let dogs = await person.dogs; assert.false(person.hasMany('dogs').hasManyRelationship.state.isEmpty); assert.strictEqual(dogs.length, 2); - assert.deepEqual(dogs.mapBy('id'), ['1', '2']); + assert.deepEqual( + dogs.map((r) => r.id), + ['1', '2'] + ); - let dog1 = dogs.firstObject; + let dog1 = dogs.at(0); await dog1.destroyRecord(); assert.strictEqual(dogs.length, 1); - assert.strictEqual(dogs.firstObject.id, '2'); + assert.strictEqual(dogs.at(0).id, '2'); }); deprecatedTest( @@ -893,18 +896,18 @@ module('inverse relationship load test', function (hooks) { assert.false(person.hasMany('dogs').hasManyRelationship.state.isEmpty); assert.strictEqual(dogs.length, 2, 'left hand side relationship is set up with correct number of records'); - let [dog1, dog2] = dogs.toArray(); + let [dog1, dog2] = dogs.slice(); let dog1Walkers = await dog1.walkers; assert.strictEqual(dog1Walkers.length, 1, 'dog1.walkers inverse relationship includes correct number of records'); - assert.strictEqual(dog1Walkers.firstObject.id, '1', 'dog1.walkers inverse relationship is set up correctly'); + assert.strictEqual(dog1Walkers.at(0).id, '1', 'dog1.walkers inverse relationship is set up correctly'); let dog2Walkers = await dog2.walkers; assert.strictEqual(dog2Walkers.length, 1, 'dog2.walkers inverse relationship includes correct number of records'); - assert.strictEqual(dog2Walkers.firstObject.id, '1', 'dog2.walkers inverse relationship is set up correctly'); + assert.strictEqual(dog2Walkers.at(0).id, '1', 'dog2.walkers inverse relationship is set up correctly'); await dog1.destroyRecord(); assert.strictEqual(dogs.length, 1, 'person.dogs relationship was updated when record removed'); - assert.strictEqual(dogs.firstObject.id, '2', 'person.dogs relationship has the correct records'); + assert.strictEqual(dogs.at(0).id, '2', 'person.dogs relationship has the correct records'); } ); @@ -978,18 +981,18 @@ module('inverse relationship load test', function (hooks) { assert.false(person.hasMany('dogs').hasManyRelationship.state.isEmpty); assert.strictEqual(dogs.length, 2, 'left hand side relationship is set up with correct number of records'); - let [dog1, dog2] = dogs.toArray(); + let [dog1, dog2] = dogs.slice(); let dog1Walkers = await dog1.walkers; assert.strictEqual(dog1Walkers.length, 1, 'dog1.walkers inverse relationship includes correct number of records'); - assert.strictEqual(dog1Walkers.firstObject.id, '1', 'dog1.walkers inverse relationship is set up correctly'); + assert.strictEqual(dog1Walkers.at(0).id, '1', 'dog1.walkers inverse relationship is set up correctly'); let dog2Walkers = await dog2.walkers; assert.strictEqual(dog2Walkers.length, 1, 'dog2.walkers inverse relationship includes correct number of records'); - assert.strictEqual(dog2Walkers.firstObject.id, '1', 'dog2.walkers inverse relationship is set up correctly'); + assert.strictEqual(dog2Walkers.at(0).id, '1', 'dog2.walkers inverse relationship is set up correctly'); await dog1.destroyRecord(); assert.strictEqual(dogs.length, 1, 'person.dogs relationship was updated when record removed'); - assert.strictEqual(dogs.firstObject.id, '2', 'person.dogs relationship has the correct records'); + assert.strictEqual(dogs.at(0).id, '2', 'person.dogs relationship has the correct records'); } ); @@ -1062,18 +1065,18 @@ module('inverse relationship load test', function (hooks) { assert.false(person.hasMany('dogs').hasManyRelationship.state.isEmpty); assert.strictEqual(dogs.length, 2, 'left hand side relationship is set up with correct number of records'); - let [dog1, dog2] = dogs.toArray(); + let [dog1, dog2] = dogs.slice(); let dog1Pals = await dog1.pals; assert.strictEqual(dog1Pals.length, 1, 'dog1.pals inverse relationship includes correct number of records'); - assert.strictEqual(dog1Pals.firstObject.id, '1', 'dog1.pals inverse relationship is set up correctly'); + assert.strictEqual(dog1Pals.at(0).id, '1', 'dog1.pals inverse relationship is set up correctly'); let dog2Pals = await dog2.pals; assert.strictEqual(dog2Pals.length, 1, 'dog2.pals inverse relationship includes correct number of records'); - assert.strictEqual(dog2Pals.firstObject.id, '1', 'dog2.pals inverse relationship is set up correctly'); + assert.strictEqual(dog2Pals.at(0).id, '1', 'dog2.pals inverse relationship is set up correctly'); await dog1.destroyRecord(); assert.strictEqual(dogs.length, 1, 'person.dogs relationship was updated when record removed'); - assert.strictEqual(dogs.firstObject.id, '2', 'person.dogs relationship has the correct records'); + assert.strictEqual(dogs.at(0).id, '2', 'person.dogs relationship has the correct records'); }); test('many-to-many (left hand async, right hand sync) - findHasMany/explicit inverse - adds parent relationship information to the payload if it is not included/added by the serializer', async function (assert) { @@ -1145,18 +1148,18 @@ module('inverse relationship load test', function (hooks) { assert.false(person.hasMany('dogs').hasManyRelationship.state.isEmpty); assert.strictEqual(dogs.length, 2, 'left hand side relationship is set up with correct number of records'); - let [dog1, dog2] = dogs.toArray(); + let [dog1, dog2] = dogs.slice(); let dog1Pals = await dog1.pals; assert.strictEqual(dog1Pals.length, 1, 'dog1.pals inverse relationship includes correct number of records'); - assert.strictEqual(dog1Pals.firstObject.id, '1', 'dog1.pals inverse relationship is set up correctly'); + assert.strictEqual(dog1Pals.at(0).id, '1', 'dog1.pals inverse relationship is set up correctly'); let dog2Pals = await dog2.pals; assert.strictEqual(dog2Pals.length, 1, 'dog2.pals inverse relationship includes correct number of records'); - assert.strictEqual(dog2Pals.firstObject.id, '1', 'dog2.pals inverse relationship is set up correctly'); + assert.strictEqual(dog2Pals.at(0).id, '1', 'dog2.pals inverse relationship is set up correctly'); await dog1.destroyRecord(); assert.strictEqual(dogs.length, 1, 'person.dogs relationship was updated when record removed'); - assert.strictEqual(dogs.firstObject.id, '2', 'person.dogs relationship has the correct records'); + assert.strictEqual(dogs.at(0).id, '2', 'person.dogs relationship has the correct records'); }); deprecatedTest( @@ -1226,7 +1229,7 @@ module('inverse relationship load test', function (hooks) { let dogs = await person.dogs; assert.strictEqual(dogs.length, 1, 'person.dogs inverse relationship includes correct number of records'); - let [dog1] = dogs.toArray(); + let [dog1] = dogs.slice(); assert.strictEqual(dog1.id, '1', 'dog1.person inverse relationship is set up correctly'); await person.destroyRecord(); @@ -1302,7 +1305,7 @@ module('inverse relationship load test', function (hooks) { let dogs = await person.dogs; assert.strictEqual(dogs.length, 1, 'person.dogs inverse relationship includes correct number of records'); - let [dog1] = dogs.toArray(); + let [dog1] = dogs.slice(); assert.strictEqual(dog1.id, '1', 'dog1.person inverse relationship is set up correctly'); await person.destroyRecord(); @@ -1377,7 +1380,7 @@ module('inverse relationship load test', function (hooks) { let dogs = await person.dogs; assert.strictEqual(dogs.length, 1, 'person.dogs inverse relationship includes correct number of records'); - let [dog1] = dogs.toArray(); + let [dog1] = dogs.slice(); assert.strictEqual(dog1.id, '1', 'dog1.person inverse relationship is set up correctly'); await person.destroyRecord(); @@ -1451,7 +1454,7 @@ module('inverse relationship load test', function (hooks) { let dogs = await person.dogs; assert.strictEqual(dogs.length, 1, 'person.dogs inverse relationship includes correct number of records'); - let [dog1] = dogs.toArray(); + let [dog1] = dogs.slice(); assert.strictEqual(dog1.id, '1', 'dog1.person inverse relationship is set up correctly'); await person.destroyRecord(); @@ -2950,14 +2953,14 @@ module('inverse relationship load test', function (hooks) { assert.false(person.hasMany('dogs').hasManyRelationship.state.isEmpty, 'relationship state was set up correctly'); assert.strictEqual(dogs.length, 2, 'hasMany relationship has correct number of records'); - let dog1 = dogs.firstObject; + let dog1 = dogs.at(0); let dogPerson1 = await dog1.person; assert.strictEqual( dogPerson1.id, '1', 'dog.person inverse relationship is set up correctly when adapter does not include parent relationships in data.relationships' ); - let dogPerson2 = await dogs.objectAt(1).person; + let dogPerson2 = await dogs.at(1).person; assert.strictEqual( dogPerson2.id, '1', @@ -2966,7 +2969,7 @@ module('inverse relationship load test', function (hooks) { await dog1.destroyRecord(); assert.strictEqual(dogs.length, 1, 'record removed from hasMany relationship after deletion'); - assert.strictEqual(dogs.firstObject.id, '2', 'hasMany relationship has correct records'); + assert.strictEqual(dogs.at(0).id, '2', 'hasMany relationship has correct records'); } ); @@ -3049,14 +3052,14 @@ module('inverse relationship load test', function (hooks) { assert.false(person.hasMany('dogs').hasManyRelationship.state.isEmpty, 'relationship state was set up correctly'); assert.strictEqual(dogs.length, 2, 'hasMany relationship has correct number of records'); - let dog1 = dogs.firstObject; + let dog1 = dogs.at(0); let dogPerson1 = await dog1.person; assert.strictEqual( dogPerson1.id, '1', 'dog.person inverse relationship is set up correctly when adapter does not include parent relationships in data.relationships' ); - let dogPerson2 = await dogs.objectAt(1).person; + let dogPerson2 = await dogs.at(1).person; assert.strictEqual( dogPerson2.id, '1', @@ -3065,7 +3068,7 @@ module('inverse relationship load test', function (hooks) { await dog1.destroyRecord(); assert.strictEqual(dogs.length, 1, 'record removed from hasMany relationship after deletion'); - assert.strictEqual(dogs.firstObject.id, '2', 'hasMany relationship has correct records'); + assert.strictEqual(dogs.at(0).id, '2', 'hasMany relationship has correct records'); } ); @@ -3147,14 +3150,14 @@ module('inverse relationship load test', function (hooks) { assert.false(person.hasMany('dogs').hasManyRelationship.state.isEmpty, 'relationship state was set up correctly'); assert.strictEqual(dogs.length, 2, 'hasMany relationship has correct number of records'); - let dog1 = dogs.firstObject; + let dog1 = dogs.at(0); let dogPerson1 = await dog1.pal; assert.strictEqual( dogPerson1.id, '1', 'dog.person inverse relationship is set up correctly when adapter does not include parent relationships in data.relationships' ); - let dogPerson2 = await dogs.objectAt(1).pal; + let dogPerson2 = await dogs.at(1).pal; assert.strictEqual( dogPerson2.id, '1', @@ -3163,7 +3166,7 @@ module('inverse relationship load test', function (hooks) { await dog1.destroyRecord(); assert.strictEqual(dogs.length, 1, 'record removed from hasMany relationship after deletion'); - assert.strictEqual(dogs.firstObject.id, '2', 'hasMany relationship has correct records'); + assert.strictEqual(dogs.at(0).id, '2', 'hasMany relationship has correct records'); }); test('one-to-many (left hand async, right hand sync) - ids/non-link/explicit inverse - ids - records loaded through ids/findRecord are linked to the parent if the response from the server does not include relationship information', async function (assert) { @@ -3244,14 +3247,14 @@ module('inverse relationship load test', function (hooks) { assert.false(person.hasMany('dogs').hasManyRelationship.state.isEmpty, 'relationship state was set up correctly'); assert.strictEqual(dogs.length, 2, 'hasMany relationship has correct number of records'); - let dog1 = dogs.firstObject; + let dog1 = dogs.at(0); let dogPerson1 = await dog1.pal; assert.strictEqual( dogPerson1.id, '1', 'dog.person inverse relationship is set up correctly when adapter does not include parent relationships in data.relationships' ); - let dogPerson2 = await dogs.objectAt(1).pal; + let dogPerson2 = await dogs.at(1).pal; assert.strictEqual( dogPerson2.id, '1', @@ -3260,7 +3263,7 @@ module('inverse relationship load test', function (hooks) { await dog1.destroyRecord(); assert.strictEqual(dogs.length, 1, 'record removed from hasMany relationship after deletion'); - assert.strictEqual(dogs.firstObject.id, '2', 'hasMany relationship has correct records'); + assert.strictEqual(dogs.at(0).id, '2', 'hasMany relationship has correct records'); }); test('one-to-many - ids/non-link/null inverse - ids - records loaded through ids/findRecord are linked to the parent if the response from the server does not include relationship information', async function (assert) { @@ -3335,11 +3338,11 @@ module('inverse relationship load test', function (hooks) { assert.false(person.hasMany('dogs').hasManyRelationship.state.isEmpty, 'relationship state was set up correctly'); assert.strictEqual(dogs.length, 2, 'hasMany relationship has correct number of records'); - let dog1 = dogs.firstObject; + let dog1 = dogs.at(0); await dog1.destroyRecord(); assert.strictEqual(dogs.length, 1, 'record removed from hasMany relationship after deletion'); - assert.strictEqual(dogs.firstObject.id, '2', 'hasMany relationship has correct records'); + assert.strictEqual(dogs.at(0).id, '2', 'hasMany relationship has correct records'); }); deprecatedTest( @@ -3455,7 +3458,7 @@ module('inverse relationship load test', function (hooks) { 'hasMany relationship on specified record has correct number of associated records' ); - let allDogs = store.peekAll('dogs').toArray(); + let allDogs = store.peekAll('dogs').slice(); for (let i = 0; i < allDogs.length; i++) { let dog = allDogs[i]; let dogPerson = await dog.person; @@ -3465,7 +3468,7 @@ module('inverse relationship load test', function (hooks) { let dog1 = store.peekRecord('dog', '1'); await dog1.destroyRecord(); assert.strictEqual(person2Dogs.length, 1, 'record removed from hasMany relationship after deletion'); - assert.strictEqual(person2Dogs.firstObject.id, '2', 'hasMany relationship has correct records'); + assert.strictEqual(person2Dogs.at(0).id, '2', 'hasMany relationship has correct records'); } ); @@ -3582,7 +3585,7 @@ module('inverse relationship load test', function (hooks) { 'hasMany relationship on specified record has correct number of associated records' ); - let allDogs = store.peekAll('dogs').toArray(); + let allDogs = store.peekAll('dogs').slice(); for (let i = 0; i < allDogs.length; i++) { let dog = allDogs[i]; let dogPerson = await dog.person; @@ -3592,7 +3595,7 @@ module('inverse relationship load test', function (hooks) { let dog1 = store.peekRecord('dog', '1'); await dog1.destroyRecord(); assert.strictEqual(person2Dogs.length, 1, 'record removed from hasMany relationship after deletion'); - assert.strictEqual(person2Dogs.firstObject.id, '2', 'hasMany relationship has correct records'); + assert.strictEqual(person2Dogs.at(0).id, '2', 'hasMany relationship has correct records'); } ); @@ -3686,7 +3689,7 @@ module('inverse relationship load test', function (hooks) { assert.strictEqual(personDogs.length, 0, 'hasMany relationship for parent is empty'); - let allDogs = store.peekAll('dogs').toArray(); + let allDogs = store.peekAll('dogs').slice(); for (let i = 0; i < allDogs.length; i++) { let dog = allDogs[i]; let dogPerson = await dog.person; @@ -3790,7 +3793,7 @@ module('inverse relationship load test', function (hooks) { assert.strictEqual(personDogs.length, 0, 'hasMany relationship for parent is empty'); - let allDogs = store.peekAll('dogs').toArray(); + let allDogs = store.peekAll('dogs').slice(); for (let i = 0; i < allDogs.length; i++) { let dog = allDogs[i]; let dogPerson = await dog.person; @@ -3916,7 +3919,7 @@ module('inverse relationship load test', function (hooks) { 'hasMany relationship on specified record has correct number of associated records' ); - let allDogs = store.peekAll('dogs').toArray(); + let allDogs = store.peekAll('dogs').slice(); for (let i = 0; i < allDogs.length; i++) { let dog = allDogs[i]; let dogPerson = await dog.pal; @@ -3926,7 +3929,7 @@ module('inverse relationship load test', function (hooks) { let dog1 = store.peekRecord('dog', '1'); await dog1.destroyRecord(); assert.strictEqual(pal2Dogs.length, 1, 'record removed from hasMany relationship after deletion'); - assert.strictEqual(pal2Dogs.firstObject.id, '2', 'hasMany relationship has correct records'); + assert.strictEqual(pal2Dogs.at(0).id, '2', 'hasMany relationship has correct records'); }); test('one-to-many (left hand async, right hand sync) - ids/non-link/explicit inverse - records loaded through ids/findRecord do not get associated with the parent if the server specifies another resource as the relationship value in the response', async function (assert) { @@ -4041,7 +4044,7 @@ module('inverse relationship load test', function (hooks) { 'hasMany relationship on specified record has correct number of associated records' ); - let allDogs = store.peekAll('dogs').toArray(); + let allDogs = store.peekAll('dogs').slice(); for (let i = 0; i < allDogs.length; i++) { let dog = allDogs[i]; let dogPerson = await dog.pal; @@ -4051,7 +4054,7 @@ module('inverse relationship load test', function (hooks) { let dog1 = store.peekRecord('dog', '1'); await dog1.destroyRecord(); assert.strictEqual(pal2Dogs.length, 1, 'record removed from hasMany relationship after deletion'); - assert.strictEqual(pal2Dogs.firstObject.id, '2', 'hasMany relationship has correct records'); + assert.strictEqual(pal2Dogs.at(0).id, '2', 'hasMany relationship has correct records'); }); test("loading belongsTo doesn't remove inverse relationship for other instances", async function (assert) { diff --git a/packages/-ember-data/tests/integration/relationships/inverse-relationships-test.js b/packages/-ember-data/tests/integration/relationships/inverse-relationships-test.js index 4aeee50941b..5a2a3853c3e 100644 --- a/packages/-ember-data/tests/integration/relationships/inverse-relationships-test.js +++ b/packages/-ember-data/tests/integration/relationships/inverse-relationships-test.js @@ -44,7 +44,7 @@ module('integration/relationships/inverse_relationships - Inverse Relationships' assert.strictEqual(comment.post, null, 'no post has been set on the comment'); - post.comments.pushObject(comment); + post.comments.push(comment); assert.strictEqual(comment.post, post, 'post was set on the comment'); }); @@ -129,7 +129,7 @@ module('integration/relationships/inverse_relationships - Inverse Relationships' assert.strictEqual(comment.redPost, null, 'redPost has not been set on the comment'); assert.strictEqual(comment.bluePost, null, 'bluePost has not been set on the comment'); - post.comments.pushObject(comment); + post.comments.push(comment); assert.strictEqual(comment.onePost, null, 'onePost has not been set on the comment'); assert.strictEqual(comment.twoPost, null, 'twoPost has not been set on the comment'); @@ -333,14 +333,14 @@ module('integration/relationships/inverse_relationships - Inverse Relationships' assert.strictEqual(post.redUser, null, 'redUser has not been set on the user'); assert.strictEqual(post.blueUser, null, 'blueUser has not been set on the user'); - user.messages.pushObject(post); + user.messages.push(post); assert.strictEqual(post.oneUser, null, 'oneUser has not been set on the user'); assert.strictEqual(post.twoUser, null, 'twoUser has not been set on the user'); assert.strictEqual(post.redUser, user, 'redUser has been set on the user'); assert.strictEqual(post.blueUser, null, 'blueUser has not been set on the user'); - user.messages.popObject(); + user.messages.pop(); assert.strictEqual(post.oneUser, null, 'oneUser has not been set on the user'); assert.strictEqual(post.twoUser, null, 'twoUser has not been set on the user'); @@ -676,7 +676,7 @@ module('integration/relationships/inverse_relationships - Inverse Relationships' const comment = store.createRecord('comment'); const post = store.createRecord('post'); const comments = await post.comments; - comments.pushObject(comment); + comments.push(comment); const identifier = recordIdentifierFor(comment); await comment.destroyRecord(); diff --git a/packages/-ember-data/tests/integration/relationships/json-api-links-test.js b/packages/-ember-data/tests/integration/relationships/json-api-links-test.js index 75b3d517f26..8b440b1065b 100644 --- a/packages/-ember-data/tests/integration/relationships/json-api-links-test.js +++ b/packages/-ember-data/tests/integration/relationships/json-api-links-test.js @@ -760,7 +760,7 @@ module('integration/relationship/json-api-links | Relationship fetching', functi assert.ok(!!pets, 'We found our pets'); if (!petRelDataWasEmpty) { - pets.objectAt(0).unloadRecord(); + pets.at(0).unloadRecord(); assert.strictEqual(pets.length, 0, 'we unloaded'); await user.pets; assert.strictEqual(pets.length, 1, 'we reloaded'); @@ -1049,7 +1049,7 @@ module('integration/relationship/json-api-links | Relationship fetching', functi assert.ok(!!pets, 'We found our pets'); - pets.objectAt(0).unloadRecord(); + pets.at(0).unloadRecord(); assert.strictEqual(pets.length, 0, 'we unloaded our pet'); await user.pets; assert.strictEqual(pets.length, 1, 'we have our pet again'); @@ -1434,7 +1434,7 @@ module('integration/relationship/json-api-links | Relationship fetching', functi let pets = await user.pets; assert.ok(!!pets, 'We found our pets'); - pets.objectAt(0).unloadRecord(); + pets.at(0).unloadRecord(); assert.strictEqual(pets.length, 0, 'we unloaded our pet'); await user.pets; assert.strictEqual(pets.length, 1, 'we reloaded our pet'); @@ -1703,7 +1703,7 @@ module('integration/relationship/json-api-links | Relationship fetching', functi assert.ok(!!pets, 'We found our pets'); assert.strictEqual(pets.length, 1, 'we loaded our pets'); - pets.objectAt(0).unloadRecord(); + pets.at(0).unloadRecord(); assert.strictEqual(pets.length, 0, 'we unloaded our pets'); // should trigger a findRecord for the unloaded pet diff --git a/packages/-ember-data/tests/integration/relationships/many-to-many-test.js b/packages/-ember-data/tests/integration/relationships/many-to-many-test.js index 6a50319ad71..9fd23c24d16 100644 --- a/packages/-ember-data/tests/integration/relationships/many-to-many-test.js +++ b/packages/-ember-data/tests/integration/relationships/many-to-many-test.js @@ -1,10 +1,9 @@ /*eslint no-unused-vars: ["error", { "varsIgnorePattern": "(ada)" }]*/ import { get } from '@ember/object'; -import { run } from '@ember/runloop'; +import { settled } from '@ember/test-helpers'; import { module, test } from 'qunit'; -import { Promise as EmberPromise } from 'rsvp'; import { setupTest } from 'ember-qunit'; @@ -45,462 +44,397 @@ module('integration/relationships/many_to_many_test - ManyToMany relationships', Server loading tests */ - test('Loading from one hasMany side reflects on the other hasMany side - async', function (assert) { + test('Loading from one hasMany side reflects on the other hasMany side - async', async function (assert) { let store = this.owner.lookup('service:store'); - run(() => { - store.push({ - data: { - id: '1', - type: 'user', - attributes: { - name: 'Stanley', - }, - relationships: { - topics: { - data: [ - { - id: '2', - type: 'topic', - }, - { - id: '3', - type: 'topic', - }, - ], - }, + store.push({ + data: { + id: '1', + type: 'user', + attributes: { + name: 'Stanley', + }, + relationships: { + topics: { + data: [ + { + id: '2', + type: 'topic', + }, + { + id: '3', + type: 'topic', + }, + ], }, }, - }); + }, }); - let topic = run(() => { - return store.push({ - data: { - id: '2', - type: 'topic', - attributes: { - title: 'EmberFest was great', - }, + let topic = store.push({ + data: { + id: '2', + type: 'topic', + attributes: { + title: 'EmberFest was great', }, - }); + }, }); - return run(() => { - return topic.users.then((fetchedUsers) => { - assert.strictEqual(fetchedUsers.length, 1, 'User relationship was set up correctly'); - }); - }); + const fetchedUsers = await topic.users; + + assert.strictEqual(fetchedUsers.length, 1, 'User relationship was set up correctly'); }); test('Relationship is available from one hasMany side even if only loaded from the other hasMany side - sync', function (assert) { let store = this.owner.lookup('service:store'); - var account; - run(() => { - account = store.push({ - data: { - id: '2', - type: 'account', - attributes: { - state: 'lonely', - }, + let account = store.push({ + data: { + id: '2', + type: 'account', + attributes: { + state: 'lonely', }, - }); - store.push({ - data: { - id: '1', - type: 'user', - attributes: { - name: 'Stanley', - }, - relationships: { - accounts: { - data: [ - { - id: '2', - type: 'account', - }, - ], - }, + }, + }); + store.push({ + data: { + id: '1', + type: 'user', + attributes: { + name: 'Stanley', + }, + relationships: { + accounts: { + data: [ + { + id: '2', + type: 'account', + }, + ], }, }, - }); - }); - - run(() => { - assert.strictEqual(account.users.length, 1, 'User relationship was set up correctly'); + }, }); + assert.strictEqual(account.users.length, 1, 'User relationship was set up correctly'); }); - test('Fetching a hasMany where a record was removed reflects on the other hasMany side - async', function (assert) { + test('Fetching a hasMany where a record was removed reflects on the other hasMany side - async', async function (assert) { let store = this.owner.lookup('service:store'); - let user, topic; - run(() => { - user = store.push({ - data: { - id: '1', - type: 'user', - attributes: { - name: 'Stanley', - }, - relationships: { - topics: { - data: [{ id: '2', type: 'topic' }], - }, - }, + let user = store.push({ + data: { + id: '1', + type: 'user', + attributes: { + name: 'Stanley', }, - }); - topic = store.push({ - data: { - id: '2', - type: 'topic', - attributes: { - title: 'EmberFest was great', + relationships: { + topics: { + data: [{ id: '2', type: 'topic' }], }, - relationships: { - users: { - data: [], - }, + }, + }, + }); + let topic = store.push({ + data: { + id: '2', + type: 'topic', + attributes: { + title: 'EmberFest was great', + }, + relationships: { + users: { + data: [], }, }, - }); + }, }); - return run(() => { - return user.topics.then((fetchedTopics) => { - assert.strictEqual(fetchedTopics.length, 0, 'Topics were removed correctly'); - assert.strictEqual(fetchedTopics.objectAt(0), undefined, "Topics can't be fetched"); - return topic.users.then((fetchedUsers) => { - assert.strictEqual(fetchedUsers.length, 0, 'Users were removed correctly'); - assert.strictEqual(fetchedUsers.objectAt(0), undefined, "User can't be fetched"); - }); - }); - }); + const fetchedTopics = await user.topics; + assert.strictEqual(fetchedTopics.length, 0, 'Topics were removed correctly'); + assert.strictEqual(fetchedTopics.at(0), undefined, "Topics can't be fetched"); + const fetchedUsers = await topic.users; + assert.strictEqual(fetchedUsers.length, 0, 'Users were removed correctly'); + assert.strictEqual(fetchedUsers.at(0), undefined, "User can't be fetched"); }); test('Fetching a hasMany where a record was removed reflects on the other hasMany side - sync', function (assert) { let store = this.owner.lookup('service:store'); - let account, user; - run(() => { - account = store.push({ - data: { - id: '2', - type: 'account', - attributes: { - state: 'lonely', - }, + let account = store.push({ + data: { + id: '2', + type: 'account', + attributes: { + state: 'lonely', }, - }); - user = store.push({ - data: { - id: '1', - type: 'user', - attributes: { - name: 'Stanley', - }, - relationships: { - accounts: { - data: [ - { - id: '2', - type: 'account', - }, - ], - }, - }, + }, + }); + let user = store.push({ + data: { + id: '1', + type: 'user', + attributes: { + name: 'Stanley', }, - }); - account = store.push({ - data: { - id: '2', - type: 'account', - attributes: { - state: 'lonely', + relationships: { + accounts: { + data: [ + { + id: '2', + type: 'account', + }, + ], }, - relationships: { - users: { - data: [], - }, + }, + }, + }); + account = store.push({ + data: { + id: '2', + type: 'account', + attributes: { + state: 'lonely', + }, + relationships: { + users: { + data: [], }, }, - }); + }, }); - run(() => { - assert.strictEqual(user.accounts.length, 0, 'Accounts were removed correctly'); - assert.strictEqual(account.users.length, 0, 'Users were removed correctly'); - }); + assert.strictEqual(user.accounts.length, 0, 'Accounts were removed correctly'); + assert.strictEqual(account.users.length, 0, 'Users were removed correctly'); }); /* Local edits */ - test('Pushing to a hasMany reflects on the other hasMany side - async', function (assert) { + test('Pushing to a hasMany reflects on the other hasMany side - async', async function (assert) { assert.expect(1); let store = this.owner.lookup('service:store'); - let user, topic; - - run(() => { - user = store.push({ - data: { - id: '1', - type: 'user', - attributes: { - name: 'Stanley', - }, - relationships: { - topics: { - data: [], - }, - }, + let user = store.push({ + data: { + id: '1', + type: 'user', + attributes: { + name: 'Stanley', }, - }); - topic = store.push({ - data: { - id: '2', - type: 'topic', - attributes: { - title: 'EmberFest was great', + relationships: { + topics: { + data: [], }, }, - }); + }, }); - - return run(() => { - return topic.users.then((fetchedUsers) => { - fetchedUsers.pushObject(user); - return user.topics.then((fetchedTopics) => { - assert.strictEqual(fetchedTopics.length, 1, 'User relationship was set up correctly'); - }); - }); + let topic = store.push({ + data: { + id: '2', + type: 'topic', + attributes: { + title: 'EmberFest was great', + }, + }, }); + + const fetchedUsers = await topic.users; + fetchedUsers.push(user); + const fetchedTopics = await user.topics; + assert.strictEqual(fetchedTopics.length, 1, 'User relationship was set up correctly'); }); test('Pushing to a hasMany reflects on the other hasMany side - sync', function (assert) { let store = this.owner.lookup('service:store'); - let account, stanley; - run(() => { - account = store.push({ - data: { - id: '2', - type: 'account', - attributes: { - state: 'lonely', - }, + let account = store.push({ + data: { + id: '2', + type: 'account', + attributes: { + state: 'lonely', }, - }); - stanley = store.push({ - data: { - id: '1', - type: 'user', - attributes: { - name: 'Stanley', - }, + }, + }); + let stanley = store.push({ + data: { + id: '1', + type: 'user', + attributes: { + name: 'Stanley', }, - }); - stanley.accounts.pushObject(account); + }, }); + stanley.accounts.push(account); - run(() => { - assert.strictEqual(account.users.length, 1, 'User relationship was set up correctly'); - }); + assert.strictEqual(account.users.length, 1, 'User relationship was set up correctly'); }); - test('Removing a record from a hasMany reflects on the other hasMany side - async', function (assert) { + test('Removing a record from a hasMany reflects on the other hasMany side - async', async function (assert) { let store = this.owner.lookup('service:store'); - let user, topic; - run(() => { - user = store.push({ - data: { - id: '1', - type: 'user', - attributes: { - name: 'Stanley', - }, - relationships: { - topics: { - data: [ - { - id: '2', - type: 'topic', - }, - ], - }, - }, + const user = store.push({ + data: { + id: '1', + type: 'user', + attributes: { + name: 'Stanley', }, - }); - topic = store.push({ - data: { - id: '2', - type: 'topic', - attributes: { - title: 'EmberFest was great', + relationships: { + topics: { + data: [ + { + id: '2', + type: 'topic', + }, + ], }, }, - }); + }, }); - - return run(() => { - return user.topics.then((fetchedTopics) => { - assert.strictEqual(fetchedTopics.length, 1, 'Topics were setup correctly'); - fetchedTopics.removeObject(topic); - return topic.users.then((fetchedUsers) => { - assert.strictEqual(fetchedUsers.length, 0, 'Users were removed correctly'); - assert.strictEqual(fetchedUsers.objectAt(0), undefined, "User can't be fetched"); - }); - }); + const topic = store.push({ + data: { + id: '2', + type: 'topic', + attributes: { + title: 'EmberFest was great', + }, + }, }); + + const fetchedTopics = await user.topics; + assert.strictEqual(fetchedTopics.length, 1, 'Topics were setup correctly'); + fetchedTopics.splice(fetchedTopics.indexOf(topic), 1); + const fetchedUsers = await topic.users; + assert.strictEqual(fetchedUsers.length, 0, 'Users were removed correctly'); + assert.strictEqual(fetchedUsers.at(0), undefined, "User can't be fetched"); }); test('Removing a record from a hasMany reflects on the other hasMany side - sync', function (assert) { let store = this.owner.lookup('service:store'); - let account, user; - run(() => { - account = store.push({ - data: { - id: '2', - type: 'account', - attributes: { - state: 'lonely', - }, + const account = store.push({ + data: { + id: '2', + type: 'account', + attributes: { + state: 'lonely', }, - }); - user = store.push({ - data: { - id: '1', - type: 'user', - attributes: { - name: 'Stanley', - }, - relationships: { - accounts: { - data: [ - { - id: '2', - type: 'account', - }, - ], - }, + }, + }); + const user = store.push({ + data: { + id: '1', + type: 'user', + attributes: { + name: 'Stanley', + }, + relationships: { + accounts: { + data: [ + { + id: '2', + type: 'account', + }, + ], }, }, - }); + }, }); - run(() => { - assert.strictEqual(account.users.length, 1, 'Users were setup correctly'); - account.users.removeObject(user); - assert.strictEqual(user.accounts.length, 0, 'Accounts were removed correctly'); - assert.strictEqual(account.users.length, 0, 'Users were removed correctly'); - }); + assert.strictEqual(account.users.length, 1, 'Users were setup correctly'); + account.users.splice(account.users.indexOf(user), 1); + assert.strictEqual(user.accounts.length, 0, 'Accounts were removed correctly'); + assert.strictEqual(account.users.length, 0, 'Users were removed correctly'); }); /* Rollback Attributes tests */ - test('Rollbacking attributes for a deleted record that has a ManyToMany relationship works correctly - async', function (assert) { + test('Rollbacking attributes for a deleted record that has a ManyToMany relationship works correctly - async', async function (assert) { let store = this.owner.lookup('service:store'); - let user, topic; - run(() => { - user = store.push({ - data: { - id: '1', - type: 'user', - attributes: { - name: 'Stanley', - }, - relationships: { - topics: { - data: [ - { - id: '2', - type: 'topic', - }, - ], - }, - }, + let user = store.push({ + data: { + id: '1', + type: 'user', + attributes: { + name: 'Stanley', }, - }); - topic = store.push({ - data: { - id: '2', - type: 'topic', - attributes: { - title: 'EmberFest was great', + relationships: { + topics: { + data: [ + { + id: '2', + type: 'topic', + }, + ], }, }, - }); + }, }); - - run(() => { - topic.deleteRecord(); - topic.rollbackAttributes(); + let topic = store.push({ + data: { + id: '2', + type: 'topic', + attributes: { + title: 'EmberFest was great', + }, + }, }); - return run(() => { - let users = topic.users.then((fetchedUsers) => { - assert.strictEqual(fetchedUsers.length, 1, 'Users are still there'); - }); + topic.deleteRecord(); + topic.rollbackAttributes(); + await settled(); - let topics = user.topics.then((fetchedTopics) => { - assert.strictEqual(fetchedTopics.length, 1, 'Topic got rollbacked into the user'); - }); + let users = await topic.users; + assert.strictEqual(users.length, 1, 'Users are still there'); - return EmberPromise.all([users, topics]); - }); + let topics = await user.topics; + assert.strictEqual(topics.length, 1, 'Topic got rollbacked into the user'); }); test('Deleting a record that has a hasMany relationship removes it from the otherMany array but does not remove the other record from itself - sync', function (assert) { let store = this.owner.lookup('service:store'); - let account, user; - run(() => { - account = store.push({ - data: { - id: '2', - type: 'account', - attributes: { - state: 'lonely', - }, + let account = store.push({ + data: { + id: '2', + type: 'account', + attributes: { + state: 'lonely', }, - }); - user = store.push({ - data: { - id: '1', - type: 'user', - attributes: { - name: 'Stanley', - }, - relationships: { - accounts: { - data: [ - { - id: '2', - type: 'account', - }, - ], - }, + }, + }); + let user = store.push({ + data: { + id: '1', + type: 'user', + attributes: { + name: 'Stanley', + }, + relationships: { + accounts: { + data: [ + { + id: '2', + type: 'account', + }, + ], }, }, - }); - }); - - run(() => { - account.deleteRecord(); - account.rollbackAttributes(); - assert.strictEqual(account.users.length, 1, 'Users are still there'); - assert.strictEqual(user.accounts.length, 1, 'Account got rolledback correctly into the user'); + }, }); + account.deleteRecord(); + account.rollbackAttributes(); + assert.strictEqual(account.users.length, 1, 'Users are still there'); + assert.strictEqual(user.accounts.length, 1, 'Account got rolledback correctly into the user'); }); test('Rollbacking attributes for a created record that has a ManyToMany relationship works correctly - async', async function (assert) { @@ -518,40 +452,35 @@ module('integration/relationships/many_to_many_test - ManyToMany relationships', let topic = store.createRecord('topic'); let fetchedTopics = await user.topics; - fetchedTopics.pushObject(topic); + fetchedTopics.push(topic); topic.rollbackAttributes(); let fetchedUsers = await topic.users; assert.strictEqual(fetchedUsers.length, 0, 'Users got removed'); - assert.strictEqual(fetchedUsers.objectAt(0), undefined, "User can't be fetched"); + assert.strictEqual(fetchedUsers.at(0), undefined, "User can't be fetched"); fetchedTopics = await user.topics; assert.strictEqual(fetchedTopics.length, 0, 'Topics got removed'); - assert.strictEqual(fetchedTopics.objectAt(0), undefined, "Topic can't be fetched"); + assert.strictEqual(fetchedTopics.at(0), undefined, "Topic can't be fetched"); }); test('Deleting an unpersisted record via rollbackAttributes that has a hasMany relationship removes it from the otherMany array but does not remove the other record from itself - sync', function (assert) { let store = this.owner.lookup('service:store'); - let account, user; - run(() => { - account = store.push({ - data: { - id: '2', - type: 'account', - attributes: { - state: 'lonely', - }, + let account = store.push({ + data: { + id: '2', + type: 'account', + attributes: { + state: 'lonely', }, - }); - - user = store.createRecord('user'); + }, }); - run(() => { - account.users.pushObject(user); - user.rollbackAttributes(); - }); + let user = store.createRecord('user'); + + account.users.push(user); + user.rollbackAttributes(); assert.strictEqual(account.users.length, 0, 'Users got removed'); assert.strictEqual(user.accounts.length, 0, 'Accounts got rolledback correctly'); @@ -564,80 +493,76 @@ module('integration/relationships/many_to_many_test - ManyToMany relationships', let store = this.owner.lookup('service:store'); - let account; - - run(() => { - account = store.push({ - data: { - id: '2', - type: 'account', - attributes: { - state: 'account 1', - }, + let account = store.push({ + data: { + id: '2', + type: 'account', + attributes: { + state: 'account 1', }, - }); - let ada = store.push({ - data: { - id: '1', - type: 'user', - attributes: { - name: 'Ada Lovelace', - }, - relationships: { - accounts: { - data: [ - { - id: '2', - type: 'account', - }, - ], - }, - }, + }, + }); + let ada = store.push({ + data: { + id: '1', + type: 'user', + attributes: { + name: 'Ada Lovelace', }, - }); - let byron = store.push({ - data: { - id: '2', - type: 'user', - attributes: { - name: 'Lord Byron', - }, - relationships: { - accounts: { - data: [ - { - id: '2', - type: 'account', - }, - ], - }, + relationships: { + accounts: { + data: [ + { + id: '2', + type: 'account', + }, + ], }, }, - }); - account.users.removeObject(byron); - account = store.push({ - data: { - id: '2', - type: 'account', - attributes: { - state: 'account 1', + }, + }); + let byron = store.push({ + data: { + id: '2', + type: 'user', + attributes: { + name: 'Lord Byron', + }, + relationships: { + accounts: { + data: [ + { + id: '2', + type: 'account', + }, + ], }, - relationships: { - users: { - data: [ - { - id: '1', - type: 'user', - }, - { - id: '2', - type: 'user', - }, - ], - }, + }, + }, + }); + account.users.splice(account.users.indexOf(byron), 1); + account = store.push({ + data: { + id: '2', + type: 'account', + attributes: { + state: 'account 1', + }, + relationships: { + users: { + data: [ + { + id: '1', + type: 'user', + }, + { + id: '2', + type: 'user', + }, + ], }, }, - }); + }, }); let state = account.hasMany('users').hasManyRelationship.remoteState; diff --git a/packages/-ember-data/tests/integration/relationships/one-to-many-test.js b/packages/-ember-data/tests/integration/relationships/one-to-many-test.js index 1e1a8a55d70..138ee64cad4 100644 --- a/packages/-ember-data/tests/integration/relationships/one-to-many-test.js +++ b/packages/-ember-data/tests/integration/relationships/one-to-many-test.js @@ -142,7 +142,7 @@ module('integration/relationships/one_to_many_test - OneToMany relationships', f ], }); const messages = await user.messages; - const messageUser = await messages.objectAt(0).user; + const messageUser = await messages.at(0).user; assert.strictEqual(messageUser, user, 'User relationship was set up correctly'); }); @@ -217,7 +217,7 @@ module('integration/relationships/one_to_many_test - OneToMany relationships', f }); run(function () { user.messages.then(function (fetchedMessages) { - assert.strictEqual(fetchedMessages.objectAt(0), message, 'Messages relationship was set up correctly'); + assert.strictEqual(fetchedMessages.at(0), message, 'Messages relationship was set up correctly'); }); }); }); @@ -255,7 +255,7 @@ module('integration/relationships/one_to_many_test - OneToMany relationships', f }); }); run(function () { - assert.strictEqual(user.accounts.objectAt(0), account, 'Accounts relationship was set up correctly'); + assert.strictEqual(user.accounts.at(0), account, 'Accounts relationship was set up correctly'); }); }); @@ -380,7 +380,7 @@ module('integration/relationships/one_to_many_test - OneToMany relationships', f }); run(function () { - assert.strictEqual(user.accounts.objectAt(0), undefined, 'Account was sucesfully removed'); + assert.strictEqual(user.accounts.at(0), undefined, 'Account was sucesfully removed'); }); }); @@ -493,7 +493,7 @@ module('integration/relationships/one_to_many_test - OneToMany relationships', f }); run(function () { - assert.strictEqual(user.accounts.objectAt(0), account, 'Account was sucesfully removed'); + assert.strictEqual(user.accounts.at(0), account, 'Account was sucesfully removed'); }); }); @@ -829,7 +829,7 @@ module('integration/relationships/one_to_many_test - OneToMany relationships', f run(function () { user.messages.then(function (fetchedMessages) { - fetchedMessages.pushObject(message2); + fetchedMessages.push(message2); message2.user.then(function (fetchedUser) { assert.strictEqual(fetchedUser, user, 'user got set correctly'); }); @@ -888,102 +888,89 @@ module('integration/relationships/one_to_many_test - OneToMany relationships', f }, }, }); - user.accounts.pushObject(account2); + user.accounts.push(account2); }); assert.strictEqual(account2.user, user, 'user got set correctly'); }); - test('Removing from the hasMany side reflects the change on the belongsTo side - async', function (assert) { + test('Removing from the hasMany side reflects the change on the belongsTo side - async', async function (assert) { let store = this.owner.lookup('service:store'); - var user, message; - run(function () { - user = store.push({ - data: { - id: '1', - type: 'user', - attributes: { - name: 'Stanley', - }, - relationships: { - messages: { - data: [ - { - id: '1', - type: 'message', - }, - ], - }, - }, + let user = store.push({ + data: { + id: '1', + type: 'user', + attributes: { + name: 'Stanley', }, - }); - message = store.push({ - data: { - id: '1', - type: 'message', - attributes: { - title: 'EmberFest was great', + relationships: { + messages: { + data: [ + { + id: '1', + type: 'message', + }, + ], }, }, - }); + }, }); - - run(function () { - user.messages.then(function (fetchedMessages) { - fetchedMessages.removeObject(message); - message.user.then(function (fetchedUser) { - assert.strictEqual(fetchedUser, null, 'user got removed correctly'); - }); - }); + let message = store.push({ + data: { + id: '1', + type: 'message', + attributes: { + title: 'EmberFest was great', + }, + }, }); + + const fetchedMessages = await user.messages; + fetchedMessages.splice(fetchedMessages.indexOf(message), 1); + const fetchedUser = await message.user; + assert.strictEqual(fetchedUser, null, 'user got removed correctly'); }); test('Removing from the hasMany side reflects the change on the belongsTo side - sync', function (assert) { let store = this.owner.lookup('service:store'); - - var user, account; - run(function () { - user = store.push({ - data: { - id: '1', - type: 'user', - attirbutes: { - name: 'Stanley', - }, - relationships: { - accounts: { - data: [ - { - id: '1', - type: 'account', - }, - ], - }, - }, + let user = store.push({ + data: { + id: '1', + type: 'user', + attirbutes: { + name: 'Stanley', }, - }); - account = store.push({ - data: { - id: '1', - type: 'account', - attirbutes: { - state: 'great', - }, - relationships: { - user: { - data: { + relationships: { + accounts: { + data: [ + { id: '1', - type: 'user', + type: 'account', }, - }, + ], }, }, - }); + }, }); - run(function () { - user.accounts.removeObject(account); + let account = store.push({ + data: { + id: '1', + type: 'account', + attirbutes: { + state: 'great', + }, + relationships: { + user: { + data: { + id: '1', + type: 'user', + }, + }, + }, + }, }); + user.accounts.splice(user.accounts.indexOf(account), 1); assert.strictEqual(account.user, null, 'user got removed correctly'); }); @@ -1036,7 +1023,7 @@ module('integration/relationships/one_to_many_test - OneToMany relationships', f run(function () { user2.messages.then(function (fetchedMessages) { - fetchedMessages.pushObject(message); + fetchedMessages.push(message); message.user.then(function (fetchedUser) { assert.strictEqual(fetchedUser, user2, 'user got set correctly'); @@ -1091,7 +1078,7 @@ module('integration/relationships/one_to_many_test - OneToMany relationships', f }, }, }); - user2.accounts.pushObject(account); + user2.accounts.push(account); }); assert.strictEqual(account.user, user2, 'user got set correctly'); assert.strictEqual(user.accounts.length, 0, 'the account got removed correctly'); @@ -1376,7 +1363,7 @@ module('integration/relationships/one_to_many_test - OneToMany relationships', f assert.strictEqual(fetchedUser, user, 'Message still has the user'); }); user.messages.then(function (fetchedMessages) { - assert.strictEqual(fetchedMessages.objectAt(0), message, 'User has the message'); + assert.strictEqual(fetchedMessages.at(0), message, 'User has the message'); }); }); }); @@ -1539,7 +1526,7 @@ module('integration/relationships/one_to_many_test - OneToMany relationships', f let fetchedMessages = await user.messages; assert.strictEqual(fetchedMessages.length, 0, 'User does not have the message anymore'); - assert.strictEqual(fetchedMessages.firstObject, undefined, "User message can't be accessed"); + assert.strictEqual(fetchedMessages.at(0), undefined, "User message can't be accessed"); }); test('Rollbacking attributes of a created record works correctly when the hasMany side has been created - sync', function (assert) { @@ -1578,14 +1565,14 @@ module('integration/relationships/one_to_many_test - OneToMany relationships', f }); let user = store.createRecord('user'); let messages = await user.messages; - messages.pushObject(message); + messages.push(message); user.rollbackAttributes(); let fetchedUser = await message.user; assert.strictEqual(fetchedUser, null, 'Message does not have the user anymore'); let fetchedMessages = await user.messages; assert.strictEqual(fetchedMessages.length, 0, 'User does not have the message anymore'); - assert.strictEqual(fetchedMessages.firstObject, undefined, "User message can't be accessed"); + assert.strictEqual(fetchedMessages.at(0), undefined, "User message can't be accessed"); }); test('Rollbacking attributes of a created record works correctly when the belongsTo side has been created - sync', function (assert) { @@ -1605,7 +1592,7 @@ module('integration/relationships/one_to_many_test - OneToMany relationships', f user = store.createRecord('user'); }); run(function () { - user.accounts.pushObject(account); + user.accounts.push(account); }); run(user, 'rollbackAttributes'); assert.strictEqual(user.accounts.length, 0, 'User does not have the account anymore'); @@ -1614,7 +1601,7 @@ module('integration/relationships/one_to_many_test - OneToMany relationships', f deprecatedTest( 'createRecord updates inverse record array which has observers', - { id: 'ember-data:deprecate-promise-many-array-behaviors', until: '5.0', count: 4 }, + { id: 'ember-data:deprecate-promise-many-array-behaviors', until: '5.0', count: 3 }, async function (assert) { let store = this.owner.lookup('service:store'); let adapter = store.adapterFor('application'); @@ -1636,12 +1623,12 @@ module('integration/relationships/one_to_many_test - OneToMany relationships', f const users = await store.findAll('user'); assert.strictEqual(users.length, 1, 'Exactly 1 user'); - let user = users.firstObject; + let user = users.at(0); assert.strictEqual(user.messages.length, 0, 'Record array is initially empty'); // set up an observer user.addObserver('messages.@each.title', () => {}); - user.messages.firstObject; + user.messages.objectAt(0); const messages = await user.messages; @@ -1652,8 +1639,9 @@ module('integration/relationships/one_to_many_test - OneToMany relationships', f assert.strictEqual(messages.length, 1, 'The message is added to the record array'); assert.strictEqual(user.messages.length, 1, 'The message is added to the record array'); - let messageFromArray = user.messages.firstObject; + let messageFromArray = user.messages.objectAt(0); assert.strictEqual(message, messageFromArray, 'Only one message record instance should be created'); + assert.expectDeprecation({ id: 'ember-data:deprecate-array-like', count: 3 }); } ); }); diff --git a/packages/-ember-data/tests/integration/relationships/one-to-one-test.js b/packages/-ember-data/tests/integration/relationships/one-to-one-test.js index 0812fb20497..17d6e35b3f9 100644 --- a/packages/-ember-data/tests/integration/relationships/one-to-one-test.js +++ b/packages/-ember-data/tests/integration/relationships/one-to-one-test.js @@ -9,6 +9,7 @@ import { setupTest } from 'ember-qunit'; import Adapter from '@ember-data/adapter'; import Model, { attr, belongsTo } from '@ember-data/model'; import JSONAPISerializer from '@ember-data/serializer/json-api'; +import { deprecatedTest } from '@ember-data/unpublished-test-infra/test-support/deprecated-test'; import testInDebug from '@ember-data/unpublished-test-infra/test-support/test-in-debug'; module('integration/relationships/one_to_one_test - OneToOne relationships', function (hooks) { @@ -448,114 +449,122 @@ module('integration/relationships/one_to_one_test - OneToOne relationships', fun assert.strictEqual(job.user, user, 'User relationship was set up correctly'); }); - test('Setting a BelongsTo to a promise unwraps the promise before setting- async', function (assert) { - let store = this.owner.lookup('service:store'); + deprecatedTest( + 'Setting a BelongsTo to a promise unwraps the promise before setting- async', + { id: 'ember-data:deprecate-promise-proxies', until: '5.0', count: 1 }, + function (assert) { + let store = this.owner.lookup('service:store'); - var stanley, stanleysFriend, newFriend; - run(function () { - stanley = store.push({ - data: { - id: '1', - type: 'user', - attributes: { - name: 'Stanley', - }, - relationships: { - bestFriend: { - data: { - id: '2', - type: 'user', + var stanley, stanleysFriend, newFriend; + run(function () { + stanley = store.push({ + data: { + id: '1', + type: 'user', + attributes: { + name: 'Stanley', + }, + relationships: { + bestFriend: { + data: { + id: '2', + type: 'user', + }, }, }, }, - }, - }); - stanleysFriend = store.push({ - data: { - id: '2', - type: 'user', - attributes: { - name: "Stanley's friend", + }); + stanleysFriend = store.push({ + data: { + id: '2', + type: 'user', + attributes: { + name: "Stanley's friend", + }, }, - }, - }); - newFriend = store.push({ - data: { - id: '3', - type: 'user', - attributes: { - name: 'New friend', + }); + newFriend = store.push({ + data: { + id: '3', + type: 'user', + attributes: { + name: 'New friend', + }, }, - }, - }); - }); - run(function () { - newFriend.set('bestFriend', stanleysFriend.bestFriend); - stanley.bestFriend.then(function (fetchedUser) { - assert.strictEqual( - fetchedUser, - newFriend, - `Stanley's bestFriend relationship was updated correctly to newFriend` - ); + }); }); - newFriend.bestFriend.then(function (fetchedUser) { - assert.strictEqual( - fetchedUser, - stanley, - `newFriend's bestFriend relationship was updated correctly to be Stanley` - ); + run(function () { + newFriend.set('bestFriend', stanleysFriend.bestFriend); + stanley.bestFriend.then(function (fetchedUser) { + assert.strictEqual( + fetchedUser, + newFriend, + `Stanley's bestFriend relationship was updated correctly to newFriend` + ); + }); + newFriend.bestFriend.then(function (fetchedUser) { + assert.strictEqual( + fetchedUser, + stanley, + `newFriend's bestFriend relationship was updated correctly to be Stanley` + ); + }); }); - }); - }); + } + ); - test('Setting a BelongsTo to a promise works when the promise returns null- async', function (assert) { - let store = this.owner.lookup('service:store'); + deprecatedTest( + 'Setting a BelongsTo to a promise works when the promise returns null- async', + { id: 'ember-data:deprecate-promise-proxies', until: '5.0', count: 1 }, + function (assert) { + let store = this.owner.lookup('service:store'); - var igor, newFriend; - run(function () { - store.push({ - data: { - id: '1', - type: 'user', - attributes: { - name: 'Stanley', - }, - }, - }); - igor = store.push({ - data: { - id: '2', - type: 'user', - attributes: { - name: 'Igor', + var igor, newFriend; + run(function () { + store.push({ + data: { + id: '1', + type: 'user', + attributes: { + name: 'Stanley', + }, }, - }, - }); - newFriend = store.push({ - data: { - id: '3', - type: 'user', - attributes: { - name: 'New friend', + }); + igor = store.push({ + data: { + id: '2', + type: 'user', + attributes: { + name: 'Igor', + }, }, - relationships: { - bestFriend: { - data: { - id: '1', - type: 'user', + }); + newFriend = store.push({ + data: { + id: '3', + type: 'user', + attributes: { + name: 'New friend', + }, + relationships: { + bestFriend: { + data: { + id: '1', + type: 'user', + }, }, }, }, - }, + }); }); - }); - run(function () { - newFriend.set('bestFriend', igor.bestFriend); - newFriend.bestFriend.then(function (fetchedUser) { - assert.strictEqual(fetchedUser, null, 'User relationship was updated correctly'); + run(function () { + newFriend.set('bestFriend', igor.bestFriend); + newFriend.bestFriend.then(function (fetchedUser) { + assert.strictEqual(fetchedUser, null, 'User relationship was updated correctly'); + }); }); - }); - }); + } + ); testInDebug("Setting a BelongsTo to a promise that didn't come from a relationship errors out", function (assert) { let store = this.owner.lookup('service:store'); @@ -597,81 +606,85 @@ module('integration/relationships/one_to_one_test - OneToOne relationships', fun }, /You passed in a promise that did not originate from an EmberData relationship. You can only pass promises that come from a belongsTo or hasMany relationship to the get call./); }); - test('Setting a BelongsTo to a promise multiple times is resistant to race conditions- async', function (assert) { - assert.expect(1); + deprecatedTest( + 'Setting a BelongsTo to a promise multiple times is resistant to race conditions- async', + { id: 'ember-data:deprecate-promise-proxies', until: '5.0', count: 2 }, + function (assert) { + assert.expect(1); - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + let store = this.owner.lookup('service:store'); + let adapter = store.adapterFor('application'); - var stanley, igor, newFriend; - run(function () { - stanley = store.push({ - data: { - id: '1', - type: 'user', - attributes: { - name: 'Stanley', - }, - relationships: { - bestFriend: { - data: { - id: '2', - type: 'user', + var stanley, igor, newFriend; + run(function () { + stanley = store.push({ + data: { + id: '1', + type: 'user', + attributes: { + name: 'Stanley', + }, + relationships: { + bestFriend: { + data: { + id: '2', + type: 'user', + }, }, }, }, - }, - }); - igor = store.push({ - data: { - id: '3', - type: 'user', - attributes: { - name: 'Igor', - }, - relationships: { - bestFriend: { - data: { - id: '5', - type: 'user', + }); + igor = store.push({ + data: { + id: '3', + type: 'user', + attributes: { + name: 'Igor', + }, + relationships: { + bestFriend: { + data: { + id: '5', + type: 'user', + }, }, }, }, - }, - }); - newFriend = store.push({ - data: { - id: '7', - type: 'user', - attributes: { - name: 'New friend', + }); + newFriend = store.push({ + data: { + id: '7', + type: 'user', + attributes: { + name: 'New friend', + }, }, - }, + }); }); - }); - adapter.findRecord = function (store, type, id, snapshot) { - if (id === '5') { - return resolve({ data: { id: '5', type: 'user', attributes: { name: "Igor's friend" } } }); - } else if (id === '2') { - let done = assert.async(); - return new EmberPromise(function (resolve, reject) { - setTimeout(function () { - done(); - resolve({ data: { id: '2', type: 'user', attributes: { name: "Stanley's friend" } } }); - }, 1); - }); - } - }; + adapter.findRecord = function (store, type, id, snapshot) { + if (id === '5') { + return resolve({ data: { id: '5', type: 'user', attributes: { name: "Igor's friend" } } }); + } else if (id === '2') { + let done = assert.async(); + return new EmberPromise(function (resolve, reject) { + setTimeout(function () { + done(); + resolve({ data: { id: '2', type: 'user', attributes: { name: "Stanley's friend" } } }); + }, 1); + }); + } + }; - run(function () { - newFriend.set('bestFriend', stanley.bestFriend); - newFriend.set('bestFriend', igor.bestFriend); - newFriend.bestFriend.then(function (fetchedUser) { - assert.strictEqual(fetchedUser.name, "Igor's friend", 'User relationship was updated correctly'); + run(function () { + newFriend.set('bestFriend', stanley.bestFriend); + newFriend.set('bestFriend', igor.bestFriend); + newFriend.bestFriend.then(function (fetchedUser) { + assert.strictEqual(fetchedUser.name, "Igor's friend", 'User relationship was updated correctly'); + }); }); - }); - }); + } + ); test('Setting a OneToOne relationship to null reflects correctly on the other side - async', function (assert) { let store = this.owner.lookup('service:store'); diff --git a/packages/-ember-data/tests/integration/relationships/polymorphic-mixins-has-many-test.js b/packages/-ember-data/tests/integration/relationships/polymorphic-mixins-has-many-test.js index 58632a5d7d8..5da55712e1f 100644 --- a/packages/-ember-data/tests/integration/relationships/polymorphic-mixins-has-many-test.js +++ b/packages/-ember-data/tests/integration/relationships/polymorphic-mixins-has-many-test.js @@ -78,8 +78,8 @@ module( }); run(function () { user.messages.then(function (messages) { - assert.strictEqual(messages.objectAt(0), video, 'The hasMany has loaded correctly'); - messages.objectAt(0).user.then(function (fetchedUser) { + assert.strictEqual(messages.at(0), video, 'The hasMany has loaded correctly'); + messages.at(0).user.then(function (fetchedUser) { assert.strictEqual(fetchedUser, user, 'The inverse was setup correctly'); }); }); @@ -123,7 +123,7 @@ module( run(function () { user.messages.then(function (fetchedMessages) { - fetchedMessages.pushObject(video); + fetchedMessages.push(video); video.user.then(function (fetchedUser) { assert.strictEqual(fetchedUser, user, 'user got set correctly'); }); @@ -169,7 +169,7 @@ module( run(function () { user.messages.then(function (fetchedMessages) { - fetchedMessages.pushObject(video); + fetchedMessages.push(video); video.user.then(function (fetchedUser) { assert.strictEqual(fetchedUser, user, 'user got set correctly'); }); @@ -217,7 +217,7 @@ module( run(function () { user.messages.then(function (fetchedMessages) { assert.expectAssertion(function () { - fetchedMessages.pushObject(notMessage); + fetchedMessages.push(notMessage); }, /The 'not-message' type does not implement 'message' and thus cannot be assigned to the 'messages' relationship in 'user'. Make it a descendant of 'message/); }); }); @@ -258,7 +258,7 @@ module( run(function () { user.messages.then(function (fetchedMessages) { - fetchedMessages.pushObject(video); + fetchedMessages.push(video); video.user.then(function (fetchedUser) { assert.strictEqual(fetchedUser, user, 'user got set correctly'); }); @@ -306,7 +306,7 @@ module( run(function () { user.messages.then(function (fetchedMessages) { assert.expectAssertion(function () { - fetchedMessages.pushObject(notMessage); + fetchedMessages.push(notMessage); }, /The 'not-message' type does not implement 'message' and thus cannot be assigned to the 'messages' relationship in 'user'. Make it a descendant of 'message'/); }); }); diff --git a/packages/-ember-data/tests/integration/relationships/promise-many-array-test.js b/packages/-ember-data/tests/integration/relationships/promise-many-array-test.js index 0f996ae3daa..d94d603269b 100644 --- a/packages/-ember-data/tests/integration/relationships/promise-many-array-test.js +++ b/packages/-ember-data/tests/integration/relationships/promise-many-array-test.js @@ -37,6 +37,7 @@ module('PromiseManyArray', (hooks) => { if (DEPRECATE_PROMISE_MANY_ARRAY_BEHAVIORS) { group.members.replace(0, 1); assert.strictEqual(group.members.length, 4, 'updated length is correct'); + assert.expectDeprecation({ id: 'ember-data:deprecate-array-like' }); } A(group.members); @@ -48,6 +49,7 @@ module('PromiseManyArray', (hooks) => { assert.strictEqual(group.members.length, 3, 'updated length is correct'); // we'll want to use a different test for this but will want to still ensure we are not side-affected assert.expectDeprecation({ id: 'ember-data:deprecate-promise-many-array-behaviors', until: '5.0', count: 2 }); + assert.expectDeprecation({ id: 'ember-data:deprecate-array-like' }); } } ); @@ -135,6 +137,7 @@ module('PromiseManyArray', (hooks) => { assert.strictEqual(johnRecords.length, 2, 'johnRecords length is correct'); assert.strictEqual(group.members.length, 6, 'members length is correct'); assert.expectDeprecation({ id: 'ember-data:no-a-with-array-like', count: 2 }); + assert.expectDeprecation({ id: 'ember-data:deprecate-array-like', count: 12 }); } ); }); diff --git a/packages/-ember-data/tests/integration/serializers/embedded-records-mixin-test.js b/packages/-ember-data/tests/integration/serializers/embedded-records-mixin-test.js index 04cbfdc4c46..822ebbdb224 100644 --- a/packages/-ember-data/tests/integration/serializers/embedded-records-mixin-test.js +++ b/packages/-ember-data/tests/integration/serializers/embedded-records-mixin-test.js @@ -1597,14 +1597,14 @@ module('integration/embedded-records-mixin', function (hooks) { superVillain, }); - superVillain.secretWeapons.pushObject(secretWeapon); + superVillain.secretWeapons.push(secretWeapon); let evilMinion = store.createRecord('evil-minion', { id: '1', name: 'Evil Minion', superVillain, }); - superVillain.evilMinions.pushObject(evilMinion); + superVillain.evilMinions.push(evilMinion); const serializer = store.serializerFor('super-villain'); const serializedRestJson = serializer.serialize(superVillain._createSnapshot()); @@ -1687,13 +1687,13 @@ module('integration/embedded-records-mixin', function (hooks) { superVillain, }); - superVillain.secretWeapons.pushObject(secretWeapon); + superVillain.secretWeapons.push(secretWeapon); let evilMinion = store.createRecord('evil-minion', { id: '1', name: 'Evil Minion', superVillain, }); - superVillain.evilMinions.pushObject(evilMinion); + superVillain.evilMinions.push(evilMinion); const serializer = store.serializerFor('super-villain'); const serializedRestJson = serializer.serialize(superVillain._createSnapshot()); @@ -1936,8 +1936,8 @@ module('integration/embedded-records-mixin', function (hooks) { superVillain, }); - superVillain.evilMinions.pushObject(evilMinion); - superVillain.secretWeapons.pushObject(secretWeapon); + superVillain.evilMinions.push(evilMinion); + superVillain.secretWeapons.push(secretWeapon); const serializer = store.serializerFor('super-villain'); const serializedRestJson = serializer.serialize(superVillain._createSnapshot()); diff --git a/packages/-ember-data/tests/integration/serializers/json-api-serializer-test.js b/packages/-ember-data/tests/integration/serializers/json-api-serializer-test.js index 035f1423379..f97796e0e41 100644 --- a/packages/-ember-data/tests/integration/serializers/json-api-serializer-test.js +++ b/packages/-ember-data/tests/integration/serializers/json-api-serializer-test.js @@ -111,8 +111,8 @@ module('integration/serializers/json-api-serializer - JSONAPISerializer', functi assert.strictEqual(user.firstName, 'Yehuda', 'firstName is correct'); assert.strictEqual(user.lastName, 'Katz', 'lastName is correct'); assert.strictEqual(company.name, 'Tilde Inc.', 'company.name is correct'); - assert.strictEqual(handles.firstObject.username, 'wycats', 'handles.firstObject.username is correct'); - assert.strictEqual(handles.lastObject.nickname, '@wycats', 'handles.lastObject.nickname is correct'); + assert.strictEqual(handles.at(0).username, 'wycats', 'handles.at(0).username is correct'); + assert.strictEqual(handles.at(-1).nickname, '@wycats', 'handles.at(-1).nickname is correct'); }); testInDebug('Warns when normalizing an unknown type', function (assert) { @@ -698,8 +698,8 @@ module('integration/serializers/json-api-serializer - JSONAPISerializer', functi let handle2 = store.peekRecord('handle', '2'); const handles = await user.handles; - handles.removeObject(handle1); - handles.removeObject(handle2); + handles.splice(handles.indexOf(handle1), 1); + handles.splice(handles.indexOf(handle2), 1); let serialized = user.serialize({ includeId: true }); diff --git a/packages/-ember-data/tests/integration/serializers/json-serializer-test.js b/packages/-ember-data/tests/integration/serializers/json-serializer-test.js index 3982ce41aa7..79ceceb31e4 100644 --- a/packages/-ember-data/tests/integration/serializers/json-serializer-test.js +++ b/packages/-ember-data/tests/integration/serializers/json-serializer-test.js @@ -307,7 +307,7 @@ module('integration/serializer/json - JSONSerializer', function (hooks) { }); run(function () { - post.comments.pushObject(comment); + post.comments.push(comment); }); let json = {}; @@ -923,7 +923,7 @@ module('integration/serializer/json - JSONSerializer', function (hooks) { let comment = store.createRecord('comment', { body: 'Omakase is delicious', post: post }); const comments = await post.comments; - comments.pushObject(comment); + comments.push(comment); const serializer = store.serializerFor('post'); const serializedProperty = serializer.keyForRelationship('comments', 'hasMany'); diff --git a/packages/-ember-data/tests/integration/serializers/rest-serializer-test.js b/packages/-ember-data/tests/integration/serializers/rest-serializer-test.js index 06daf32a21e..21549468a1a 100644 --- a/packages/-ember-data/tests/integration/serializers/rest-serializer-test.js +++ b/packages/-ember-data/tests/integration/serializers/rest-serializer-test.js @@ -794,8 +794,8 @@ module('integration/serializer/rest - RESTSerializer', function (hooks) { return superVillain.evilMinions; }) .then((evilMinions) => { - assert.ok(evilMinions.firstObject instanceof YellowMinion, 'we have an instance'); - assert.strictEqual(evilMinions.firstObject.eyes, 3, 'we have the right minion'); + assert.ok(evilMinions.at(0) instanceof YellowMinion, 'we have an instance'); + assert.strictEqual(evilMinions.at(0).eyes, 3, 'we have the right minion'); }); }); }); diff --git a/packages/-ember-data/tests/integration/snapshot-test.js b/packages/-ember-data/tests/integration/snapshot-test.js index 43ff444b0af..c01a0a7116b 100644 --- a/packages/-ember-data/tests/integration/snapshot-test.js +++ b/packages/-ember-data/tests/integration/snapshot-test.js @@ -559,49 +559,46 @@ module('integration/snapshot - Snapshot', function (hooks) { }, }, }); - let comment = store.peekRecord('comment', 2); + let comment = store.peekRecord('comment', '2'); - await comment.post.then((post) => { - store.push({ - data: [ - { - type: 'post', - id: '1', - attributes: { - title: 'Hello World', - }, + const post = await comment.post; + store.push({ + data: [ + { + type: 'post', + id: '1', + attributes: { + title: 'Hello World', }, - { - type: 'comment', - id: '2', - attributes: { - body: 'This is comment', - }, + }, + { + type: 'comment', + id: '2', + attributes: { + body: 'This is comment', }, - ], - }); - let comment = store.peekRecord('comment', 2); + }, + ], + }); - post.comments.then((comments) => { - comments.addObject(comment); + const comments = await post.comments; + comments.push(comment); - let postSnapshot = post._createSnapshot(); - let commentSnapshot = comment._createSnapshot(); + let postSnapshot = post._createSnapshot(); + let commentSnapshot = comment._createSnapshot(); - let hasManyRelationship = postSnapshot.hasMany('comments'); - let belongsToRelationship = commentSnapshot.belongsTo('post'); + let hasManyRelationship = postSnapshot.hasMany('comments'); + let belongsToRelationship = commentSnapshot.belongsTo('post'); - assert.ok(hasManyRelationship instanceof Array, 'hasMany relationship is an instance of Array'); - assert.strictEqual(hasManyRelationship.length, 1, 'hasMany relationship contains related object'); + assert.ok(hasManyRelationship instanceof Array, 'hasMany relationship is an instance of Array'); + assert.strictEqual(hasManyRelationship.length, 1, 'hasMany relationship contains related object'); - assert.ok(belongsToRelationship instanceof Snapshot, 'belongsTo relationship is an instance of Snapshot'); - assert.strictEqual( - belongsToRelationship.attr('title'), - 'Hello World', - 'belongsTo relationship contains related object' - ); - }); - }); + assert.ok(belongsToRelationship instanceof Snapshot, 'belongsTo relationship is an instance of Snapshot'); + assert.strictEqual( + belongsToRelationship.attr('title'), + 'Hello World', + 'belongsTo relationship contains related object' + ); }); test('snapshot.belongsTo() and snapshot.hasMany() returns correctly when adding an object to a hasMany relationship', async function (assert) { @@ -625,11 +622,11 @@ module('integration/snapshot - Snapshot', function (hooks) { }, ], }); - let post = store.peekRecord('post', 1); - let comment = store.peekRecord('comment', 2); + let post = store.peekRecord('post', '1'); + let comment = store.peekRecord('comment', '2'); const comments = await post.comments; - comments.addObject(comment); + comments.push(comment); let postSnapshot = post._createSnapshot(); let commentSnapshot = comment._createSnapshot(); @@ -1116,8 +1113,8 @@ module('integration/snapshot - Snapshot', function (hooks) { let comment3 = store.peekRecord('comment', 3); let post = store.peekRecord('post', 4); const comments = await post.comments; - comments.removeObject(comment3); - comments.insertAt(0, comment3); + comments.splice(comments.indexOf(comment3), 1); + comments.unshift(comment3); let snapshot = post._createSnapshot(); let relationship = snapshot.hasMany('comments'); diff --git a/packages/-ember-data/tests/integration/store-test.js b/packages/-ember-data/tests/integration/store-test.js index 33e36c2eda6..501ceea072b 100644 --- a/packages/-ember-data/tests/integration/store-test.js +++ b/packages/-ember-data/tests/integration/store-test.js @@ -4,9 +4,9 @@ import { settled } from '@ember/test-helpers'; import { module, test } from 'qunit'; import { Promise, resolve } from 'rsvp'; -import DS from 'ember-data'; import { setupTest } from 'ember-qunit'; +import Adapter from '@ember-data/adapter'; import JSONAPIAdapter from '@ember-data/adapter/json-api'; import RESTAdapter from '@ember-data/adapter/rest'; import Model, { attr, belongsTo, hasMany } from '@ember-data/model'; @@ -65,7 +65,7 @@ module('integration/store - destroy', function (hooks) { this.owner.register('model:car', Car); this.owner.register('model:person', Person); - this.owner.register('adapter:application', DS.Adapter.extend()); + this.owner.register('adapter:application', Adapter.extend()); this.owner.register('serializer:application', class extends JSONAPISerializer {}); }); @@ -74,7 +74,7 @@ module('integration/store - destroy', function (hooks) { let store = this.owner.lookup('service:store'); - let TestAdapter = DS.Adapter.extend({ + let TestAdapter = Adapter.extend({ findRecord(store, type, id, snapshot) { return new Promise((resolve, reject) => { store.unloadAll(type.modelName); @@ -107,7 +107,7 @@ module('integration/store - destroy', function (hooks) { let store = this.owner.lookup('service:store'); - let TestAdapter = DS.Adapter.extend({ + let TestAdapter = Adapter.extend({ findRecord(store, type, id, snapshot) { return new Promise((resolve, reject) => { store.unloadAll(type.modelName); @@ -135,7 +135,7 @@ module('integration/store - destroy', function (hooks) { let store = this.owner.lookup('service:store'); let next; let nextPromise = new Promise((resolve) => (next = resolve)); - let TestAdapter = DS.Adapter.extend({ + let TestAdapter = Adapter.extend({ findRecord() { next(); nextPromise = new Promise((resolve) => { @@ -226,7 +226,7 @@ module('integration/store - destroy', function (hooks) { let personWillDestroy = tap(person, 'willDestroy'); let carWillDestroy = tap(car, 'willDestroy'); - let carsWillDestroy = tap(car.person.cars, 'willDestroy'); + const cars = car.person.cars; adapter.query = function () { return { @@ -244,20 +244,17 @@ module('integration/store - destroy', function (hooks) { someCrazy: 'query', }); - let adapterPopulatedPeopleWillDestroy = tap(adapterPopulatedPeople, 'willDestroy'); - await store.findRecord('person', '2'); assert.strictEqual(personWillDestroy.called.length, 0, 'expected person.willDestroy to not have been called'); assert.strictEqual(carWillDestroy.called.length, 0, 'expected car.willDestroy to not have been called'); - assert.strictEqual(carsWillDestroy.called.length, 0, 'expected cars.willDestroy to not have been called'); - assert.strictEqual( - adapterPopulatedPeopleWillDestroy.called.length, - 0, + assert.false(cars.isDestroyed, 'expected cars.willDestroy to not have been called'); + assert.false( + adapterPopulatedPeople.isDestroyed, 'expected adapterPopulatedPeople.willDestroy to not have been called' ); assert.strictEqual(car.person, person, "expected car's person to be the correct person"); - assert.strictEqual(person.cars.firstObject, car, " expected persons cars's firstRecord to be the correct car"); + assert.strictEqual(person.cars.at(0), car, " expected persons cars's firstRecord to be the correct car"); store.destroy(); @@ -265,12 +262,8 @@ module('integration/store - destroy', function (hooks) { assert.strictEqual(personWillDestroy.called.length, 1, 'expected person to have received willDestroy once'); assert.strictEqual(carWillDestroy.called.length, 1, 'expected car to have received willDestroy once'); - assert.strictEqual(carsWillDestroy.called.length, 1, 'expected person.cars to have received willDestroy once'); - assert.strictEqual( - adapterPopulatedPeopleWillDestroy.called.length, - 1, - 'expected adapterPopulatedPeople to receive willDestroy once' - ); + assert.true(cars.isDestroyed, 'expected person.cars to have received willDestroy once'); + assert.true(adapterPopulatedPeople.isDestroyed, 'expected adapterPopulatedPeople to receive willDestroy once'); }); }); @@ -461,7 +454,7 @@ module('integration/store - findRecord', function (hooks) { test('store#findRecord { reload: true } ignores cached record and reloads record from server', async function (assert) { assert.expect(2); - const testAdapter = DS.RESTAdapter.extend({ + const testAdapter = RESTAdapter.extend({ shouldReloadRecord(store, type, id, snapshot) { assert.ok(false, 'shouldReloadRecord should not be called when { reload: true }'); }, @@ -502,11 +495,11 @@ module('integration/store - findRecord', function (hooks) { }); test('store#findRecord { reload: true } ignores cached record and reloads record from server even after previous findRecord', async function (assert) { - assert.expect(5); + assert.expect(4); let calls = 0; - const testAdapter = DS.JSONAPIAdapter.extend({ + const testAdapter = JSONAPIAdapter.extend({ shouldReloadRecord(store, type, id, snapshot) { assert.ok(false, 'shouldReloadRecord should not be called when { reload: true }'); }, @@ -536,11 +529,7 @@ module('integration/store - findRecord', function (hooks) { assert.strictEqual(calls, 1, 'We made one call to findRecord'); assert.strictEqual(car.model, 'Mini', 'cached car has expected model'); - let promiseCar = store.findRecord('car', '1', { reload: true }); - - assert.strictEqual(promiseCar.model, undefined, `We don't have early access to local data`); - - car = await promiseCar; + car = await store.findRecord('car', '1', { reload: true }); assert.strictEqual(calls, 2, 'We made a second call to findRecord'); assert.strictEqual(car.model, 'Princess', 'cached record ignored, record reloaded via server'); @@ -607,7 +596,7 @@ module('integration/store - findRecord', function (hooks) { }, }; - const testAdapter = DS.JSONAPIAdapter.extend({ + const testAdapter = JSONAPIAdapter.extend({ shouldReloadRecord(store, type, id, snapshot) { assert.ok(false, 'shouldReloadRecord should not be called when { reload: true }'); }, @@ -644,7 +633,7 @@ module('integration/store - findRecord', function (hooks) { test('store#findRecord { backgroundReload: false } returns cached record and does not reload in the background', async function (assert) { assert.expect(2); - const testAdapter = DS.RESTAdapter.extend({ + const testAdapter = RESTAdapter.extend({ shouldBackgroundReloadRecord() { assert.ok(false, 'shouldBackgroundReloadRecord should not be called when { backgroundReload: false }'); }, @@ -679,7 +668,7 @@ module('integration/store - findRecord', function (hooks) { test('store#findRecord { backgroundReload: true } returns cached record and reloads record in background', async function (assert) { assert.expect(2); - const testAdapter = DS.RESTAdapter.extend({ + const testAdapter = RESTAdapter.extend({ shouldBackgroundReloadRecord() { assert.ok(false, 'shouldBackgroundReloadRecord should not be called when { backgroundReload: true }'); }, @@ -729,7 +718,7 @@ module('integration/store - findRecord', function (hooks) { test('store#findRecord { backgroundReload: false } is ignored if adapter.shouldReloadRecord is true', async function (assert) { assert.expect(2); - const testAdapter = DS.RESTAdapter.extend({ + const testAdapter = RESTAdapter.extend({ shouldReloadRecord() { return true; }, @@ -893,7 +882,7 @@ module('integration/store - findAll', function (hooks) { assert.strictEqual(cars.length, 2, 'There is 2 cars in the store now'); - let mini = cars.findBy('id', '1'); + let mini = cars.find((car) => car.id === '1'); assert.strictEqual(mini.model, 'New Mini', 'Existing records have been updated'); }); @@ -901,7 +890,7 @@ module('integration/store - findAll', function (hooks) { test('store#findAll { backgroundReload: false } skips shouldBackgroundReloadAll, returns cached records & does not reload in the background', async function (assert) { assert.expect(4); - let testAdapter = DS.RESTAdapter.extend({ + let testAdapter = RESTAdapter.extend({ shouldBackgroundReloadAll() { assert.ok(false, 'shouldBackgroundReloadAll should not be called when { backgroundReload: false }'); }, @@ -931,20 +920,20 @@ module('integration/store - findAll', function (hooks) { let cars = await store.findAll('car', { backgroundReload: false }); assert.strictEqual(cars.length, 1, 'single cached car record is returned'); - assert.strictEqual(cars.firstObject.model, 'Mini', 'correct cached car record is returned'); + assert.strictEqual(cars[0].model, 'Mini', 'correct cached car record is returned'); await settled(); cars = store.peekAll('car'); assert.strictEqual(cars.length, 1, 'single cached car record is returned again'); - assert.strictEqual(cars.firstObject.model, 'Mini', 'correct cached car record is returned again'); + assert.strictEqual(cars[0].model, 'Mini', 'correct cached car record is returned again'); }); test('store#findAll { backgroundReload: true } skips shouldBackgroundReloadAll, returns cached records, & reloads in background', async function (assert) { assert.expect(5); - let testAdapter = DS.RESTAdapter.extend({ + let testAdapter = RESTAdapter.extend({ shouldBackgroundReloadAll() { assert.ok(false, 'shouldBackgroundReloadAll should not be called when { backgroundReload: true }'); }, @@ -990,21 +979,21 @@ module('integration/store - findAll', function (hooks) { let cars = await store.findAll('car', { backgroundReload: true }); assert.strictEqual(cars.length, 1, 'single cached car record is returned'); - assert.strictEqual(cars.firstObject.model, 'Mini', 'correct cached car record is returned'); + assert.strictEqual(cars[0].model, 'Mini', 'correct cached car record is returned'); await settled(); // IE11 hack cars = store.peekAll('car'); assert.strictEqual(cars.length, 2, 'multiple cars now in the store'); - assert.strictEqual(cars.firstObject.model, 'New Mini', 'existing record updated correctly'); - assert.strictEqual(cars.lastObject.model, 'Isetta', 'new record added to the store'); + assert.strictEqual(cars[0].model, 'New Mini', 'existing record updated correctly'); + assert.strictEqual(cars[cars.length - 1].model, 'Isetta', 'new record added to the store'); }); test('store#findAll { backgroundReload: false } is ignored if adapter.shouldReloadAll is true', async function (assert) { assert.expect(5); - let testAdapter = DS.RESTAdapter.extend({ + let testAdapter = RESTAdapter.extend({ shouldReloadAll() { return true; }, @@ -1052,13 +1041,13 @@ module('integration/store - findAll', function (hooks) { let cars = store.peekAll('car'); assert.strictEqual(cars.length, 1, 'one car in the store'); - assert.strictEqual(cars.firstObject.model, 'Mini', 'correct car is in the store'); + assert.strictEqual(cars.at(0).model, 'Mini', 'correct car is in the store'); cars = await store.findAll('car', { backgroundReload: false }); assert.strictEqual(cars.length, 2, 'multiple car records are returned'); - assert.strictEqual(cars.firstObject.model, 'New Mini', 'initial car record was updated'); - assert.strictEqual(cars.lastObject.model, 'Isetta', 'second car record was loaded'); + assert.strictEqual(cars.at(0).model, 'New Mini', 'initial car record was updated'); + assert.strictEqual(cars.at(-1).model, 'Isetta', 'second car record was loaded'); }); test('store#findAll should eventually return all known records even if they are not in the adapter response', async function (assert) { @@ -1118,7 +1107,7 @@ module('integration/store - findAll', function (hooks) { assert.strictEqual(cars.length, 2, 'It returns all cars'); - let mini = cars.findBy('id', '1'); + let mini = cars.find((car) => car.id === '1'); assert.strictEqual(mini.model, 'Mini', 'Records have not yet been updated'); resolvefindAll(); @@ -1129,7 +1118,7 @@ module('integration/store - findAll', function (hooks) { const peeked = store.peekAll('car'); assert.strictEqual(peeked, cars, 'findAll and peekAll result are the same'); - mini = cars.findBy('id', '1'); + mini = cars.find((car) => car.id === '1'); assert.strictEqual(mini.model, 'New Mini', 'Existing records have been updated'); }); @@ -1262,7 +1251,7 @@ module('integration/store - deleteRecord', function (hooks) { }); testInDebug('store#findRecord that returns an array should assert', async function (assert) { - const ApplicationAdapter = DS.JSONAPIAdapter.extend({ + const ApplicationAdapter = JSONAPIAdapter.extend({ findRecord() { return { data: [] }; }, @@ -1304,7 +1293,7 @@ module('integration/store - queryRecord', function (hooks) { hooks.beforeEach(function () { this.owner.register('model:car', Car); - this.owner.register('adapter:application', DS.Adapter.extend()); + this.owner.register('adapter:application', Adapter.extend()); this.owner.register('serializer:application', class extends JSONAPISerializer {}); }); diff --git a/packages/-ember-data/tests/integration/store/query-test.js b/packages/-ember-data/tests/integration/store/query-test.js index 1e44f30d51c..101b9803a67 100644 --- a/packages/-ember-data/tests/integration/store/query-test.js +++ b/packages/-ember-data/tests/integration/store/query-test.js @@ -1,4 +1,4 @@ -import { module, test } from 'qunit'; +import { module } from 'qunit'; import RSVP from 'rsvp'; import { setupTest } from 'ember-qunit'; @@ -6,6 +6,7 @@ import { setupTest } from 'ember-qunit'; import Adapter from '@ember-data/adapter'; import Model from '@ember-data/model'; import JSONAPISerializer from '@ember-data/serializer/json-api'; +import { deprecatedTest } from '@ember-data/unpublished-test-infra/test-support/deprecated-test'; module('integration/store/query', function (hooks) { setupTest(hooks); @@ -18,27 +19,31 @@ module('integration/store/query', function (hooks) { this.owner.register('serializer:application', class extends JSONAPISerializer {}); }); - test('meta is proxied correctly on the PromiseArray', async function (assert) { - let store = this.owner.lookup('service:store'); + deprecatedTest( + 'meta is proxied correctly on the PromiseArray', + { id: 'ember-data:deprecate-promise-proxies', until: '5.0', count: 2 }, + async function (assert) { + let store = this.owner.lookup('service:store'); - let defered = RSVP.defer(); + let defered = RSVP.defer(); - this.owner.register( - 'adapter:person', - Adapter.extend({ - query(store, type, query) { - return defered.promise; - }, - }) - ); + this.owner.register( + 'adapter:person', + Adapter.extend({ + query(store, type, query) { + return defered.promise; + }, + }) + ); - let result = store.query('person', {}); + let result = store.query('person', {}); - assert.notOk(result.meta?.foo, 'precond: meta is not yet set'); + assert.notOk(result.meta?.foo, 'precond: meta is not yet set'); - defered.resolve({ data: [], meta: { foo: 'bar' } }); - await result; + defered.resolve({ data: [], meta: { foo: 'bar' } }); + await result; - assert.strictEqual(result.meta.foo, 'bar'); - }); + assert.strictEqual(result.meta?.foo, 'bar', 'meta is now proxied'); + } + ); }); diff --git a/packages/-ember-data/tests/test-helper.js b/packages/-ember-data/tests/test-helper.js index 147cf2a3d4d..e14a8c6b475 100644 --- a/packages/-ember-data/tests/test-helper.js +++ b/packages/-ember-data/tests/test-helper.js @@ -21,7 +21,7 @@ if (window.Promise === undefined) { if (QUnit.urlParams.enableoptionalfeatures) { window.EmberDataENV = { ENABLE_OPTIONAL_FEATURES: true }; } - +QUnit.dump.maxDepth = 3; setup(QUnit.assert); configureAsserts(); diff --git a/packages/-ember-data/tests/unit/many-array-test.js b/packages/-ember-data/tests/unit/many-array-test.js index a2c324210b1..1540112978a 100644 --- a/packages/-ember-data/tests/unit/many-array-test.js +++ b/packages/-ember-data/tests/unit/many-array-test.js @@ -1,5 +1,3 @@ -import { run } from '@ember/runloop'; - import { module, test } from 'qunit'; import { resolve } from 'rsvp'; @@ -10,7 +8,7 @@ import Model, { attr, belongsTo, hasMany } from '@ember-data/model'; module('unit/many_array - ManyArray', function (hooks) { setupTest(hooks); - test('manyArray.save() calls save() on all records', function (assert) { + test('manyArray.save() calls save() on all records', async function (assert) { assert.expect(3); class Post extends Model { @@ -21,57 +19,54 @@ module('unit/many_array - ManyArray', function (hooks) { class Tag extends Model { @attr('string') name; @belongsTo('post', { async: false, inverse: 'tags' }) post; - - save() { - assert.ok(true, 'record.save() was called'); - return resolve(); - } } this.owner.register('model:post', Post); this.owner.register('model:tag', Tag); const store = this.owner.lookup('service:store'); + store.saveRecord = function (record) { + assert.ok(true, 'record.save() was called'); + return resolve(); + }; - return run(() => { - store.push({ - data: [ - { - type: 'tag', - id: '1', - attributes: { - name: 'Ember.js', - }, + store.push({ + data: [ + { + type: 'tag', + id: '1', + attributes: { + name: 'Ember.js', }, - { - type: 'tag', - id: '2', - attributes: { - name: 'Tomster', - }, + }, + { + type: 'tag', + id: '2', + attributes: { + name: 'Tomster', }, - { - type: 'post', - id: '3', - attributes: { - title: 'A framework for creating ambitious web applications', - }, - relationships: { - tags: { - data: [ - { type: 'tag', id: '1' }, - { type: 'tag', id: '2' }, - ], - }, + }, + { + type: 'post', + id: '3', + attributes: { + title: 'A framework for creating ambitious web applications', + }, + relationships: { + tags: { + data: [ + { type: 'tag', id: '1' }, + { type: 'tag', id: '2' }, + ], }, }, - ], - }); + }, + ], + }); - let post = store.peekRecord('post', 3); + let post = store.peekRecord('post', 3); - return post.tags.save().then(() => { - assert.ok(true, 'manyArray.save() promise resolved'); - }); + await post.tags.save().then(() => { + assert.ok(true, 'manyArray.save() promise resolved'); }); }); }); diff --git a/packages/-ember-data/tests/unit/model/errors-test.js b/packages/-ember-data/tests/unit/model/errors-test.js index 24688eca4b4..66eb35a1af2 100644 --- a/packages/-ember-data/tests/unit/model/errors-test.js +++ b/packages/-ember-data/tests/unit/model/errors-test.js @@ -54,16 +54,16 @@ module('unit/model/errors', function (hooks) { }); testInDebug('get error', function (assert) { - assert.ok(errors.firstObject === undefined, 'returns undefined'); + assert.ok(errors.objectAt(0) === undefined, 'returns undefined'); errors.trigger = assert.becameInvalid; errors.add('firstName', 'error'); errors.trigger = assert.unexpectedSend; assert.ok(errors.get('firstName').length === 1, 'returns errors'); - assert.deepEqual(errors.firstObject, { attribute: 'firstName', message: 'error' }); + assert.deepEqual(errors.objectAt(0), { attribute: 'firstName', message: 'error' }); errors.add('firstName', 'error2'); assert.ok(errors.get('firstName').length === 2, 'returns errors'); errors.add('lastName', 'error3'); - assert.deepEqual(errors.toArray(), [ + assert.deepEqual(errors.slice(), [ { attribute: 'firstName', message: 'error' }, { attribute: 'firstName', message: 'error2' }, { attribute: 'lastName', message: 'error3' }, diff --git a/packages/-ember-data/tests/unit/model/init-properties-test.js b/packages/-ember-data/tests/unit/model/init-properties-test.js index bac459f4fa9..711df9b2b35 100644 --- a/packages/-ember-data/tests/unit/model/init-properties-test.js +++ b/packages/-ember-data/tests/unit/model/init-properties-test.js @@ -64,10 +64,7 @@ module('unit/model - init properties', function (hooks) { assert.strictEqual(get(record, 'title'), 'My Post', 'Attrs are available as expected'); assert.strictEqual(get(record, 'randomProp'), 'An unknown prop', 'Unknown properties are available as expected'); assert.ok(get(record, 'author') instanceof types.Author, 'belongsTo relationships are available as expected'); - assert.ok( - get(record, 'comments.firstObject') instanceof types.Comment, - 'hasMany relationships are available as expected' - ); + assert.ok(record.comments.at(0) instanceof types.Comment, 'hasMany relationships are available as expected'); } let { store } = setupModels(this.owner, testState); @@ -109,10 +106,7 @@ module('unit/model - init properties', function (hooks) { function testState(types, record) { assert.strictEqual(get(record, 'title'), 'My Post', 'Attrs are available as expected'); assert.ok(get(record, 'author') instanceof types.Author, 'belongsTo relationships are available as expected'); - assert.ok( - get(record, 'comments.firstObject') instanceof types.Comment, - 'hasMany relationships are available as expected' - ); + assert.ok(record.comments.at(0) instanceof types.Comment, 'hasMany relationships are available as expected'); } let { store } = setupModels(this.owner, testState); @@ -160,10 +154,7 @@ module('unit/model - init properties', function (hooks) { function testState(types, record) { assert.strictEqual(get(record, 'title'), 'My Post', 'Attrs are available as expected'); assert.ok(get(record, 'author') instanceof types.Author, 'belongsTo relationships are available as expected'); - assert.ok( - get(record, 'comments.firstObject') instanceof types.Comment, - 'hasMany relationships are available as expected' - ); + assert.ok(record.comments.at(0) instanceof types.Comment, 'hasMany relationships are available as expected'); } let { adapter, store } = setupModels(this.owner, testState); @@ -213,10 +204,7 @@ module('unit/model - init properties', function (hooks) { function testState(types, record) { assert.strictEqual(get(record, 'title'), 'My Post', 'Attrs are available as expected'); assert.ok(get(record, 'author') instanceof types.Author, 'belongsTo relationships are available as expected'); - assert.ok( - get(record, 'comments.firstObject') instanceof types.Comment, - 'hasMany relationships are available as expected' - ); + assert.ok(record.comments.at(0) instanceof types.Comment, 'hasMany relationships are available as expected'); } let { adapter, store } = setupModels(this.owner, testState); diff --git a/packages/-ember-data/tests/unit/model/relationships/belongs-to-test.js b/packages/-ember-data/tests/unit/model/relationships/belongs-to-test.js index 4991d12e45a..69e3270a689 100644 --- a/packages/-ember-data/tests/unit/model/relationships/belongs-to-test.js +++ b/packages/-ember-data/tests/unit/model/relationships/belongs-to-test.js @@ -4,7 +4,6 @@ import { run } from '@ember/runloop'; import { module, test } from 'qunit'; import { Promise } from 'rsvp'; -import DS from 'ember-data'; import { setupTest } from 'ember-qunit'; import Adapter from '@ember-data/adapter'; @@ -587,8 +586,8 @@ module('unit/model/relationships - belongsTo', function (hooks) { .then((occupations) => { assert.strictEqual(get(occupations, 'length'), 2, 'the list of occupations should have the correct length'); - assert.strictEqual(get(occupations.objectAt(0), 'description'), 'fifth', 'the occupation is the fifth'); - assert.true(get(occupations.objectAt(0), 'isLoaded'), 'the occupation is now loaded'); + assert.strictEqual(get(occupations.at(0), 'description'), 'fifth', 'the occupation is the fifth'); + assert.true(get(occupations.at(0), 'isLoaded'), 'the occupation is now loaded'); }); }); }); @@ -830,11 +829,11 @@ module('unit/model/relationships - belongsTo', function (hooks) { }); }); - test('belongsTo should be async by default', function (assert) { - const Tag = Model.extend({ - name: attr('string'), - people: hasMany('person', { async: false, inverse: 'tag' }), - }); + test('belongsTo should be async by default', async function (assert) { + class Tag extends Model { + @attr name; + @hasMany('person', { async: false, inverse: 'tag' }) people; + } const Person = Model.extend({ name: attr('string'), @@ -846,8 +845,13 @@ module('unit/model/relationships - belongsTo', function (hooks) { let store = this.owner.lookup('service:store'); - let person = store.createRecord('person'); - - assert.ok(person.tag instanceof DS.PromiseObject, 'tag should be an async relationship'); + const theTag = store.push({ data: { type: 'tag', id: '1', attributes: { name: 'ember' } } }); + let person = store.createRecord('person', { tag: theTag }); + const personTag = person.tag; + assert.ok(personTag.then, 'tag should be an async relationship'); + const tag = await personTag; + assert.notStrictEqual(personTag, tag, 'we are not the proxy'); + assert.strictEqual(theTag, tag, 'we are the instance'); + assert.true(tag instanceof Tag, 'we are an instance of Tag'); }); }); diff --git a/packages/-ember-data/tests/unit/model/relationships/has-many-test.js b/packages/-ember-data/tests/unit/model/relationships/has-many-test.js index d317dc79e2a..7d17f85e493 100644 --- a/packages/-ember-data/tests/unit/model/relationships/has-many-test.js +++ b/packages/-ember-data/tests/unit/model/relationships/has-many-test.js @@ -1,4 +1,4 @@ -import EmberObject, { get, observer } from '@ember/object'; +import EmberObject, { get } from '@ember/object'; import { run } from '@ember/runloop'; import { settled } from '@ember/test-helpers'; @@ -9,8 +9,10 @@ import { setupTest } from 'ember-qunit'; import Adapter from '@ember-data/adapter'; import Model, { attr, belongsTo, hasMany } from '@ember-data/model'; -import { PromiseManyArray } from '@ember-data/model/-private'; +import { LEGACY_SUPPORT, PromiseManyArray } from '@ember-data/model/-private'; +import { DEPRECATE_ARRAY_LIKE } from '@ember-data/private-build-infra/deprecations'; import JSONAPISerializer from '@ember-data/serializer/json-api'; +import { deprecatedTest } from '@ember-data/unpublished-test-infra/test-support/deprecated-test'; import testInDebug from '@ember-data/unpublished-test-infra/test-support/test-in-debug'; import todo from '@ember-data/unpublished-test-infra/test-support/todo'; @@ -131,7 +133,7 @@ module('unit/model/relationships - hasMany', function (hooks) { let tags = get(person, 'tags'); assert.strictEqual(get(tags, 'length'), 1, 'the list of tags should have the correct length'); - assert.strictEqual(get(tags.objectAt(0), 'name'), 'friendly', 'the first tag should be a Tag'); + assert.strictEqual(get(tags.at(0), 'name'), 'friendly', 'the first tag should be a Tag'); run(() => { store.push({ @@ -157,12 +159,12 @@ module('unit/model/relationships - hasMany', function (hooks) { assert.strictEqual(get(get(person, 'tags'), 'length'), 2, 'the length is updated after new data is loaded'); assert.strictEqual( - get(person, 'tags').objectAt(0), - get(person, 'tags').objectAt(0), + get(person, 'tags').at(0), + get(person, 'tags').at(0), 'the returned object is always the same' ); assert.strictEqual( - get(person, 'tags').objectAt(0), + get(person, 'tags').at(0), store.peekRecord('tag', 5), 'relationship objects are the same as objects retrieved directly' ); @@ -211,7 +213,7 @@ module('unit/model/relationships - hasMany', function (hooks) { let pets = get(cyvid, 'pets'); assert.strictEqual(get(pets, 'length'), 1, 'the list of pets should have the correct length'); - assert.strictEqual(get(pets.objectAt(0), 'name'), 'fluffy', 'the first pet should be correct'); + assert.strictEqual(get(pets.at(0), 'name'), 'fluffy', 'the first pet should be correct'); run(() => { store.push({ @@ -302,7 +304,11 @@ module('unit/model/relationships - hasMany', function (hooks) { assert.ok(false, 'observer is not called'); }); - assert.deepEqual(tag.people.mapBy('name'), ['David J. Hamilton'], 'relationship is correct'); + assert.deepEqual( + tag.people.map((r) => r.name), + ['David J. Hamilton'], + 'relationship is correct' + ); }); }); @@ -778,7 +784,11 @@ module('unit/model/relationships - hasMany', function (hooks) { }); let eddy = store.peekRecord('person', 1); - assert.deepEqual(eddy.trueFriends.mapBy('name'), ['Edward II'], 'hasMany supports reflexive self-relationships'); + assert.deepEqual( + eddy.trueFriends.map((r) => r.name), + ['Edward II'], + 'hasMany supports reflexive self-relationships' + ); }); test('hasMany lazily loads async relationships', function (assert) { @@ -898,15 +908,11 @@ module('unit/model/relationships - hasMany', function (hooks) { }) .then((records) => { assert.strictEqual(get(records.tags, 'length'), 1, 'the list of tags should have the correct length'); - assert.strictEqual(get(records.tags.objectAt(0), 'name'), 'oohlala', 'the first tag should be a Tag'); + assert.strictEqual(get(records.tags.at(0), 'name'), 'oohlala', 'the first tag should be a Tag'); + assert.strictEqual(records.tags.at(0), records.tags.at(0), 'the returned object is always the same'); assert.strictEqual( - records.tags.objectAt(0), - records.tags.objectAt(0), - 'the returned object is always the same' - ); - assert.strictEqual( - records.tags.objectAt(0), + records.tags.at(0), store.peekRecord('tag', 12), 'relationship objects are the same as objects retrieved directly' ); @@ -915,7 +921,7 @@ module('unit/model/relationships - hasMany', function (hooks) { }) .then((tags) => { let newTag = store.createRecord('tag'); - tags.pushObject(newTag); + tags.push(newTag); }); }); }); @@ -1132,8 +1138,8 @@ module('unit/model/relationships - hasMany', function (hooks) { }) .then((tags) => { assert.strictEqual(get(tags, 'length'), 2, 'the tags object still exists'); - assert.strictEqual(get(tags.objectAt(0), 'name'), 'friendly', 'Tom Dale is now friendly'); - assert.true(get(tags.objectAt(0), 'isLoaded'), 'Tom Dale is now loaded'); + assert.strictEqual(get(tags.at(0), 'name'), 'friendly', 'Tom Dale is now friendly'); + assert.true(get(tags.at(0), 'isLoaded'), 'Tom Dale is now loaded'); }); }); }); @@ -1187,14 +1193,14 @@ module('unit/model/relationships - hasMany', function (hooks) { return run(() => { return store.findRecord('person', 1).then((person) => { - let tag = get(person, 'tags').objectAt(0); + let tag = get(person, 'tags').at(0); assert.strictEqual(get(tag, 'name'), 'ember', 'precond - relationships work'); tag = store.createRecord('tag', { name: 'js' }); - get(person, 'tags').pushObject(tag); + get(person, 'tags').push(tag); - assert.strictEqual(get(person, 'tags').objectAt(1), tag, 'newly added relationship works'); + assert.strictEqual(get(person, 'tags').at(1), tag, 'newly added relationship works'); }); }); }); @@ -1266,7 +1272,7 @@ module('unit/model/relationships - hasMany', function (hooks) { const person = store.peekRecord('person', '1'); const pets = run(() => person.pets); - const shen = pets.objectAt(0); + const shen = pets.at(0); const rambo = store.peekRecord('pet', '2'); const rebel = store.peekRecord('pet', '3'); @@ -1278,7 +1284,7 @@ module('unit/model/relationships - hasMany', function (hooks) { ); run(() => { - pets.pushObjects([rambo, rebel]); + pets.push(rambo, rebel); }); assert.deepEqual( @@ -1365,7 +1371,7 @@ module('unit/model/relationships - hasMany', function (hooks) { const person = store.peekRecord('person', '1'); const pets = run(() => person.pets); - const shen = pets.objectAt(0); + const shen = pets.at(0); const rebel = store.peekRecord('pet', '3'); assert.strictEqual(get(shen, 'name'), 'Shenanigans', 'precond - relationships work'); @@ -1376,7 +1382,7 @@ module('unit/model/relationships - hasMany', function (hooks) { ); run(() => { - pets.pushObjects([rebel]); + pets.push(rebel); }); assert.deepEqual( @@ -1481,21 +1487,19 @@ module('unit/model/relationships - hasMany', function (hooks) { }); const person = store.peekRecord('person', '1'); - const pets = run(() => person.pets); + const pets = person.pets; - const shen = pets.objectAt(0); + const shen = pets.at(0); const rebel = store.peekRecord('pet', '3'); - assert.strictEqual(get(shen, 'name'), 'Shenanigans', 'precond - relationships work'); + assert.strictEqual(shen.name, 'Shenanigans', 'precond - relationships work'); assert.deepEqual( - pets.map((p) => get(p, 'id')), + pets.map((p) => p.id), ['1', '3'], 'precond - relationship has the correct pets to start' ); - run(() => { - pets.removeObject(rebel); - }); + pets.splice(pets.indexOf(rebel), 1); assert.deepEqual( pets.map((p) => get(p, 'id')), @@ -1603,7 +1607,7 @@ module('unit/model/relationships - hasMany', function (hooks) { const petsProxy = run(() => person.pets); return petsProxy.then((pets) => { - const shen = pets.objectAt(0); + const shen = pets.at(0); const rambo = store.peekRecord('pet', '2'); const rebel = store.peekRecord('pet', '3'); @@ -1615,7 +1619,7 @@ module('unit/model/relationships - hasMany', function (hooks) { ); assert.strictEqual(get(petsProxy, 'length'), 1, 'precond - proxy has only one pet to start'); - pets.pushObjects([rambo, rebel]); + pets.push(rambo, rebel); assert.deepEqual( pets.map((p) => get(p, 'id')), @@ -1873,7 +1877,7 @@ module('unit/model/relationships - hasMany', function (hooks) { }); }); - test('hasMany.firstObject.unloadRecord should not break that hasMany', function (assert) { + test('hasMany.at(0).unloadRecord should not break that hasMany', function (assert) { const Person = Model.extend({ cars: hasMany('car', { async: false, inverse: null }), name: attr(), @@ -1926,7 +1930,7 @@ module('unit/model/relationships - hasMany', function (hooks) { assert.strictEqual(cars.length, 2); run(() => { - cars.firstObject.unloadRecord(); + cars.at(0).unloadRecord(); assert.strictEqual(cars.length, 1); // unload now.. assert.strictEqual(person.cars.length, 1); // unload now.. }); @@ -2041,7 +2045,7 @@ module('unit/model/relationships - hasMany', function (hooks) { ); run(() => { - pets.pushObjects([rebel]); + pets.push(rebel); }); assert.deepEqual( @@ -2063,84 +2067,7 @@ module('unit/model/relationships - hasMany', function (hooks) { }); test('possible to replace items in a relationship using setObjects w/ Ember Enumerable Array/Object as the argument (GH-2533)', function (assert) { - assert.expect(2); - - const Tag = Model.extend({ - name: attr('string'), - person: belongsTo('person', { async: false, inverse: 'tags' }), - }); - - const Person = Model.extend({ - name: attr('string'), - tags: hasMany('tag', { async: false, inverse: 'person' }), - }); - - this.owner.register('model:tag', Tag); - this.owner.register('model:person', Person); - - let store = this.owner.lookup('service:store'); - - run(() => { - store.push({ - data: [ - { - type: 'person', - id: '1', - attributes: { - name: 'Tom Dale', - }, - relationships: { - tags: { - data: [{ type: 'tag', id: '1' }], - }, - }, - }, - { - type: 'person', - id: '2', - attributes: { - name: 'Sylvain Mina', - }, - relationships: { - tags: { - data: [{ type: 'tag', id: '2' }], - }, - }, - }, - { - type: 'tag', - id: '1', - attributes: { - name: 'ember', - }, - }, - { - type: 'tag', - id: '2', - attributes: { - name: 'ember-data', - }, - }, - ], - }); - }); - - let tom, sylvain; - - run(() => { - tom = store.peekRecord('person', '1'); - sylvain = store.peekRecord('person', '2'); - // Test that since sylvain.tags instanceof ManyArray, - // adding records on Relationship iterates correctly. - tom.tags.setObjects(sylvain.tags); - }); - - assert.strictEqual(tom.tags.length, 1); - assert.strictEqual(tom.tags.firstObject, store.peekRecord('tag', 2)); - }); - - test('Replacing `has-many` with non-array will throw assertion', function (assert) { - assert.expect(1); + assert.expect(DEPRECATE_ARRAY_LIKE ? 3 : 2); const Tag = Model.extend({ name: attr('string'), @@ -2171,6 +2098,18 @@ module('unit/model/relationships - hasMany', function (hooks) { }, }, }, + { + type: 'person', + id: '2', + attributes: { + name: 'Sylvain Mina', + }, + relationships: { + tags: { + data: [{ type: 'tag', id: '2' }], + }, + }, + }, { type: 'tag', id: '1', @@ -2189,34 +2128,42 @@ module('unit/model/relationships - hasMany', function (hooks) { }); let tom = store.peekRecord('person', '1'); - let tag = store.peekRecord('tag', '2'); - assert.expectAssertion(() => { - tom.tags.setObjects(tag); - }, /The third argument to replace needs to be an array./); + let sylvain = store.peekRecord('person', '2'); + // Test that since sylvain.tags instanceof ManyArray, + // adding records on Relationship iterates correctly. + if (DEPRECATE_ARRAY_LIKE) { + tom.tags.setObjects(sylvain.tags); + assert.expectDeprecation({ id: 'ember-data:deprecate-array-like' }); + } else { + tom.tags.length = 0; + tom.tags.push(...sylvain.tags); + } + + assert.strictEqual(tom.tags.length, 1); + assert.strictEqual(tom.tags.at(0), store.peekRecord('tag', 2)); }); - test('it is possible to remove an item from a relationship', function (assert) { - assert.expect(2); + deprecatedTest( + 'Replacing `has-many` with non-array will throw assertion', + { id: 'ember-data:deprecate-array-like', until: '5.0' }, + function (assert) { + assert.expect(1); - const Tag = Model.extend({ - name: attr('string'), - person: belongsTo('person', { async: false, inverse: 'tags' }), - }); - - const Person = Model.extend({ - name: attr('string'), - tags: hasMany('tag', { async: false, inverse: 'person' }), - }); + const Tag = Model.extend({ + name: attr('string'), + person: belongsTo('person', { async: false, inverse: 'tags' }), + }); - this.owner.register('model:tag', Tag); - this.owner.register('model:person', Person); + const Person = Model.extend({ + name: attr('string'), + tags: hasMany('tag', { async: false, inverse: 'person' }), + }); - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + this.owner.register('model:tag', Tag); + this.owner.register('model:person', Person); - adapter.shouldBackgroundReloadRecord = () => false; + let store = this.owner.lookup('service:store'); - run(() => { store.push({ data: [ { @@ -2238,21 +2185,77 @@ module('unit/model/relationships - hasMany', function (hooks) { name: 'ember', }, }, + { + type: 'tag', + id: '2', + attributes: { + name: 'ember-data', + }, + }, ], }); + + let tom = store.peekRecord('person', '1'); + let tag = store.peekRecord('tag', '2'); + assert.expectAssertion(() => { + tom.tags.setObjects(tag); + }, /Assertion Failed: ManyArray.setObjects expects to receive an array as its argument/); + } + ); + + test('it is possible to remove an item from a relationship', async function (assert) { + assert.expect(2); + + const Tag = Model.extend({ + name: attr('string'), + person: belongsTo('person', { async: false, inverse: 'tags' }), }); - return run(() => { - return store.findRecord('person', 1).then((person) => { - let tag = get(person, 'tags').objectAt(0); + const Person = Model.extend({ + name: attr('string'), + tags: hasMany('tag', { async: false, inverse: 'person' }), + }); - assert.strictEqual(get(tag, 'name'), 'ember', 'precond - relationships work'); + this.owner.register('model:tag', Tag); + this.owner.register('model:person', Person); - run(() => get(person, 'tags').removeObject(tag)); + let store = this.owner.lookup('service:store'); + let adapter = store.adapterFor('application'); - assert.strictEqual(get(person, 'tags.length'), 0, 'object is removed from the relationship'); - }); + adapter.shouldBackgroundReloadRecord = () => false; + + const person = store.push({ + data: { + type: 'person', + id: '1', + attributes: { + name: 'Tom Dale', + }, + relationships: { + tags: { + data: [{ type: 'tag', id: '1' }], + }, + }, + }, + + included: [ + { + type: 'tag', + id: '1', + attributes: { + name: 'ember', + }, + }, + ], }); + + let tag = person.tags.at(0); + + assert.strictEqual(tag.name, 'ember', 'precond - relationships work'); + + person.tags.splice(0, 1); + + assert.strictEqual(person.tags.length, 0, 'object is removed from the relationship'); }); test('it is possible to add an item to a relationship, remove it, then add it again', function (assert) { @@ -2275,25 +2278,25 @@ module('unit/model/relationships - hasMany', function (hooks) { let tag1 = store.createRecord('tag'); let tag2 = store.createRecord('tag'); let tag3 = store.createRecord('tag'); - let tags = get(person, 'tags'); + let tags = person.tags; run(() => { - tags.pushObjects([tag1, tag2, tag3]); - tags.removeObject(tag2); + tags.push(tag1, tag2, tag3); + tags.splice(tags.indexOf(tag2), 1); }); - assert.strictEqual(tags.objectAt(0), tag1); - assert.strictEqual(tags.objectAt(1), tag3); - assert.strictEqual(get(person, 'tags.length'), 2, 'object is removed from the relationship'); + assert.strictEqual(tags.at(0), tag1); + assert.strictEqual(tags.at(1), tag3); + assert.strictEqual(person.tags.length, 2, 'object is removed from the relationship'); run(() => { - tags.insertAt(0, tag2); + tags.unshift(tag2); }); - assert.strictEqual(get(person, 'tags.length'), 3, 'object is added back to the relationship'); - assert.strictEqual(tags.objectAt(0), tag2); - assert.strictEqual(tags.objectAt(1), tag1); - assert.strictEqual(tags.objectAt(2), tag3); + assert.strictEqual(person.tags.length, 3, 'object is added back to the relationship'); + assert.strictEqual(tags.at(0), tag2); + assert.strictEqual(tags.at(1), tag1); + assert.strictEqual(tags.at(2), tag3); }); test('hasMany is async by default', function (assert) { @@ -2592,7 +2595,7 @@ module('unit/model/relationships - hasMany', function (hooks) { const user = await store.findRecord('post-author', '1'); const posts = await user.posts; assert.strictEqual(posts.length, 2, 'we loaded two posts'); - const firstPost = posts.objectAt(0); + const firstPost = posts.at(0); const firstPostCommentsPromise = firstPost.comments; const originalPromise = firstPostCommentsPromise.promise; firstPost.comments; // trigger an extra access @@ -2610,9 +2613,6 @@ module('unit/model/relationships - hasMany', function (hooks) { const Tag = Model.extend({ name: attr('string'), people: hasMany('person', { async: true, inverse: 'tag' }), - peopleDidChange: observer('people.@each', function () { - peopleDidChange++; - }), }); const Person = Model.extend({ @@ -2625,30 +2625,33 @@ module('unit/model/relationships - hasMany', function (hooks) { let store = this.owner.lookup('service:store'); let tag = store.createRecord('tag'); - // TODO replace with a test that checks for wherever the new ManyArray location is - //let hasManyRelationship = tag.hasMany('people').hasManyRelationship; - - //assert.ok(!hasManyRelationship._manyArray); + tag.hasMany('people').hasManyRelationship; + const support = LEGACY_SUPPORT.get(tag); + const sync = support._syncArray; + support._syncArray = function () { + peopleDidChange++; + return sync.apply(this, arguments); + }; - assert.strictEqual(peopleDidChange, 0, 'expect people hasMany to not emit a change event (before access)'); - tag.people; // access async relationship - assert.strictEqual(peopleDidChange, 0, 'expect people hasMany to not emit a change event (sync after access)'); + const peoplePromise = tag.people; // access async relationship + assert.strictEqual(peopleDidChange, 0, 'expect hasMany to initially populate'); - await settled(); + let people = await peoplePromise; - assert.strictEqual( - peopleDidChange, - 0, - 'expect people hasMany to not emit a change event (after access, but after the current run loop)' - ); - //assert.ok(hasManyRelationship._manyArray instanceof ManyArray); + assert.strictEqual(peopleDidChange, 0, 'expect hasMany to not sync before access after fetch completes'); + assert.strictEqual(people.length, 0, 'we have the right number of people'); + assert.strictEqual(peopleDidChange, 1, 'expect people hasMany to dirty after fetch completes'); let person = store.createRecord('person'); - assert.strictEqual(peopleDidChange, 0, 'expect people hasMany to not emit a change event (before access)'); - const people = await tag.people; - people.addObject(person); - assert.strictEqual(peopleDidChange, 1, 'expect people hasMany to have changed exactly once'); + assert.strictEqual(peopleDidChange, 1, 'expect people hasMany to not sync before access'); + people = await tag.people; + assert.strictEqual(people.length, 0, 'we have the right number of people'); + assert.strictEqual(peopleDidChange, 1, 'expect people hasMany to not sync before access'); + people.push(person); + assert.strictEqual(peopleDidChange, 1, 'expect hasMany to not sync after push of new related data'); + assert.strictEqual(people.length, 1, 'we have the right number of people'); + assert.strictEqual(peopleDidChange, 2, 'expect hasMany to sync on access after push'); }); test('fetch hasMany loads full relationship after a parent and child have been loaded', function (assert) { @@ -2750,34 +2753,39 @@ module('unit/model/relationships - hasMany', function (hooks) { }); }); - testInDebug('checks if passed array only contains instances of Model', function (assert) { - const Person = Model.extend(); - const Tag = Model.extend({ - people: hasMany('person', { async: true, inverse: null }), - }); + deprecatedTest( + 'checks if passed array only contains instances of Model', + { id: 'ember-data:deprecate-promise-proxies', count: 4, until: '5.0' }, + async function (assert) { + const Person = Model.extend(); + const Tag = Model.extend({ + people: hasMany('person', { async: true, inverse: null }), + }); - this.owner.register('model:tag', Tag); - this.owner.register('model:person', Person); + this.owner.register('model:tag', Tag); + this.owner.register('model:person', Person); - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + let store = this.owner.lookup('service:store'); + let adapter = store.adapterFor('application'); - adapter.findRecord = function () { - return { - data: { - type: 'person', - id: '1', - }, + adapter.findRecord = function () { + return { + data: { + type: 'person', + id: '1', + }, + }; }; - }; - let tag = store.createRecord('tag'); - let person = run(() => store.findRecord('person', 1)); + let tag = store.createRecord('tag'); + let person = store.findRecord('person', '1'); + await person; + + tag.people = [person]; - run(() => { assert.expectAssertion(() => { - tag.set('people', [person]); + tag.people = [person, {}]; }, /All elements of a hasMany relationship must be instances of Model/); - }); - }); + } + ); }); diff --git a/packages/-ember-data/tests/unit/model/relationships/record-array-test.js b/packages/-ember-data/tests/unit/model/relationships/record-array-test.js index 1e728bae973..8d0aa8ab456 100644 --- a/packages/-ember-data/tests/unit/model/relationships/record-array-test.js +++ b/packages/-ember-data/tests/unit/model/relationships/record-array-test.js @@ -1,77 +1,25 @@ -import { A } from '@ember/array'; -import { get, set } from '@ember/object'; +import { get } from '@ember/object'; import { module, test } from 'qunit'; -import DS from 'ember-data'; import { setupTest } from 'ember-qunit'; -import { recordIdentifierFor } from '@ember-data/store'; +import Model, { attr, belongsTo, hasMany } from '@ember-data/model'; module('unit/model/relationships - RecordArray', function (hooks) { setupTest(hooks); - test('updating the content of a RecordArray updates its content', async function (assert) { - let Tag = DS.Model.extend({ - name: DS.attr('string'), - }); - - this.owner.register('model:tag', Tag); - - let store = this.owner.lookup('service:store'); - let tags; - - let records = store.push({ - data: [ - { - type: 'tag', - id: '5', - attributes: { - name: 'friendly', - }, - }, - { - type: 'tag', - id: '2', - attributes: { - name: 'smarmy', - }, - }, - { - type: 'tag', - id: '12', - attributes: { - name: 'oohlala', - }, - }, - ], - }); - tags = DS.RecordArray.create({ - content: A(records.map((r) => recordIdentifierFor(r)).slice(0, 2)), - store: store, - modelName: 'tag', - }); - - let tag = tags.objectAt(0); - assert.strictEqual(get(tag, 'name'), 'friendly', `precond - we're working with the right tags`); - - set(tags, 'content', A(records.map(recordIdentifierFor).slice(1, 3))); - - tag = tags.objectAt(0); - assert.strictEqual(get(tag, 'name'), 'smarmy', 'the lookup was updated'); - }); - test('can create child record from a hasMany relationship', async function (assert) { assert.expect(3); - const Tag = DS.Model.extend({ - name: DS.attr('string'), - person: DS.belongsTo('person', { async: false, inverse: 'tags' }), + const Tag = Model.extend({ + name: attr('string'), + person: belongsTo('person', { async: false, inverse: 'tags' }), }); - const Person = DS.Model.extend({ - name: DS.attr('string'), - tags: DS.hasMany('tag', { async: false, inverse: 'person' }), + const Person = Model.extend({ + name: attr('string'), + tags: hasMany('tag', { async: false, inverse: 'person' }), }); this.owner.register('model:tag', Tag); @@ -97,6 +45,6 @@ module('unit/model/relationships - RecordArray', function (hooks) { assert.strictEqual(get(person, 'name'), 'Tom Dale', 'precond - retrieves person record from store'); assert.strictEqual(get(person, 'tags.length'), 1, 'tag is added to the parent record'); - assert.strictEqual(get(person, 'tags').objectAt(0).name, 'cool', 'tag values are passed along'); + assert.strictEqual(get(person, 'tags').at(0).name, 'cool', 'tag values are passed along'); }); }); diff --git a/packages/-ember-data/tests/unit/model/rollback-attributes-test.js b/packages/-ember-data/tests/unit/model/rollback-attributes-test.js index dd0a0940694..2d3549768e6 100644 --- a/packages/-ember-data/tests/unit/model/rollback-attributes-test.js +++ b/packages/-ember-data/tests/unit/model/rollback-attributes-test.js @@ -195,7 +195,7 @@ module('unit/model/rollbackAttributes - model.rollbackAttributes()', function (h run(() => person.deleteRecord()); assert.strictEqual(people.length, 1, 'a deleted record appears in record array until it is saved'); - assert.strictEqual(people.objectAt(0), person, 'a deleted record appears in record array until it is saved'); + assert.strictEqual(people.at(0), person, 'a deleted record appears in record array until it is saved'); return run(() => { return person @@ -341,7 +341,7 @@ module('unit/model/rollbackAttributes - model.rollbackAttributes()', function (h }); assert.strictEqual(people.length, 1, 'a deleted record appears in the record array until it is saved'); - assert.strictEqual(people.objectAt(0), person, 'a deleted record appears in the record array until it is saved'); + assert.strictEqual(people.at(0), person, 'a deleted record appears in the record array until it is saved'); assert.true(person.isDeleted, 'must be deleted'); diff --git a/packages/-ember-data/tests/unit/record-arrays/adapter-populated-record-array-test.js b/packages/-ember-data/tests/unit/record-arrays/adapter-populated-record-array-test.js index 7ed43dea230..70c457c1af9 100644 --- a/packages/-ember-data/tests/unit/record-arrays/adapter-populated-record-array-test.js +++ b/packages/-ember-data/tests/unit/record-arrays/adapter-populated-record-array-test.js @@ -1,15 +1,11 @@ -import { A } from '@ember/array'; -import Evented from '@ember/object/evented'; - -import { module, test } from 'qunit'; +import { module, skip, test } from 'qunit'; import RSVP from 'rsvp'; -import DS from 'ember-data'; import { setupTest } from 'ember-qunit'; import Model, { attr } from '@ember-data/model'; - -const { AdapterPopulatedRecordArray, RecordArrayManager } = DS; +import { AdapterPopulatedRecordArray, RecordArrayManager, SOURCE } from '@ember-data/store/-private'; +import testInDebug from '@ember-data/unpublished-test-infra/test-support/test-in-debug'; class Tag extends Model { @attr() @@ -20,28 +16,27 @@ module('unit/record-arrays/adapter-populated-record-array - DS.AdapterPopulatedR setupTest(hooks); test('default initial state', async function (assert) { - let recordArray = AdapterPopulatedRecordArray.create({ - modelName: 'recordType', + let recordArray = new AdapterPopulatedRecordArray({ + type: 'recordType', isLoaded: false, - content: A(), + identifiers: [], store: null, }); assert.false(recordArray.isLoaded, 'expected isLoaded to be false'); assert.strictEqual(recordArray.modelName, 'recordType', 'has modelName'); - assert.deepEqual(recordArray.content, [], 'has no content'); + assert.deepEqual(recordArray.slice(), [], 'has no content'); assert.strictEqual(recordArray.query, null, 'no query'); assert.strictEqual(recordArray.store, null, 'no store'); assert.strictEqual(recordArray.links, null, 'no links'); }); test('custom initial state', async function (assert) { - let content = A([]); let store = {}; - let recordArray = AdapterPopulatedRecordArray.create({ - modelName: 'apple', + let recordArray = new AdapterPopulatedRecordArray({ + type: 'apple', isLoaded: true, - content, + identifiers: ['1'], store, query: 'some-query', links: 'foo', @@ -49,22 +44,23 @@ module('unit/record-arrays/adapter-populated-record-array - DS.AdapterPopulatedR assert.true(recordArray.isLoaded); assert.false(recordArray.isUpdating); assert.strictEqual(recordArray.modelName, 'apple'); - assert.deepEqual(recordArray.content, content); + assert.deepEqual(recordArray[SOURCE].slice(), ['1']); assert.strictEqual(recordArray.store, store); assert.strictEqual(recordArray.query, 'some-query'); assert.strictEqual(recordArray.links, 'foo'); }); - test('#replace() throws error', function (assert) { - let recordArray = AdapterPopulatedRecordArray.create({ modelName: 'recordType' }); + testInDebug('#replace() throws error', function (assert) { + let recordArray = new AdapterPopulatedRecordArray({ type: 'recordType', identifiers: [] }); assert.throws( () => { recordArray.replace(); }, - Error('The result of a server query (on recordType) is immutable.'), + Error('Assertion Failed: Mutating this array of records via splice is not allowed.'), 'throws error' ); + assert.expectDeprecation({ id: 'ember-data:deprecate-array-like' }); }); test('#update uses _update enabling query specific behavior', async function (assert) { @@ -82,10 +78,10 @@ module('unit/record-arrays/adapter-populated-record-array - DS.AdapterPopulatedR }, }; - let recordArray = AdapterPopulatedRecordArray.create({ - modelName: 'recordType', + let recordArray = new AdapterPopulatedRecordArray({ + type: 'recordType', store, - content: A(), + identifiers: [], isLoaded: true, query: 'some-query', }); @@ -97,7 +93,7 @@ module('unit/record-arrays/adapter-populated-record-array - DS.AdapterPopulatedR let updateResult = recordArray.update(); assert.strictEqual(queryCalled, 1); - const expectedResult = A(); + const expectedResult = []; deferred.resolve(expectedResult); assert.true(recordArray.isUpdating, 'should be updating'); @@ -107,7 +103,7 @@ module('unit/record-arrays/adapter-populated-record-array - DS.AdapterPopulatedR assert.false(recordArray.isUpdating, 'should no longer be updating'); }); - test('change events when receiving a new query payload', async function (assert) { + skip('change events when receiving a new query payload', async function (assert) { assert.expect(29); let arrayDidChange = 0; @@ -119,10 +115,10 @@ module('unit/record-arrays/adapter-populated-record-array - DS.AdapterPopulatedR let manager = new RecordArrayManager({ store, }); - let recordArray = AdapterPopulatedRecordArray.extend(Evented).create({ + let recordArray = new AdapterPopulatedRecordArray({ query: 'some-query', manager, - content: A(), + identifiers: [], store, }); diff --git a/packages/-ember-data/tests/unit/record-arrays/record-array-test.js b/packages/-ember-data/tests/unit/record-arrays/record-array-test.js index 16f21d75f17..abb72dd308f 100644 --- a/packages/-ember-data/tests/unit/record-arrays/record-array-test.js +++ b/packages/-ember-data/tests/unit/record-arrays/record-array-test.js @@ -1,17 +1,12 @@ -import { A } from '@ember/array'; -import { get } from '@ember/object'; - import { module, test } from 'qunit'; -import RSVP, { resolve } from 'rsvp'; +import RSVP from 'rsvp'; -import DS from 'ember-data'; import { setupTest } from 'ember-qunit'; import Model, { attr } from '@ember-data/model'; import { recordIdentifierFor } from '@ember-data/store'; -import { SnapshotRecordArray } from '@ember-data/store/-private'; - -const { RecordArray } = DS; +import { RecordArray, SnapshotRecordArray, SOURCE } from '@ember-data/store/-private'; +import testInDebug from '@ember-data/unpublished-test-infra/test-support/test-in-debug'; class Tag extends Model { @attr @@ -22,44 +17,53 @@ module('unit/record-arrays/record-array - DS.RecordArray', function (hooks) { setupTest(hooks); test('default initial state', async function (assert) { - let recordArray = RecordArray.create({ modelName: 'recordType', isLoaded: false, store: null }); + let recordArray = new RecordArray({ type: 'recordType', identifiers: [], store: null }); - assert.false(get(recordArray, 'isLoaded'), 'record is not loaded'); - assert.false(get(recordArray, 'isUpdating'), 'record is not updating'); - assert.strictEqual(get(recordArray, 'modelName'), 'recordType', 'has modelName'); - assert.strictEqual(get(recordArray, 'content'), null, 'content is not defined'); - assert.strictEqual(get(recordArray, 'store'), null, 'no store with recordArray'); + assert.false(recordArray.isUpdating, 'record is not updating'); + assert.strictEqual(recordArray.modelName, 'recordType', 'has modelName'); + assert.deepEqual(recordArray[SOURCE], [], 'content is not defined'); + assert.strictEqual(recordArray.store, null, 'no store with recordArray'); }); test('custom initial state', async function (assert) { - let content = A(); let store = {}; - let recordArray = RecordArray.create({ - modelName: 'apple', - isLoaded: true, - content, + let recordArray = new RecordArray({ + type: 'apple', + identifiers: [], store, }); - assert.true(get(recordArray, 'isLoaded')); - assert.false(get(recordArray, 'isUpdating')); // cannot set as default value: - assert.strictEqual(get(recordArray, 'modelName'), 'apple'); - assert.deepEqual(get(recordArray, 'content'), content); - assert.strictEqual(get(recordArray, 'store'), store); + assert.false(recordArray.isUpdating); // cannot set as default value: + assert.strictEqual(recordArray.modelName, 'apple'); + assert.deepEqual(recordArray[SOURCE], []); + assert.strictEqual(recordArray.store, store); }); - test('#replace() throws error', async function (assert) { - let recordArray = RecordArray.create({ modelName: 'recordType' }); + testInDebug('#replace() throws error', async function (assert) { + let recordArray = new RecordArray({ identifiers: [], type: 'recordType' }); assert.throws( () => { recordArray.replace(); }, - Error('The result of a server query (for all recordType types) is immutable. To modify contents, use toArray()'), + Error('Assertion Failed: Mutating this array of records via splice is not allowed.'), 'throws error' ); + assert.expectDeprecation({ id: 'ember-data:deprecate-array-like' }); }); - test('#objectAtContent', async function (assert) { + testInDebug('Mutation throws error', async function (assert) { + let recordArray = new RecordArray({ identifiers: [], type: 'recordType' }); + + assert.throws( + () => { + recordArray.splice(0, 1); + }, + Error('Assertion Failed: Mutating this array of records via splice is not allowed.'), + 'throws error' + ); + }); + + test('#access by index', async function (assert) { this.owner.register('model:tag', Tag); let store = this.owner.lookup('service:store'); @@ -80,17 +84,17 @@ module('unit/record-arrays/record-array - DS.RecordArray', function (hooks) { ], }); - let recordArray = RecordArray.create({ - modelName: 'recordType', - content: A(records.map((r) => recordIdentifierFor(r))), + let recordArray = new RecordArray({ + type: 'recordType', + identifiers: records.map(recordIdentifierFor), store, }); - assert.strictEqual(get(recordArray, 'length'), 3); - assert.strictEqual(recordArray.objectAtContent(0).id, '1'); - assert.strictEqual(recordArray.objectAtContent(1).id, '3'); - assert.strictEqual(recordArray.objectAtContent(2).id, '5'); - assert.strictEqual(recordArray.objectAtContent(3), undefined); + assert.strictEqual(recordArray.length, 3); + assert.strictEqual(recordArray[0].id, '1'); + assert.strictEqual(recordArray[1].id, '3'); + assert.strictEqual(recordArray[2].id, '5'); + assert.strictEqual(recordArray[3], undefined); }); test('#update', async function (assert) { @@ -106,12 +110,13 @@ module('unit/record-arrays/record-array - DS.RecordArray', function (hooks) { }, }; - let recordArray = RecordArray.create({ - modelName: 'recordType', + let recordArray = new RecordArray({ + type: 'recordType', + identifiers: [], store, }); - assert.false(get(recordArray, 'isUpdating'), 'should not yet be updating'); + assert.false(recordArray.isUpdating, 'should not yet be updating'); assert.strictEqual(findAllCalled, 0); @@ -121,11 +126,11 @@ module('unit/record-arrays/record-array - DS.RecordArray', function (hooks) { deferred.resolve('return value'); - assert.true(get(recordArray, 'isUpdating'), 'should be updating'); + assert.true(recordArray.isUpdating, 'should be updating'); return updateResult.then((result) => { assert.strictEqual(result, 'return value'); - assert.false(get(recordArray, 'isUpdating'), 'should no longer be updating'); + assert.false(recordArray.isUpdating, 'should no longer be updating'); }); }); @@ -139,12 +144,13 @@ module('unit/record-arrays/record-array - DS.RecordArray', function (hooks) { }, }; - let recordArray = RecordArray.create({ - modelName: { modelName: 'recordType' }, + let recordArray = new RecordArray({ + type: 'recordType', + identifiers: [], store, }); - assert.false(get(recordArray, 'isUpdating'), 'should not be updating'); + assert.false(recordArray.isUpdating, 'should not be updating'); assert.strictEqual(findAllCalled, 0); let updateResult1 = recordArray.update(); @@ -159,79 +165,12 @@ module('unit/record-arrays/record-array - DS.RecordArray', function (hooks) { deferred.resolve('return value'); - assert.true(get(recordArray, 'isUpdating'), 'should be updating'); + assert.true(recordArray.isUpdating, 'should be updating'); return updateResult1.then((result) => { assert.strictEqual(result, 'return value'); - assert.false(get(recordArray, 'isUpdating'), 'should no longer be updating'); - }); - }); - - test('#_updateState', async function (assert) { - let content = A(); - let recordArray = RecordArray.create({ - content, + assert.false(recordArray.isUpdating, 'should no longer be updating'); }); - - let model1 = { lid: '@lid:model-1' }; - let model2 = { lid: '@lid:model-2' }; - let model3 = { lid: '@lid:model-3' }; - - assert.strictEqual(recordArray.content.length, 0); - assert.strictEqual( - recordArray._updateState(new Map([[model1, 'del']])), - undefined, - '_updateState has no return value' - ); - assert.deepEqual(recordArray.content, [], 'now contains no models'); - - recordArray._updateState( - new Map([ - [model1, 'add'], - [model2, 'add'], - ]) - ); - - assert.deepEqual(recordArray.content, [model1, model2], 'now contains model1, model2,'); - assert.strictEqual( - recordArray._updateState(new Map([[model1, 'del']])), - undefined, - '_updateState has no return value' - ); - assert.deepEqual(recordArray.content, [model2], 'now only contains model2'); - assert.strictEqual( - recordArray._updateState(new Map([[model2, 'del']])), - undefined, - '_updateState has no return value' - ); - assert.deepEqual(recordArray.content, [], 'now contains no models'); - - recordArray._updateState( - new Map([ - [model1, 'add'], - [model2, 'add'], - [model3, 'add'], - ]) - ); - - assert.strictEqual( - recordArray._updateState( - new Map([ - [model1, 'del'], - [model3, 'del'], - ]) - ), - undefined, - '_updateState has no return value' - ); - - assert.deepEqual(recordArray.content, [model2], 'now contains model2'); - assert.strictEqual( - recordArray._updateState(new Map([[model2, 'del']])), - undefined, - '_updateState has no return value' - ); - assert.deepEqual(recordArray.content, [], 'now contains no models'); }); test('#save', async function (assert) { @@ -250,22 +189,18 @@ module('unit/record-arrays/record-array - DS.RecordArray', function (hooks) { let [record1, record2] = store.push({ data: [model1, model2], }); - let identifiers = A([recordIdentifierFor(record1), recordIdentifierFor(record2)]); - let recordArray = RecordArray.create({ - content: identifiers, + let identifiers = [recordIdentifierFor(record1), recordIdentifierFor(record2)]; + let recordArray = new RecordArray({ + identifiers, store, }); - record1.save = () => { - model1Saved++; - return resolve(this); - }; - record2.save = () => { - model2Saved++; - return resolve(this); - }; let model1Saved = 0; let model2Saved = 0; + store.saveRecord = (record) => { + record === record1 ? model1Saved++ : model2Saved++; + return Promise.resolve(record); + }; assert.strictEqual(model1Saved, 0, 'save not yet called'); assert.strictEqual(model2Saved, 0, 'save not yet called'); @@ -276,7 +211,7 @@ module('unit/record-arrays/record-array - DS.RecordArray', function (hooks) { assert.strictEqual(model2Saved, 1, 'save was called for mode2'); const r = await result; - assert.strictEqual(r.id, result.id, 'save promise should fulfill with the original recordArray'); + assert.strictEqual(r, recordArray, 'save promise should fulfill with the original recordArray'); }); test('Create A SnapshotRecordArray', async function (assert) { @@ -296,8 +231,8 @@ module('unit/record-arrays/record-array - DS.RecordArray', function (hooks) { data: [model1, model2], }); - let recordArray = RecordArray.create({ - content: A(records.map((r) => recordIdentifierFor(r))), + let recordArray = new RecordArray({ + identifiers: records.map(recordIdentifierFor), store, }); diff --git a/packages/-ember-data/tests/unit/store/adapter-interop-test.js b/packages/-ember-data/tests/unit/store/adapter-interop-test.js index 10c0bd1940e..64c84ec1295 100644 --- a/packages/-ember-data/tests/unit/store/adapter-interop-test.js +++ b/packages/-ember-data/tests/unit/store/adapter-interop-test.js @@ -343,7 +343,7 @@ module('unit/store/adapter-interop - Store working with a Adapter', function (ho let results = store.peekAll('person'); assert.strictEqual(get(results, 'length'), 1, 'record array should have the original object'); - assert.strictEqual(get(results.objectAt(0), 'name'), 'Tom Dale', 'record has the correct information'); + assert.strictEqual(results.at(0).name, 'Tom Dale', 'record has the correct information'); run(() => { store.push({ @@ -357,8 +357,8 @@ module('unit/store/adapter-interop - Store working with a Adapter', function (ho }); }); - assert.strictEqual(get(results, 'length'), 2, 'record array should have the new object'); - assert.strictEqual(get(results.objectAt(1), 'name'), 'Yehuda Katz', 'record has the correct information'); + assert.strictEqual(results.length, 2, 'record array should have the new object'); + assert.strictEqual(results.at(1).name, 'Yehuda Katz', 'record has the correct information'); assert.strictEqual(results, store.peekAll('person'), 'subsequent calls to peekAll return the same recordArray)'); }); @@ -1160,7 +1160,7 @@ module('unit/store/adapter-interop - Store working with a Adapter', function (ho return run(() => { return store.findAll('person').then((records) => { - assert.strictEqual(records.firstObject.name, 'Tom'); + assert.strictEqual(records.at(0).name, 'Tom'); }); }); }); @@ -1193,7 +1193,7 @@ module('unit/store/adapter-interop - Store working with a Adapter', function (ho return run(() => { return store.findAll('person').then((records) => { - assert.strictEqual(records.firstObject.name, 'Tom'); + assert.strictEqual(records[0].name, 'Tom'); }); }); }); @@ -1224,7 +1224,7 @@ module('unit/store/adapter-interop - Store working with a Adapter', function (ho return run(() => { return store.findAll('person').then((records) => { - assert.strictEqual(records.firstObject, undefined); + assert.strictEqual(records.at(0), undefined); }); }); }); @@ -1265,7 +1265,7 @@ module('unit/store/adapter-interop - Store working with a Adapter', function (ho const records = await store.findAll('person'); - assert.strictEqual(records.firstObject.name, 'John', 'on initial load name is stale'); + assert.strictEqual(records.at(0).name, 'John', 'on initial load name is stale'); await settled(); assert.strictEqual(store.peekRecord('person', 1).name, 'Tom', 'after background reload name is loaded'); diff --git a/packages/-ember-data/tests/unit/store/create-record-test.js b/packages/-ember-data/tests/unit/store/create-record-test.js index f59d5a2ea33..bdbc0487284 100644 --- a/packages/-ember-data/tests/unit/store/create-record-test.js +++ b/packages/-ember-data/tests/unit/store/create-record-test.js @@ -100,17 +100,17 @@ module('unit/store/createRecord - Store creating records', function (hooks) { ], }); - let records = store.peekAll('record').toArray(); + let records = store.peekAll('record').slice(); let storage = store.createRecord('storage', { name: 'Great store', records: records }); assert.strictEqual(storage.name, 'Great store', 'The attribute is well defined'); assert.strictEqual( - storage.records.findBy('id', '1'), + storage.records.find((r) => r.id === '1'), records.find((r) => r.id === '1'), 'Defined relationships are allowed in createRecord' ); assert.strictEqual( - storage.records.findBy('id', '2'), + storage.records.find((r) => r.id === '2'), records.find((r) => r.id === '2'), 'Defined relationships are allowed in createRecord' ); diff --git a/packages/-ember-data/tests/unit/store/push-test.js b/packages/-ember-data/tests/unit/store/push-test.js index 88a8446603d..486f066102a 100644 --- a/packages/-ember-data/tests/unit/store/push-test.js +++ b/packages/-ember-data/tests/unit/store/push-test.js @@ -371,7 +371,7 @@ module('unit/store/push - Store#push', function (hooks) { let person = store.peekRecord('person', 1); assert.strictEqual(person.phoneNumbers.length, 1); - assert.strictEqual(person.get('phoneNumbers.firstObject.number'), '1-800-DATA'); + assert.strictEqual(person.phoneNumbers.at(0).number, '1-800-DATA'); // GET /persons/1 assert.expectNoWarning(() => { @@ -391,7 +391,7 @@ module('unit/store/push - Store#push', function (hooks) { }); assert.strictEqual(person.phoneNumbers.length, 1); - assert.strictEqual(person.get('phoneNumbers.firstObject.number'), '1-800-DATA'); + assert.strictEqual(person.phoneNumbers.at(0).number, '1-800-DATA'); } ); @@ -925,8 +925,8 @@ module('unit/store/push - Store#pushPayload', function (hooks) { let robert = store.peekRecord('person', '1'); const friends = robert.friends; - assert.strictEqual(friends.firstObject.id, '2', 'first object is unchanged'); - assert.strictEqual(friends.lastObject.id, '3', 'last object is unchanged'); + assert.strictEqual(friends.at(0).id, '2', 'first object is unchanged'); + assert.strictEqual(friends.at(-1).id, '3', 'last object is unchanged'); } ); }); diff --git a/packages/-ember-data/tests/unit/system/relationships/polymorphic-relationship-payloads-test.js b/packages/-ember-data/tests/unit/system/relationships/polymorphic-relationship-payloads-test.js index 8d3dfffd1ad..394cafa50b8 100644 --- a/packages/-ember-data/tests/unit/system/relationships/polymorphic-relationship-payloads-test.js +++ b/packages/-ember-data/tests/unit/system/relationships/polymorphic-relationship-payloads-test.js @@ -64,7 +64,7 @@ module('unit/relationships/relationship-payloads-manager (polymorphic)', functio const user = run(() => this.store.push(userData)); - const finalResult = user.hats.mapBy('type'); + const finalResult = user.hats.map((r) => r.type); assert.deepEqual(finalResult, ['hat', 'big-hat', 'small-hat'], 'We got all our hats!'); }); @@ -118,7 +118,7 @@ module('unit/relationships/relationship-payloads-manager (polymorphic)', functio }; const user = run(() => this.store.push(userData)), - finalResult = user.hats.mapBy('type'), + finalResult = user.hats.map((r) => r.type), expectedResults = included.map((m) => m.type); assert.deepEqual(finalResult, expectedResults, 'We got all our hats!'); @@ -174,7 +174,7 @@ module('unit/relationships/relationship-payloads-manager (polymorphic)', functio }; const user = run(() => this.store.push(userData)), - finalResult = user.hats.mapBy('type'), + finalResult = user.hats.map((r) => r.type), expectedResults = included.map((m) => m.type); assert.deepEqual(finalResult, expectedResults, 'We got all our hats!'); @@ -230,7 +230,7 @@ module('unit/relationships/relationship-payloads-manager (polymorphic)', functio const expectedAlienResults = alienIncluded.map((m) => m.type), alien = run(() => this.store.push(alienData)), - alienFinalHats = alien.hats.mapBy('type'); + alienFinalHats = alien.hats.map((r) => r.type); assert.deepEqual(alienFinalHats, expectedAlienResults, 'We got all alien hats!'); }); @@ -309,8 +309,8 @@ module('unit/relationships/relationship-payloads-manager (polymorphic)', functio return this.store.push(smallPersonData); }); - const finalBigResult = bigPerson.hats.toArray(); - const finalSmallResult = smallPerson.hats.toArray(); + const finalBigResult = bigPerson.hats.slice(); + const finalSmallResult = smallPerson.hats.slice(); assert.strictEqual(finalBigResult.length, 4, 'We got all our hats!'); assert.strictEqual(finalSmallResult.length, 2, 'We got all our hats!'); @@ -403,7 +403,7 @@ module('unit/relationships/relationship-payloads-manager (polymorphic)', functio return this.store.push(payload); }); - const familyResultReferences = boyInstance.family.toArray().map((i) => { + const familyResultReferences = boyInstance.family.slice().map((i) => { return { type: i.constructor.modelName, id: i.id }; }); const twinResult = boyInstance.twin; @@ -503,7 +503,7 @@ module('unit/relationships/relationship-payloads-manager (polymorphic)', functio return this.store.push(payload); }); - const familyResultReferences = boyInstance.family.toArray().map((i) => { + const familyResultReferences = boyInstance.family.slice().map((i) => { return { type: i.constructor.modelName, id: i.id }; }); const twinResult = boyInstance.twin; @@ -552,7 +552,7 @@ module('unit/relationships/relationship-payloads-manager (polymorphic)', functio const expectedHatReference = { id: '2', type: 'big-hat' }; const expectedHatsReferences = [{ id: '1', type: 'big-hat' }]; - const finalHatsReferences = hat2.hats.toArray().map((i) => { + const finalHatsReferences = hat2.hats.slice().map((i) => { return { type: i.constructor.modelName, id: i.id }; }); const hatResult = hat1.hat; @@ -596,7 +596,7 @@ module('unit/relationships/relationship-payloads-manager (polymorphic)', functio const expectedHatReference = { id: '1', type: 'big-hat' }; const expectedHatsReferences = [{ id: '1', type: 'big-hat' }]; - const finalHatsReferences = hat.hats.toArray().map((i) => { + const finalHatsReferences = hat.hats.slice().map((i) => { return { type: i.constructor.modelName, id: i.id }; }); const hatResult = hat.hat; @@ -798,8 +798,8 @@ module('unit/relationships/relationship-payloads-manager (polymorphic)', functio return this.store.push(smallPersonData); }); - const finalBigResult = bigPerson.hats.toArray(); - const finalSmallResult = smallPerson.hats.toArray(); + const finalBigResult = bigPerson.hats.slice(); + const finalSmallResult = smallPerson.hats.slice(); assert.deepEqual( finalBigResult.map((h) => ({ type: h.constructor.modelName, id: h.id })), diff --git a/packages/-ember-data/tests/unit/system/snapshot-record-array-test.js b/packages/-ember-data/tests/unit/system/snapshot-record-array-test.js index 063ec2bdf7b..e70e5812d0d 100644 --- a/packages/-ember-data/tests/unit/system/snapshot-record-array-test.js +++ b/packages/-ember-data/tests/unit/system/snapshot-record-array-test.js @@ -2,10 +2,15 @@ import { A } from '@ember/array'; import { module, test } from 'qunit'; +import { setupTest } from 'ember-qunit'; + +import Model, { attr } from '@ember-data/model'; import { SnapshotRecordArray } from '@ember-data/store/-private'; import { deprecatedTest } from '@ember-data/unpublished-test-infra/test-support/deprecated-test'; -module('Unit - snapshot-record-array', function () { +module('Unit - snapshot-record-array', function (hooks) { + setupTest(hooks); + test('constructor', function (assert) { let array = A([1, 2]); array.content = [1, 2]; @@ -22,33 +27,44 @@ module('Unit - snapshot-record-array', function () { }); test('#snapshot', function (assert) { - let array = A([1, 2]); - let didTakeSnapshot = 0; - let snapshotTaken = {}; - - const mockStore = { - _instanceCache: { - createSnapshot() { - didTakeSnapshot++; - return snapshotTaken; - }, + const { owner } = this; + owner.register( + 'model:dog', + class extends Model { + @attr name; + } + ); + const store = owner.lookup('service:store'); + const array = store.peekAll('dog'); + + store.push({ + data: { + type: 'dog', + id: '1', + attributes: { name: 'Shen' }, }, - }; - - array.type = 'some type'; - array.content = ['1']; + }); let options = { adapterOptions: 'some options', include: 'include me', }; - - let snapshot = new SnapshotRecordArray(mockStore, array, options); + let didTakeSnapshot = 0; + let snapshotsTaken = []; + + const create = store._instanceCache.createSnapshot; + store._instanceCache.createSnapshot = function () { + didTakeSnapshot++; + let snapshot = create.apply(this, arguments); + snapshotsTaken.push(snapshot); + return snapshot; + }; + let snapshot = new SnapshotRecordArray(store, array, options); assert.strictEqual(didTakeSnapshot, 0, 'no shapshot should yet be taken'); - assert.strictEqual(snapshot.snapshots()[0], snapshotTaken, 'should be correct snapshot'); + assert.strictEqual(snapshot.snapshots()[0], snapshotsTaken[0], 'should be correct snapshot'); assert.strictEqual(didTakeSnapshot, 1, 'one snapshot should have been taken'); - assert.strictEqual(snapshot.snapshots()[0], snapshotTaken, 'should return the exact same snapshot'); + assert.strictEqual(snapshot.snapshots()[0], snapshotsTaken[0], 'should return the exact same snapshot'); assert.strictEqual(didTakeSnapshot, 1, 'still only one snapshot should have been taken'); }); diff --git a/packages/adapter/addon/index.ts b/packages/adapter/addon/index.ts index 817098a7309..948780846e1 100644 --- a/packages/adapter/addon/index.ts +++ b/packages/adapter/addon/index.ts @@ -322,7 +322,7 @@ export default class Adapter extends EmberObject implements MinimumAdapterInterf @param {Store} store @param {Model} type @param {Object} query - @param {AdapterPopulatedRecordArray} recordArray + @param {Collection} recordArray @param {Object} adapterOptions @return {Promise} promise @public diff --git a/packages/adapter/addon/rest.ts b/packages/adapter/addon/rest.ts index a45441e03ce..77e2e5331cd 100644 --- a/packages/adapter/addon/rest.ts +++ b/packages/adapter/addon/rest.ts @@ -616,7 +616,7 @@ class RESTAdapter extends Adapter.extend(BuildURLMixin) { @param {Store} store @param {Model} type @param {Object} query - @param {AdapterPopulatedRecordArray} recordArray + @param {Collection} recordArray @param {Object} adapterOptions @return {Promise} promise */ diff --git a/packages/model/addon/-private/deprecated-promise-proxy.ts b/packages/model/addon/-private/deprecated-promise-proxy.ts new file mode 100644 index 00000000000..15f4143a10b --- /dev/null +++ b/packages/model/addon/-private/deprecated-promise-proxy.ts @@ -0,0 +1,54 @@ +import { deprecate } from '@ember/debug'; + +import { resolve } from 'rsvp'; + +import { PromiseObject } from './promise-proxy-base'; + +function promiseObject(promise: Promise): PromiseObject { + return PromiseObject.create({ + promise: resolve(promise), + }) as PromiseObject; +} + +// constructor is accessed in some internals but not including it in the copyright for the deprecation +const ALLOWABLE_METHODS = ['constructor', 'then', 'catch', 'finally']; +const PROXIED_OBJECT_PROPS = ['content', 'isPending', 'isSettled', 'isRejected', 'isFulfilled', 'promise', 'reason']; + +export function deprecatedPromiseObject(promise: Promise): PromiseObject { + const promiseObjectProxy: PromiseObject = promiseObject(promise); + const handler = { + get(target: object, prop: string, receiver?: object): unknown { + if (typeof prop === 'symbol') { + return Reflect.get(target, prop, receiver); + } + if (!ALLOWABLE_METHODS.includes(prop)) { + deprecate( + `Accessing ${prop} is deprecated. The return type is being changed fomr PromiseObjectProxy to a Promise. The only available methods to access on this promise are .then, .catch and .finally`, + false, + { + id: 'ember-data:model-save-promise', + until: '5.0', + for: '@ember-data/store', + since: { + available: '4.4', + enabled: '4.4', + }, + } + ); + } + + const value: unknown = target[prop]; + if (value && typeof value === 'function' && typeof value.bind === 'function') { + return value.bind(target); + } + + if (PROXIED_OBJECT_PROPS.includes(prop)) { + return value; + } + + return undefined; + }, + }; + + return new Proxy(promiseObjectProxy, handler); +} diff --git a/packages/model/addon/-private/has-many.js b/packages/model/addon/-private/has-many.js index 9de1a97b612..da6d8c28181 100644 --- a/packages/model/addon/-private/has-many.js +++ b/packages/model/addon/-private/has-many.js @@ -260,8 +260,10 @@ function hasMany(type, options) { } } const support = lookupLegacySupport(this); + const manyArray = support.getManyArray(key); + assert(`You must pass an array of records to set a hasMany relationship`, Array.isArray(records)); this.store._join(() => { - support.setDirtyHasMany(key, records); + manyArray.splice(0, manyArray.length, ...records); }); return support.getHasMany(key); diff --git a/packages/model/addon/-private/legacy-relationships-support.ts b/packages/model/addon/-private/legacy-relationships-support.ts index 9205f3bcaf1..ee8f54bd168 100644 --- a/packages/model/addon/-private/legacy-relationships-support.ts +++ b/packages/model/addon/-private/legacy-relationships-support.ts @@ -1,17 +1,19 @@ -import { assert } from '@ember/debug'; +import { assert, deprecate } from '@ember/debug'; import { DEBUG } from '@glimmer/env'; import { importSync } from '@embroider/macros'; import { all, resolve } from 'rsvp'; import { HAS_RECORD_DATA_PACKAGE } from '@ember-data/private-build-infra'; +import { DEPRECATE_PROMISE_PROXIES, DEPRECATE_V1_RECORD_DATA } from '@ember-data/private-build-infra/deprecations'; import type { UpgradedMeta } from '@ember-data/record-data/-private/graph/-edge-definition'; +import type { LocalRelationshipOperation } from '@ember-data/record-data/-private/graph/-operations'; import type { ImplicitRelationship } from '@ember-data/record-data/-private/graph/index'; import type BelongsToRelationship from '@ember-data/record-data/-private/relationships/state/belongs-to'; import type ManyRelationship from '@ember-data/record-data/-private/relationships/state/has-many'; import type Store from '@ember-data/store'; -import { isStableIdentifier, recordIdentifierFor, storeFor } from '@ember-data/store/-private'; -import { NonSingletonRecordDataManager } from '@ember-data/store/-private/managers/record-data-manager'; +import { fastPush, isStableIdentifier, recordIdentifierFor, SOURCE, storeFor } from '@ember-data/store/-private'; +import type { NonSingletonRecordDataManager } from '@ember-data/store/-private/managers/record-data-manager'; import type { DSModel } from '@ember-data/types/q/ds-model'; import { CollectionResourceRelationship, SingleResourceRelationship } from '@ember-data/types/q/ember-data-json-api'; import type { StableRecordIdentifier } from '@ember-data/types/q/identifier'; @@ -23,8 +25,7 @@ import type { Dict } from '@ember-data/types/q/utils'; import { _findBelongsTo, _findHasMany } from './legacy-data-fetch'; import { assertIdentifierHasId } from './legacy-data-utils'; -import type { ManyArrayCreateArgs } from './many-array'; -import ManyArray from './many-array'; +import RelatedCollection from './many-array'; import type { BelongsToProxyCreateArgs, BelongsToProxyMeta } from './promise-belongs-to'; import PromiseBelongsTo from './promise-belongs-to'; import type { HasManyProxyCreateArgs } from './promise-many-array'; @@ -32,7 +33,6 @@ import PromiseManyArray from './promise-many-array'; import BelongsToReference from './references/belongs-to'; import HasManyReference from './references/has-many'; -type ManyArrayFactory = { create(args: ManyArrayCreateArgs): ManyArray }; type PromiseBelongsToFactory = { create(args: BelongsToProxyCreateArgs): PromiseBelongsTo }; export class LegacySupport { @@ -41,8 +41,8 @@ export class LegacySupport { declare recordData: RecordData; declare references: Dict; declare identifier: StableRecordIdentifier; - declare _manyArrayCache: Dict; - declare _relationshipPromisesCache: Dict>; + declare _manyArrayCache: Dict; + declare _relationshipPromisesCache: Dict>; declare _relationshipProxyCache: Dict; declare isDestroying: boolean; @@ -54,12 +54,69 @@ export class LegacySupport { this.identifier = recordIdentifierFor(record); this.recordData = this.store._instanceCache.getRecordData(this.identifier); - this._manyArrayCache = Object.create(null) as Dict; - this._relationshipPromisesCache = Object.create(null) as Dict>; + this._manyArrayCache = Object.create(null) as Dict; + this._relationshipPromisesCache = Object.create(null) as Dict>; this._relationshipProxyCache = Object.create(null) as Dict; this.references = Object.create(null) as Dict; } + _syncArray(array: RelatedCollection) { + // It’s possible the parent side of the relationship may have been destroyed by this point + if (this.isDestroyed || this.isDestroying) { + return; + } + const currentState = array[SOURCE]; + const identifier = this.identifier; + + let [identifiers, jsonApi] = this._getCurrentState(identifier, array.key); + + if (jsonApi.meta) { + array.meta = jsonApi.meta; + } + + if (jsonApi.links) { + array.links = jsonApi.links; + } + + currentState.length = 0; + fastPush(currentState, identifiers); + } + + updateCache(operation: LocalRelationshipOperation): void { + if (DEPRECATE_V1_RECORD_DATA) { + switch (operation.op) { + case 'addToRelatedRecords': + this.recordData.addToHasMany( + operation.record, + operation.field, + operation.value as StableRecordIdentifier[], + operation.index + ); + return; + case 'removeFromRelatedRecords': + this.recordData.removeFromHasMany( + operation.record, + operation.field, + operation.value as StableRecordIdentifier[] + ); + return; + case 'replaceRelatedRecords': + this.recordData.setHasMany(operation.record, operation.field, operation.value as StableRecordIdentifier[]); + return; + case 'replaceRelatedRecord': + this.recordData.removeFromHasMany(operation.record, operation.field, [operation.prior!]); + this.recordData.addToHasMany(operation.record, operation.field, [operation.value!], operation.index); + return; + case 'sortRelatedRecords': + return; + default: + return; + } + } else { + this.recordData.update(operation); + } + } + _findBelongsTo( key: string, resource: SingleResourceRelationship, @@ -174,9 +231,9 @@ export class LegacySupport { return [identifiers, jsonApi]; } - getManyArray(key: string, definition?: UpgradedMeta): ManyArray { + getManyArray(key: string, definition?: UpgradedMeta): RelatedCollection { assert('hasMany only works with the @ember-data/record-data package', HAS_RECORD_DATA_PACKAGE); - let manyArray: ManyArray | undefined = this._manyArrayCache[key]; + let manyArray: RelatedCollection | undefined = this._manyArrayCache[key]; if (!definition) { const graphFor = ( importSync('@ember-data/record-data/-private') as typeof import('@ember-data/record-data/-private') @@ -186,20 +243,22 @@ export class LegacySupport { if (!manyArray) { const [identifiers, doc] = this._getCurrentState(this.identifier, key); - manyArray = (ManyArray as unknown as ManyArrayFactory).create({ + + manyArray = new RelatedCollection({ store: this.store, - type: this.store.modelFor(definition.type), + type: definition.type, identifier: this.identifier, recordData: this.recordData, + identifiers, key, - currentState: identifiers, meta: doc.meta || null, links: doc.links || null, isPolymorphic: definition.isPolymorphic, isAsync: definition.isAsync, _inverseIsAsync: definition.inverseIsAsync, - legacySupport: this, + manager: this, isLoaded: !definition.isAsync, + allowMutation: true, }); this._manyArrayCache[key] = manyArray; } @@ -210,11 +269,11 @@ export class LegacySupport { fetchAsyncHasMany( key: string, relationship: ManyRelationship, - manyArray: ManyArray, + manyArray: RelatedCollection, options?: FindOptions - ): Promise { + ): Promise { if (HAS_RECORD_DATA_PACKAGE) { - let loadingPromise = this._relationshipPromisesCache[key] as Promise | undefined; + let loadingPromise = this._relationshipPromisesCache[key] as Promise | undefined; if (loadingPromise) { return loadingPromise; } @@ -263,7 +322,7 @@ export class LegacySupport { assert(`hasMany only works with the @ember-data/record-data package`); } - getHasMany(key: string, options?: FindOptions): PromiseManyArray | ManyArray { + getHasMany(key: string, options?: FindOptions): PromiseManyArray | RelatedCollection { if (HAS_RECORD_DATA_PACKAGE) { const graphFor = ( importSync('@ember-data/record-data/-private') as typeof import('@ember-data/record-data/-private') @@ -445,6 +504,9 @@ export class LegacySupport { return; } assert(`Expected collection to be an array`, Array.isArray(resource.data)); + if (allInverseRecordsAreLoaded) { + return; + } let finds = new Array(resource.data.length); let cache = this.store._instanceCache; for (let i = 0; i < resource.data.length; i++) { @@ -594,8 +656,8 @@ function handleCompletedRelationshipRequest( recordExt: LegacySupport, key: string, relationship: ManyRelationship, - value: ManyArray -): ManyArray; + value: RelatedCollection +): RelatedCollection; function handleCompletedRelationshipRequest( recordExt: LegacySupport, key: string, @@ -607,16 +669,16 @@ function handleCompletedRelationshipRequest( recordExt: LegacySupport, key: string, relationship: ManyRelationship, - value: ManyArray, + value: RelatedCollection, error: Error ): never; function handleCompletedRelationshipRequest( recordExt: LegacySupport, key: string, relationship: BelongsToRelationship | ManyRelationship, - value: ManyArray | StableRecordIdentifier | null, + value: RelatedCollection | StableRecordIdentifier | null, error?: Error -): ManyArray | RecordInstance | null { +): RelatedCollection | RecordInstance | null { delete recordExt._relationshipPromisesCache[key]; relationship.state.shouldForceReload = false; const isHasMany = relationship.definition.kind === 'hasMany'; @@ -624,7 +686,7 @@ function handleCompletedRelationshipRequest( if (isHasMany) { // we don't notify the record property here to avoid refetch // only the many array - (value as ManyArray).notify(); + (value as RelatedCollection).notify(); } if (error) { @@ -648,7 +710,7 @@ function handleCompletedRelationshipRequest( } if (isHasMany) { - (value as ManyArray).set('isLoaded', true); + (value as RelatedCollection).isLoaded = true; } relationship.state.hasFailedLoadAttempt = false; @@ -656,7 +718,7 @@ function handleCompletedRelationshipRequest( relationship.state.isStale = false; return isHasMany || !value - ? (value as ManyArray | null) + ? (value as RelatedCollection | null) : recordExt.store.peekRecord(value as StableRecordIdentifier); } @@ -690,12 +752,25 @@ function extractIdentifierFromRecord(recordOrPromiseRecord: PromiseProxyRecord | return null; } - if (isPromiseRecord(recordOrPromiseRecord)) { + if (DEPRECATE_PROMISE_PROXIES && isPromiseRecord(recordOrPromiseRecord)) { let content = recordOrPromiseRecord.content; assert( 'You passed in a promise that did not originate from an EmberData relationship. You can only pass promises that come from a belongsTo or hasMany relationship to the get call.', content !== undefined ); + deprecate( + `You passed in a PromiseProxy to a Relationship API that now expects a resolved value. await the value before setting it.`, + false, + { + id: 'ember-data:deprecate-promise-proxies', + until: '5.0', + since: { + enabled: '4.8', + available: '4.8', + }, + for: 'ember-data', + } + ); return content ? recordIdentifierFor(content) : null; } diff --git a/packages/model/addon/-private/many-array.ts b/packages/model/addon/-private/many-array.ts index 0ff2271db99..943a23a036a 100644 --- a/packages/model/addon/-private/many-array.ts +++ b/packages/model/addon/-private/many-array.ts @@ -1,48 +1,38 @@ /** @module @ember-data/store */ -import EmberArray from '@ember/array'; -import MutableArray from '@ember/array/mutable'; -import { assert } from '@ember/debug'; -import EmberObject, { get } from '@ember/object'; - -import { all } from 'rsvp'; +import { assert, deprecate } from '@ember/debug'; +import { DEPRECATE_PROMISE_PROXIES } from '@ember-data/private-build-infra/deprecations'; import type Store from '@ember-data/store'; -import { isStableIdentifier, PromiseArray, recordIdentifierFor } from '@ember-data/store/-private'; +import { IDENTIFIER_ARRAY_TAG, MUTATE, RecordArray, recordIdentifierFor, SOURCE } from '@ember-data/store/-private'; import type ShimModelClass from '@ember-data/store/-private/legacy-model-support/shim-model-class'; -import type { NonSingletonRecordDataManager } from '@ember-data/store/-private/managers/record-data-manager'; +import { IdentifierArrayCreateOptions } from '@ember-data/store/-private/record-arrays/identifier-array'; import type { CreateRecordProperties } from '@ember-data/store/-private/store-service'; -import type { DSModelSchema } from '@ember-data/types/q/ds-model'; -import type { CollectionResourceRelationship, Links, PaginationLinks } from '@ember-data/types/q/ember-data-json-api'; +import type { Links, PaginationLinks } from '@ember-data/types/q/ember-data-json-api'; import type { StableRecordIdentifier } from '@ember-data/types/q/identifier'; import type { RecordData } from '@ember-data/types/q/record-data'; import type { RecordInstance } from '@ember-data/types/q/record-instance'; import type { FindOptions } from '@ember-data/types/q/store'; import type { Dict } from '@ember-data/types/q/utils'; -import diffArray from './diff-array'; import { LegacySupport } from './legacy-relationships-support'; -interface MutableArrayWithObject extends EmberObject, MutableArray {} -const MutableArrayWithObject = EmberObject.extend(MutableArray) as unknown as new < - T, - M = T ->() => MutableArrayWithObject; - export interface ManyArrayCreateArgs { + identifiers: StableRecordIdentifier[]; + type: string; store: Store; - type: ShimModelClass; + allowMutation: boolean; + manager: LegacySupport; + identifier: StableRecordIdentifier; recordData: RecordData; - currentState: StableRecordIdentifier[]; meta: Dict | null; links: Links | PaginationLinks | null; key: string; isPolymorphic: boolean; isAsync: boolean; _inverseIsAsync: boolean; - legacySupport: LegacySupport; isLoaded: boolean; } /** @@ -87,42 +77,26 @@ export interface ManyArrayCreateArgs { @class ManyArray @public - @extends Ember.EmberObject - @uses Ember.MutableArray */ -export default class ManyArray extends MutableArrayWithObject { +export default class RelatedCollection extends RecordArray { declare isAsync: boolean; - declare isLoaded: boolean; - declare isPolymorphic: boolean; - declare _isDirty: boolean; - declare _isUpdating: boolean; - declare _hasNotified: boolean; - declare __hasArrayObservers: boolean; - declare hasArrayObservers: boolean; // override the base declaration - declare _length: number; - declare _meta: Dict | null; - declare _links: Links | PaginationLinks | null; - declare currentState: StableRecordIdentifier[]; - declare identifier: StableRecordIdentifier; - declare recordData: RecordData; - declare legacySupport: LegacySupport; - declare store: Store; - declare key: string; - declare type: DSModelSchema; - - init() { - super.init(); - - /** + /** The loading state of this array @property {Boolean} isLoaded @public */ - this.isLoaded = this.isLoaded || false; - this.isAsync = this.isAsync || false; - /** + declare isLoaded: boolean; + /** + `true` if the relationship is polymorphic, `false` otherwise. + + @property {Boolean} isPolymorphic + @private + */ + declare isPolymorphic: boolean; + declare _inverseIsAsync: boolean; + /** Metadata associated with the request for async hasMany relationships. Example @@ -159,205 +133,146 @@ export default class ManyArray extends MutableArrayWithObject | null; + /** * Retrieve the links for this relationship * @property {Object | null} links @public */ - this._links = this._links || null; - - /** - `true` if the relationship is polymorphic, `false` otherwise. - - @property {Boolean} isPolymorphic - @private - */ - this.isPolymorphic = this.isPolymorphic || false; - - /** - The relationship which manages this array. - - @property {ManyRelationship} relationship - @private - */ - this.currentState = this.currentState || []; - this._length = this.currentState.length || 0; - this._isUpdating = false; - this._isDirty = false; - /* - * Unfortunately ArrayProxy adds it's observers lazily, - * so in a first-render situation we may sometimes notify - * prior to the ArrayProxy having installed it's observers - * (which occurs during _revalidate()). - * - * This leads to the flush occuring on access, the flush takes - * the hasObservers codepath which in code out of our control - * notifies again leading to a glimmer rendering invalidation error. - * - * We use this flag to detect the case where we notified without - * array observers but observers were installed prior to flush. - * - * We do not need to fire array observers at all in this case - * since it will be the first-access for those observers. - */ - this._hasNotified = false; - } - - // TODO refactor away _hasArrayObservers for tests - get _hasArrayObservers() { - // cast necessary because hasArrayObservers is typed as a ComputedProperty vs a boolean; - return this.hasArrayObservers || this.__hasArrayObservers; - } - - notify() { - this._isDirty = true; - if (this._hasArrayObservers && !this._hasNotified) { - this.retrieveLatest(); - } else { - this._hasNotified = true; - this.notifyPropertyChange('[]'); - this.notifyPropertyChange('firstObject'); - this.notifyPropertyChange('lastObject'); - } - } - - get length() { - if (this._isDirty) { - this.retrieveLatest(); - } - // By using `get()`, the tracking system knows to pay attention to changes that occur. - get(this, '[]'); - - return this._length; - } - - set length(value) { - this._length = value; - } - - get links() { - get(this, '[]'); - if (this._isDirty) { - this.retrieveLatest(); - } - return this._links; - } - set links(v) { - this._links = v; - } - - get meta() { - get(this, '[]'); - if (this._isDirty) { - this.retrieveLatest(); - } - return this._meta; - } - set meta(v) { - this._meta = v; - } - - objectAt(index: number): RecordInstance | undefined { - if (this._isDirty) { - this.retrieveLatest(); - } - let identifier = this.currentState[index]; - if (identifier === undefined) { - return; - } - - return this.store._instanceCache.getRecord(identifier); + declare links: Links | PaginationLinks | null; + declare identifier: StableRecordIdentifier; + declare recordData: RecordData; + // @ts-expect-error + declare _manager: LegacySupport; + declare store: Store; + declare key: string; + declare type: ShimModelClass; + + constructor(options: ManyArrayCreateArgs) { + super(options as unknown as IdentifierArrayCreateOptions); + this.isLoaded = options.isLoaded || false; + this.isAsync = options.isAsync || false; + this.isPolymorphic = options.isPolymorphic || false; + this.identifier = options.identifier; + this.key = options.key; } - replace(idx: number, amt: number, objects?: RecordInstance[]) { - assert(`Cannot push mutations to the cache while updating the relationship from cache`, !this._isUpdating); - assert( - 'The third argument to replace needs to be an array.', - !objects || Array.isArray(objects) || EmberArray.detect(objects) - ); - const { store, identifier } = this; - store._join(() => { - let identifiers: StableRecordIdentifier[]; - if (amt > 0) { - identifiers = this.currentState.slice(idx, idx + amt); - this.recordData.removeFromHasMany(identifier, this.key, identifiers); + [MUTATE](prop: string, args: unknown[], result?: unknown) { + switch (prop) { + case 'length 0': { + this._manager.updateCache({ + op: 'replaceRelatedRecords', + record: this.identifier, + field: this.key, + value: [], + }); + break; } - if (objects && objects.length > 0) { - this.recordData.addToHasMany( - identifier, - this.key, - objects.map((obj: RecordInstance) => recordIdentifierFor(obj)), - idx - ); + case 'replace cell': { + const [index, prior, value] = args as [number, StableRecordIdentifier, StableRecordIdentifier]; + this._manager.updateCache({ + op: 'replaceRelatedRecord', + record: this.identifier, + field: this.key, + value, + prior, + index, + }); + break; } - this.notify(); - }); - } - - retrieveLatest() { - // It’s possible the parent side of the relationship may have been destroyed by this point - if (this.isDestroyed || this.isDestroying || this._isUpdating) { - return; - } - this._isDirty = false; - this._isUpdating = true; - const identifier = this.identifier; - - let jsonApi = (this.recordData as NonSingletonRecordDataManager).getRelationship( - identifier, - this.key, - true - ) as CollectionResourceRelationship; - const cache = this.store._instanceCache; - - let identifiers: StableRecordIdentifier[] = []; - const data = jsonApi.data; - if (data) { - for (let i = 0; i < data.length; i++) { - const identifier: StableRecordIdentifier = data[i] as unknown as StableRecordIdentifier; - assert(`expected a stable identifier`, isStableIdentifier(identifier)); - - if (cache.recordIsLoaded(identifier, true)) { - identifiers.push(identifier); + case 'push': + this._manager.updateCache({ + op: 'addToRelatedRecords', + record: this.identifier, + field: this.key, + value: extractIdentifiersFromRecords(args as RecordInstance[]), + }); + break; + case 'pop': + if (result) { + this._manager.updateCache({ + op: 'removeFromRelatedRecords', + record: this.identifier, + field: this.key, + value: recordIdentifierFor(result as RecordInstance), + }); + } + break; + + case 'unshift': + this._manager.updateCache({ + op: 'addToRelatedRecords', + record: this.identifier, + field: this.key, + value: extractIdentifiersFromRecords(args as RecordInstance[]), + index: 0, + }); + break; + + case 'shift': + if (result) { + this._manager.updateCache({ + op: 'removeFromRelatedRecords', + record: this.identifier, + field: this.key, + value: recordIdentifierFor(result as RecordInstance), + index: 0, + }); + } + break; + + case 'sort': + this._manager.updateCache({ + op: 'sortRelatedRecords', + record: this.identifier, + field: this.key, + value: (result as RecordInstance[]).map(recordIdentifierFor), + }); + break; + + case 'splice': { + const [start, removeCount, ...adds] = args as [number, number, RecordInstance]; + // detect a full replace + if (removeCount > 0 && adds.length === this[SOURCE].length) { + this._manager.updateCache({ + op: 'replaceRelatedRecords', + record: this.identifier, + field: this.key, + value: extractIdentifiersFromRecords(adds), + }); + return; + } + if (removeCount > 0) { + this._manager.updateCache({ + op: 'removeFromRelatedRecords', + record: this.identifier, + field: this.key, + value: (result as RecordInstance[]).map(recordIdentifierFor), + index: start, + }); + } + if (adds?.length) { + this._manager.updateCache({ + op: 'addToRelatedRecords', + record: this.identifier, + field: this.key, + value: extractIdentifiersFromRecords(adds), + index: start, + }); } - } - } - - if (jsonApi.meta) { - this._meta = jsonApi.meta; - } - - if (jsonApi.links) { - this._links = jsonApi.links; - } - if (this._hasArrayObservers && !this._hasNotified) { - // diff to find changes - let diff = diffArray(this.currentState, identifiers); - // it's null if no change found - if (diff.firstChangeIndex !== null) { - // we found a change - this.arrayContentWillChange(diff.firstChangeIndex, diff.removedCount, diff.addedCount); - this._length = identifiers.length; - this.currentState = identifiers; - this.arrayContentDidChange(diff.firstChangeIndex, diff.removedCount, diff.addedCount); + break; } - } else { - this._hasNotified = false; - this._length = identifiers.length; - this.currentState = identifiers; + default: + assert(`unable to convert ${prop} into a transaction that updates the cache state for this record array`); } - - this._isUpdating = false; } - destroy() { - this._length = 0; - this.currentState = []; - return super.destroy(); + notify() { + const tag = this[IDENTIFIER_ARRAY_TAG]; + tag.ref = null; + tag.shouldReset = true; } /** @@ -384,7 +299,7 @@ export default class ManyArray extends MutableArrayWithObject manyArray, - null, - 'DS: ManyArray#save return ManyArray' - ); - - // TODO deprecate returning a promiseArray here - return PromiseArray.create({ promise }); - } /** Create a child record within the owner @@ -427,11 +330,70 @@ export default class ManyArray extends MutableArrayWithObject { notifyChanges(identifier, type, key, this, store); }); } destroy() { + const identifier = recordIdentifierFor(this); this.___recordState?.destroy(); const store = storeFor(this); - const identifier = recordIdentifierFor(this); store._notificationManager.unsubscribe(this.___private_notifications); // Legacy behavior is to notify the relationships on destroy // such that they "clear". It's uncertain this behavior would @@ -162,6 +162,7 @@ class Model extends EmberObject { // to simply not notify, for this reason the store does not itself // notify individual changes once the delete has been signaled, // this decision is left to model instances. + this.eachRelationship((key, meta) => { if (meta.kind === 'belongsTo') { this.notifyPropertyChange(key); @@ -170,6 +171,7 @@ class Model extends EmberObject { LEGACY_SUPPORT.get(this)?.destroy(); LEGACY_SUPPORT.delete(this); LEGACY_SUPPORT.delete(identifier); + super.destroy(); } @@ -846,6 +848,7 @@ class Model extends EmberObject { rollbackAttributes() { const { currentState } = this; const { isNew } = currentState; + storeFor(this)._join(() => { recordDataFor(this).rollbackAttrs(recordIdentifierFor(this)); this.errors.clear(); @@ -1152,7 +1155,7 @@ class Model extends EmberObject { record.eachRelationship(function(name, descriptor) { if (descriptor.kind === 'hasMany') { let serializedHasManyName = name.toUpperCase() + '_IDS'; - json[serializedHasManyName] = record.get(name).mapBy('id'); + json[serializedHasManyName] = record.get(name).map(r => r.id); } }); diff --git a/packages/model/addon/-private/promise-belongs-to.ts b/packages/model/addon/-private/promise-belongs-to.ts index bf9dc9a8a0e..7be67d77546 100644 --- a/packages/model/addon/-private/promise-belongs-to.ts +++ b/packages/model/addon/-private/promise-belongs-to.ts @@ -4,11 +4,11 @@ import type PromiseProxyMixin from '@ember/object/promise-proxy-mixin'; import type ObjectProxy from '@ember/object/proxy'; import type Store from '@ember-data/store'; -import { PromiseObject } from '@ember-data/store/-private'; import type { RecordInstance } from '@ember-data/types/q/record-instance'; import type { Dict } from '@ember-data/types/q/utils'; import { LegacySupport } from './legacy-relationships-support'; +import { PromiseObject } from './promise-proxy-base'; export interface BelongsToProxyMeta { key: string; diff --git a/packages/model/addon/-private/promise-many-array.ts b/packages/model/addon/-private/promise-many-array.ts index d24f120da91..42b58d030ff 100644 --- a/packages/model/addon/-private/promise-many-array.ts +++ b/packages/model/addon/-private/promise-many-array.ts @@ -8,8 +8,6 @@ import Ember from 'ember'; import { resolve } from 'rsvp'; -import type { ManyArray } from 'ember-data/-private'; - import { DEPRECATE_A_USAGE, DEPRECATE_PROMISE_MANY_ARRAY_BEHAVIORS, @@ -18,6 +16,8 @@ import { StableRecordIdentifier } from '@ember-data/types/q/identifier'; import type { RecordInstance } from '@ember-data/types/q/record-instance'; import { FindOptions } from '@ember-data/types/q/store'; +import type ManyArray from './many-array'; + export interface HasManyProxyCreateArgs { promise: Promise; content?: ManyArray; @@ -66,6 +66,7 @@ export default class PromiseManyArray { since: { enabled: '4.8', available: '4.8' }, for: 'ember-data', }); + // @ts-expect-error ArrayMixin is more than a type if (mixin === NativeArray || mixin === ArrayMixin) { return true; } @@ -92,7 +93,7 @@ export default class PromiseManyArray { get length(): number { // shouldn't be needed, but ends up being needed // for computed chains even in 4.x - this['[]']; + // this['[]']; return this.content ? this.content.length : 0; } @@ -102,7 +103,8 @@ export default class PromiseManyArray { // to recompute. We entangle the '[]' tag from @dependentKeyCompat get '[]'() { - return this.content ? this.content['[]'] : this.content; + return this.content?.length && this.content; + // return this.content ? this.content['[]'] : this.content; } /** @@ -116,7 +118,7 @@ export default class PromiseManyArray { * @private */ forEach(cb) { - this['[]']; // needed for < 3.23 support e.g. 3.20 lts + // this['[]']; // needed for < 3.23 support e.g. 3.20 lts if (this.content && this.length) { this.content.forEach(cb); } diff --git a/packages/model/addon/-private/promise-proxy-base.d.ts b/packages/model/addon/-private/promise-proxy-base.d.ts new file mode 100644 index 00000000000..8a6267a55b6 --- /dev/null +++ b/packages/model/addon/-private/promise-proxy-base.d.ts @@ -0,0 +1,32 @@ +import ObjectProxy from '@ember/object/proxy'; + +export interface PromiseObject extends Promise {} +export class PromiseObject extends ObjectProxy { + declare content?: T | null; + + /* + * If the proxied promise is rejected this will contain the reason + * provided. + */ + reason: string | Error; + /* + * Once the proxied promise has settled this will become `false`. + */ + isPending: boolean; + /* + * Once the proxied promise has settled this will become `true`. + */ + isSettled: boolean; + /* + * Will become `true` if the proxied promise is rejected. + */ + isRejected: boolean; + /* + * Will become `true` if the proxied promise is fulfilled. + */ + isFulfilled: boolean; + /* + * The promise whose fulfillment value is being proxied by this object. + */ + promise: Promise; +} diff --git a/packages/model/addon/-private/promise-proxy-base.js b/packages/model/addon/-private/promise-proxy-base.js new file mode 100644 index 00000000000..76dd30feab6 --- /dev/null +++ b/packages/model/addon/-private/promise-proxy-base.js @@ -0,0 +1,4 @@ +import PromiseProxyMixin from '@ember/object/promise-proxy-mixin'; +import ObjectProxy from '@ember/object/proxy'; + +export const PromiseObject = ObjectProxy.extend(PromiseProxyMixin); diff --git a/packages/model/addon/-private/references/belongs-to.ts b/packages/model/addon/-private/references/belongs-to.ts index d814ad696bc..2e73cbbfb56 100644 --- a/packages/model/addon/-private/references/belongs-to.ts +++ b/packages/model/addon/-private/references/belongs-to.ts @@ -1,9 +1,11 @@ +import { deprecate } from '@ember/debug'; import { dependentKeyCompat } from '@ember/object/compat'; import { cached, tracked } from '@glimmer/tracking'; import type { Object as JSONObject, Value as JSONValue } from 'json-typescript'; import { resolve } from 'rsvp'; +import { DEPRECATE_PROMISE_PROXIES } from '@ember-data/private-build-infra/deprecations'; import type { Graph } from '@ember-data/record-data/-private/graph'; import type BelongsToRelationship from '@ember-data/record-data/-private/relationships/state/belongs-to'; import type Store from '@ember-data/store'; @@ -385,8 +387,25 @@ export default class BelongsToReference { @return {Promise} A promise that resolves with the new value in this belongs-to relationship. */ async push(data: SingleResourceDocument | Promise): Promise { - // TODO @deprecate pushing unresolved payloads - const jsonApiDoc = await resolve(data); + let jsonApiDoc: SingleResourceDocument = data as SingleResourceDocument; + if (DEPRECATE_PROMISE_PROXIES && (data as { then: unknown }).then) { + jsonApiDoc = await resolve(data); + if (jsonApiDoc !== data) { + deprecate( + `You passed in a Promise to a Reference API that now expects a resolved value. await the value before setting it.`, + false, + { + id: 'ember-data:deprecate-promise-proxies', + until: '5.0', + since: { + enabled: '4.8', + available: '4.8', + }, + for: 'ember-data', + } + ); + } + } let record = this.store.push(jsonApiDoc); // eslint-disable-next-line @typescript-eslint/no-unsafe-call diff --git a/packages/model/addon/-private/references/has-many.ts b/packages/model/addon/-private/references/has-many.ts index f883befc6c0..83b79b908e4 100644 --- a/packages/model/addon/-private/references/has-many.ts +++ b/packages/model/addon/-private/references/has-many.ts @@ -1,3 +1,4 @@ +import { deprecate } from '@ember/debug'; import { dependentKeyCompat } from '@ember/object/compat'; import { DEBUG } from '@glimmer/env'; import { cached, tracked } from '@glimmer/tracking'; @@ -7,6 +8,7 @@ import { resolve } from 'rsvp'; import { ManyArray } from 'ember-data/-private'; +import { DEPRECATE_PROMISE_PROXIES } from '@ember-data/private-build-infra/deprecations'; import type { Graph } from '@ember-data/record-data/-private/graph'; import type ManyRelationship from '@ember-data/record-data/-private/relationships/state/has-many'; import type Store from '@ember-data/store'; @@ -397,7 +399,25 @@ export default class HasManyReference { async push( objectOrPromise: ExistingResourceObject[] | CollectionResourceDocument | { data: SingleResourceDocument[] } ): Promise { - const payload = await resolve(objectOrPromise); + let payload = objectOrPromise; + if (DEPRECATE_PROMISE_PROXIES && (objectOrPromise as unknown as { then: unknown }).then) { + payload = await resolve(objectOrPromise); + if (payload !== objectOrPromise) { + deprecate( + `You passed in a Promise to a Reference API that now expects a resolved value. await the value before setting it.`, + false, + { + id: 'ember-data:deprecate-promise-proxies', + until: '5.0', + since: { + enabled: '4.8', + available: '4.8', + }, + for: 'ember-data', + } + ); + } + } let array: Array; if (!Array.isArray(payload) && typeof payload === 'object' && Array.isArray(payload.data)) { diff --git a/packages/model/index.js b/packages/model/index.js index 55f8503c966..179b256456b 100644 --- a/packages/model/index.js +++ b/packages/model/index.js @@ -20,6 +20,8 @@ module.exports = Object.assign({}, addonBaseConfig, { '@ember/string', '@embroider/macros/es-compat', + '@ember/object/proxy', + '@ember/object/promise-proxy-mixin', '@ember/application', '@ember/array', '@ember/array/mutable', diff --git a/packages/private-build-infra/addon/current-deprecations.ts b/packages/private-build-infra/addon/current-deprecations.ts index ec6673dfd61..948997fa3ea 100644 --- a/packages/private-build-infra/addon/current-deprecations.ts +++ b/packages/private-build-infra/addon/current-deprecations.ts @@ -57,4 +57,6 @@ export default { DEPRECATE_RELATIONSHIPS_WITHOUT_INVERSE: '4.8', DEPRECATE_V1_RECORD_DATA: '4.8', DEPRECATE_A_USAGE: '4.8', + DEPRECATE_PROMISE_PROXIES: '4.8', + DEPRECATE_ARRAY_LIKE: '4.8', }; diff --git a/packages/private-build-infra/addon/deprecations.ts b/packages/private-build-infra/addon/deprecations.ts index 7e93b5ed2be..6ec28d697cc 100644 --- a/packages/private-build-infra/addon/deprecations.ts +++ b/packages/private-build-infra/addon/deprecations.ts @@ -26,3 +26,5 @@ export const DEPRECATE_RELATIONSHIPS_WITHOUT_ASYNC = deprecationState('DEPRECATE export const DEPRECATE_RELATIONSHIPS_WITHOUT_INVERSE = deprecationState('DEPRECATE_RELATIONSHIPS_WITHOUT_INVERSE'); export const DEPRECATE_V1_RECORD_DATA = deprecationState('DEPRECATE_V1_RECORD_DATA'); export const DEPRECATE_A_USAGE = deprecationState('DEPRECATE_A_USAGE'); +export const DEPRECATE_PROMISE_PROXIES = deprecationState('DEPRECATE_PROMISE_PROXIES'); +export const DEPRECATE_ARRAY_LIKE = deprecationState('DEPRECATE_ARRAY_LIKE'); diff --git a/packages/record-data/addon/-private/graph/-operations.ts b/packages/record-data/addon/-private/graph/-operations.ts index 8a0ae6f2a91..b6ad0ea5a4f 100644 --- a/packages/record-data/addon/-private/graph/-operations.ts +++ b/packages/record-data/addon/-private/graph/-operations.ts @@ -40,30 +40,44 @@ export interface RemoveFromRelatedRecordsOperation { record: StableRecordIdentifier; field: string; // "relationship" propertyName value: StableRecordIdentifier | StableRecordIdentifier[]; // related record + index?: number; // optional the index at which we're expected to start the removal } export interface ReplaceRelatedRecordOperation { op: 'replaceRelatedRecord'; record: StableRecordIdentifier; field: string; - value: StableRecordIdentifier | null; + value: StableRecordIdentifier | null; // never null if field is a collection + prior?: StableRecordIdentifier; // if field is a collection, the value we are swapping with + index?: number; // if field is a collection, the index at which we are replacing a value +} + +export interface SortRelatedRecords { + op: 'sortRelatedRecords'; + record: StableRecordIdentifier; + field: string; + value: StableRecordIdentifier[]; } export interface ReplaceRelatedRecordsOperation { op: 'replaceRelatedRecords'; record: StableRecordIdentifier; field: string; - value: StableRecordIdentifier[]; + value: StableRecordIdentifier[]; // the records to add. If no prior/index specified all existing should be removed + prior?: StableRecordIdentifier[]; // if this is a "splice" the records we expect to be removed + index?: number; // if this is a "splice" the index to start from } export type RemoteRelationshipOperation = | UpdateRelationshipOperation | ReplaceRelatedRecordOperation | ReplaceRelatedRecordsOperation - | DeleteRecordOperation; + | DeleteRecordOperation + | SortRelatedRecords; export type LocalRelationshipOperation = | ReplaceRelatedRecordsOperation | ReplaceRelatedRecordOperation | RemoveFromRelatedRecordsOperation - | AddToRelatedRecordsOperation; + | AddToRelatedRecordsOperation + | SortRelatedRecords; diff --git a/packages/record-data/addon/-private/record-data.ts b/packages/record-data/addon/-private/record-data.ts index 51f882c93a4..6c362ba8f67 100644 --- a/packages/record-data/addon/-private/record-data.ts +++ b/packages/record-data/addon/-private/record-data.ts @@ -19,6 +19,7 @@ import { AttributeSchema, RelationshipSchema } from '@ember-data/types/q/record- import { RecordDataStoreWrapper, V2RecordDataStoreWrapper } from '@ember-data/types/q/record-data-store-wrapper'; import { Dict } from '@ember-data/types/q/utils'; +import { LocalRelationshipOperation } from './graph/-operations'; import { isImplicit } from './graph/-utils'; import { graphFor } from './graph/index'; import type BelongsToRelationship from './relationships/state/belongs-to'; @@ -809,6 +810,11 @@ class SingletonRecordData implements RecordData { return changedKeys; } + + update(operation: LocalRelationshipOperation): void { + graphFor(this.__storeWrapper).update(operation, false); + } + clientDidCreate(identifier: StableRecordIdentifier, options?: Dict | undefined): Dict { const cached = this.__peek(identifier); cached.isNew = true; diff --git a/packages/record-data/tests/integration/graph/edge-removal/helpers.ts b/packages/record-data/tests/integration/graph/edge-removal/helpers.ts index 37dc849e4ea..2479b9962f1 100644 --- a/packages/record-data/tests/integration/graph/edge-removal/helpers.ts +++ b/packages/record-data/tests/integration/graph/edge-removal/helpers.ts @@ -133,9 +133,9 @@ export async function setInitialState(context: Context, config: TestConfig, asse if (config.dirtyLocal) { if (isMany) { let friends = await john.bestFriends; - friends.pushObject(chris); + friends.push(chris); friends = await chris.bestFriends; - friends.pushObject(john); + friends.push(john); } else { john.bestFriends = chris; chris.bestFriends = john; diff --git a/packages/store/addon/-private/index.ts b/packages/store/addon/-private/index.ts index c862f232f64..e2a7fd0a138 100644 --- a/packages/store/addon/-private/index.ts +++ b/packages/store/addon/-private/index.ts @@ -42,12 +42,15 @@ export function normalizeModelName(modelName: string) { // to also eliminate export { default as coerceId } from './utils/coerce-id'; -export { PromiseArray, PromiseObject, deprecatedPromiseObject } from './proxies/promise-proxies'; - -export { default as RecordArray } from './record-arrays/record-array'; -export { default as AdapterPopulatedRecordArray } from './record-arrays/adapter-populated-record-array'; - -export { default as RecordArrayManager } from './managers/record-array-manager'; +export { + default as RecordArray, + default as IdentifierArray, + Collection as AdapterPopulatedRecordArray, + SOURCE, + MUTATE, + IDENTIFIER_ARRAY_TAG, +} from './record-arrays/identifier-array'; +export { default as RecordArrayManager, fastPush } from './managers/record-array-manager'; // // Used by tests export { default as SnapshotRecordArray } from './network/snapshot-record-array'; diff --git a/packages/store/addon/-private/legacy-model-support/shim-model-class.ts b/packages/store/addon/-private/legacy-model-support/shim-model-class.ts index 5da0e7481c4..c7be2f42cb1 100644 --- a/packages/store/addon/-private/legacy-model-support/shim-model-class.ts +++ b/packages/store/addon/-private/legacy-model-support/shim-model-class.ts @@ -15,6 +15,7 @@ export function getShimClass(store: Store, modelName: string): ShimModelClass { shims = Object.create(null) as Dict; AvailableShims.set(store, shims); } + let shim = shims[modelName]; if (shim === undefined) { shim = shims[modelName] = new ShimModelClass(store, modelName); diff --git a/packages/store/addon/-private/managers/record-array-manager.ts b/packages/store/addon/-private/managers/record-array-manager.ts index c0eb3c77104..b715a7134af 100644 --- a/packages/store/addon/-private/managers/record-array-manager.ts +++ b/packages/store/addon/-private/managers/record-array-manager.ts @@ -1,23 +1,72 @@ /** @module @ember-data/store */ - -import { A } from '@ember/array'; -import { _backburner as emberBackburner } from '@ember/runloop'; - import type { CollectionResourceDocument } from '@ember-data/types/q/ember-data-json-api'; import type { StableRecordIdentifier } from '@ember-data/types/q/identifier'; import type { Dict } from '@ember-data/types/q/utils'; -import AdapterPopulatedRecordArray, { - AdapterPopulatedRecordArrayCreateArgs, -} from '../record-arrays/adapter-populated-record-array'; -import RecordArray from '../record-arrays/record-array'; +import IdentifierArray, { + Collection, + CollectionCreateOptions, + IDENTIFIER_ARRAY_TAG, + SOURCE, +} from '../record-arrays/identifier-array'; import type Store from '../store-service'; -const RecordArraysCache = new Map>(); +const RecordArraysCache = new Map>(); const FAKE_ARR = {}; +const SLICE_BATCH_SIZE = 1200; +/** + * This is a clever optimization. + * + * clever optimizations rarely stand the test of time, so if you're + * ever curious or think something better is possible please benchmark + * and discuss. The benchmark for this at the time of writing is in + * `scripts/benchmark-push.js` + * + * This approach turns out to be 150x faster in Chrome and node than + * simply using push or concat. It's highly susceptible to the specifics + * of the batch size, and may require tuning. + * + * Clever optimizations should always come with a `why`. This optimization + * exists for two reasons. + * + * 1) array.push(...objects) and Array.prototype.push.apply(arr, objects) + * are susceptible to stack overflows. The size of objects at which this + * occurs varies by environment, browser, and current stack depth and memory + * pressure; however, it occurs in all browsers in fairly pristine conditions + * somewhere around 125k to 200k elements. Since EmberData regularly encounters + * arrays larger than this in size, we cannot use push. + * + * 2) `array.concat` or simply setting the array to a new reference is often an + * easier approach; however, native Proxy to an array cannot swap it's target array + * and attempts at juggling multiple array sources have proven to be victim to a number + * of browser implementation bugs. Should these bugs be addressed then we could + * simplify to using `concat`, however, do note this is currently 150x faster + * than concat, and due to the overloaded signature of concat will likely always + * be faster. + * + * Sincerely, + * - runspired (Chris Thoburn) 08/21/2022 + * + * @function fastPush + * @internal + * @param target the array to push into + * @param source the items to push into target + */ +export function fastPush(target: T[], source: T[]) { + let startLength = 0; + let newLength = source.length; + while (newLength - startLength > SLICE_BATCH_SIZE) { + // eslint-disable-next-line prefer-spread + target.push.apply(target, source.slice(startLength, SLICE_BATCH_SIZE)); + startLength += SLICE_BATCH_SIZE; + } + // eslint-disable-next-line prefer-spread + target.push.apply(target, source.slice(startLength)); +} + type ChangeSet = Map; /** @@ -28,11 +77,10 @@ class RecordArrayManager { declare store: Store; declare isDestroying: boolean; declare isDestroyed: boolean; - declare _live: Map; - declare _managed: Set; - declare _pending: Map; - declare _willFlush: boolean; - declare _identifiers: Map>; + declare _live: Map; + declare _managed: Set; + declare _pending: Map; + declare _identifiers: Map>; declare _staged: Map; constructor(options: { store: Store }) { @@ -43,18 +91,17 @@ class RecordArrayManager { this._managed = new Set(); this._pending = new Map(); this._staged = new Map(); - this._willFlush = false; this._identifiers = RecordArraysCache; } - _syncArray(array: RecordArray | AdapterPopulatedRecordArray) { + _syncArray(array: IdentifierArray) { const pending = this._pending.get(array); if (!pending || this.isDestroying || this.isDestroyed) { return; } - array._updateState(pending); + sync(array, pending); this._pending.delete(array); } @@ -67,7 +114,7 @@ class RecordArrayManager { @param {String} modelName @return {RecordArray} */ - liveArrayFor(type: string): RecordArray { + liveArrayFor(type: string): IdentifierArray { let array = this._live.get(type); let identifiers: StableRecordIdentifier[] = []; let staged = this._staged.get(type); @@ -81,19 +128,14 @@ class RecordArrayManager { } if (!array) { - array = RecordArray.create({ - modelName: type, - content: A(identifiers), + array = new IdentifierArray({ + type, + identifiers, store: this.store, - isLoaded: true, + allowMutation: false, manager: this, }); this._live.set(type, array); - } else { - let pending = this._pending.get(array); - if (pending) { - array._notify(); - } } return array; @@ -104,18 +146,19 @@ class RecordArrayManager { query?: Dict; identifiers?: StableRecordIdentifier[]; doc?: CollectionResourceDocument; - }): AdapterPopulatedRecordArray { - let options: AdapterPopulatedRecordArrayCreateArgs = { - modelName: config.type, + }): Collection { + let options: CollectionCreateOptions = { + type: config.type, links: config.doc?.links || null, meta: config.doc?.meta || null, query: config.query || null, - content: A(config.identifiers || []), + identifiers: config.identifiers || [], isLoaded: !!config.identifiers?.length, + allowMutation: false, store: this.store, manager: this, }; - let array = AdapterPopulatedRecordArray.create(options); + let array = new Collection(options); this._managed.add(array); if (config.identifiers) { associate(array, config.identifiers); @@ -124,35 +167,29 @@ class RecordArrayManager { return array; } - dirtyArray(array: RecordArray | AdapterPopulatedRecordArray): void { - if (array === FAKE_ARR || this._willFlush) { + dirtyArray(array: IdentifierArray): void { + if (array === FAKE_ARR) { return; } - this._willFlush = true; - // eslint-disable-next-line @typescript-eslint/unbound-method - emberBackburner.schedule('actions', this, this._flush); - } - - _flush() { - this._willFlush = false; - let pending = this._pending; - pending.forEach((_changes, recordArray) => { - recordArray._notify(); - }); + let tag = array[IDENTIFIER_ARRAY_TAG]; + if (!tag.shouldReset) { + tag.shouldReset = true; + tag.ref = null; + } } _getPendingFor( identifier: StableRecordIdentifier, includeManaged: boolean, isRemove?: boolean - ): Map | void { + ): Map | void { if (this.isDestroying || this.isDestroyed) { return; } let liveArray = this._live.get(identifier.type); const allPending = this._pending; - let pending: Map = new Map(); + let pending: Map = new Map(); if (includeManaged) { let managed = RecordArraysCache.get(identifier); @@ -170,7 +207,7 @@ class RecordArrayManager { // during unloadAll we can ignore removes since we've already // cleared the array. - if (liveArray && liveArray.content.length === 0 && isRemove) { + if (liveArray && liveArray[SOURCE].length === 0 && isRemove) { return pending; } @@ -182,7 +219,7 @@ class RecordArrayManager { changes = new Map(); this._staged.set(identifier.type, changes); } - pending.set(FAKE_ARR as RecordArray, changes); + pending.set(FAKE_ARR as IdentifierArray, changes); } else { let changes = allPending.get(liveArray); if (!changes) { @@ -195,14 +232,13 @@ class RecordArrayManager { return pending; } - populateManagedArray( - array: AdapterPopulatedRecordArray, - identifiers: StableRecordIdentifier[], - payload: CollectionResourceDocument - ) { + populateManagedArray(array: Collection, identifiers: StableRecordIdentifier[], payload: CollectionResourceDocument) { this._pending.delete(array); - const old = array.content; - array.content.setObjects(identifiers); // this will also notify + const source = array[SOURCE]; + const old = source.slice(); + source.length = 0; + fastPush(source, identifiers); + array[IDENTIFIER_ARRAY_TAG].ref = null; array.meta = payload.meta || null; array.links = payload.links || null; array.isLoaded = true; @@ -272,7 +308,7 @@ class RecordArrayManager { } } -function associate(array: AdapterPopulatedRecordArray, identifiers: StableRecordIdentifier[]) { +function associate(array: Collection, identifiers: StableRecordIdentifier[]) { for (let i = 0; i < identifiers.length; i++) { let identifier = identifiers[i]; let cache = RecordArraysCache.get(identifier); @@ -283,16 +319,59 @@ function associate(array: AdapterPopulatedRecordArray, identifiers: StableRecord cache.add(array); } } -function disassociate(array: AdapterPopulatedRecordArray, identifiers: StableRecordIdentifier[]) { + +function disassociate(array: Collection, identifiers: StableRecordIdentifier[]) { for (let i = 0; i < identifiers.length; i++) { disassociateIdentifier(array, identifiers[i]); } } -export function disassociateIdentifier(array: AdapterPopulatedRecordArray, identifier: StableRecordIdentifier) { + +export function disassociateIdentifier(array: Collection, identifier: StableRecordIdentifier) { let cache = RecordArraysCache.get(identifier); if (cache) { cache.delete(array); } } +function sync(array: IdentifierArray, changes: Map) { + let state = array[SOURCE]; + const adds: StableRecordIdentifier[] = []; + const removes: StableRecordIdentifier[] = []; + changes.forEach((value, key) => { + if (value === 'add') { + // likely we want to keep a Set along-side + if (state.includes(key)) { + return; + } + adds.push(key); + } else { + removes.push(key); + } + }); + if (removes.length) { + if (removes.length === state.length) { + state.length = 0; + // changing the reference breaks the Proxy + // state = array[SOURCE] = []; + } else { + removes.forEach((i) => { + state.splice(state.indexOf(i), 1); + }); + } + } + + if (adds.length) { + fastPush(state, adds); + // changing the reference breaks the Proxy + // else we could do this + /* + if (state.length === 0) { + array[SOURCE] = adds; + } else { + array[SOURCE] = state.concat(adds); + } + */ + } +} + export default RecordArrayManager; diff --git a/packages/store/addon/-private/managers/record-data-manager.ts b/packages/store/addon/-private/managers/record-data-manager.ts index 7a51f836c70..b175e572e99 100644 --- a/packages/store/addon/-private/managers/record-data-manager.ts +++ b/packages/store/addon/-private/managers/record-data-manager.ts @@ -1,13 +1,17 @@ -import { assert } from '@ember/debug'; - -import { CollectionResourceRelationship, SingleResourceRelationship } from '@ember-data/types/q/ember-data-json-api'; -import { StableRecordIdentifier } from '@ember-data/types/q/identifier'; -import { ChangedAttributesHash, RecordData, RecordDataV1 } from '@ember-data/types/q/record-data'; -import { JsonApiResource, JsonApiValidationError } from '@ember-data/types/q/record-data-json-api'; -import { Dict } from '@ember-data/types/q/utils'; +import { assert, deprecate } from '@ember/debug'; + +import type { LocalRelationshipOperation } from '@ember-data/record-data/-private/graph/-operations'; +import type { + CollectionResourceRelationship, + SingleResourceRelationship, +} from '@ember-data/types/q/ember-data-json-api'; +import type { StableRecordIdentifier } from '@ember-data/types/q/identifier'; +import type { ChangedAttributesHash, RecordData, RecordDataV1 } from '@ember-data/types/q/record-data'; +import type { JsonApiResource, JsonApiValidationError } from '@ember-data/types/q/record-data-json-api'; +import type { Dict } from '@ember-data/types/q/utils'; import { isStableIdentifier } from '../caches/identifier-cache'; -import Store from '../store-service'; +import type Store from '../store-service'; /** * The RecordDataManager wraps a RecordData cache @@ -111,6 +115,26 @@ export class NonSingletonRecordDataManager implements RecordData { return recordData.pushData(identifier, data, hasRecord); } + update(operation: LocalRelationshipOperation): void { + if (this.#isDeprecated(this.#recordData)) { + deprecate( + `RecordData.update(operation) can only be used with V2 RecordData Implementations. Upgrade this RecordData to V2 to make use of this feature. This Relationship update will be ignored.`, + false, + { + id: 'ember-data:deprecate-v1-record-data', + until: '5.0', + since: { + enabled: '4.8', + available: '4.8', + }, + for: 'ember-data', + } + ); + } else { + this.#recordData.update(operation); + } + } + /** * [LIFECYLCE] Signal to the cache that a new record has been instantiated on the client * @@ -713,6 +737,9 @@ export class SingletonRecordDataManager implements RecordData { this.#store = store; this.#recordDatas = new Map(); } + update(operation: LocalRelationshipOperation): void { + this.#recordData(operation.record).update(operation); + } _addRecordData(identifier: StableRecordIdentifier, recordData: RecordData) { this.#recordDatas.set(identifier, recordData); diff --git a/packages/store/addon/-private/managers/record-data-store-wrapper.ts b/packages/store/addon/-private/managers/record-data-store-wrapper.ts index 64cf4ec933d..d6779c9f1ce 100644 --- a/packages/store/addon/-private/managers/record-data-store-wrapper.ts +++ b/packages/store/addon/-private/managers/record-data-store-wrapper.ts @@ -350,7 +350,13 @@ class V2RecordDataStoreWrapper implements StoreWrapper { } this._willNotify = true; - this._store._schedule('notify', () => this._flushNotifications()); + // it's possible a RecordData adhoc notifies us, + // in which case we sync flush + if (this._store._cbs) { + this._store._schedule('notify', () => this._flushNotifications()); + } else { + this._flushNotifications(); + } } _flushNotifications(): void { diff --git a/packages/store/addon/-private/managers/record-notification-manager.ts b/packages/store/addon/-private/managers/record-notification-manager.ts index 9656f687f20..db999b8573a 100644 --- a/packages/store/addon/-private/managers/record-notification-manager.ts +++ b/packages/store/addon/-private/managers/record-notification-manager.ts @@ -53,6 +53,7 @@ export default class NotificationManager { map = new Map(); Cache.set(identifier, map); } + let unsubToken = DEBUG ? { _tokenRef: tokenId++ } : {}; map.set(unsubToken, callback); Tokens.set(unsubToken, identifier); diff --git a/packages/store/addon/-private/network/snapshot-record-array.ts b/packages/store/addon/-private/network/snapshot-record-array.ts index da0a4a6294f..0f09d6ea587 100644 --- a/packages/store/addon/-private/network/snapshot-record-array.ts +++ b/packages/store/addon/-private/network/snapshot-record-array.ts @@ -10,7 +10,8 @@ import { StableRecordIdentifier } from '@ember-data/types/q/identifier'; import type { FindOptions } from '@ember-data/types/q/store'; import type { Dict } from '@ember-data/types/q/utils'; -import type RecordArray from '../record-arrays/record-array'; +import type IdentifierArray from '../record-arrays/identifier-array'; +import { SOURCE } from '../record-arrays/identifier-array'; import Store from '../store-service'; import type Snapshot from './snapshot'; /** @@ -23,12 +24,11 @@ import type Snapshot from './snapshot'; */ export default class SnapshotRecordArray { declare _snapshots: Snapshot[] | null; - declare _recordArray: RecordArray; + declare _recordArray: IdentifierArray; declare _type: ModelSchema | null; declare __store: Store; declare length: number; - declare meta: Dict | null; declare adapterOptions?: Dict; declare include?: string; @@ -41,10 +41,9 @@ export default class SnapshotRecordArray { @private @constructor @param {RecordArray} recordArray - @param {Object} meta @param options */ - constructor(store: Store, recordArray: RecordArray, options: FindOptions = {}) { + constructor(store: Store, recordArray: IdentifierArray, options: FindOptions = {}) { this.__store = store; /** An array of snapshots @@ -181,8 +180,9 @@ export default class SnapshotRecordArray { if (this._snapshots !== null) { return this._snapshots; } + const { _instanceCache } = this.__store; - this._snapshots = this._recordArray.content.map((identifier: StableRecordIdentifier) => + this._snapshots = this._recordArray[SOURCE].map((identifier: StableRecordIdentifier) => _instanceCache.createSnapshot(identifier) ); diff --git a/packages/store/addon/-private/proxies/promise-proxies.ts b/packages/store/addon/-private/proxies/promise-proxies.ts index 9896835b125..28bb8e69bd1 100644 --- a/packages/store/addon/-private/proxies/promise-proxies.ts +++ b/packages/store/addon/-private/proxies/promise-proxies.ts @@ -91,13 +91,13 @@ export class PromiseArray> extends PromiseArrayPr */ export { PromiseObjectProxy as PromiseObject }; -export function promiseObject(promise: Promise, label?: string): PromiseObjectProxy { +function _promiseObject(promise: Promise, label?: string): PromiseObjectProxy { return PromiseObjectProxy.create({ promise: resolve(promise, label), }) as PromiseObjectProxy; } -export function promiseArray>(promise: Promise, label?: string): PromiseArray { +function _promiseArray>(promise: Promise, label?: string): PromiseArray { return PromiseArray.create({ promise: resolve(promise, label), }) as unknown as PromiseArray; @@ -105,35 +105,96 @@ export function promiseArray>(promise: Promise // constructor is accessed in some internals but not including it in the copyright for the deprecation const ALLOWABLE_METHODS = ['constructor', 'then', 'catch', 'finally']; +const PROXIED_ARRAY_PROPS = [ + 'length', + '[]', + 'firstObject', + 'lastObject', + 'meta', + 'content', + 'isPending', + 'isSettled', + 'isRejected', + 'isFulfilled', + 'promise', + 'reason', +]; +const PROXIED_OBJECT_PROPS = ['content', 'isPending', 'isSettled', 'isRejected', 'isFulfilled', 'promise', 'reason']; + +export function promiseArray>(promise: Promise): PromiseArray { + const promiseObjectProxy: PromiseArray = _promiseArray(promise); + const handler = { + get(target: object, prop: string, receiver?: object): unknown { + if (typeof prop === 'symbol') { + return Reflect.get(target, prop, receiver); + } + if (!ALLOWABLE_METHODS.includes(prop)) { + deprecate( + `Accessing ${prop} on this PromiseArray is deprecated. The return type is being changed from PromiseArray to a Promise. The only available methods to access on this promise are .then, .catch and .finally`, + false, + { + id: 'ember-data:deprecate-promise-proxies', + until: '5.0', + for: '@ember-data/store', + since: { + available: '4.8', + enabled: '4.8', + }, + } + ); + } -export function deprecatedPromiseObject(promise: Promise): PromiseObjectProxy { - const promiseObjectProxy: PromiseObjectProxy = promiseObject(promise); + const value: unknown = target[prop]; + if (value && typeof value === 'function' && typeof value.bind === 'function') { + return value.bind(target); + } + + if (PROXIED_ARRAY_PROPS.includes(prop)) { + return value; + } + + return undefined; + }, + }; + + return new Proxy(promiseObjectProxy, handler); +} + +export function promiseObject(promise: Promise): PromiseObjectProxy { + const promiseObjectProxy: PromiseObjectProxy = _promiseObject(promise); const handler = { get(target: object, prop: string, receiver?: object): unknown { + if (typeof prop === 'symbol') { + return Reflect.get(target, prop, receiver); + } if (!ALLOWABLE_METHODS.includes(prop)) { deprecate( - `Accessing ${prop} is deprecated. The return type is being changed fomr PromiseObjectProxy to a Promise. The only available methods to access on this promise are .then, .catch and .finally`, + `Accessing ${prop} on this PromiseObject is deprecated. The return type is being changed from PromiseObject to a Promise. The only available methods to access on this promise are .then, .catch and .finally`, false, { - id: 'ember-data:model-save-promise', + id: 'ember-data:deprecate-promise-proxies', until: '5.0', for: '@ember-data/store', since: { - available: '4.4', - enabled: '4.4', + available: '4.8', + enabled: '4.8', }, } ); } - const value: unknown = Reflect.get(target, prop, receiver); + const value: unknown = target[prop]; if (value && typeof value === 'function' && typeof value.bind === 'function') { return value.bind(target); } - return value; + if (PROXIED_OBJECT_PROPS.includes(prop)) { + return value; + } + + return undefined; }, }; - return new Proxy(promiseObjectProxy, handler) as PromiseObjectProxy; + return new Proxy(promiseObjectProxy, handler); } diff --git a/packages/store/addon/-private/record-arrays/adapter-populated-record-array.ts b/packages/store/addon/-private/record-arrays/adapter-populated-record-array.ts deleted file mode 100644 index 0eff74a62d9..00000000000 --- a/packages/store/addon/-private/record-arrays/adapter-populated-record-array.ts +++ /dev/null @@ -1,99 +0,0 @@ -import type NativeArray from '@ember/array/-private/native-array'; -import { assert } from '@ember/debug'; - -import type { Links, PaginationLinks } from '@ember-data/types/q/ember-data-json-api'; -import type { StableRecordIdentifier } from '@ember-data/types/q/identifier'; -import type { RecordInstance } from '@ember-data/types/q/record-instance'; -import type { Dict } from '@ember-data/types/q/utils'; - -import type RecordArrayManager from '../managers/record-array-manager'; -import type { PromiseArray } from '../proxies/promise-proxies'; -import { promiseArray } from '../proxies/promise-proxies'; -import type Store from '../store-service'; -import RecordArray, { MANAGED } from './record-array'; - -export interface AdapterPopulatedRecordArrayCreateArgs { - modelName: string; - store: Store; - manager: RecordArrayManager; - content: NativeArray; - isLoaded: boolean; - query: Dict | null; - meta: Dict | null; - links: Links | PaginationLinks | null; -} - -/** - @module @ember-data/store -*/ - -/** - Represents an ordered list of records whose order and membership is - determined by the adapter. For example, a query sent to the adapter - may trigger a search on the server, whose results would be loaded - into an instance of the `AdapterPopulatedRecordArray`. - - This class should not be imported and instantiated by consuming applications. - - --- - - If you want to update the array and get the latest records from the - adapter, you can invoke [`update()`](AdapterPopulatedRecordArray/methods/update?anchor=update): - - Example - - ```javascript - // GET /users?isAdmin=true - store.query('user', { isAdmin: true }).then(function(admins) { - - admins.get("length"); // 42 - - // somewhere later in the app code, when new admins have been created - // in the meantime - // - // GET /users?isAdmin=true - admins.update().then(function() { - admins.isUpdating; // false - admins.get("length"); // 123 - }); - - admins.isUpdating; // true - } - ``` - - @class AdapterPopulatedRecordArray - @public - @extends RecordArray -*/ -export default class AdapterPopulatedRecordArray extends RecordArray { - declare links: Links | PaginationLinks | null; - declare meta: Dict | null; - declare query: Dict | null; - [MANAGED] = true; - - init(props?: AdapterPopulatedRecordArrayCreateArgs) { - assert(`Cannot initialize AdapterPopulatedRecordArray with isUpdating`, !props || !('isUpdating' in props)); - super.init(); - this.query = this.query || null; - this.links = this.links || null; - this.meta = this.meta || null; - } - - replace() { - throw new Error(`The result of a server query (on ${this.modelName}) is immutable.`); - } - - _update(): PromiseArray { - const { store, query } = this; - const promise = store.query(this.modelName, query, { _recordArray: this }); - - // TODO save options from initial request? - return promiseArray(promise); - } - - willDestroy() { - super.willDestroy(); - this.manager._managed.delete(this); - this.manager._pending.delete(this); - } -} diff --git a/packages/store/addon/-private/record-arrays/identifier-array.ts b/packages/store/addon/-private/record-arrays/identifier-array.ts new file mode 100644 index 00000000000..71b9384cd24 --- /dev/null +++ b/packages/store/addon/-private/record-arrays/identifier-array.ts @@ -0,0 +1,837 @@ +/** + @module @ember-data/store +*/ +import { assert, deprecate } from '@ember/debug'; +import { get } from '@ember/object'; +import { dependentKeyCompat } from '@ember/object/compat'; +import { compare } from '@ember/utils'; +import { DEBUG } from '@glimmer/env'; +import { tracked } from '@glimmer/tracking'; +import Ember from 'ember'; + +import { + DEPRECATE_ARRAY_LIKE, + DEPRECATE_PROMISE_PROXIES, + DEPRECATE_SNAPSHOT_MODEL_CLASS_ACCESS, +} from '@ember-data/private-build-infra/deprecations'; +import { Links, PaginationLinks } from '@ember-data/types/q/ember-data-json-api'; +import type { StableRecordIdentifier } from '@ember-data/types/q/identifier'; +import type { RecordInstance } from '@ember-data/types/q/record-instance'; +import { Dict } from '@ember-data/types/q/utils'; + +import { recordIdentifierFor } from '../caches/instance-cache'; +import type RecordArrayManager from '../managers/record-array-manager'; +import { PromiseArray, promiseArray } from '../proxies/promise-proxies'; +import type Store from '../store-service'; + +type KeyType = string | symbol | number; +const ARRAY_GETTER_METHODS = new Set([ + Symbol.iterator, + 'concat', + 'entries', + 'every', + 'fill', + 'filter', + 'find', + 'findIndex', + 'flat', + 'flatMap', + 'forEach', + 'includes', + 'indexOf', + 'join', + 'keys', + 'lastIndexOf', + 'map', + 'reduce', + 'reduceRight', + 'slice', + 'some', + 'values', +]); +const ARRAY_SETTER_METHODS = new Set(['push', 'pop', 'unshift', 'shift', 'splice', 'sort']); +const SYNC_PROPS = new Set(['[]', 'length', 'links', 'meta']); +function isArrayGetter(prop: KeyType): boolean { + return ARRAY_GETTER_METHODS.has(prop); +} +function isArraySetter(prop: KeyType): boolean { + return ARRAY_SETTER_METHODS.has(prop); +} + +export const IDENTIFIER_ARRAY_TAG = Symbol('#tag'); +export const SOURCE = Symbol('#source'); +export const MUTATE = Symbol('#update'); + +function convertToInt(prop: KeyType): number | null { + if (typeof prop === 'symbol') return null; + + const num = Number(prop); + + if (isNaN(num)) return null; + + return num % 1 === 0 ? num : null; +} + +class Tag { + @tracked ref = null; + shouldReset: boolean = false; +} + +type ProxiedMethod = (...args: unknown[]) => unknown; +declare global { + interface ProxyConstructor { + new (target: TSource, handler: ProxyHandler): TTarget; + } +} + +export type IdentifierArrayCreateOptions = { + identifiers: StableRecordIdentifier[]; + type: string; + store: Store; + allowMutation: boolean; + manager: RecordArrayManager; + links?: Links | PaginationLinks | null; + meta?: Dict | null; +}; + +function deprecateArrayLike(className: string, fnName: string, replName: string) { + deprecate( + `The \`${fnName}\` method on the class ${className} is deprecated. Use the native array method \`${replName}\` instead.`, + false, + { + id: 'ember-data:deprecate-array-like', + until: '5.0', + since: { enabled: '4.8', available: '4.8' }, + for: 'ember-data', + } + ); +} + +interface PrivateState { + links: Links | PaginationLinks | null; + meta: Dict | null; +} + +/** + A record array is an array that contains records of a certain type (or modelName). + The record array materializes records as needed when they are retrieved for the first + time. You should not create record arrays yourself. Instead, an instance of + `RecordArray` or its subclasses will be returned by your application's store + in response to queries. + + This class should not be imported and instantiated by consuming applications. + + @class RecordArray + @public +*/ + +interface IdentifierArray { + [MUTATE]?(prop: string, args: unknown[], result?: unknown): void; +} +interface IdentifierArray extends Array {} +class IdentifierArray { + declare DEPRECATED_CLASS_NAME: string; + /** + The flag to signal a `RecordArray` is currently loading data. + Example + ```javascript + let people = store.peekAll('person'); + people.isUpdating; // false + people.update(); + people.isUpdating; // true + ``` + @property isUpdating + @public + @type Boolean + */ + @tracked isUpdating: boolean = false; + isLoaded: boolean = true; + isDestroying: boolean = false; + isDestroyed: boolean = false; + _updatingPromise: PromiseArray | Promise | null = null; + + [IDENTIFIER_ARRAY_TAG] = new Tag(); + [SOURCE]: StableRecordIdentifier[]; + + declare links: Links | PaginationLinks | null; + declare meta: Dict | null; + + /** + The modelClass represented by this record array. + + @property type + @public + @deprecated + @type {subclass of Model} + */ + declare modelName: string; + /** + The store that created this record array. + + @property store + @private + @type Store + */ + declare store: Store; + declare _manager: RecordArrayManager; + + destroy() { + this.isDestroying = true; + // changing the reference breaks the Proxy + // this[SOURCE] = []; + this[SOURCE].length = 0; + this[IDENTIFIER_ARRAY_TAG].ref = null; + this.isDestroyed = true; + } + + // length must be on self for proxied methods to work properly + @dependentKeyCompat + get length() { + return this[SOURCE].length; + } + set length(value) { + this[SOURCE].length = value; + } + + constructor(options: IdentifierArrayCreateOptions) { + // eslint-disable-next-line @typescript-eslint/no-this-alias + let self = this; + this.modelName = options.type; + this.store = options.store; + this._manager = options.manager; + this[SOURCE] = options.identifiers; + const store = options.store; + const boundFns = new Map(); + const _TAG = this[IDENTIFIER_ARRAY_TAG]; + const PrivateState: PrivateState = { + links: options.links || null, + meta: options.meta || null, + }; + let transaction: boolean = false; + + // when a mutation occurs + // we track all mutations within the call + // and forward them as one + + const proxy = new Proxy(this[SOURCE], { + get(target: StableRecordIdentifier[], prop: KeyType, receiver: IdentifierArray): unknown { + let index = convertToInt(prop); + if (_TAG.shouldReset && (index !== null || SYNC_PROPS.has(prop) || isArrayGetter(prop))) { + options.manager._syncArray(receiver as unknown as IdentifierArray); + _TAG.shouldReset = false; + } + + if (index !== null) { + const identifier = target[index]; + if (!transaction) { + _TAG.ref; + } + return identifier && store._instanceCache.getRecord(identifier); + } + + if (prop === 'meta') return _TAG.ref, PrivateState.meta; + if (prop === 'links') return _TAG.ref, PrivateState.links; + if (prop === '[]') return _TAG.ref, receiver; + + if (isArrayGetter(prop)) { + let fn = boundFns.get(prop); + + if (fn === undefined) { + fn = function () { + _TAG.ref; + // array functions must run through Reflect to work properly + // binding via other means will not work. + transaction = true; + let result = Reflect.apply(target[prop] as ProxiedMethod, receiver, arguments) as unknown; + transaction = false; + return result; + }; + + boundFns.set(prop, fn); + } + + return fn; + } + + if (isArraySetter(prop)) { + let fn = boundFns.get(prop); + + if (fn === undefined) { + fn = function () { + // array functions must run through Reflect to work properly + // binding via other means will not work. + if (!options.allowMutation) { + assert(`Mutating this array of records via ${String(prop)} is not allowed.`, options.allowMutation); + return; + } + const args: unknown[] = Array.prototype.slice.call(arguments); + assert(`Cannot start a new array transaction while a previous transaction is underway`, !transaction); + transaction = true; + let result = Reflect.apply(target[prop] as ProxiedMethod, receiver, args) as unknown; + self[MUTATE]!(prop as string, args, result); + _TAG.ref = null; + // TODO handle cache updates + transaction = false; + return result; + }; + + boundFns.set(prop, fn); + } + + return fn; + } + + if (prop in self) { + if (DEPRECATE_ARRAY_LIKE) { + if (prop === 'firstObject') { + deprecateArrayLike(self.DEPRECATED_CLASS_NAME, prop, '[0]'); + return receiver.at(0); + } else if (prop === 'lastObject') { + deprecateArrayLike(self.DEPRECATED_CLASS_NAME, prop, 'at(-1)'); + return receiver.at(-1); + } + } + + let fn = boundFns.get(prop); + if (fn) return fn; + + let outcome: unknown = self[prop]; + + if (typeof outcome === 'function') { + fn = function () { + _TAG.ref; + // array functions must run through Reflect to work properly + // binding via other means will not work. + return Reflect.apply(outcome as ProxiedMethod, receiver, arguments) as unknown; + }; + + boundFns.set(prop, fn); + return fn; + } + + return _TAG.ref, outcome; + } + + return target[prop]; + }, + + set(target: StableRecordIdentifier[], prop: KeyType, value: unknown /*, receiver */): boolean { + if (prop === 'length') { + if (!transaction && value === 0) { + transaction = true; + _TAG.ref = null; + Reflect.set(target, prop, value); + self[MUTATE]!('length 0', []); + transaction = false; + return true; + } else if (transaction) { + return Reflect.set(target, prop, value); + } else { + assert(`unexpected length set`); + } + } + if (prop === 'links') { + PrivateState.links = (value || null) as PaginationLinks | Links | null; + return true; + } + if (prop === 'meta') { + PrivateState.meta = (value || null) as Dict | null; + return true; + } + let index = convertToInt(prop); + + if (index === null || index > target.length) { + if (prop in self) { + self[prop] = value; + return true; + } + return false; + } + + if (!options.allowMutation) { + assert(`Mutating ${String(prop)} on this RecordArray is not allowed.`, options.allowMutation); + return false; + } + + let original: StableRecordIdentifier | undefined = target[index]; + let newIdentifier = extractIdentifierFromRecord(value as RecordInstance); + (target as unknown as Record)[index] = newIdentifier; + if (!transaction) { + self[MUTATE]!('replace cell', [index, original, newIdentifier]); + _TAG.ref = null; + } + + return true; + }, + + deleteProperty(target: StableRecordIdentifier[], prop: string | symbol): boolean { + assert(`Deleting keys on managed arrays is disallowed`, transaction); + if (!transaction) { + return false; + } + return Reflect.deleteProperty(target, prop); + }, + + getPrototypeOf() { + return IdentifierArray.prototype; + }, + }) as IdentifierArray; + + if (DEBUG) { + // prevent `A()` from clobbering us + const meta = Ember.meta(proxy); + meta.hasMixin = (mixin: Object) => { + assert(`Do not call A() on EmberData RecordArrays`); + }; + } + + return proxy; + } + + /** + Used to get the latest version of all of the records in this array + from the adapter. + + Example + + ```javascript + let people = store.peekAll('person'); + people.isUpdating; // false + + people.update().then(function() { + people.isUpdating; // false + }); + + people.isUpdating; // true + ``` + + @method update + @public + */ + update(): PromiseArray | Promise { + if (this.isUpdating) { + return this._updatingPromise!; + } + + this.isUpdating = true; + + let updatingPromise = this._update(); + updatingPromise.finally(() => { + this._updatingPromise = null; + if (this.isDestroying || this.isDestroyed) { + return; + } + this.isUpdating = false; + }); + + this._updatingPromise = updatingPromise; + + return updatingPromise; + } + + /* + Update this RecordArray and return a promise which resolves once the update + is finished. + */ + _update(): PromiseArray | Promise { + return this.store.findAll(this.modelName, { reload: true }); + } + + // TODO deprecate + /** + Saves all of the records in the `RecordArray`. + + Example + + ```javascript + let messages = store.peekAll('message'); + messages.forEach(function(message) { + message.hasBeenSeen = true; + }); + messages.save(); + ``` + + @method save + @public + @return {PromiseArray} promise + */ + save(): PromiseArray | Promise { + let promise = Promise.all(this.map((record) => this.store.saveRecord(record))).then(() => this); + + if (DEPRECATE_PROMISE_PROXIES) { + return promiseArray(promise); + } + + return promise; + } +} + +Object.setPrototypeOf(IdentifierArray.prototype, Array.prototype); + +export default IdentifierArray; + +if (DEPRECATE_SNAPSHOT_MODEL_CLASS_ACCESS) { + Object.defineProperty(IdentifierArray.prototype, 'type', { + get() { + deprecate( + `Using RecordArray.type to access the ModelClass for a record is deprecated. Use store.modelFor() instead.`, + false, + { + id: 'ember-data:deprecate-snapshot-model-class-access', + until: '5.0', + for: 'ember-data', + since: { available: '4.5.0', enabled: '4.5.0' }, + } + ); + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + if (!this.modelName) { + return null; + } + // eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call + return this.store.modelFor(this.modelName); + }, + }); +} + +export type CollectionCreateOptions = IdentifierArrayCreateOptions & { + query: Dict | null; + isLoaded: boolean; +}; +export class Collection extends IdentifierArray { + query: Dict | null = null; + + constructor(options: CollectionCreateOptions) { + super(options as IdentifierArrayCreateOptions); + this.query = options.query || null; + this.isLoaded = options.isLoaded || false; + } + + _update(): PromiseArray | Promise { + const { store, query } = this; + + // TODO save options from initial request? + const promise = store.query(this.modelName, query, { _recordArray: this }); + + if (DEPRECATE_PROMISE_PROXIES) { + return promiseArray(promise); + } + return promise; + } + + destroy() { + super.destroy(); + this._manager._managed.delete(this); + this._manager._pending.delete(this); + } +} +// trick the proxy "in" check +Collection.prototype.query = null; + +// Ensure instanceof works correctly +// Object.setPrototypeOf(IdentifierArray.prototype, Array.prototype); + +if (DEPRECATE_ARRAY_LIKE) { + IdentifierArray.prototype.DEPRECATED_CLASS_NAME = 'RecordArray'; + Collection.prototype.DEPRECATED_CLASS_NAME = 'RecordArray'; + const EmberObjectMethods = [ + 'addObserver', + 'cacheFor', + 'decrementProperty', + 'get', + 'getProperties', + 'incrementProperty', + 'notifyPropertyChange', + 'removeObserver', + 'set', + 'setProperties', + 'toggleProperty', + ]; + EmberObjectMethods.forEach((method) => { + IdentifierArray.prototype[method] = function delegatedMethod(...args: unknown[]): unknown { + deprecate( + `The EmberObject ${method} method on the class ${this.DEPRECATED_CLASS_NAME} is deprecated. Use dot-notation javascript get/set access instead.`, + false, + { + id: 'ember-data:deprecate-array-like', + until: '5.0', + since: { enabled: '4.8', available: '4.8' }, + for: 'ember-data', + } + ); + return (Ember[method] as (...args: unknown[]) => unknown)(this, ...args); + }; + }); + + IdentifierArray.prototype.addObject = function (obj: RecordInstance) { + deprecateArrayLike(this.DEPRECATED_CLASS_NAME, 'addObject', 'push'); + let index = this.indexOf(obj); + if (index === -1) { + this.push(obj); + } + return this; + }; + + IdentifierArray.prototype.addObjects = function (objs: RecordInstance[]) { + deprecateArrayLike(this.DEPRECATED_CLASS_NAME, 'addObjects', 'push'); + objs.forEach((obj: RecordInstance) => { + let index = this.indexOf(obj); + if (index === -1) { + this.push(obj); + } + }); + return this; + }; + + IdentifierArray.prototype.popObject = function () { + deprecateArrayLike(this.DEPRECATED_CLASS_NAME, 'popObject', 'pop'); + return this.pop() as RecordInstance; + }; + + IdentifierArray.prototype.pushObject = function (obj: RecordInstance) { + deprecateArrayLike(this.DEPRECATED_CLASS_NAME, 'pushObject', 'push'); + this.push(obj); + return obj; + }; + + IdentifierArray.prototype.pushObjects = function (objs: RecordInstance[]) { + deprecateArrayLike(this.DEPRECATED_CLASS_NAME, 'pushObjects', 'push'); + this.push(...objs); + return this; + }; + + IdentifierArray.prototype.shiftObject = function () { + deprecateArrayLike(this.DEPRECATED_CLASS_NAME, 'shiftObject', 'shift'); + return this.shift()!; + }; + + IdentifierArray.prototype.unshiftObject = function (obj: RecordInstance) { + deprecateArrayLike(this.DEPRECATED_CLASS_NAME, 'unshiftObject', 'unshift'); + this.unshift(obj); + return obj; + }; + + IdentifierArray.prototype.unshiftObjects = function (objs: RecordInstance[]) { + deprecateArrayLike(this.DEPRECATED_CLASS_NAME, 'unshiftObjects', 'unshift'); + this.unshift(...objs); + return this; + }; + + IdentifierArray.prototype.objectAt = function (index: number) { + deprecateArrayLike(this.DEPRECATED_CLASS_NAME, 'objectAt', 'at'); + return this.at(index); + }; + + IdentifierArray.prototype.objectsAt = function (indeces: number[]) { + deprecateArrayLike(this.DEPRECATED_CLASS_NAME, 'objectsAt', 'at'); + return indeces.map((index) => this.at(index)!); + }; + + IdentifierArray.prototype.removeAt = function (index: number) { + deprecateArrayLike(this.DEPRECATED_CLASS_NAME, 'removeAt', 'splice'); + this.splice(index, 1); + return this; + }; + + IdentifierArray.prototype.insertAt = function (index: number, obj: RecordInstance) { + deprecateArrayLike(this.DEPRECATED_CLASS_NAME, 'insertAt', 'splice'); + this.splice(index, 0, obj); + return this; + }; + + IdentifierArray.prototype.removeObject = function (obj: RecordInstance) { + deprecateArrayLike(this.DEPRECATED_CLASS_NAME, 'removeObject', 'splice'); + this.splice(this.indexOf(obj), 1); + return this; + }; + + IdentifierArray.prototype.removeObjects = function (objs: RecordInstance[]) { + deprecateArrayLike(this.DEPRECATED_CLASS_NAME, 'removeObjects', 'splice'); + objs.forEach((obj) => this.splice(this.indexOf(obj), 1)); + return this; + }; + + IdentifierArray.prototype.toArray = function () { + deprecateArrayLike(this.DEPRECATED_CLASS_NAME, 'unshiftObjects', 'unshift'); + return this.slice(); + }; + + IdentifierArray.prototype.replace = function (idx: number, amt: number, objects?: RecordInstance[]) { + deprecateArrayLike(this.DEPRECATED_CLASS_NAME, 'replace', 'splice'); + if (objects) { + this.splice(idx, amt, ...objects); + } else { + this.splice(idx, amt); + } + }; + + IdentifierArray.prototype.clear = function () { + deprecateArrayLike(this.DEPRECATED_CLASS_NAME, 'clear', 'length = 0'); + this.splice(0, this.length); + return this; + }; + + IdentifierArray.prototype.setObjects = function (objects: RecordInstance[]) { + deprecateArrayLike(this.DEPRECATED_CLASS_NAME, 'clear', 'length = 0'); + assert( + `${this.DEPRECATED_CLASS_NAME}.setObjects expects to receive an array as its argument`, + Array.isArray(objects) + ); + this.splice(0, this.length); + this.push(...objects); + return this; + }; + + IdentifierArray.prototype.reverseObjects = function () { + deprecateArrayLike(this.DEPRECATED_CLASS_NAME, 'reverseObjects', 'reverse'); + this.reverse(); + return this; + }; + + IdentifierArray.prototype.compact = function () { + deprecateArrayLike(this.DEPRECATED_CLASS_NAME, 'compact', 'filter'); + return this.filter((v) => v !== null && v !== undefined); + }; + + IdentifierArray.prototype.any = function (callback, target) { + deprecateArrayLike(this.DEPRECATED_CLASS_NAME, 'any', 'some'); + return this.some(callback, target); + }; + + IdentifierArray.prototype.isAny = function (prop, value) { + deprecateArrayLike(this.DEPRECATED_CLASS_NAME, 'isAny', 'some'); + let hasValue = arguments.length === 2; + return this.some((v) => (hasValue ? v[prop] === value : v[prop] === true)); + }; + + IdentifierArray.prototype.isEvery = function (prop, value) { + deprecateArrayLike(this.DEPRECATED_CLASS_NAME, 'isEvery', 'every'); + let hasValue = arguments.length === 2; + return this.every((v) => (hasValue ? v[prop] === value : v[prop] === true)); + }; + + IdentifierArray.prototype.getEach = function (key: string) { + deprecateArrayLike(this.DEPRECATED_CLASS_NAME, 'getEach', 'map'); + return this.map((value) => get(value, key)); + }; + + IdentifierArray.prototype.mapBy = function (key: string) { + deprecateArrayLike(this.DEPRECATED_CLASS_NAME, 'mapBy', 'map'); + return this.map((value) => get(value, key)); + }; + + IdentifierArray.prototype.findBy = function (key: string, value?: unknown) { + deprecateArrayLike(this.DEPRECATED_CLASS_NAME, 'findBy', 'find'); + if (arguments.length === 2) { + return this.find((val) => { + return get(val, key) === value; + }); + } else { + return this.find((val) => Boolean(get(val, key))); + } + }; + + IdentifierArray.prototype.filterBy = function (key: string, value?: unknown) { + deprecateArrayLike(this.DEPRECATED_CLASS_NAME, 'filterBy', 'filter'); + if (arguments.length === 2) { + return this.filter((value) => { + return Boolean(get(value, key)); + }); + } + return this.filter((value) => { + return Boolean(get(value, key)); + }); + }; + + IdentifierArray.prototype.sortBy = function (...sortKeys: string[]) { + deprecateArrayLike(this.DEPRECATED_CLASS_NAME, 'sortBy', '.slice().sort'); + return this.slice().sort((a, b) => { + for (let i = 0; i < sortKeys.length; i++) { + let key = sortKeys[i]; + let propA = get(a, key); + let propB = get(b, key); + // return 1 or -1 else continue to the next sortKey + let compareValue = compare(propA, propB); + + if (compareValue) { + return compareValue; + } + } + return 0; + }); + }; + + // @ts-expect-error + IdentifierArray.prototype.invoke = function (key: string, ...args: unknown[]) { + deprecateArrayLike(this.DEPRECATED_CLASS_NAME, 'invoke', 'forEach'); + return this.map((value) => (value[key] as (...args: unknown[]) => unknown)(...args)); + }; + + /* + Object.defineProperty(IdentifierArray.prototype, '[]', { + get() { + return this as IdentifierArray; + }, + }); + */ + + // @ts-expect-error + IdentifierArray.prototype.firstObject = null; + // @ts-expect-error + IdentifierArray.prototype.lastObject = null; + + /* + const InheritedProxyMethods = [ + 'findBy', + 'reject', + 'rejectBy', + 'setEach', + 'uniq', + 'uniqBy', + 'without', + + 'addArrayObserver', + 'arrayContentDidChange', + 'arrayContentWillChange', + 'removeArrayObserver', + ]; + */ +} + +type PromiseProxyRecord = { then(): void; content: RecordInstance | null | undefined }; + +function assertRecordPassedToHasMany(record: RecordInstance | PromiseProxyRecord) { + assert( + `All elements of a hasMany relationship must be instances of Model, you passed $${typeof record}`, + (function () { + try { + recordIdentifierFor(record); + return true; + } catch { + return false; + } + })() + ); +} + +function extractIdentifierFromRecord(recordOrPromiseRecord: PromiseProxyRecord | RecordInstance | null) { + if (!recordOrPromiseRecord) { + return null; + } + + if (isPromiseRecord(recordOrPromiseRecord)) { + let content = recordOrPromiseRecord.content; + assert( + 'You passed in a promise that did not originate from an EmberData relationship. You can only pass promises that come from a belongsTo relationship.', + content !== undefined && content !== null + ); + assertRecordPassedToHasMany(content); + return recordIdentifierFor(content); + } + + assertRecordPassedToHasMany(recordOrPromiseRecord); + return recordIdentifierFor(recordOrPromiseRecord); +} + +function isPromiseRecord(record: PromiseProxyRecord | RecordInstance): record is PromiseProxyRecord { + return !!record.then; +} diff --git a/packages/store/addon/-private/record-arrays/record-array.ts b/packages/store/addon/-private/record-arrays/record-array.ts deleted file mode 100644 index 1129ee6ea52..00000000000 --- a/packages/store/addon/-private/record-arrays/record-array.ts +++ /dev/null @@ -1,289 +0,0 @@ -/** - @module @ember-data/store -*/ -import type NativeArray from '@ember/array/-private/native-array'; -import ArrayProxy from '@ember/array/proxy'; -import { assert, deprecate } from '@ember/debug'; -import { set } from '@ember/object'; -import { tracked } from '@glimmer/tracking'; - -import { Promise } from 'rsvp'; - -import { DEPRECATE_SNAPSHOT_MODEL_CLASS_ACCESS } from '@ember-data/private-build-infra/deprecations'; -import type { StableRecordIdentifier } from '@ember-data/types/q/identifier'; -import type { RecordInstance } from '@ember-data/types/q/record-instance'; - -import RecordArrayManager, { disassociateIdentifier } from '../managers/record-array-manager'; -import type { PromiseArray } from '../proxies/promise-proxies'; -import { promiseArray } from '../proxies/promise-proxies'; -import type Store from '../store-service'; -import type AdapterPopulatedRecordArray from './adapter-populated-record-array'; - -function recordForIdentifier(store: Store, identifier: StableRecordIdentifier): RecordInstance { - return store._instanceCache.getRecord(identifier); -} - -export interface RecordArrayCreateArgs { - modelName: string; - store: Store; - manager: RecordArrayManager; - content: NativeArray; - isLoaded: boolean; -} -export const MANAGED = Symbol('#managed'); - -/** - A record array is an array that contains records of a certain modelName. The record - array materializes records as needed when they are retrieved for the first - time. You should not create record arrays yourself. Instead, an instance of - `RecordArray` or its subclasses will be returned by your application's store - in response to queries. - - This class should not be imported and instantiated by consuming applications. - - @class RecordArray - @public - @extends Ember.ArrayProxy -*/ -export default class RecordArray extends ArrayProxy { - /** - The array of client ids backing the record array. When a - record is requested from the record array, the record - for the client id at the same index is materialized, if - necessary, by the store. - - @property content - @private - @type Ember.Array - */ - declare content: NativeArray; - declare modelName: string; - /** - The flag to signal a `RecordArray` is finished loading data. - - Example - - ```javascript - let people = store.peekAll('person'); - people.isLoaded; // true - ``` - - @property isLoaded - @public - @type Boolean - */ - declare isLoaded: boolean; - /** - The store that created this record array. - - @property store - @private - @type Store - */ - declare store: Store; - declare _updatingPromise: PromiseArray | null; - declare manager: RecordArrayManager; - - /** - The flag to signal a `RecordArray` is currently loading data. - Example - ```javascript - let people = store.peekAll('person'); - people.isUpdating; // false - people.update(); - people.isUpdating; // true - ``` - @property isUpdating - @public - @type Boolean - */ - @tracked isUpdating: boolean = false; - [MANAGED]: boolean = false; - - init(props?: RecordArrayCreateArgs) { - assert(`Cannot initialize RecordArray with isUpdating`, !props || !('isUpdating' in props)); - assert(`Cannot initialize RecordArray with isUpdating`, !props || !('_updatingPromise' in props)); - super.init(); - - // TODO can we get rid of this? - this.content = this.content || null; - this._updatingPromise = null; - } - - _notify() { - // until we kill ArrayProxy we immediately sync - // because otherwise we double notify - this.manager._syncArray(this); - } - - replace() { - throw new Error( - `The result of a server query (for all ${this.modelName} types) is immutable. To modify contents, use toArray()` - ); - } - - /** - The modelClass represented by this record array. - - @property type - @public - @deprecated - @type {subclass of Model} - */ - - /** - Retrieves an object from the content by index. - - @method objectAtContent - @private - @param {Number} index - @return {Model} record - */ - objectAtContent(index: number): RecordInstance | undefined { - let identifier = this.content.objectAt(index); - return identifier ? recordForIdentifier(this.store, identifier) : undefined; - } - - /** - Used to get the latest version of all of the records in this array - from the adapter. - - Example - - ```javascript - let people = store.peekAll('person'); - people.isUpdating; // false - - people.update().then(function() { - people.isUpdating; // false - }); - - people.isUpdating; // true - ``` - - @method update - @public - */ - update(): PromiseArray { - if (this.isUpdating) { - return this._updatingPromise!; - } - - this.isUpdating = true; - - let updatingPromise = this._update(); - updatingPromise.finally(() => { - this._updatingPromise = null; - if (this.isDestroying || this.isDestroyed) { - return; - } - this.isUpdating = false; - }); - - this._updatingPromise = updatingPromise; - - return updatingPromise; - } - - /* - Update this RecordArray and return a promise which resolves once the update - is finished. - */ - _update(): PromiseArray { - return this.store.findAll(this.modelName, { reload: true }); - } - - /** - Saves all of the records in the `RecordArray`. - - Example - - ```javascript - let messages = store.peekAll('message'); - messages.forEach(function(message) { - message.hasBeenSeen = true; - }); - messages.save(); - ``` - - @method save - @public - @return {PromiseArray} promise - */ - save(): PromiseArray { - let promiseLabel = `DS: RecordArray#save ${this.modelName}`; - let promise = Promise.all(this.invoke('save'), promiseLabel).then( - () => this, - null, - 'DS: RecordArray#save return RecordArray' - ); - - return promiseArray(promise); - } - - willDestroy() { - // TODO: we should not do work during destroy: - // * when objects are destroyed, they should simply be left to do - // * if logic errors do to this, that logic needs to be more careful during - // teardown (ember provides isDestroying/isDestroyed) for this reason - // * the exception being: if an dominator has a reference to this object, - // and must be informed to release e.g. e.g. removing itself from th - // recordArrayMananger - // TODO we have to use set here vs dot notation because computed properties don't clear - // otherwise for destroyed records and will not update their value. - set(this, 'content', null as unknown as NativeArray); - set(this, 'length', 0); - super.willDestroy(); - } - - _updateState(changes: Map) { - const content = this.content; - const adds: StableRecordIdentifier[] = []; - const removes: StableRecordIdentifier[] = []; - const isManaged = this[MANAGED]; - changes.forEach((value, key) => { - value === 'add' ? adds.push(key) : removes.push(key); - if (isManaged && value === 'del') { - disassociateIdentifier(this as unknown as AdapterPopulatedRecordArray, key); - } - }); - if (removes.length) { - if (removes.length === content.length) { - content.clear(); - } else { - content.removeObjects(removes); - } - } - - if (adds.length) { - if (content.length === 0) { - content.setObjects(adds); - } else { - content.addObjects(adds); - } - } - } -} - -if (DEPRECATE_SNAPSHOT_MODEL_CLASS_ACCESS) { - Object.defineProperty(RecordArray.prototype, 'type', { - get() { - deprecate( - `Using RecordArray.type to access the ModelClass for a record is deprecated. Use store.modelFor() instead.`, - false, - { - id: 'ember-data:deprecate-snapshot-model-class-access', - until: '5.0', - for: 'ember-data', - since: { available: '4.5.0', enabled: '4.5.0' }, - } - ); - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - if (!this.modelName) { - return null; - } - // eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call - return this.store.modelFor(this.modelName); - }, - }); -} diff --git a/packages/store/addon/-private/store-service.ts b/packages/store/addon/-private/store-service.ts index 38757e064a0..b49e20225fd 100644 --- a/packages/store/addon/-private/store-service.ts +++ b/packages/store/addon/-private/store-service.ts @@ -17,6 +17,7 @@ import { HAS_MODEL_PACKAGE, HAS_RECORD_DATA_PACKAGE } from '@ember-data/private- import { DEPRECATE_HAS_RECORD, DEPRECATE_JSON_API_FALLBACK, + DEPRECATE_PROMISE_PROXIES, DEPRECATE_STORE_FIND, DEPRECATE_V1CACHE_STORE_APIS, } from '@ember-data/private-build-infra/deprecations'; @@ -64,8 +65,7 @@ import type RequestCache from './network/request-cache'; import type Snapshot from './network/snapshot'; import SnapshotRecordArray from './network/snapshot-record-array'; import { PromiseArray, promiseArray, PromiseObject, promiseObject } from './proxies/promise-proxies'; -import AdapterPopulatedRecordArray from './record-arrays/adapter-populated-record-array'; -import RecordArray from './record-arrays/record-array'; +import IdentifierArray, { Collection } from './record-arrays/identifier-array'; import coerceId, { ensureStringId } from './utils/coerce-id'; import constructResource from './utils/construct-resource'; import normalizeModelName from './utils/normalize-model-name'; @@ -293,6 +293,7 @@ class Store extends Service { _schedule(name: 'coalesce' | 'sync' | 'notify', cb: () => void): void { assert(`EmberData expects to schedule only when there is an active run`, !!this._cbs); assert(`EmberData expects only one flush per queue name, cannot schedule ${name}`, !this._cbs[name]); + this._cbs[name] = cb; } @@ -338,8 +339,8 @@ class Store extends Service { let modelName = identifier.type; let recordData = this._instanceCache.getRecordData(identifier); + // TODO deprecate allowing unknown args setting let createOptions: any = { - // TODO deprecate allowing unknown args setting _createProps: createRecordArgs, // TODO @deprecate consider deprecating accessing record properties during init which the below is necessary for _secretInit: { @@ -1127,7 +1128,11 @@ class Store extends Service { } } - return promiseRecord(this, promise, `DS: Store#findRecord ${identifier}`); + if (DEPRECATE_PROMISE_PROXIES) { + return promiseRecord(this, promise); + } + + return promise.then((identifier: StableRecordIdentifier) => this.peekRecord(identifier)); } /** @@ -1358,8 +1363,8 @@ class Store extends Service { decoded: "/api/v1/person?ids[]=1&ids[]=2&ids[]=3" ``` - This method returns a promise, which is resolved with an - [`AdapterPopulatedRecordArray`](/ember-data/release/classes/AdapterPopulatedRecordArray) + This method returns a promise, which is resolved with a + [`Collection`](/ember-data/release/classes/Collection) once the server returns. @since 1.13.0 @@ -1370,7 +1375,7 @@ class Store extends Service { @param {Object} options optional, may include `adapterOptions` hash which will be passed to adapter.query @return {Promise} promise */ - query(modelName: string, query, options): PromiseArray { + query(modelName: string, query, options): PromiseArray | Promise { if (DEBUG) { assertDestroyingStore(this, 'query'); } @@ -1404,9 +1409,12 @@ class Store extends Service { query, recordArray, adapterOptionsWrapper - ) as unknown as Promise; + ) as unknown as Promise; - return promiseArray(queryPromise); + if (DEPRECATE_PROMISE_PROXIES) { + return promiseArray(queryPromise); + } + return queryPromise; } /** @@ -1507,7 +1515,11 @@ class Store extends Service { @param {Object} options optional, may include `adapterOptions` hash which will be passed to adapter.queryRecord @return {Promise} promise which resolves with the found record or `null` */ - queryRecord(modelName: string, query, options?): PromiseObject { + queryRecord( + modelName: string, + query, + options? + ): PromiseObject | Promise { if (DEBUG) { assertDestroyingStore(this, 'queryRecord'); } @@ -1540,7 +1552,10 @@ class Store extends Service { adapterOptionsWrapper ) as Promise; - return promiseObject(promise.then((identifier) => identifier && this.peekRecord(identifier))); + if (DEPRECATE_PROMISE_PROXIES) { + return promiseObject(promise.then((identifier) => identifier && this.peekRecord(identifier))); + } + return promise.then((identifier) => identifier && this.peekRecord(identifier)); } /** @@ -1734,7 +1749,7 @@ class Store extends Service { findAll( modelName: string, options: { reload?: boolean; backgroundReload?: boolean } = {} - ): PromiseArray { + ): PromiseArray { if (DEBUG) { assertDestroyingStore(this, 'findAll'); } @@ -1768,7 +1783,7 @@ class Store extends Service { (!adapter.shouldReloadAll && snapshotArray.length === 0) ) { array.isUpdating = true; - fetch = _findAll(adapter, this, modelName, options); + fetch = _findAll(adapter, this, modelName, options, snapshotArray); } } @@ -1788,7 +1803,10 @@ class Store extends Service { } } - return promiseArray(fetch); + if (DEPRECATE_PROMISE_PROXIES) { + return promiseArray(fetch); + } + return fetch; } /** @@ -1816,7 +1834,7 @@ class Store extends Service { @param {String} modelName @return {RecordArray} */ - peekAll(modelName) { + peekAll(modelName: string): IdentifierArray { if (DEBUG) { assertDestroyingStore(this, 'peekAll'); } @@ -1825,8 +1843,9 @@ class Store extends Service { `Passing classes to store methods has been removed. Please pass a dasherized string instead of ${modelName}`, typeof modelName === 'string' ); - let normalizedModelName = normalizeModelName(modelName); - return this.recordArrayManager.liveArrayFor(normalizedModelName); + + let type = normalizeModelName(modelName); + return this.recordArrayManager.liveArrayFor(type); } /** @@ -1868,6 +1887,8 @@ class Store extends Service { } } this._notificationManager.destroy(); + + this.recordArrayManager.clear(); this._instanceCache.clear(); } else { let normalizedModelName = normalizeModelName(modelName); @@ -2787,12 +2808,25 @@ function extractIdentifierFromRecord( } const extract = isForV1 ? recordDataFor : recordIdentifierFor; - if (isPromiseRecord(recordOrPromiseRecord)) { + if (DEPRECATE_PROMISE_PROXIES && isPromiseRecord(recordOrPromiseRecord)) { let content = recordOrPromiseRecord.content; assert( 'You passed in a promise that did not originate from an EmberData relationship. You can only pass promises that come from a belongsTo or hasMany relationship to the get call.', content !== undefined ); + deprecate( + `You passed in a PromiseProxy to a Relationship API that now expects a resolved value. await the value before setting it.`, + false, + { + id: 'ember-data:deprecate-promise-proxies', + until: '5.0', + since: { + enabled: '4.8', + available: '4.8', + }, + for: 'ember-data', + } + ); return content ? extract(content) : null; } diff --git a/packages/store/addon/-private/utils/promise-record.ts b/packages/store/addon/-private/utils/promise-record.ts index 2df603774a9..328e946004f 100644 --- a/packages/store/addon/-private/utils/promise-record.ts +++ b/packages/store/addon/-private/utils/promise-record.ts @@ -7,10 +7,9 @@ import type Store from '../store-service'; export default function promiseRecord( store: Store, - promise: Promise, - label?: string + promise: Promise ): PromiseObject { let toReturn = promise.then((identifier: StableRecordIdentifier) => store.peekRecord(identifier)!); - return promiseObject(toReturn, label); + return promiseObject(toReturn); } diff --git a/packages/unpublished-adapter-encapsulation-test-app/tests/integration/coalescing-test.js b/packages/unpublished-adapter-encapsulation-test-app/tests/integration/coalescing-test.js index 50b4363aebe..36271fe8846 100644 --- a/packages/unpublished-adapter-encapsulation-test-app/tests/integration/coalescing-test.js +++ b/packages/unpublished-adapter-encapsulation-test-app/tests/integration/coalescing-test.js @@ -174,7 +174,7 @@ module('integration/coalescing - Coalescing Tests', function (hooks) { let promises = expectedResults.data.map((result) => result.id).map((id) => store.findRecord('person', id)); let records = await all(promises); - let serializedRecords = records.toArray().map((record) => record.serialize()); + let serializedRecords = records.slice().map((record) => record.serialize()); expectedResults = expectedResults.data.map((result) => ({ data: result })); assert.strictEqual(findRecordCalled, 0, 'findRecord is not called'); @@ -369,7 +369,7 @@ module('integration/coalescing - Coalescing Tests', function (hooks) { let promises = expectedResults.data.map((result) => result.id).map((id) => store.findRecord('person', id)); let records = await all(promises); - let serializedRecords = records.toArray().map((record) => record.serialize()); + let serializedRecords = records.slice().map((record) => record.serialize()); expectedResults = expectedResults.data.map((result) => ({ data: result })); assert.strictEqual(findRecordCalled, 0, 'findRecord is not called'); diff --git a/packages/unpublished-adapter-encapsulation-test-app/tests/integration/has-many-test.js b/packages/unpublished-adapter-encapsulation-test-app/tests/integration/has-many-test.js index d22a3d6a475..0829e788a2c 100644 --- a/packages/unpublished-adapter-encapsulation-test-app/tests/integration/has-many-test.js +++ b/packages/unpublished-adapter-encapsulation-test-app/tests/integration/has-many-test.js @@ -195,7 +195,7 @@ module('integration/has-many - Has Many Tests', function (hooks) { let comments = await post.comments; let serializedComments = { - data: comments.toArray().map((comment) => comment.serialize().data), + data: comments.slice().map((comment) => comment.serialize().data), }; assert.strictEqual(findRecordCalled, 0, 'findRecord is not called'); @@ -304,7 +304,7 @@ module('integration/has-many - Has Many Tests', function (hooks) { let post = store.push(initialRecord); let comments = await post.comments; let serializedComments = { - data: comments.toArray().map((comment) => comment.serialize().data), + data: comments.slice().map((comment) => comment.serialize().data), }; assert.strictEqual(findRecordCalled, 2, 'findRecord is called twice'); @@ -376,7 +376,7 @@ module('integration/has-many - Has Many Tests', function (hooks) { let post = store.push(initialRecord); let comments = await post.comments; let serializedComments = { - data: comments.toArray().map((comment) => comment.serialize().data), + data: comments.slice().map((comment) => comment.serialize().data), }; assert.strictEqual(findRecordCalled, 2, 'findRecord is called twice'); @@ -459,7 +459,7 @@ module('integration/has-many - Has Many Tests', function (hooks) { let post = store.push(initialRecord); let comments = await post.comments; let serializedComments = { - data: comments.toArray().map((comment) => comment.serialize().data), + data: comments.slice().map((comment) => comment.serialize().data), }; assert.strictEqual(findRecordCalled, 0, 'findRecord is not called'); @@ -538,7 +538,7 @@ module('integration/has-many - Has Many Tests', function (hooks) { let post = store.push(initialRecord); let comments = await post.comments; let serializedComments = { - data: comments.toArray().map((comment) => comment.serialize().data), + data: comments.slice().map((comment) => comment.serialize().data), }; assert.strictEqual(findRecordCalled, 0, 'findRecord is not called'); @@ -617,7 +617,7 @@ module('integration/has-many - Has Many Tests', function (hooks) { let comments = await post.comments; let serializedComments = { - data: comments.toArray().map((comment) => comment.serialize().data), + data: comments.slice().map((comment) => comment.serialize().data), }; assert.strictEqual(findRecordCalled, 0, 'findRecord is not called'); @@ -699,7 +699,7 @@ module('integration/has-many - Has Many Tests', function (hooks) { let post = store.push(initialRecord); let comments = await post.comments; let serializedComments = { - data: comments.toArray().map((comment) => comment.serialize().data), + data: comments.slice().map((comment) => comment.serialize().data), }; assert.strictEqual(findRecordCalled, 0, 'findRecord is not called'); @@ -770,7 +770,7 @@ module('integration/has-many - Has Many Tests', function (hooks) { let post = store.push(initialRecord); let comments = await post.comments; let serializedComments = { - data: comments.toArray().map((comment) => comment.serialize().data), + data: comments.slice().map((comment) => comment.serialize().data), }; assert.strictEqual(findRecordCalled, 2, 'findRecord is called twice'); diff --git a/packages/unpublished-adapter-encapsulation-test-app/tests/integration/queries-test.js b/packages/unpublished-adapter-encapsulation-test-app/tests/integration/queries-test.js index e7d8b9f64a0..beb61300fd7 100644 --- a/packages/unpublished-adapter-encapsulation-test-app/tests/integration/queries-test.js +++ b/packages/unpublished-adapter-encapsulation-test-app/tests/integration/queries-test.js @@ -134,7 +134,7 @@ module('integration/queries - Queries Tests', function (hooks) { let manyArray = await store.findAll('person'); - let result = manyArray.toArray().map((person) => person.serialize()); + let result = manyArray.slice().map((person) => person.serialize()); expectedResult = expectedResult.data.map((person) => ({ data: person })); assert.strictEqual(findAllCalled, 1, 'findAll is called once'); @@ -209,7 +209,7 @@ module('integration/queries - Queries Tests', function (hooks) { assert.strictEqual(passedStore, store, 'instance of store is passed to query'); assert.strictEqual(type, Person, 'model is passed to query'); assert.deepEqual(query, { firstName: 'Chris' }, 'query is passed to query'); - assert.deepEqual(recordArray.toArray(), [], 'recordArray is passsed to query'); + assert.deepEqual(recordArray.slice(), [], 'recordArray is passsed to query'); assert.deepEqual(options, {}, 'options is passed to query'); return resolve(expectedResult); @@ -220,7 +220,7 @@ module('integration/queries - Queries Tests', function (hooks) { let manyArray = await store.query('person', { firstName: 'Chris' }); - let result = manyArray.toArray().map((person) => person.serialize()); + let result = manyArray.slice().map((person) => person.serialize()); expectedResult = expectedResult.data.map((person) => ({ data: person })); assert.strictEqual(queryCalled, 1, 'query is called once'); diff --git a/packages/unpublished-relationship-performance-test-app/app/routes/basic-record-materialization.js b/packages/unpublished-relationship-performance-test-app/app/routes/basic-record-materialization.js index 47fbaf3412a..94405974002 100644 --- a/packages/unpublished-relationship-performance-test-app/app/routes/basic-record-materialization.js +++ b/packages/unpublished-relationship-performance-test-app/app/routes/basic-record-materialization.js @@ -13,8 +13,8 @@ export default Route.extend({ const peekedChildren = this.store.peekAll('child'); const peekedParents = this.store.peekAll('parent'); performance.mark('start-record-materialization'); - peekedChildren.toArray(); - peekedParents.toArray(); + peekedChildren.slice(); + peekedParents.slice(); performance.mark('end-record-materialization'); }, }); diff --git a/packages/unpublished-relationship-performance-test-app/app/routes/destroy.js b/packages/unpublished-relationship-performance-test-app/app/routes/destroy.js index 43844da8fcf..159bb96725b 100644 --- a/packages/unpublished-relationship-performance-test-app/app/routes/destroy.js +++ b/packages/unpublished-relationship-performance-test-app/app/routes/destroy.js @@ -15,7 +15,7 @@ export default Route.extend({ performance.mark('start-destroy-records'); const children = await parent.children; - const childrenPromise = all(children.toArray().map((child) => child.destroyRecord())); + const childrenPromise = all(children.slice().map((child) => child.destroyRecord())); const parentPromise = parent.destroyRecord(); await all([childrenPromise, parentPromise]); diff --git a/packages/unpublished-relationship-performance-test-app/app/routes/relationship-materialization-complex.js b/packages/unpublished-relationship-performance-test-app/app/routes/relationship-materialization-complex.js index fe708b77b1e..c9db131cdab 100644 --- a/packages/unpublished-relationship-performance-test-app/app/routes/relationship-materialization-complex.js +++ b/packages/unpublished-relationship-performance-test-app/app/routes/relationship-materialization-complex.js @@ -13,8 +13,8 @@ export default Route.extend({ const peekedChildren = this.store.peekAll('child'); const peekedParents = this.store.peekAll('parent'); performance.mark('start-record-materialization'); - peekedChildren.toArray(); - peekedParents.toArray(); + peekedChildren.slice(); + peekedParents.slice(); performance.mark('start-relationship-materialization'); let seen = new Set(); peekedParents.forEach((parent) => iterateParent(parent, seen)); diff --git a/packages/unpublished-relationship-performance-test-app/app/routes/unload-all.js b/packages/unpublished-relationship-performance-test-app/app/routes/unload-all.js index c8faed9f79f..02a4ece9679 100644 --- a/packages/unpublished-relationship-performance-test-app/app/routes/unload-all.js +++ b/packages/unpublished-relationship-performance-test-app/app/routes/unload-all.js @@ -11,8 +11,8 @@ export default Route.extend({ performance.mark('start-push-payload'); this.store.push(payload); performance.mark('start-materialization'); - this.store.peekAll('child').toArray(); - this.store.peekAll('parent').toArray(); + this.store.peekAll('child').slice(); + this.store.peekAll('parent').slice(); performance.mark('start-unload-all'); run(() => { diff --git a/packages/unpublished-relationship-performance-test-app/app/routes/unload.js b/packages/unpublished-relationship-performance-test-app/app/routes/unload.js index 2973255a6bb..871250753ce 100644 --- a/packages/unpublished-relationship-performance-test-app/app/routes/unload.js +++ b/packages/unpublished-relationship-performance-test-app/app/routes/unload.js @@ -15,7 +15,7 @@ export default Route.extend({ // runloop to ensure destroy does not escape bounds of the test run(() => { - children.toArray().forEach((child) => child.unloadRecord()); + children.slice().forEach((child) => child.unloadRecord()); }); performance.mark('end-unload-records'); }, diff --git a/packages/unpublished-serializer-encapsulation-test-app/tests/integration/relationships-test.js b/packages/unpublished-serializer-encapsulation-test-app/tests/integration/relationships-test.js index 6a784dd4ac6..f3f8f71582a 100644 --- a/packages/unpublished-serializer-encapsulation-test-app/tests/integration/relationships-test.js +++ b/packages/unpublished-serializer-encapsulation-test-app/tests/integration/relationships-test.js @@ -119,7 +119,11 @@ module( let comments = await post.comments; assert.strictEqual(normalizeResponseCalled, 1, 'normalizeResponse is called once'); - assert.deepEqual(comments.mapBy('message'), ['Message 1', 'Message 2'], 'response is expected response'); + assert.deepEqual( + comments.map((r) => r.message), + ['Message 1', 'Message 2'], + 'response is expected response' + ); }); test('accessing an async hasMany relationship with links results in serializer.normalizeResponse being called with the requestType findHasMany', async function (assert) { @@ -182,7 +186,11 @@ module( let comments = await post.comments; assert.strictEqual(normalizeResponseCalled, 1, 'normalizeResponse is called once'); - assert.deepEqual(comments.mapBy('message'), ['Message 1', 'Message 2'], 'response is expected response'); + assert.deepEqual( + comments.map((r) => r.message), + ['Message 1', 'Message 2'], + 'response is expected response' + ); }); test('accessing an async belongsTo relationship with links results in serializer.normalizeResponse being called with the requestType findBelongsTo', async function (assert) { diff --git a/packages/unpublished-serializer-encapsulation-test-app/tests/integration/requests-test.js b/packages/unpublished-serializer-encapsulation-test-app/tests/integration/requests-test.js index 73fe34cdd5c..c2f66d58453 100644 --- a/packages/unpublished-serializer-encapsulation-test-app/tests/integration/requests-test.js +++ b/packages/unpublished-serializer-encapsulation-test-app/tests/integration/requests-test.js @@ -57,7 +57,11 @@ module('integration/requests - running requests with minimum serializer', functi let response = await store.findAll('person'); assert.strictEqual(normalizeResponseCalled, 1, 'normalizeResponse is called once'); - assert.deepEqual(response.mapBy('id'), ['urn:person:1'], 'response is expected response'); + assert.deepEqual( + response.map((r) => r.id), + ['urn:person:1'], + 'response is expected response' + ); }); test('findRecord calls normalizeResponse', async function (assert) { @@ -147,7 +151,11 @@ module('integration/requests - running requests with minimum serializer', functi let response = await store.query('person', { name: 'Chris' }); assert.strictEqual(normalizeResponseCalled, 1, 'normalizeResponse is called once'); - assert.deepEqual(response.mapBy('id'), ['urn:person:1'], 'response is expected response'); + assert.deepEqual( + response.map((r) => r.id), + ['urn:person:1'], + 'response is expected response' + ); }); test('queryRecord calls normalizeResponse', async function (assert) { From a91ceb8ec111f712c550ec6a0411cdae3f1d88cf Mon Sep 17 00:00:00 2001 From: Chris Thoburn Date: Thu, 25 Aug 2022 16:09:49 -0700 Subject: [PATCH 2/2] minor fixes, deprecate chains --- .../unit/model/relationships/has-many-test.js | 14 ++++++++----- .../addon/-private/promise-many-array.ts | 11 ++++++---- .../addon/current-deprecations.ts | 1 + .../private-build-infra/addon/deprecations.ts | 1 + .../record-arrays/identifier-array.ts | 21 ++++++++++++++++--- 5 files changed, 36 insertions(+), 12 deletions(-) diff --git a/packages/-ember-data/tests/unit/model/relationships/has-many-test.js b/packages/-ember-data/tests/unit/model/relationships/has-many-test.js index 7d17f85e493..4eba33fd92b 100644 --- a/packages/-ember-data/tests/unit/model/relationships/has-many-test.js +++ b/packages/-ember-data/tests/unit/model/relationships/has-many-test.js @@ -2640,18 +2640,22 @@ module('unit/model/relationships - hasMany', function (hooks) { assert.strictEqual(peopleDidChange, 0, 'expect hasMany to not sync before access after fetch completes'); assert.strictEqual(people.length, 0, 'we have the right number of people'); - assert.strictEqual(peopleDidChange, 1, 'expect people hasMany to dirty after fetch completes'); + assert.strictEqual( + peopleDidChange, + 0, + 'expect people hasMany to not dirty after fetch completes, as we did not hit network' + ); let person = store.createRecord('person'); - assert.strictEqual(peopleDidChange, 1, 'expect people hasMany to not sync before access'); + assert.strictEqual(peopleDidChange, 0, 'expect people hasMany to not sync before access'); people = await tag.people; assert.strictEqual(people.length, 0, 'we have the right number of people'); - assert.strictEqual(peopleDidChange, 1, 'expect people hasMany to not sync before access'); + assert.strictEqual(peopleDidChange, 0, 'expect people hasMany to not sync after access'); people.push(person); - assert.strictEqual(peopleDidChange, 1, 'expect hasMany to not sync after push of new related data'); + assert.strictEqual(peopleDidChange, 0, 'expect hasMany to not sync after push of new related data'); assert.strictEqual(people.length, 1, 'we have the right number of people'); - assert.strictEqual(peopleDidChange, 2, 'expect hasMany to sync on access after push'); + assert.strictEqual(peopleDidChange, 1, 'expect hasMany to sync on access after push'); }); test('fetch hasMany loads full relationship after a parent and child have been loaded', function (assert) { diff --git a/packages/model/addon/-private/promise-many-array.ts b/packages/model/addon/-private/promise-many-array.ts index 42b58d030ff..4c435a3d7d7 100644 --- a/packages/model/addon/-private/promise-many-array.ts +++ b/packages/model/addon/-private/promise-many-array.ts @@ -10,6 +10,7 @@ import { resolve } from 'rsvp'; import { DEPRECATE_A_USAGE, + DEPRECATE_COMPUTED_CHAINS, DEPRECATE_PROMISE_MANY_ARRAY_BEHAVIORS, } from '@ember-data/private-build-infra/deprecations'; import { StableRecordIdentifier } from '@ember-data/types/q/identifier'; @@ -93,7 +94,9 @@ export default class PromiseManyArray { get length(): number { // shouldn't be needed, but ends up being needed // for computed chains even in 4.x - // this['[]']; + if (DEPRECATE_COMPUTED_CHAINS) { + this['[]']; + } return this.content ? this.content.length : 0; } @@ -103,8 +106,9 @@ export default class PromiseManyArray { // to recompute. We entangle the '[]' tag from @dependentKeyCompat get '[]'() { - return this.content?.length && this.content; - // return this.content ? this.content['[]'] : this.content; + if (DEPRECATE_COMPUTED_CHAINS) { + return this.content?.length && this.content; + } } /** @@ -118,7 +122,6 @@ export default class PromiseManyArray { * @private */ forEach(cb) { - // this['[]']; // needed for < 3.23 support e.g. 3.20 lts if (this.content && this.length) { this.content.forEach(cb); } diff --git a/packages/private-build-infra/addon/current-deprecations.ts b/packages/private-build-infra/addon/current-deprecations.ts index 948997fa3ea..85f68d0d5c0 100644 --- a/packages/private-build-infra/addon/current-deprecations.ts +++ b/packages/private-build-infra/addon/current-deprecations.ts @@ -59,4 +59,5 @@ export default { DEPRECATE_A_USAGE: '4.8', DEPRECATE_PROMISE_PROXIES: '4.8', DEPRECATE_ARRAY_LIKE: '4.8', + DEPRECATE_COMPUTED_CHAINS: '4.8', }; diff --git a/packages/private-build-infra/addon/deprecations.ts b/packages/private-build-infra/addon/deprecations.ts index 6ec28d697cc..28581f7ac65 100644 --- a/packages/private-build-infra/addon/deprecations.ts +++ b/packages/private-build-infra/addon/deprecations.ts @@ -28,3 +28,4 @@ export const DEPRECATE_V1_RECORD_DATA = deprecationState('DEPRECATE_V1_RECORD_DA export const DEPRECATE_A_USAGE = deprecationState('DEPRECATE_A_USAGE'); export const DEPRECATE_PROMISE_PROXIES = deprecationState('DEPRECATE_PROMISE_PROXIES'); export const DEPRECATE_ARRAY_LIKE = deprecationState('DEPRECATE_ARRAY_LIKE'); +export const DEPRECATE_COMPUTED_CHAINS = deprecationState('DEPRECATE_COMPUTED_CHAINS'); diff --git a/packages/store/addon/-private/record-arrays/identifier-array.ts b/packages/store/addon/-private/record-arrays/identifier-array.ts index 71b9384cd24..d7c1af56af0 100644 --- a/packages/store/addon/-private/record-arrays/identifier-array.ts +++ b/packages/store/addon/-private/record-arrays/identifier-array.ts @@ -10,6 +10,7 @@ import { tracked } from '@glimmer/tracking'; import Ember from 'ember'; import { + DEPRECATE_A_USAGE, DEPRECATE_ARRAY_LIKE, DEPRECATE_PROMISE_PROXIES, DEPRECATE_SNAPSHOT_MODEL_CLASS_ACCESS, @@ -377,9 +378,23 @@ class IdentifierArray { }, }) as IdentifierArray; - if (DEBUG) { - // prevent `A()` from clobbering us - const meta = Ember.meta(proxy); + if (DEPRECATE_A_USAGE) { + const meta = Ember.meta(this); + meta.hasMixin = (mixin: Object) => { + deprecate(`Do not call A() on EmberData RecordArrays`, false, { + id: 'ember-data:no-a-with-array-like', + until: '5.0', + since: { enabled: '4.8', available: '4.8' }, + for: 'ember-data', + }); + // @ts-expect-error ArrayMixin is more than a type + if (mixin === NativeArray || mixin === ArrayMixin) { + return true; + } + return false; + }; + } else if (DEBUG) { + const meta = Ember.meta(this); meta.hasMixin = (mixin: Object) => { assert(`Do not call A() on EmberData RecordArrays`); };