diff --git a/ember-data-types/q/record-data-record-wrapper.ts b/ember-data-types/q/record-data-record-wrapper.ts
index 8b0241ea189..079898863ac 100644
--- a/ember-data-types/q/record-data-record-wrapper.ts
+++ b/ember-data-types/q/record-data-record-wrapper.ts
@@ -7,7 +7,7 @@ import type { JsonApiValidationError } from './record-data-json-api';
@module @ember-data/store
*/
-export interface RecordDataRecordWrapper {
+export interface RecordDataWrapper {
rollbackAttributes(): string[];
changedAttributes(): ChangedAttributesHash;
hasChangedAttributes(): boolean;
@@ -16,17 +16,15 @@ export interface RecordDataRecordWrapper {
getAttr(key: string): any;
getHasMany(key: string): CollectionResourceRelationship;
- addToHasMany(key: string, recordDatas: RecordDataRecordWrapper[], idx?: number): void;
- removeFromHasMany(key: string, recordDatas: RecordDataRecordWrapper[]): void;
- setDirtyHasMany(key: string, recordDatas: RecordDataRecordWrapper[]): void;
+ addToHasMany(key: string, recordDatas: RecordDataWrapper[], idx?: number): void;
+ removeFromHasMany(key: string, recordDatas: RecordDataWrapper[]): void;
+ setDirtyHasMany(key: string, recordDatas: RecordDataWrapper[]): void;
getBelongsTo(key: string): SingleResourceRelationship;
- setDirtyBelongsTo(name: string, recordData: RecordDataRecordWrapper | null): void;
+ setDirtyBelongsTo(name: string, recordData: RecordDataWrapper | null): void;
// ----- unspecced
- isAttrDirty(key: string): boolean;
- removeFromInverseRelationships(): void;
hasAttr(key: string): boolean;
// new
diff --git a/ember-data-types/q/record-data.ts b/ember-data-types/q/record-data.ts
index 097a24ab7db..410e0391d32 100644
--- a/ember-data-types/q/record-data.ts
+++ b/ember-data-types/q/record-data.ts
@@ -45,8 +45,6 @@ export interface RecordData {
didCommit(data: JsonApiResource | null): void;
// ----- unspecced
- isAttrDirty(key: string): boolean;
- removeFromInverseRelationships(): void;
hasAttr(key: string): boolean;
isRecordInUse(): boolean;
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 0f29c76699f..b42884fcc05 100644
--- a/packages/-ember-data/tests/acceptance/relationships/has-many-test.js
+++ b/packages/-ember-data/tests/acceptance/relationships/has-many-test.js
@@ -18,6 +18,7 @@ import Model, { attr, belongsTo, hasMany } from '@ember-data/model';
import { LEGACY_SUPPORT } from '@ember-data/model/-private';
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';
class Person extends Model {
@attr()
@@ -751,14 +752,212 @@ module('autotracking has-many', function (hooks) {
assert.deepEqual(names, ['RGB', 'RGB'], 'rendered 2 children');
});
+ deprecatedTest(
+ 'We can re-render hasMany w/PromiseManyArray.sortBy',
+ { id: 'ember-data:deprecate-promise-many-array-behaviors', until: '5.0', count: 6 },
+ async function (assert) {
+ class ChildrenList extends Component {
+ @service store;
+
+ get sortedChildren() {
+ return this.args.person.children.sortBy('name');
+ }
+
+ @action
+ createChild() {
+ const parent = this.args.person;
+ const name = 'RGB';
+ this.store.createRecord('person', { name, parent });
+ }
+ }
+
+ let layout = hbs`
+
+
+
{{this.sortedChildren.length}}
+
+ {{#each this.sortedChildren as |child|}}
+ - {{child.name}}
+ {{/each}}
+
+ `;
+ this.owner.register('component:children-list', ChildrenList);
+ this.owner.register('template:components/children-list', layout);
+
+ store.createRecord('person', { id: '1', name: 'Doodad' });
+ this.person = store.peekRecord('person', '1');
+
+ await render(hbs``);
+
+ let names = findAll('li').map((e) => e.textContent);
+
+ assert.deepEqual(names, [], 'rendered no children');
+
+ await click('#createChild');
+
+ names = findAll('li').map((e) => e.textContent);
+ assert.deepEqual(names, ['RGB'], 'rendered 1 child');
+
+ await click('#createChild');
+
+ names = findAll('li').map((e) => e.textContent);
+ assert.deepEqual(names, ['RGB', 'RGB'], 'rendered 2 children');
+ }
+ );
+
+ deprecatedTest(
+ 'We can re-render hasMany with sort computed macro on PromiseManyArray',
+ { id: 'ember-data:deprecate-promise-many-array-behaviors', until: '5.0', count: 6 },
+ async function (assert) {
+ class ChildrenList extends Component {
+ @service store;
+
+ sortProperties = ['name'];
+ @sort('args.person.children', 'sortProperties') sortedChildren;
+
+ @action
+ createChild() {
+ const parent = this.args.person;
+ const name = 'RGB';
+ this.store.createRecord('person', { name, parent });
+ }
+ }
+
+ let layout = hbs`
+
+
+ {{this.sortedChildren.length}}
+
+ {{#each this.sortedChildren as |child|}}
+ - {{child.name}}
+ {{/each}}
+
+ `;
+ this.owner.register('component:children-list', ChildrenList);
+ this.owner.register('template:components/children-list', layout);
+
+ store.createRecord('person', { id: '1', name: 'Doodad' });
+ this.person = store.peekRecord('person', '1');
+
+ await render(hbs``);
+
+ let names = findAll('li').map((e) => e.textContent);
+
+ assert.deepEqual(names, [], 'rendered no children');
+
+ await click('#createChild');
+
+ names = findAll('li').map((e) => e.textContent);
+ assert.deepEqual(names, ['RGB'], 'rendered 1 child');
+
+ await click('#createChild');
+
+ names = findAll('li').map((e) => e.textContent);
+ assert.deepEqual(names, ['RGB', 'RGB'], 'rendered 2 children');
+ }
+ );
+
+ deprecatedTest(
+ 'We can re-render hasMany with PromiseManyArray.objectAt',
+ { id: 'ember-data:deprecate-promise-many-array-behaviors', until: '5.0', count: 6 },
+ async function (assert) {
+ class ChildrenList extends Component {
+ @service store;
+
+ get firstChild() {
+ return this.args.person.children.objectAt(0);
+ }
+
+ @action
+ createChild() {
+ const parent = this.args.person;
+ const name = 'RGB';
+ this.store.createRecord('person', { name, parent });
+ }
+ }
+
+ let layout = hbs`
+
+
+ {{this.firstChild.name}}
+ `;
+ this.owner.register('component:children-list', ChildrenList);
+ this.owner.register('template:components/children-list', layout);
+
+ store.createRecord('person', { id: '1', name: 'Doodad' });
+ this.person = store.peekRecord('person', '1');
+
+ await render(hbs``);
+
+ assert.dom('h2').hasText('', 'rendered no children');
+
+ await click('#createChild');
+
+ assert.dom('h2').hasText('RGB', 'renders first child');
+
+ await click('#createChild');
+
+ assert.dom('h2').hasText('RGB', 'renders first child');
+ }
+ );
+
+ deprecatedTest(
+ 'We can re-render hasMany with PromiseManyArray.map',
+ { id: 'ember-data:deprecate-promise-many-array-behaviors', until: '5.0', count: 6 },
+ async function (assert) {
+ class ChildrenList extends Component {
+ @service store;
+
+ get children() {
+ return this.args.person.children.map((child) => child);
+ }
+
+ @action
+ createChild() {
+ const parent = this.args.person;
+ const name = 'RGB';
+ this.store.createRecord('person', { name, parent });
+ }
+ }
+
+ let layout = hbs`
+
+
+ {{this.children.length}}
+
+ {{#each this.children as |child|}}
+ - {{child.name}}
+ {{/each}}
+
+ `;
+ this.owner.register('component:children-list', ChildrenList);
+ this.owner.register('template:components/children-list', layout);
+
+ store.createRecord('person', { id: '1', name: 'Doodad' });
+ this.person = store.peekRecord('person', '1');
+
+ await render(hbs``);
+
+ let names = findAll('li').map((e) => e.textContent);
+
+ assert.deepEqual(names, [], 'rendered no children');
+
+ await click('#createChild');
+
+ names = findAll('li').map((e) => e.textContent);
+ assert.deepEqual(names, ['RGB'], 'rendered 1 child');
+
+ await click('#createChild');
+
+ names = findAll('li').map((e) => e.textContent);
+ assert.deepEqual(names, ['RGB', 'RGB'], 'rendered 2 children');
+ }
+ );
+
test('We can re-render hasMany', async function (assert) {
class ChildrenList extends Component {
@service store;
- get sortedChildren() {
- return this.args.person.children.sortBy('name');
- }
-
@action
createChild() {
const parent = this.args.person;
@@ -770,9 +969,9 @@ module('autotracking has-many', function (hooks) {
let layout = hbs`
- {{this.sortedChildren.length}}
+ {{@person.children.length}}
- {{#each this.sortedChildren as |child|}}
+ {{#each @person.children as |child|}}
- {{child.name}}
{{/each}}
@@ -805,7 +1004,7 @@ module('autotracking has-many', function (hooks) {
@service store;
sortProperties = ['name'];
- @sort('args.person.children', 'sortProperties') sortedChildren;
+ @sort('args.children', 'sortProperties') sortedChildren;
@action
createChild() {
@@ -830,8 +1029,9 @@ module('autotracking has-many', function (hooks) {
store.createRecord('person', { id: '1', name: 'Doodad' });
this.person = store.peekRecord('person', '1');
+ this.children = await this.person.children;
- await render(hbs``);
+ await render(hbs``);
let names = findAll('li').map((e) => e.textContent);
@@ -853,7 +1053,7 @@ module('autotracking has-many', function (hooks) {
@service store;
get firstChild() {
- return this.args.person.children.objectAt(0);
+ return this.args.children.objectAt(0);
}
@action
@@ -874,8 +1074,9 @@ module('autotracking has-many', function (hooks) {
store.createRecord('person', { id: '1', name: 'Doodad' });
this.person = store.peekRecord('person', '1');
+ this.children = await this.person.children;
- await render(hbs``);
+ await render(hbs``);
assert.dom('h2').hasText('', 'rendered no children');
@@ -893,7 +1094,7 @@ module('autotracking has-many', function (hooks) {
@service store;
get children() {
- return this.args.person.children.map((child) => child);
+ return this.args.children.map((child) => child);
}
@action
@@ -919,8 +1120,59 @@ module('autotracking has-many', function (hooks) {
store.createRecord('person', { id: '1', name: 'Doodad' });
this.person = store.peekRecord('person', '1');
+ this.children = await this.person.children;
- await render(hbs``);
+ await render(hbs``);
+
+ let names = findAll('li').map((e) => e.textContent);
+
+ assert.deepEqual(names, [], 'rendered no children');
+
+ await click('#createChild');
+
+ names = findAll('li').map((e) => e.textContent);
+ assert.deepEqual(names, ['RGB'], 'rendered 1 child');
+
+ await click('#createChild');
+
+ names = findAll('li').map((e) => e.textContent);
+ assert.deepEqual(names, ['RGB', 'RGB'], 'rendered 2 children');
+ });
+
+ test('We can re-render hasMany with toArray', async function (assert) {
+ class ChildrenList extends Component {
+ @service store;
+
+ get children() {
+ return this.args.children.toArray();
+ }
+
+ @action
+ createChild() {
+ const parent = this.args.person;
+ const name = 'RGB';
+ this.store.createRecord('person', { name, parent });
+ }
+ }
+
+ let layout = hbs`
+
+
+ {{this.children.length}}
+
+ {{#each this.children as |child|}}
+ - {{child.name}}
+ {{/each}}
+
+ `;
+ this.owner.register('component:children-list', ChildrenList);
+ this.owner.register('template:components/children-list', layout);
+
+ store.createRecord('person', { id: '1', name: 'Doodad' });
+ this.person = store.peekRecord('person', '1');
+ this.children = await this.person.children;
+
+ await render(hbs``);
let names = findAll('li').map((e) => e.textContent);
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 87397505871..fdb64043c13 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
@@ -200,15 +200,18 @@ module('integration/adapter/json-api-adapter - JSONAPIAdapter', function (hooks)
'The author for the last post is loaded and has the correct last name'
);
- assert.strictEqual(posts.firstObject.comments.length, 0, 'First post doesnt have comments');
+ const firstComments = await posts.firstObject.comments;
+ const lastComments = await posts.lastObject.comments;
+
+ assert.strictEqual(firstComments.length, 0, 'First post doesnt have comments');
assert.strictEqual(
- posts.lastObject.comments.firstObject.text,
+ lastComments.firstObject.text,
'This is the first comment',
'Loads first comment for second post'
);
assert.strictEqual(
- posts.lastObject.comments.lastObject.text,
+ lastComments.lastObject.text,
'This is the second comment',
'Loads second comment for second post'
);
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 83e30108db4..6e89d8076da 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
@@ -185,7 +185,8 @@ module('integration/adapter/rest_adapter - REST Adapter - createRecord', functio
const post = store.peekRecord('post', 1);
const comment = store.createRecord('comment', { name: 'The Parley Letter' });
- post.get('comments').pushObject(comment);
+ const comments = await post.get('comments');
+ comments.pushObject(comment);
assert.strictEqual(comment.get('post'), post, 'the post has been set correctly');
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 f042aa0729a..bf20ebc7d08 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
@@ -151,7 +151,7 @@ module(
});
});
- test('looks up belongsTo using under_scored strings', function (assert) {
+ test('looks up belongsTo using under_scored strings', async function (assert) {
assert.expect(1);
let store = this.owner.lookup('service:store');
@@ -181,13 +181,11 @@ module(
});
});
- run(() => {
- store.findRecord('long_model_name', 1).then((longModelName) => {
- const postNotes = get(longModelName, 'postNotes').toArray();
+ const longModel = await store.findRecord('long_model_name', '1');
+ const postNotesRel = await longModel.postNotes;
+ const postNotes = postNotesRel.toArray();
- assert.deepEqual(postNotes, [store.peekRecord('postNote', 1)], 'inverse records found');
- });
- });
+ assert.deepEqual(postNotes, [store.peekRecord('postNote', 1)], 'inverse records found');
});
}
);
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 921abafc9f5..5e46119609e 100644
--- a/packages/-ember-data/tests/integration/records/delete-record-test.js
+++ b/packages/-ember-data/tests/integration/records/delete-record-test.js
@@ -429,8 +429,9 @@ module('integration/deletedRecord - Deleting Records', function (hooks) {
// Sanity Check
assert.ok(group, 'expected group to be found');
assert.strictEqual(group.get('company.name'), 'Inc.', 'group belongs to our company');
- assert.strictEqual(group.get('employees.length'), 1, 'expected 1 related record before delete');
- employee = group.get('employees').objectAt(0);
+ assert.strictEqual(group.employees.length, 1, 'expected 1 related record before delete');
+ const employees = await group.employees;
+ employee = employees.objectAt(0);
assert.strictEqual(employee.get('name'), 'Adam Sunderland', 'expected related records to be loaded');
await group.destroyRecord();
@@ -444,6 +445,6 @@ module('integration/deletedRecord - Deleting Records', function (hooks) {
store.push(jsonGroup);
group = store.peekRecord('group', '1');
- assert.strictEqual(group.get('employees.length'), 1, 'expected 1 related record after delete and restore');
+ assert.strictEqual(group.employees.length, 1, 'expected 1 related record after delete and restore');
});
});
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 d2239fcf01a..766d0346915 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
@@ -1,4 +1,5 @@
import { run } from '@ember/runloop';
+import { settled } from '@ember/test-helpers';
import { module, test } from 'qunit';
import { Promise as EmberPromise, resolve } from 'rsvp';
@@ -1009,27 +1010,23 @@ module('integration/relationships/one_to_one_test - OneToOne relationships', fun
});
});
- test('Rollbacking attributes of created record removes the relationship on both sides - sync', function (assert) {
+ test('Rollbacking attributes of created record removes the relationship on both sides - sync', async function (assert) {
let store = this.owner.lookup('service:store');
- var user, job;
- run(function () {
- user = store.push({
- data: {
- id: 1,
- type: 'user',
- attributes: {
- name: 'Stanley',
- },
+ const user = store.push({
+ data: {
+ id: 1,
+ type: 'user',
+ attributes: {
+ name: 'Stanley',
},
- });
-
- job = store.createRecord('job', { user: user });
- });
- run(function () {
- job.rollbackAttributes();
+ },
});
- assert.strictEqual(user.get('job'), null, 'Job got rollbacked correctly');
- assert.strictEqual(job.get('user'), null, 'Job does not have user anymore');
+ const job = store.createRecord('job', { user: user });
+ job.rollbackAttributes();
+ await settled();
+
+ assert.strictEqual(user.job, null, 'Job got rollbacked correctly');
+ assert.true(job.isDestroyed, 'Job is destroyed');
});
});
diff --git a/packages/-ember-data/tests/unit/custom-class-support/custom-class-model-test.ts b/packages/-ember-data/tests/unit/custom-class-support/custom-class-model-test.ts
index 30df23e5a59..983d6008ba1 100644
--- a/packages/-ember-data/tests/unit/custom-class-support/custom-class-model-test.ts
+++ b/packages/-ember-data/tests/unit/custom-class-support/custom-class-model-test.ts
@@ -11,7 +11,7 @@ import Store from '@ember-data/store';
import type { RecordDataStoreWrapper, Snapshot } from '@ember-data/store/-private';
import type NotificationManager from '@ember-data/store/-private/managers/record-notification-manager';
import type { RecordIdentifier, StableRecordIdentifier } from '@ember-data/types/q/identifier';
-import type { RecordDataRecordWrapper } from '@ember-data/types/q/record-data-record-wrapper';
+import type { RecordDataWrapper } from '@ember-data/types/q/record-data-record-wrapper';
import type { AttributesSchema, RelationshipsSchema } from '@ember-data/types/q/record-data-schemas';
import type { RecordInstance } from '@ember-data/types/q/record-instance';
import type { SchemaDefinitionService } from '@ember-data/types/q/schema-definition-service';
@@ -313,7 +313,7 @@ module('unit/model - Custom Class Model', function (hooks) {
});
test('store.deleteRecord', async function (assert) {
- let rd: RecordDataRecordWrapper;
+ let rd: RecordDataWrapper;
assert.expect(9);
this.owner.register(
'adapter:application',
diff --git a/packages/-ember-data/tests/unit/store/unload-test.js b/packages/-ember-data/tests/unit/store/unload-test.js
index 797d884fc3c..e7bcf6ae673 100644
--- a/packages/-ember-data/tests/unit/store/unload-test.js
+++ b/packages/-ember-data/tests/unit/store/unload-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 { recordIdentifierFor } from '@ember-data/store';
import testInDebug from '@ember-data/unpublished-test-infra/test-support/test-in-debug';
let store, tryToFind;
@@ -70,7 +71,7 @@ module('unit/store/unload - Store unloading records', function (hooks) {
function () {
record.unloadRecord();
},
- 'You can only unload a record which is not inFlight. `' + record.toString() + '`',
+ `You can only unload a record which is not inFlight. '` + recordIdentifierFor(record).toString() + `'`,
'can not unload dirty record'
);
diff --git a/packages/model/addon/-private/attr.js b/packages/model/addon/-private/attr.js
index ec53d9e57db..f1821b82bff 100644
--- a/packages/model/addon/-private/attr.js
+++ b/packages/model/addon/-private/attr.js
@@ -137,6 +137,14 @@ function attr(type, options) {
}
}
let recordData = recordDataFor(this);
+ // TODO hasAttr is not spec'd
+ // essentially this is needed because
+ // there is a difference between "undefined" meaning never set
+ // and "undefined" meaning set to "undefined". In the "key present"
+ // case we want to return undefined. In the "key absent" case
+ // we want to return getDefaultValue. RecordDataV2 can fix this
+ // by providing the attributes blob such that we can make our
+ // own determination.
if (recordData.hasAttr(key)) {
return recordData.getAttr(key);
} else {
diff --git a/packages/model/addon/-private/legacy-relationships-support.ts b/packages/model/addon/-private/legacy-relationships-support.ts
index 2a44477df31..4baac480a46 100644
--- a/packages/model/addon/-private/legacy-relationships-support.ts
+++ b/packages/model/addon/-private/legacy-relationships-support.ts
@@ -14,9 +14,9 @@ import type { UpgradedMeta } from '@ember-data/record-data/-private/graph/-edge-
import type { RelationshipState } from '@ember-data/record-data/-private/graph/-state';
import type Store from '@ember-data/store';
import { recordDataFor, recordIdentifierFor, storeFor } from '@ember-data/store/-private';
-import type { IdentifierCache } from '@ember-data/store/-private/caches/identifier-cache';
+import { IdentifierCache } from '@ember-data/store/-private/caches/identifier-cache';
import type { DSModel } from '@ember-data/types/q/ds-model';
-import type { ResourceIdentifierObject } from '@ember-data/types/q/ember-data-json-api';
+import { ResourceIdentifierObject } 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 { JsonApiRelationship } from '@ember-data/types/q/record-data-json-api';
@@ -140,8 +140,7 @@ export class LegacySupport {
`You looked up the '${key}' relationship on a '${identifier.type}' with id ${
identifier.id || 'null'
} but some of the associated records were not loaded. Either make sure they are all loaded together with the parent record, or specify that the relationship is async (\`belongsTo({ async: true })\`)`,
- toReturn === null ||
- !store._instanceCache.peek({ identifier: relatedIdentifier, bucket: 'recordData' })?.isEmpty?.()
+ toReturn === null || store._instanceCache.recordIsLoaded(relatedIdentifier)
);
return toReturn;
}
@@ -674,16 +673,6 @@ function anyUnloaded(store: Store, relationship: ManyRelationship) {
return unloaded || false;
}
-/**
- * Flag indicating whether all inverse records are available
- *
- * true if the inverse exists and is loaded (not empty)
- * true if there is no inverse
- * false if the inverse exists and is not loaded (empty)
- *
- * @internal
- * @return {boolean}
- */
function areAllInverseRecordsLoaded(store: Store, resource: JsonApiRelationship): boolean {
const cache = store.identifierCache;
diff --git a/packages/model/addon/-private/model.js b/packages/model/addon/-private/model.js
index 25f2cf7ed31..a87289149f5 100644
--- a/packages/model/addon/-private/model.js
+++ b/packages/model/addon/-private/model.js
@@ -818,9 +818,13 @@ class Model extends EmberObject {
*/
rollbackAttributes() {
const { currentState } = this;
+ const { isNew } = currentState;
recordDataFor(this).rollbackAttributes();
this.errors.clear();
currentState.cleanErrorRequests();
+ if (isNew) {
+ this.unloadRecord();
+ }
}
/**
@@ -1805,7 +1809,7 @@ class Model extends EmberObject {
let fields = Blog.fields;
fields.forEach(function(kind, field) {
- console.log(field, kind);
+ // do thing
});
// prints:
@@ -1987,7 +1991,7 @@ class Model extends EmberObject {
let attributes = Person.attributes
attributes.forEach(function(meta, name) {
- console.log(name, meta);
+ // do thing
});
// prints:
@@ -2064,7 +2068,7 @@ class Model extends EmberObject {
let transformedAttributes = Person.transformedAttributes
transformedAttributes.forEach(function(field, type) {
- console.log(field, type);
+ // do thing
});
// prints:
@@ -2137,7 +2141,7 @@ class Model extends EmberObject {
}
PersonModel.eachAttribute(function(name, meta) {
- console.log(name, meta);
+ // do thing
});
// prints:
@@ -2206,7 +2210,7 @@ class Model extends EmberObject {
});
Person.eachTransformedAttribute(function(name, type) {
- console.log(name, type);
+ // do thing
});
// prints:
diff --git a/packages/model/addon/-private/promise-many-array.ts b/packages/model/addon/-private/promise-many-array.ts
index 6f8336055d6..8495c28598b 100644
--- a/packages/model/addon/-private/promise-many-array.ts
+++ b/packages/model/addon/-private/promise-many-array.ts
@@ -1,6 +1,6 @@
import ArrayMixin, { NativeArray } from '@ember/array';
import type ArrayProxy from '@ember/array/proxy';
-import { assert } from '@ember/debug';
+import { assert, deprecate } from '@ember/debug';
import { dependentKeyCompat } from '@ember/object/compat';
import { tracked } from '@glimmer/tracking';
import Ember from 'ember';
@@ -9,8 +9,10 @@ import { resolve } from 'rsvp';
import type { ManyArray } from 'ember-data/-private';
+import { DEPRECATE_PROMISE_MANY_ARRAY_BEHAVIORS } from '@ember-data/private-build-infra/deprecations';
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';
export interface HasManyProxyCreateArgs {
promise: Promise;
@@ -24,16 +26,10 @@ export interface HasManyProxyCreateArgs {
This class is returned as the result of accessing an async hasMany relationship
on an instance of a Model extending from `@ember-data/model`.
- A PromiseManyArray is an array-like proxy that also proxies certain method calls
- to the underlying ManyArray in addition to being "promisified".
+ A PromiseManyArray is an iterable proxy that allows templates to consume related
+ ManyArrays and update once their contents are no longer pending.
- Right now we proxy:
-
- * `reload()`
- * `createRecord()`
-
- This promise-proxy behavior is primarily to ensure that async relationship interact
- nicely with templates. In your JS code you should resolve the promise first.
+ In your JS code you should resolve the promise first.
```js
const comments = await post.comments;
@@ -42,10 +38,14 @@ export interface HasManyProxyCreateArgs {
@class PromiseManyArray
@public
*/
-export default interface PromiseManyArray extends Omit, 'destroy'> {}
+export default interface PromiseManyArray extends Omit, 'destroy'> {
+ createRecord(): RecordInstance;
+ reload(options: FindOptions): PromiseManyArray;
+}
export default class PromiseManyArray {
declare promise: Promise | null;
declare isDestroyed: boolean;
+ // @deprecated (isDestroyed is not deprecated)
declare isDestroying: boolean;
constructor(promise: Promise, content?: ManyArray) {
@@ -90,6 +90,8 @@ export default class PromiseManyArray {
/**
* Iterate the proxied content. Called by the glimmer iterator in #each
+ * We do not guarantee that forEach will always be available. This
+ * may eventually be made to use Symbol.Iterator once glimmer supports it.
*
* @method forEach
* @param cb
@@ -201,19 +203,6 @@ export default class PromiseManyArray {
return this.content ? this.content.meta : undefined;
}
- /**
- * Reload the relationship
- * @method reload
- * @public
- * @param options
- * @returns
- */
- reload(options) {
- assert('You are trying to reload an async manyArray before it has been created', this.content);
- this.content.reload(options);
- return this;
- }
-
//---- Our own stuff
_update(promise: Promise, content?: ManyArray) {
@@ -227,22 +216,79 @@ export default class PromiseManyArray {
static create({ promise, content }: HasManyProxyCreateArgs): PromiseManyArray {
return new this(promise, content);
}
+}
- // Methods on ManyArray which people should resolve the relationship first before calling
- createRecord(...args) {
+if (DEPRECATE_PROMISE_MANY_ARRAY_BEHAVIORS) {
+ // TODO update RFC to also note reload deprecation
+ /**
+ * Reload the relationship
+ * @method reload
+ * @deprecated
+ * @public
+ * @param options
+ * @returns
+ */
+ PromiseManyArray.prototype.reload = function reload(options: FindOptions) {
+ deprecate(
+ `The reload method on ember-data's PromiseManyArray is deprecated. await the promise and work with the ManyArray directly or use the HasManyReference interface.`,
+ false,
+ {
+ id: 'ember-data:deprecate-promise-many-array-behaviors',
+ until: '5.0',
+ since: { enabled: '4.8', available: '4.8' },
+ for: 'ember-data',
+ }
+ );
+ assert('You are trying to reload an async manyArray before it has been created', this.content);
+ this.content.reload(options);
+ return this;
+ };
+ PromiseManyArray.prototype.createRecord = function createRecord(...args) {
+ deprecate(
+ `The createRecord method on ember-data's PromiseManyArray is deprecated. await the promise and work with the ManyArray directly.`,
+ false,
+ {
+ id: 'ember-data:deprecate-promise-many-array-behaviors',
+ until: '5.0',
+ since: { enabled: '4.8', available: '4.8' },
+ for: 'ember-data',
+ }
+ );
assert('You are trying to createRecord on an async manyArray before it has been created', this.content);
return this.content.createRecord(...args);
- }
-
- // Properties/Methods on ArrayProxy we should deprecate
-
- get firstObject() {
- return this.content ? this.content.firstObject : undefined;
- }
+ };
- get lastObject() {
- return this.content ? this.content.lastObject : undefined;
- }
+ Object.defineProperty(PromiseManyArray.prototype, 'firstObject', {
+ get() {
+ deprecate(
+ `The firstObject property on ember-data's PromiseManyArray is deprecated. await the promise and work with the ManyArray directly.`,
+ false,
+ {
+ id: 'ember-data:deprecate-promise-many-array-behaviors',
+ until: '5.0',
+ since: { enabled: '4.8', available: '4.8' },
+ for: 'ember-data',
+ }
+ );
+ return this.content ? this.content.firstObject : undefined;
+ },
+ });
+
+ Object.defineProperty(PromiseManyArray.prototype, 'lastObject', {
+ get() {
+ deprecate(
+ `The lastObject property on ember-data's PromiseManyArray is deprecated. await the promise and work with the ManyArray directly.`,
+ false,
+ {
+ id: 'ember-data:deprecate-promise-many-array-behaviors',
+ until: '5.0',
+ since: { enabled: '4.8', available: '4.8' },
+ for: 'ember-data',
+ }
+ );
+ return this.content ? this.content.lastObject : undefined;
+ },
+ });
}
function tapPromise(proxy: PromiseManyArray, promise: Promise) {
@@ -268,78 +314,101 @@ function tapPromise(proxy: PromiseManyArray, promise: Promise) {
);
}
-const EmberObjectMethods = [
- 'addObserver',
- 'cacheFor',
- 'decrementProperty',
- 'get',
- 'getProperties',
- 'incrementProperty',
- 'notifyPropertyChange',
- 'removeObserver',
- 'set',
- 'setProperties',
- 'toggleProperty',
-];
-EmberObjectMethods.forEach((method) => {
- PromiseManyArray.prototype[method] = function delegatedMethod(...args) {
- return Ember[method](this, ...args);
- };
-});
-
-const InheritedProxyMethods = [
- 'addArrayObserver',
- 'addObject',
- 'addObjects',
- 'any',
- 'arrayContentDidChange',
- 'arrayContentWillChange',
- 'clear',
- 'compact',
- 'every',
- 'filter',
- 'filterBy',
- 'find',
- 'findBy',
- 'getEach',
- 'includes',
- 'indexOf',
- 'insertAt',
- 'invoke',
- 'isAny',
- 'isEvery',
- 'lastIndexOf',
- 'map',
- 'mapBy',
- 'objectAt',
- 'objectsAt',
- 'popObject',
- 'pushObject',
- 'pushObjects',
- 'reduce',
- 'reject',
- 'rejectBy',
- 'removeArrayObserver',
- 'removeAt',
- 'removeObject',
- 'removeObjects',
- 'replace',
- 'reverseObjects',
- 'setEach',
- 'setObjects',
- 'shiftObject',
- 'slice',
- 'sortBy',
- 'toArray',
- 'uniq',
- 'uniqBy',
- 'unshiftObject',
- 'unshiftObjects',
- 'without',
-];
-InheritedProxyMethods.forEach((method) => {
- PromiseManyArray.prototype[method] = function proxiedMethod(...args) {
- assert(`Cannot call ${method} before content is assigned.`, this.content);
- return this.content[method](...args);
- };
-});
+if (DEPRECATE_PROMISE_MANY_ARRAY_BEHAVIORS) {
+ const EmberObjectMethods = [
+ 'addObserver',
+ 'cacheFor',
+ 'decrementProperty',
+ 'get',
+ 'getProperties',
+ 'incrementProperty',
+ 'notifyPropertyChange',
+ 'removeObserver',
+ 'set',
+ 'setProperties',
+ 'toggleProperty',
+ ];
+ EmberObjectMethods.forEach((method) => {
+ PromiseManyArray.prototype[method] = function delegatedMethod(...args) {
+ deprecate(
+ `The ${method} method on ember-data's PromiseManyArray is deprecated. await the promise and work with the ManyArray directly.`,
+ false,
+ {
+ id: 'ember-data:deprecate-promise-many-array-behaviors',
+ until: '5.0',
+ since: { enabled: '4.8', available: '4.8' },
+ for: 'ember-data',
+ }
+ );
+ return Ember[method](this, ...args);
+ };
+ });
+
+ const InheritedProxyMethods = [
+ 'addArrayObserver',
+ 'addObject',
+ 'addObjects',
+ 'any',
+ 'arrayContentDidChange',
+ 'arrayContentWillChange',
+ 'clear',
+ 'compact',
+ 'every',
+ 'filter',
+ 'filterBy',
+ 'find',
+ 'findBy',
+ 'getEach',
+ 'includes',
+ 'indexOf',
+ 'insertAt',
+ 'invoke',
+ 'isAny',
+ 'isEvery',
+ 'lastIndexOf',
+ 'map',
+ 'mapBy',
+ // TODO update RFC to note objectAt was deprecated (forEach was left for iteration)
+ 'objectAt',
+ 'objectsAt',
+ 'popObject',
+ 'pushObject',
+ 'pushObjects',
+ 'reduce',
+ 'reject',
+ 'rejectBy',
+ 'removeArrayObserver',
+ 'removeAt',
+ 'removeObject',
+ 'removeObjects',
+ 'replace',
+ 'reverseObjects',
+ 'setEach',
+ 'setObjects',
+ 'shiftObject',
+ 'slice',
+ 'sortBy',
+ 'toArray',
+ 'uniq',
+ 'uniqBy',
+ 'unshiftObject',
+ 'unshiftObjects',
+ 'without',
+ ];
+ InheritedProxyMethods.forEach((method) => {
+ PromiseManyArray.prototype[method] = function proxiedMethod(...args) {
+ deprecate(
+ `The ${method} method on ember-data's PromiseManyArray is deprecated. await the promise and work with the ManyArray directly.`,
+ false,
+ {
+ id: 'ember-data:deprecate-promise-many-array-behaviors',
+ until: '5.0',
+ since: { enabled: '4.8', available: '4.8' },
+ for: 'ember-data',
+ }
+ );
+ assert(`Cannot call ${method} before content is assigned.`, this.content);
+ return this.content[method](...args);
+ };
+ });
+}
diff --git a/packages/private-build-infra/addon/current-deprecations.ts b/packages/private-build-infra/addon/current-deprecations.ts
index 64b70f3a597..df8eea70cb6 100644
--- a/packages/private-build-infra/addon/current-deprecations.ts
+++ b/packages/private-build-infra/addon/current-deprecations.ts
@@ -50,4 +50,5 @@ export default {
DEPRECATE_EARLY_STATIC: '4.8',
DEPRECATE_CLASSIC: '4.9',
DEPRECATE_HELPERS: '4.8',
+ DEPRECATE_PROMISE_MANY_ARRAY_BEHAVIORS: '4.8',
};
diff --git a/packages/private-build-infra/addon/deprecations.ts b/packages/private-build-infra/addon/deprecations.ts
index f0dd718a135..acf33fd4e0a 100644
--- a/packages/private-build-infra/addon/deprecations.ts
+++ b/packages/private-build-infra/addon/deprecations.ts
@@ -18,4 +18,4 @@ export const DEPRECATE_JSON_API_FALLBACK = deprecationState('DEPRECATE_JSON_API_
export const DEPRECATE_MODEL_REOPEN = deprecationState('DEPRECATE_MODEL_REOPEN');
export const DEPRECATE_EARLY_STATIC = deprecationState('DEPRECATE_EARLY_STATIC');
export const DEPRECATE_CLASSIC = deprecationState('DEPRECATE_CLASSIC');
-export const DEPRECATE_HELPERS = deprecationState('DEPRECATE_HELPERS');
+export const DEPRECATE_PROMISE_MANY_ARRAY_BEHAVIORS = deprecationState('DEPRECATE_PROMISE_MANY_ARRAY_BEHAVIORS');
diff --git a/packages/record-data/addon/-private/record-data.ts b/packages/record-data/addon/-private/record-data.ts
index 393c3470863..dfbdb59ab6c 100644
--- a/packages/record-data/addon/-private/record-data.ts
+++ b/packages/record-data/addon/-private/record-data.ts
@@ -556,20 +556,6 @@ export default class RecordDataDefault implements RelationshipRecordData {
return array;
}
- isAttrDirty(key: string): boolean {
- if (this._attributes[key] === undefined) {
- return false;
- }
- let originalValue;
- if (this._inFlightAttributes[key] !== undefined) {
- originalValue = this._inFlightAttributes[key];
- } else {
- originalValue = this._data[key];
- }
-
- return originalValue !== this._attributes[key];
- }
-
get _attributes() {
if (this.__attributes === null) {
this.__attributes = Object.create(null);
@@ -661,20 +647,6 @@ export default class RecordDataDefault implements RelationshipRecordData {
return createOptions;
}
- /*
- TODO IGOR AND DAVID this shouldn't be public
- This method should only be called by records in the `isNew()` state OR once the record
- has been deleted and that deletion has been persisted.
-
- It will remove this record from any associated relationships.
-
- If `isNew` is true (default false), it will also completely reset all
- relationships to an empty state as well.
-
- @method removeFromInverseRelationships
- @param {Boolean} isNew whether to unload from the `isNew` perspective
- @private
- */
removeFromInverseRelationships() {
graphFor(this.storeWrapper).push({
op: 'deleteRecord',
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
index b47e5c38856..98a643ccc76 100644
--- a/packages/store/addon/-private/record-arrays/adapter-populated-record-array.ts
+++ b/packages/store/addon/-private/record-arrays/adapter-populated-record-array.ts
@@ -48,9 +48,7 @@ export interface AdapterPopulatedRecordArrayCreateArgs {
// GET /users?isAdmin=true
store.query('user', { isAdmin: true }).then(function(admins) {
- admins.then(function() {
- console.log(admins.get("length")); // 42
- });
+ admins.get("length"); // 42
// somewhere later in the app code, when new admins have been created
// in the meantime
@@ -58,7 +56,7 @@ export interface AdapterPopulatedRecordArrayCreateArgs {
// GET /users?isAdmin=true
admins.update().then(function() {
admins.get('isUpdating'); // false
- console.log(admins.get("length")); // 123
+ admins.get("length"); // 123
});
admins.get('isUpdating'); // true
diff --git a/packages/store/addon/-private/store-service.ts b/packages/store/addon/-private/store-service.ts
index b2c33a781ed..b84369b4cb3 100644
--- a/packages/store/addon/-private/store-service.ts
+++ b/packages/store/addon/-private/store-service.ts
@@ -33,7 +33,7 @@ import type { MinimumAdapterInterface } from '@ember-data/types/q/minimum-adapte
import type { MinimumSerializerInterface } from '@ember-data/types/q/minimum-serializer-interface';
import type { RecordData } from '@ember-data/types/q/record-data';
import { JsonApiValidationError } from '@ember-data/types/q/record-data-json-api';
-import type { RecordDataRecordWrapper } from '@ember-data/types/q/record-data-record-wrapper';
+import type { RecordDataWrapper } from '@ember-data/types/q/record-data-record-wrapper';
import type { RecordInstance } from '@ember-data/types/q/record-instance';
import type { SchemaDefinitionService } from '@ember-data/types/q/schema-definition-service';
import type { FindOptions } from '@ember-data/types/q/store';
@@ -285,7 +285,7 @@ class Store extends Service {
instantiateRecord(
identifier: StableRecordIdentifier,
createRecordArgs: { [key: string]: unknown },
- recordDataFor: (identifier: StableRecordIdentifier) => RecordDataRecordWrapper,
+ recordDataFor: (identifier: StableRecordIdentifier) => RecordDataWrapper,
notificationManager: NotificationManager
): DSModel | RecordInstance {
if (HAS_MODEL_PACKAGE) {
@@ -466,7 +466,7 @@ class Store extends Service {
const identifier = this.identifierCache.peekRecordIdentifier(resource as ResourceIdentifierObject);
assert(
- `The id ${properties.id} has already been used with another '${normalizeModelName}' record.`,
+ `The id ${properties.id} has already been used with another '${normalizedModelName}' record.`,
!identifier
);
}
@@ -1352,7 +1352,7 @@ class Store extends Service {
```javascript
store.queryRecord('user', {}).then(function(user) {
let username = user.get('username');
- console.log(`Currently logged in as ${username}`);
+ // do thing
});
```
@@ -1409,7 +1409,7 @@ class Store extends Service {
```javascript
store.queryRecord('user', { username: 'unique' }).then(function(user) {
- console.log(user); // null
+ // user is null
});
```