From 8144c9cd8afe97b50116c9cbae5db7110e058e25 Mon Sep 17 00:00:00 2001 From: Chris Thoburn Date: Fri, 12 Jun 2020 18:51:04 -0700 Subject: [PATCH 01/13] feat: Native Model Co-Authored-By: Scott Newcomer fix docs tests getWithDefault bad rebase fix doc tests add computeds dont invalidate prop within isError dont install tracking within our meta object more prop cleanup more get cleanup cycle less on id cycle less on id cleanup update test Revert "cycle less on id cleanup" This reverts commit 173c42b215d34266e02daa68be969690ada167aa. Revert "cycle less on id" This reverts commit 38f76aa6b5c7c10ccb225f0c8adae4304a4b861e. less cycling but without whatever weird build issue remove mixin if deprecation is resolved and strip deprecations for perf tests new tests for refactoring refactor to simpler definitions --- .../node-tests/docs/test-coverage.js | 2 +- .../node-tests/fixtures/expected.js | 7 +- .../integration/adapter/store-adapter-test.js | 40 +- packages/-ember-data/tests/unit/model-test.js | 26 +- .../-ember-data/tests/unit/model/attr-test.js | 184 +++ .../unit/model/rollback-attributes-test.js | 11 +- packages/model/addon/-private/attr.js | 4 +- packages/model/addon/-private/belongs-to.js | 28 +- packages/model/addon/-private/has-many.js | 14 +- packages/model/addon/-private/model.js | 1006 +++++++++-------- .../-private/system/relationships/ext.js | 79 -- .../system/relationships/relationship-meta.ts | 7 +- packages/model/index.js | 17 +- .../-private/system/model/internal-model.ts | 27 +- .../system/store/internal-model-factory.ts | 2 +- packages/store/index.js | 30 +- packages/store/package.json | 1 + yarn.lock | 13 + 18 files changed, 865 insertions(+), 633 deletions(-) create mode 100644 packages/-ember-data/tests/unit/model/attr-test.js delete mode 100644 packages/model/addon/-private/system/relationships/ext.js diff --git a/packages/-ember-data/node-tests/docs/test-coverage.js b/packages/-ember-data/node-tests/docs/test-coverage.js index 15a3a38996e..39187b85509 100644 --- a/packages/-ember-data/node-tests/docs/test-coverage.js +++ b/packages/-ember-data/node-tests/docs/test-coverage.js @@ -19,7 +19,7 @@ QUnit.module('Docs coverage', function(hooks) { QUnit.module('modules', function() { test('We have all expected modules', function(assert) { - assert.deepEqual(Object.keys(docs.modules), expected.modules, 'We have all modules'); + assert.deepEqual(Object.keys(docs.modules).sort(), expected.modules, 'We have all modules'); }); }); diff --git a/packages/-ember-data/node-tests/fixtures/expected.js b/packages/-ember-data/node-tests/fixtures/expected.js index 077824e1f1e..e4a569356e1 100644 --- a/packages/-ember-data/node-tests/fixtures/expected.js +++ b/packages/-ember-data/node-tests/fixtures/expected.js @@ -3,11 +3,11 @@ module.exports = { '@ember-data/adapter', '@ember-data/canary-features', '@ember-data/debug', - '@ember-data/model', - '@ember-data/store', '@ember-data/deprecations', + '@ember-data/model', '@ember-data/record-data', - '@ember-data/serializer' + '@ember-data/serializer', + '@ember-data/store', ], classitems: [ '(private) @ember-data/adapter BuildURLMixin#_buildURL', @@ -32,7 +32,6 @@ module.exports = { '(private) @ember-data/model Model#_notifyProperties', '(private) @ember-data/model Model#create', '(private) @ember-data/model Model#currentState', - '(private) @ember-data/model Model#recordData', '(private) @ember-data/model Model#send', '(private) @ember-data/model Model#transitionTo', '(private) @ember-data/model Model#trigger', 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 9eb3875ab69..11bad8736f6 100644 --- a/packages/-ember-data/tests/integration/adapter/store-adapter-test.js +++ b/packages/-ember-data/tests/integration/adapter/store-adapter-test.js @@ -532,7 +532,7 @@ module('integration/adapter/store-adapter - DS.Store and DS.Adapter integration }); }); - test('if a created record is marked as invalid by the server, it enters an error state', function(assert) { + test('if a created record is marked as invalid by the server, it enters an error state', async function(assert) { let store = this.owner.lookup('service:store'); let adapter = store.adapterFor('application'); let Person = store.modelFor('person'); @@ -560,32 +560,30 @@ module('integration/adapter/store-adapter - DS.Store and DS.Adapter integration let yehuda = store.createRecord('person', { id: 1, name: 'Yehuda Katz' }); // Wrap this in an Ember.run so that all chained async behavior is set up // before flushing any scheduled behavior. - return run(function() { - return yehuda - .save() - .catch(error => { - assert.false(get(yehuda, 'isValid'), 'the record is invalid'); - assert.ok(get(yehuda, 'errors.name'), 'The errors.name property exists'); - set(yehuda, 'updatedAt', true); - assert.false(get(yehuda, 'isValid'), 'the record is still invalid'); + try { + await yehuda.save(); + } catch (e) { + assert.false(get(yehuda, 'isValid'), 'the record is invalid'); + assert.ok(get(yehuda, 'errors.name'), 'The errors.name property exists'); - set(yehuda, 'name', 'Brohuda Brokatz'); + set(yehuda, 'updatedAt', true); + assert.false(get(yehuda, 'isValid'), 'the record is still invalid'); - assert.true(get(yehuda, 'isValid'), 'the record is no longer invalid after changing'); - assert.true(get(yehuda, 'hasDirtyAttributes'), 'the record has outstanding changes'); + set(yehuda, 'name', 'Brohuda Brokatz'); - assert.true(get(yehuda, 'isNew'), 'precond - record is still new'); + assert.true(get(yehuda, 'isValid'), 'the record is no longer invalid after changing'); + assert.true(get(yehuda, 'hasDirtyAttributes'), 'the record has outstanding changes'); - return yehuda.save(); - }) - .then(person => { - assert.strictEqual(person, yehuda, 'The promise resolves with the saved record'); + assert.true(get(yehuda, 'isNew'), 'precond - record is still new'); - assert.true(get(yehuda, 'isValid'), 'record remains valid after committing'); - assert.false(get(yehuda, 'isNew'), 'record is no longer new'); - }); - }); + let person = await yehuda.save(); + + assert.strictEqual(person, yehuda, 'The promise resolves with the saved record'); + + assert.true(get(yehuda, 'isValid'), 'record remains valid after committing'); + assert.false(get(yehuda, 'isNew'), 'record is no longer new'); + } }); test('allows errors on arbitrary properties on create', function(assert) { diff --git a/packages/-ember-data/tests/unit/model-test.js b/packages/-ember-data/tests/unit/model-test.js index 93d97c59207..ca0d038dc8e 100644 --- a/packages/-ember-data/tests/unit/model-test.js +++ b/packages/-ember-data/tests/unit/model-test.js @@ -373,21 +373,31 @@ module('unit/model - Model', function(hooks) { test('ID mutation (complicated)', async function(assert) { let idChange = 0; + let compChange = 0; const OddPerson = Model.extend({ name: DSattr('string'), - idComputed: computed('id', function() {}), - idDidChange: observer('id', () => idChange++), + idComputed: computed('id', function() { + // we intentionally don't access the id here + return 'not-the-id:' + compChange++; + }), + idDidChange: observer('id', function() { + idChange++; + }), }); this.owner.register('model:odd-person', OddPerson); let person = store.createRecord('odd-person'); - person.get('idComputed'); - assert.equal(idChange, 0); + assert.strictEqual(person.get('idComputed'), 'not-the-id:0'); + assert.equal(idChange, 0, 'we have had no changes initially'); - assert.equal(person.get('id'), null, 'initial created model id should be null'); - assert.equal(idChange, 0); + let personId = person.get('id'); + assert.strictEqual(personId, null, 'initial created model id should be null'); + assert.equal(idChange, 0, 'we should still have no id changes'); + + // simulate an update from the store or RecordData that doesn't + // go through the internalModelFactory person._internalModel.setId('john'); - assert.equal(idChange, 1); + assert.equal(idChange, 1, 'we should have one change after updating id'); let recordData = recordDataFor(person); assert.equal( recordData.getResourceIdentifier().id, @@ -729,7 +739,7 @@ module('unit/model - Model', function(hooks) { assert.expectAssertion(() => { record.set('isLoaded', true); - }, /Cannot set read-only property "isLoaded"/); + }, /Cannot set property isLoaded of \[object Object\] which has only a getter/); }); class NativePostWithInternalModel extends Model { diff --git a/packages/-ember-data/tests/unit/model/attr-test.js b/packages/-ember-data/tests/unit/model/attr-test.js new file mode 100644 index 00000000000..e7726107b82 --- /dev/null +++ b/packages/-ember-data/tests/unit/model/attr-test.js @@ -0,0 +1,184 @@ +import { module, skip, test } from 'qunit'; + +import { setupTest } from 'ember-qunit'; + +import Model, { attr } from '@ember-data/model'; + +module('unit/model/attr | attr syntax', function(hooks) { + setupTest(hooks); + + let store; + let owner; + hooks.beforeEach(function() { + owner = this.owner; + store = owner.lookup('service:store'); + }); + + test('attr can be used with classic syntax', async function(assert) { + const User = Model.extend({ + name: attr(), + nameWithTransform: attr('string'), + nameWithOptions: attr({}), + nameWithTransformAndOptions: attr('string', {}), + }); + + owner.register('model:user', User); + + let UserModel = store.modelFor('user'); + let attrs = UserModel.attributes; + assert.true(attrs.has('name'), 'We have the attr: name'); + assert.true(attrs.has('nameWithTransform'), 'We have the attr: nameWithTransform'); + assert.true(attrs.has('nameWithOptions'), 'We have the attr: nameWithOptions'); + assert.true(attrs.has('nameWithTransformAndOptions'), 'We have the attr: nameWithTransformAndOptions'); + + let userRecord = store.push({ + data: { + type: 'user', + id: '1', + attributes: { + name: 'Chris', + nameWithTransform: '@runspired', + nameWithOptions: 'Contributor', + nameWithTransformAndOptions: '@runspired contribution', + }, + }, + }); + + assert.strictEqual(userRecord.name, 'Chris', 'attr is correctly set: name'); + assert.strictEqual(userRecord.nameWithTransform, '@runspired', 'attr is correctly set: nameWithTransform'); + assert.strictEqual(userRecord.nameWithOptions, 'Contributor', 'attr is correctly set: nameWithOptions'); + assert.strictEqual( + userRecord.nameWithTransformAndOptions, + '@runspired contribution', + 'attr is correctly set: nameWithTransformAndOptions' + ); + }); + + test('attr can be used with native syntax decorator style', async function(assert) { + class User extends Model { + @attr() name; + @attr('string') nameWithTransform; + @attr({}) nameWithOptions; + @attr('string', {}) nameWithTransformAndOptions; + } + + owner.register('model:user', User); + + let UserModel = store.modelFor('user'); + let attrs = UserModel.attributes; + assert.true(attrs.has('name'), 'We have the attr: name'); + assert.true(attrs.has('nameWithTransform'), 'We have the attr: nameWithTransform'); + assert.true(attrs.has('nameWithOptions'), 'We have the attr: nameWithOptions'); + assert.true(attrs.has('nameWithTransformAndOptions'), 'We have the attr: nameWithTransformAndOptions'); + + let userRecord = store.push({ + data: { + type: 'user', + id: '1', + attributes: { + name: 'Chris', + nameWithTransform: '@runspired', + nameWithOptions: 'Contributor', + nameWithTransformAndOptions: '@runspired contribution', + }, + }, + }); + + assert.strictEqual(userRecord.name, 'Chris', 'attr is correctly set: name'); + assert.strictEqual(userRecord.nameWithTransform, '@runspired', 'attr is correctly set: nameWithTransform'); + assert.strictEqual(userRecord.nameWithOptions, 'Contributor', 'attr is correctly set: nameWithOptions'); + assert.strictEqual( + userRecord.nameWithTransformAndOptions, + '@runspired contribution', + 'attr is correctly set: nameWithTransformAndOptions' + ); + }); + + skip('attr cannot be used with native syntax prop style', async function(assert) { + class User extends Model { + name = attr(); + nameWithTransform = attr('string'); + nameWithOptions = attr({}); + nameWithTransformAndOptions = attr('string', {}); + } + + owner.register('model:user', User); + + let UserModel = store.modelFor('user'); + let attrs = UserModel.attributes; + assert.true(attrs.has('name'), 'We have the attr: name'); + assert.true(attrs.has('nameWithTransform'), 'We have the attr: nameWithTransform'); + assert.true(attrs.has('nameWithOptions'), 'We have the attr: nameWithOptions'); + assert.true(attrs.has('nameWithTransformAndOptions'), 'We have the attr: nameWithTransformAndOptions'); + + let userRecord = store.push({ + data: { + type: 'user', + id: '1', + attributes: { + name: 'Chris', + nameWithTransform: '@runspired', + nameWithOptions: 'Contributor', + nameWithTransformAndOptions: '@runspired contribution', + }, + }, + }); + + assert.strictEqual(userRecord.name, 'Chris', 'attr is correctly set: name'); + assert.strictEqual(userRecord.nameWithTransform, '@runspired', 'attr is correctly set: nameWithTransform'); + assert.strictEqual(userRecord.nameWithOptions, 'Contributor', 'attr is correctly set: nameWithOptions'); + assert.strictEqual( + userRecord.nameWithTransformAndOptions, + '@runspired contribution', + 'attr is correctly set: nameWithTransformAndOptions' + ); + }); + + skip('attr can be used with native syntax decorator style without parens', async function(assert) { + class User extends Model { + @attr name; + } + + owner.register('model:user', User); + + let UserModel = store.modelFor('user'); + let attrs = UserModel.attributes; + assert.true(attrs.has('name'), 'We have the attr: name'); + + let userRecord = store.push({ + data: { + type: 'user', + id: '1', + attributes: { + name: 'Chris', + }, + }, + }); + + assert.strictEqual(userRecord.name, 'Chris', 'attr is correctly set: name'); + }); + + skip('attr can not be used classic syntax without parens', async function(assert) { + const User = Model.extend({ + name: attr, + }); + + owner.register('model:user', User); + + let UserModel = store.modelFor('user'); + let attrs = UserModel.attributes; + assert.true(attrs.has('name'), 'We have the attr: name'); + + let userRecord = store.push({ + data: { + type: 'user', + id: '1', + attributes: { + name: 'Chris', + }, + }, + }); + + assert.strictEqual(userRecord.name, 'Chris', 'attr is correctly set: name'); + }); +}); 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 c512b76dff6..bb01ae67f53 100644 --- a/packages/-ember-data/tests/unit/model/rollback-attributes-test.js +++ b/packages/-ember-data/tests/unit/model/rollback-attributes-test.js @@ -66,17 +66,18 @@ module('unit/model/rollbackAttributes - model.rollbackAttributes()', function(ho return person; }); - assert.equal(person.get('firstName'), 'Thomas'); + assert.equal(person.get('firstName'), 'Thomas', 'PreCond: we mutated firstName'); if (DEPRECATE_RECORD_LIFECYCLE_EVENT_METHODS) { - assert.equal(person.get('rolledBackCount'), 0); + assert.equal(person.get('rolledBackCount'), 0, 'PreCond: we have not yet rolled back'); } run(() => person.rollbackAttributes()); - assert.equal(person.get('firstName'), 'Tom'); - assert.false(person.get('hasDirtyAttributes')); + assert.equal(person.get('firstName'), 'Tom', 'We rolled back firstName'); + assert.false(person.get('hasDirtyAttributes'), 'We expect the record to be clean'); + if (DEPRECATE_RECORD_LIFECYCLE_EVENT_METHODS) { - assert.equal(person.get('rolledBackCount'), 1); + assert.equal(person.get('rolledBackCount'), 1, 'We rolled back once'); } }); diff --git a/packages/model/addon/-private/attr.js b/packages/model/addon/-private/attr.js index 37af3106a57..1bba908d9ab 100644 --- a/packages/model/addon/-private/attr.js +++ b/packages/model/addon/-private/attr.js @@ -130,7 +130,7 @@ function attr(type, options) { return computed({ get(key) { if (DEBUG) { - if (['_internalModel', 'recordData', 'currentState'].indexOf(key) !== -1) { + if (['_internalModel', 'currentState'].indexOf(key) !== -1) { throw new Error( `'${key}' is a reserved property name on instances of classes extending Model. Please choose a different property name for your attr on ${this.constructor.toString()}` ); @@ -145,7 +145,7 @@ function attr(type, options) { }, set(key, value) { if (DEBUG) { - if (['_internalModel', 'recordData', 'currentState'].indexOf(key) !== -1) { + if (['_internalModel', 'currentState'].indexOf(key) !== -1) { throw new Error( `'${key}' is a reserved property name on instances of classes extending Model. Please choose a different property name for your attr on ${this.constructor.toString()}` ); diff --git a/packages/model/addon/-private/belongs-to.js b/packages/model/addon/-private/belongs-to.js index 8e780af054f..8ece3938018 100644 --- a/packages/model/addon/-private/belongs-to.js +++ b/packages/model/addon/-private/belongs-to.js @@ -112,32 +112,20 @@ import { computedMacroWithOptionalParams } from './util'; @return {Ember.computed} relationship */ function belongsTo(modelName, options) { - let opts, userEnteredModelName; - if (typeof modelName === 'object') { - opts = modelName; - userEnteredModelName = undefined; - } else { - opts = options; - userEnteredModelName = modelName; - } - - if (typeof userEnteredModelName === 'string') { - userEnteredModelName = normalizeModelName(userEnteredModelName); - } - assert( 'The first argument to belongsTo must be a string representing a model type key, not an instance of ' + - inspect(userEnteredModelName) + + inspect(modelName) + ". E.g., to define a relation to the Person model, use belongsTo('person')", - typeof userEnteredModelName === 'string' || typeof userEnteredModelName === 'undefined' + typeof modelName !== 'string' ); - opts = opts || {}; + modelName = normalizeModelName(modelName); + options = options || {}; let meta = { - type: userEnteredModelName, + type: modelName, isRelationship: true, - options: opts, + options, kind: 'belongsTo', name: 'Belongs To', key: null, @@ -151,7 +139,7 @@ function belongsTo(modelName, options) { `'${key}' is a reserved property name on instances of classes extending Model. Please choose a different property name for your belongsTo on ${this.constructor.toString()}` ); } - if (Object.prototype.hasOwnProperty.call(opts, 'serialize')) { + if (Object.prototype.hasOwnProperty.call(options, 'serialize')) { warn( `You provided a serialize option on the "${key}" property in the "${this._internalModel.modelName}" class, this belongs in the serializer. See Serializer and it's implementations https://api.emberjs.com/ember-data/release/classes/Serializer`, false, @@ -161,7 +149,7 @@ function belongsTo(modelName, options) { ); } - if (Object.prototype.hasOwnProperty.call(opts, 'embedded')) { + if (Object.prototype.hasOwnProperty.call(options, 'embedded')) { warn( `You provided an embedded option on the "${key}" property in the "${this._internalModel.modelName}" class, this belongs in the serializer. See EmbeddedRecordsMixin https://api.emberjs.com/ember-data/release/classes/EmbeddedRecordsMixin`, false, diff --git a/packages/model/addon/-private/has-many.js b/packages/model/addon/-private/has-many.js index 9d05e740d3b..8f263521811 100644 --- a/packages/model/addon/-private/has-many.js +++ b/packages/model/addon/-private/has-many.js @@ -5,6 +5,8 @@ import { assert, inspect } from '@ember/debug'; import { computed } from '@ember/object'; import { DEBUG } from '@glimmer/env'; +import { singularize } from 'ember-inflector'; + import { normalizeModelName } from '@ember-data/store'; import { computedMacroWithOptionalParams } from './util'; @@ -152,23 +154,15 @@ import { computedMacroWithOptionalParams } from './util'; @return {Ember.computed} relationship */ function hasMany(type, options) { - if (typeof type === 'object') { - options = type; - type = undefined; - } - assert( `The first argument to hasMany must be a string representing a model type key, not an instance of ${inspect( type )}. E.g., to define a relation to the Comment model, use hasMany('comment')`, - typeof type === 'string' || typeof type === 'undefined' + typeof type !== 'string' ); options = options || {}; - - if (typeof type === 'string') { - type = normalizeModelName(type); - } + type = singularize(normalizeModelName(type)); // Metadata about relationships is stored on the meta of // the relationship. This is used for introspection and diff --git a/packages/model/addon/-private/model.js b/packages/model/addon/-private/model.js index c7ee9fd6a25..5591964331f 100644 --- a/packages/model/addon/-private/model.js +++ b/packages/model/addon/-private/model.js @@ -19,16 +19,31 @@ import { PromiseObject, recordDataFor, recordIdentifierFor, - RootState, } from '@ember-data/store/-private'; import Errors from './errors'; -import { - relatedTypesDescriptor, - relationshipsByNameDescriptor, - relationshipsDescriptor, - relationshipsObjectDescriptor, -} from './system/relationships/ext'; +import { relationshipFromMeta } from './system/relationships/relationship-meta'; + +const RecordMeta = new WeakMap(); +function getRecordMeta(record) { + let meta = RecordMeta.get(record); + if (meta === undefined) { + meta = Object.create(null); + RecordMeta.set(record, meta); + if (DEBUG) { + record.____recordMetaCache = meta; + } + } + return meta; +} + +function getWithDefault(meta, prop, value) { + let v = meta[prop]; + if (v === undefined) { + return value; + } + return v; +} const { changeProperties } = Ember; @@ -39,7 +54,7 @@ function isInvalidError(error) { function findPossibleInverses(type, inverseType, name, relationshipsSoFar) { let possibleRelationships = relationshipsSoFar || []; - let relationshipMap = get(inverseType, 'relationships'); + let relationshipMap = inverseType.relationships; if (!relationshipMap) { return possibleRelationships; } @@ -69,88 +84,25 @@ function findPossibleInverses(type, inverseType, name, relationshipsSoFar) { return possibleRelationships; } -const retrieveFromCurrentState = computed('currentState', function(key) { - return get(this._internalModel.currentState, key); -}).readOnly(); - -const isValidRecordData = computed('errors.length', function(key) { - return !(this.get('errors.length') > 0); -}).readOnly(); - -const isValid = RECORD_DATA_ERRORS ? isValidRecordData : retrieveFromCurrentState; - -let isDeletedCP; -if (RECORD_DATA_STATE) { - isDeletedCP = computed('currentState', function() { - let rd = recordDataFor(this); - if (rd.isDeleted) { - return rd.isDeleted(); - } else { - return get(this._internalModel.currentState, 'isDeleted'); - } - }).readOnly(); -} else { - isDeletedCP = retrieveFromCurrentState; -} - -let isNewCP; -if (RECORD_DATA_STATE) { - isNewCP = computed('currentState', function() { - let rd = recordDataFor(this); - if (rd.isNew) { - return rd.isNew(); - } else { - return get(this._internalModel.currentState, 'isNew'); - } - }).readOnly(); -} else { - isNewCP = retrieveFromCurrentState; -} +function computeOnce(target, key, desc) { + const cache = new WeakMap(); + let getter = desc.get; + desc.get = function() { + let meta = cache.get(this); -let adapterError; -if (REQUEST_SERVICE) { - adapterError = computed(function() { - let request = this._lastError; - if (!request) { - return null; + if (!meta) { + meta = { hasComputed: false, value: undefined }; + cache.set(this, meta); } - return request.state === 'rejected' && request.response.data; - }); -} else { - adapterError = null; -} -let isError; -if (REQUEST_SERVICE) { - isError = computed(function() { - let errorReq = this._errorRequests[this._errorRequests.length - 1]; - if (!errorReq) { - return false; - } else { - return true; + if (!meta.hasComputed) { + meta.value = getter.call(this); + meta.hasComputed = true; } - }); -} else { - isError = false; -} -let isReloading; -if (REQUEST_SERVICE) { - isReloading = computed({ - get() { - if (this._isReloading === undefined) { - let requests = this.store.getRequestStateService().getPendingRequestsForRecord(recordIdentifierFor(this)); - let value = !!requests.find(req => req.request.data[0].options.isReloading); - return (this._isReloading = value); - } - return this._isReloading; - }, - set(_, value) { - return (this._isReloading = value); - }, - }); -} else { - isReloading = false; + return meta.value; + }; + return desc; } /** @@ -159,9 +111,9 @@ if (REQUEST_SERVICE) { @extends EmberObject @uses EmberData.DeprecatedEvented */ -const Model = EmberObject.extend(DeprecatedEvented, { +class Model extends EmberObject { init() { - this._super(...arguments); + super.init(...arguments); if (DEBUG) { if (!this._internalModel) { @@ -195,15 +147,15 @@ const Model = EmberObject.extend(DeprecatedEvented, { this._errorRequests = []; this._lastError = null; } - }, + } - _notifyNetworkChanges: function() { + _notifyNetworkChanges() { if (REQUEST_SERVICE) { ['isSaving', 'isValid', 'isError', 'adapterError', 'isReloading'].forEach(key => this.notifyPropertyChange(key)); } else { ['isValid'].forEach(key => this.notifyPropertyChange(key)); } - }, + } /** If this property is `true` the record is in the `empty` @@ -218,7 +170,10 @@ const Model = EmberObject.extend(DeprecatedEvented, { @type {Boolean} @readOnly */ - isEmpty: retrieveFromCurrentState, + get isEmpty() { + return this._internalModel.currentState.isEmpty; + } + /** If this property is `true` the record is in the `loading` state. A record enters this state when the store asks the adapter for its @@ -229,7 +184,9 @@ const Model = EmberObject.extend(DeprecatedEvented, { @type {Boolean} @readOnly */ - isLoading: retrieveFromCurrentState, + get isLoading() { + return this._internalModel.currentState.isLoading; + } /** If this property is `true` the record is in the `loaded` state. A record enters this state when its data is populated. Most of a @@ -251,7 +208,11 @@ const Model = EmberObject.extend(DeprecatedEvented, { @type {Boolean} @readOnly */ - isLoaded: retrieveFromCurrentState, + + get isLoaded() { + return this._internalModel.currentState.isLoaded; + } + /** If this property is `true` the record is in the `dirty` state. The record has local changes that have not yet been saved by the @@ -276,9 +237,10 @@ const Model = EmberObject.extend(DeprecatedEvented, { @type {Boolean} @readOnly */ - hasDirtyAttributes: computed('currentState.isDirty', function() { - return this.get('currentState.isDirty'); - }), + get hasDirtyAttributes() { + return this._internalModel.currentState.isDirty; + } + /** If this property is `true` the record is in the `saving` state. A record enters the saving state when `save` is called, but the @@ -301,7 +263,10 @@ const Model = EmberObject.extend(DeprecatedEvented, { @type {Boolean} @readOnly */ - isSaving: retrieveFromCurrentState, + get isSaving() { + return this._internalModel.currentState.isSaving; + } + /** If this property is `true` the record is in the `deleted` state and has been marked for deletion. When `isDeleted` is true and @@ -339,7 +304,21 @@ const Model = EmberObject.extend(DeprecatedEvented, { @type {Boolean} @readOnly */ - isDeleted: isDeletedCP, + get isDeleted() { + if (RECORD_DATA_STATE) { + // currently we call notifyPropertyChange from + // the notification manager but probably + // we should consume a tag here which the manager + // would dirty. + let rd = recordDataFor(this); + if (rd.isDeleted) { + return rd.isDeleted(); + } + } + + return this._internalModel.currentState.isDeleted; + } + /** If this property is `true` the record is in the `new` state. A record will be in the `new` state when it has been created on the @@ -361,7 +340,21 @@ const Model = EmberObject.extend(DeprecatedEvented, { @type {Boolean} @readOnly */ - isNew: isNewCP, + get isNew() { + if (RECORD_DATA_STATE) { + // currently we call notifyPropertyChange from + // the notification manager but probably + // we should consume a tag here which the manager + // would dirty. + let rd = recordDataFor(this); + if (rd.isNew) { + return rd.isNew(); + } + } + + return this._internalModel.currentState.isNew; + } + /** If this property is `true` the record is in the `valid` state. @@ -372,14 +365,20 @@ const Model = EmberObject.extend(DeprecatedEvented, { @type {Boolean} @readOnly */ - isValid: isValid, + get isValid() { + if (RECORD_DATA_ERRORS) { + return !(this.errors.length > 0); + } + + return this._internalModel.currentState.isValid; + } _markInvalidRequestAsClean() { if (RECORD_DATA_ERRORS) { this._invalidRequests = []; this._notifyNetworkChanges(); } - }, + } /** If the record is in the dirty state this property will report what @@ -401,7 +400,9 @@ const Model = EmberObject.extend(DeprecatedEvented, { @type {String} @readOnly */ - dirtyType: retrieveFromCurrentState, + get dirtyType() { + return this._internalModel.currentState.dirtyType; + } /** If `true` the adapter reported that it was unable to save local @@ -422,13 +423,32 @@ const Model = EmberObject.extend(DeprecatedEvented, { @type {Boolean} @readOnly */ - isError: isError, + get isError() { + if (REQUEST_SERVICE) { + let errorReq = this._errorRequests[this._errorRequests.length - 1]; + if (!errorReq) { + return false; + } else { + return true; + } + } + let meta = getRecordMeta(this); + return getWithDefault(meta, 'isError', false); + } + set isError(v) { + if (REQUEST_SERVICE && DEBUG) { + throw new Error(`isError is not directly settable when REQUEST_SERVICE is enabled`); + } else { + let meta = getRecordMeta(this); + meta.isError = v; + } + } _markErrorRequestAsClean() { this._errorRequests = []; this._lastError = null; this._notifyNetworkChanges(); - }, + } /** If `true` the store is attempting to reload the record from the adapter. @@ -445,7 +465,25 @@ const Model = EmberObject.extend(DeprecatedEvented, { @type {Boolean} @readOnly */ - isReloading: isReloading, + @computed() + get isReloading() { + let meta = getRecordMeta(this); + let isReloading = meta.isReloading; + + if (REQUEST_SERVICE) { + if (isReloading === undefined) { + let requests = this.store.getRequestStateService().getPendingRequestsForRecord(recordIdentifierFor(this)); + let value = !!requests.find(req => req.request.data[0].options.isReloading); + meta.isReloading = value; + return value; + } + } + return isReloading || false; + } + set isReloading(v) { + let meta = getRecordMeta(this); + meta.isReloading = v; + } /** All ember models have an id property. This is an identifier @@ -472,26 +510,16 @@ const Model = EmberObject.extend(DeprecatedEvented, { @private @type {Object} */ - currentState: RootState.empty, // defined here to avoid triggering setUnknownProperty /** @property _internalModel @private @type {Object} */ - _internalModel: null, // defined here to avoid triggering setUnknownProperty - - /** - @property recordData - @private - @type undefined (reserved) - */ - // will be defined here to avoid triggering setUnknownProperty /** @property store */ - store: null, // defined here to avoid triggering setUnknownProperty /** When the record is in the `invalid` state this object will contain @@ -544,7 +572,8 @@ const Model = EmberObject.extend(DeprecatedEvented, { @property errors @type {Errors} */ - errors: computed(function() { + @computeOnce + get errors() { let errors = Errors.create(); errors._registerHandlers( @@ -571,7 +600,7 @@ const Model = EmberObject.extend(DeprecatedEvented, { } } return errors; - }).readOnly(), + } invalidErrorsChanged(jsonApiErrors) { if (RECORD_DATA_ERRORS) { @@ -584,7 +613,7 @@ const Model = EmberObject.extend(DeprecatedEvented, { errors._add(errorKeys[i], newErrors[errorKeys[i]]); } } - }, + } /** This property holds the `AdapterError` object with which @@ -593,7 +622,25 @@ const Model = EmberObject.extend(DeprecatedEvented, { @property adapterError @type {AdapterError} */ - adapterError: adapterError, + get adapterError() { + if (REQUEST_SERVICE) { + let request = this._lastError; + if (!request) { + return null; + } + return request.state === 'rejected' && request.response.data; + } + let meta = getRecordMeta(this); + return getWithDefault(meta, 'adapterError', null); + } + set adapterError(v) { + if (REQUEST_SERVICE && DEBUG) { + throw new Error(`adapterError is not directly settable when REQUEST_SERVICE is enabled`); + } else { + let meta = getRecordMeta(this); + meta.adapterError = v; + } + } /** Create a JSON representation of the record, using the serialization @@ -611,7 +658,7 @@ const Model = EmberObject.extend(DeprecatedEvented, { */ serialize(options) { return this._internalModel.createSnapshot().serialize(options); - }, + } /** Fired when the record is ready to be interacted with, @@ -619,56 +666,48 @@ const Model = EmberObject.extend(DeprecatedEvented, { @event ready */ - ready: null, /** Fired when the record is loaded from the server. @event didLoad */ - didLoad: null, /** Fired when the record is updated. @event didUpdate */ - didUpdate: null, /** Fired when a new record is commited to the server. @event didCreate */ - didCreate: null, /** Fired when the record is deleted. @event didDelete */ - didDelete: null, /** Fired when the record becomes invalid. @event becameInvalid */ - becameInvalid: null, /** Fired when the record enters the error state. @event becameError */ - becameError: null, /** Fired when the record is rolled back. @event rolledBack */ - rolledBack: null, //TODO Do we want to deprecate these? /** @@ -679,7 +718,7 @@ const Model = EmberObject.extend(DeprecatedEvented, { */ send(name, context) { return this._internalModel.send(name, context); - }, + } /** @method transitionTo @@ -688,7 +727,7 @@ const Model = EmberObject.extend(DeprecatedEvented, { */ transitionTo(name) { return this._internalModel.transitionTo(name); - }, + } /** Marks the record as deleted but does not save it. You must call @@ -724,7 +763,7 @@ const Model = EmberObject.extend(DeprecatedEvented, { */ deleteRecord() { this._internalModel.deleteRecord(); - }, + } /** Same as `deleteRecord`, but saves the record immediately. @@ -773,7 +812,7 @@ const Model = EmberObject.extend(DeprecatedEvented, { destroyRecord(options) { this.deleteRecord(); return this.save(options); - }, + } /** Unloads the record from the store. This will not send a delete request @@ -786,7 +825,7 @@ const Model = EmberObject.extend(DeprecatedEvented, { return; } this._internalModel.unloadRecord(); - }, + } /** @method _notifyProperties @@ -803,7 +842,7 @@ const Model = EmberObject.extend(DeprecatedEvented, { this.notifyPropertyChange(key); } }); - }, + } /** Returns an object, whose keys are changed properties, and value is @@ -852,7 +891,7 @@ const Model = EmberObject.extend(DeprecatedEvented, { */ changedAttributes() { return this._internalModel.changedAttributes(); - }, + } /** If the model `hasDirtyAttributes` this function will discard any unsaved @@ -879,7 +918,7 @@ const Model = EmberObject.extend(DeprecatedEvented, { if (REQUEST_SERVICE) { this._markErrorRequestAsClean(); } - }, + } /* @method _createSnapshot @@ -887,14 +926,14 @@ const Model = EmberObject.extend(DeprecatedEvented, { */ _createSnapshot() { return this._internalModel.createSnapshot(); - }, + } toStringExtension() { // the _internalModel guard exists, because some dev-only deprecation code // (addListener via validatePropertyInjections) invokes toString before the // object is real. return this._internalModel && this._internalModel.id; - }, + } /** Save the record and persist any changes to the record to an @@ -940,7 +979,7 @@ const Model = EmberObject.extend(DeprecatedEvented, { return PromiseObject.create({ promise: this._internalModel.save(options).then(() => this), }); - }, + } /** Reload the record from the adapter. @@ -982,14 +1021,14 @@ const Model = EmberObject.extend(DeprecatedEvented, { return PromiseObject.create({ promise: this._internalModel.reload(wrappedAdapterOptions).then(() => this), }); - }, + } attr() { assert( 'The `attr` method is not available on Model, a Snapshot was probably expected. Are you passing a Model instead of a Snapshot to your serializer?', false ); - }, + } /** Get the reference for the specified belongsTo relationship. @@ -1056,7 +1095,7 @@ const Model = EmberObject.extend(DeprecatedEvented, { */ belongsTo(name) { return this._internalModel.referenceFor('belongsTo', name); - }, + } /** Get the reference for the specified hasMany relationship. @@ -1118,7 +1157,7 @@ const Model = EmberObject.extend(DeprecatedEvented, { */ hasMany(name) { return this._internalModel.referenceFor('hasMany', name); - }, + } /** Provides info about the model for debugging purposes @@ -1180,11 +1219,11 @@ const Model = EmberObject.extend(DeprecatedEvented, { expensiveProperties: expensiveProperties, }, }; - }, + } notifyBelongsToChange(key) { this.notifyPropertyChange(key); - }, + } /** Given a callback, iterates over each of the relationships in the model, invoking the callback with the name of each relationship and its relationship @@ -1239,292 +1278,82 @@ const Model = EmberObject.extend(DeprecatedEvented, { */ eachRelationship(callback, binding) { this.constructor.eachRelationship(callback, binding); - }, + } relationshipFor(name) { - return get(this.constructor, 'relationshipsByName').get(name); - }, + return this.constructor.relationshipsByName.get(name); + } inverseFor(key) { return this.constructor.inverseFor(key, this._internalModel.store); - }, + } notifyHasManyAdded(key) { //We need to notifyPropertyChange in the adding case because we need to make sure //we fetch the newly added record in case it is unloaded //TODO(Igor): Consider whether we could do this only if the record state is unloaded this.notifyPropertyChange(key); - }, + } eachAttribute(callback, binding) { this.constructor.eachAttribute(callback, binding); - }, -}); + } -if (DEPRECATE_EVENTED_API_USAGE) { - /** - Override the default event firing from Ember.Evented to - also call methods with the given name. + static isModel = true; - @method trigger - @private - @param {String} name -*/ - Model.reopen({ - trigger(name) { - if (DEPRECATE_RECORD_LIFECYCLE_EVENT_METHODS) { - let fn = this[name]; - if (typeof fn === 'function') { - let length = arguments.length; - let args = new Array(length - 1); + /** + Create should only ever be called by the store. To create an instance of a + `Model` in a dirty state use `store.createRecord`. - for (let i = 1; i < length; i++) { - args[i - 1] = arguments[i]; - } - fn.apply(this, args); - } - } + To create instances of `Model` in a clean state, use `store.push` - const _hasEvent = DEBUG ? this._has(name) : this.has(name); - if (_hasEvent) { - this._super(...arguments); - } - }, - }); -} + @method create + @private + @static + */ -if (DEPRECATE_MODEL_TOJSON) { /** - Use [JSONSerializer](JSONSerializer.html) to - get the JSON representation of a record. + Represents the model's class name as a string. This can be used to look up the model's class name through + `Store`'s modelFor method. - `toJSON` takes an optional hash as a parameter, currently - supported options are: + `modelName` is generated for you by Ember Data. It will be a lowercased, dasherized string. + For example: - - `includeId`: `true` if the record's ID should be included in the - JSON representation. + ```javascript + store.modelFor('post').modelName; // 'post' + store.modelFor('blog-post').modelName; // 'blog-post' + ``` - @method toJSON - @param {Object} options - @return {Object} A JSON representation of the object. + The most common place you'll want to access `modelName` is in your serializer's `payloadKeyFromModelName` method. For example, to change payload + keys to underscore (instead of dasherized), you might use the following code: + + ```javascript + import RESTSerializer from '@ember-data/serializer/rest'; + import { underscore } from '@ember/string'; + + export default const PostSerializer = RESTSerializer.extend({ + payloadKeyFromModelName(modelName) { + return underscore(modelName); + } + }); + ``` + @property modelName + @type String + @readonly + @static */ - Model.reopen({ - toJSON(options) { - // container is for lazy transform lookups - deprecate( - `Called the built-in \`toJSON\` on the record "${this.constructor.modelName}:${this.id}". The built-in \`toJSON\` method on instances of classes extending \`Model\` is deprecated. For more information see the link below.`, - false, - { - id: 'ember-data:model.toJSON', - until: '4.0', - url: 'https://deprecations.emberjs.com/ember-data/v3.x#toc_record-toJSON', - for: '@ember-data/model', - since: { - available: '3.15', - enabled: '3.15', - }, - } - ); - let serializer = this._internalModel.store.serializerFor('-default'); - let snapshot = this._internalModel.createSnapshot(); + static modelName = null; - return serializer.serialize(snapshot, options); - }, - }); -} + /* + These class methods below provide relationship + introspection abilities about relationships. -const ID_DESCRIPTOR = { - configurable: false, - set(id) { - const normalizedId = coerceId(id); + A note about the computed properties contained here: - if (normalizedId !== null) { - this._internalModel.setId(normalizedId); - } - }, - - get() { - // the _internalModel guard exists, because some dev-only deprecation code - // (addListener via validatePropertyInjections) invokes toString before the - // object is real. - if (DEBUG) { - if (!this._internalModel) { - return; - } - } - get(this._internalModel, '_tag'); - return this._internalModel.id; - }, -}; - -Object.defineProperty(Model.prototype, 'id', ID_DESCRIPTOR); - -if (DEBUG) { - let lookupDescriptor = function lookupDescriptor(obj, keyName) { - let current = obj; - do { - let descriptor = Object.getOwnPropertyDescriptor(current, keyName); - if (descriptor !== undefined) { - return descriptor; - } - current = Object.getPrototypeOf(current); - } while (current !== null); - return null; - }; - let isBasicDesc = function isBasicDesc(desc) { - return ( - !desc || - (!desc.get && !desc.set && desc.enumerable === true && desc.writable === true && desc.configurable === true) - ); - }; - let isDefaultEmptyDescriptor = function isDefaultEmptyDescriptor(obj, keyName) { - let instanceDesc = lookupDescriptor(obj, keyName); - return isBasicDesc(instanceDesc) && lookupDescriptor(obj.constructor, keyName) === null; - }; - - let lookupDeprecations; - let _deprecatedLifecycleMethods; - - if (DEPRECATE_RECORD_LIFECYCLE_EVENT_METHODS) { - const INSTANCE_DEPRECATIONS = new WeakMap(); - _deprecatedLifecycleMethods = [ - 'becameError', - 'becameInvalid', - 'didCreate', - 'didDelete', - 'didLoad', - 'didUpdate', - 'ready', - 'rolledBack', - ]; - - lookupDeprecations = function lookupInstanceDeprecations(instance) { - let deprecations = INSTANCE_DEPRECATIONS.get(instance); - - if (!deprecations) { - deprecations = new Set(); - INSTANCE_DEPRECATIONS.set(instance, deprecations); - } - - return deprecations; - }; - } - - Model.reopen({ - init() { - this._super(...arguments); - - if (DEPRECATE_EVENTED_API_USAGE) { - this._getDeprecatedEventedInfo = () => `${this._internalModel.modelName}#${this.id}`; - } - - if (!isDefaultEmptyDescriptor(this, '_internalModel') || !(this._internalModel instanceof InternalModel)) { - throw new Error( - `'_internalModel' is a reserved property name on instances of classes extending Model. Please choose a different property name for ${this.constructor.toString()}` - ); - } - - if ( - !isDefaultEmptyDescriptor(this, 'currentState') || - this.get('currentState') !== this._internalModel.currentState - ) { - throw new Error( - `'currentState' is a reserved property name on instances of classes extending Model. Please choose a different property name for ${this.constructor.toString()}` - ); - } - - let idDesc = lookupDescriptor(this, 'id'); - - if (idDesc.get !== ID_DESCRIPTOR.get) { - throw new EmberError( - `You may not set 'id' as an attribute on your model. Please remove any lines that look like: \`id: attr('')\` from ${this.constructor.toString()}` - ); - } - - if (DEPRECATE_RECORD_LIFECYCLE_EVENT_METHODS) { - let lifecycleDeprecations = lookupDeprecations(this.constructor); - - _deprecatedLifecycleMethods.forEach(methodName => { - if (typeof this[methodName] === 'function' && !lifecycleDeprecations.has(methodName)) { - deprecate( - `You defined a \`${methodName}\` method for ${this.constructor.toString()} but lifecycle events for models have been deprecated.`, - false, - { - id: 'ember-data:record-lifecycle-event-methods', - until: '4.0', - url: 'https://deprecations.emberjs.com/ember-data/v3.x#toc_record-lifecycle-event-methods', - for: '@ember-data/model', - since: { - available: '3.12', - enabled: '3.12', - }, - } - ); - - lifecycleDeprecations.add(methodName); - } - }); - } - }, - }); -} - -Model.reopenClass({ - isModel: true, - - /** - Create should only ever be called by the store. To create an instance of a - `Model` in a dirty state use `store.createRecord`. - - To create instances of `Model` in a clean state, use `store.push` - - @method create - @private - @static - */ - - /** - Represents the model's class name as a string. This can be used to look up the model's class name through - `Store`'s modelFor method. - - `modelName` is generated for you by Ember Data. It will be a lowercased, dasherized string. - For example: - - ```javascript - store.modelFor('post').modelName; // 'post' - store.modelFor('blog-post').modelName; // 'blog-post' - ``` - - The most common place you'll want to access `modelName` is in your serializer's `payloadKeyFromModelName` method. For example, to change payload - keys to underscore (instead of dasherized), you might use the following code: - - ```javascript - import RESTSerializer from '@ember-data/serializer/rest'; - import { underscore } from '@ember/string'; - - export default const PostSerializer = RESTSerializer.extend({ - payloadKeyFromModelName(modelName) { - return underscore(modelName); - } - }); - ``` - @property modelName - @type String - @readonly - @static - */ - modelName: null, - - /* - These class methods below provide relationship - introspection abilities about relationships. - - A note about the computed properties contained here: - - **These properties are effectively sealed once called for the first time.** - To avoid repeatedly doing expensive iteration over a model's fields, these - values are computed once and then cached for the remainder of the runtime of - your application. + **These properties are effectively sealed once called for the first time.** + To avoid repeatedly doing expensive iteration over a model's fields, these + values are computed once and then cached for the remainder of the runtime of + your application. If your application needs to modify a class after its initial definition (for example, using `reopen()` to add additional attributes), make sure you @@ -1553,14 +1382,15 @@ Model.reopenClass({ @param {store} store an instance of Store @return {Model} the type of the relationship, or undefined */ - typeForRelationship(name, store) { - let relationship = get(this, 'relationshipsByName').get(name); + static typeForRelationship(name, store) { + let relationship = this.relationshipsByName.get(name); return relationship && store.modelFor(relationship.type); - }, + } - inverseMap: computed(function() { + @computeOnce + static get inverseMap() { return Object.create(null); - }), + } /** Find the relationship which is the inverse of the one asked for. @@ -1595,8 +1425,8 @@ Model.reopenClass({ @param {Store} store @return {Object} the inverse relationship, or null */ - inverseFor(name, store) { - let inverseMap = get(this, 'inverseMap'); + static inverseFor(name, store) { + let inverseMap = this.inverseMap; if (inverseMap[name]) { return inverseMap[name]; } else { @@ -1604,10 +1434,10 @@ Model.reopenClass({ inverseMap[name] = inverse; return inverse; } - }, + } //Calculate the inverse, ignoring the cache - _findInverseFor(name, store) { + static _findInverseFor(name, store) { let inverseType = this.typeForRelationship(name, store); if (!inverseType) { return null; @@ -1625,7 +1455,7 @@ Model.reopenClass({ //If inverse is specified manually, return the inverse if (options.inverse) { inverseName = options.inverse; - inverse = get(inverseType, 'relationshipsByName').get(inverseName); + inverse = inverseType.relationshipsByName.get(inverseName); assert( "We found no inverse relationships by the name of '" + @@ -1706,7 +1536,7 @@ Model.reopenClass({ kind: inverseKind, options: inverseOptions, }; - }, + } /** The model's relationships as a map, keyed on the type of the @@ -1735,7 +1565,7 @@ Model.reopenClass({ import User from 'app/models/user'; import Post from 'app/models/post'; - let relationships = get(Blog, 'relationships'); + let relationships = Blog.relationships; relationships.get('user'); //=> [ { name: 'users', kind: 'hasMany' }, // { name: 'owner', kind: 'belongsTo' } ] @@ -1749,7 +1579,24 @@ Model.reopenClass({ @readOnly */ - relationships: relationshipsDescriptor, + @computeOnce + static get relationships() { + let map = new Map(); + let relationshipsByName = this.relationshipsByName; + + // Loop through each computed property on the class + relationshipsByName.forEach(desc => { + let { type } = desc; + + if (!map.has(type)) { + map.set(type, []); + } + + map.get(type).push(desc); + }); + + return map; + } /** A hash containing lists of the model's relationships, grouped @@ -1773,7 +1620,7 @@ Model.reopenClass({ import { get } from '@ember/object'; import Blog from 'app/models/blog'; - let relationshipNames = get(Blog, 'relationshipNames'); + let relationshipNames = Blog.relationshipNames; relationshipNames.hasMany; //=> ['users', 'posts'] relationshipNames.belongsTo; @@ -1785,7 +1632,8 @@ Model.reopenClass({ @type Object @readOnly */ - relationshipNames: computed(function() { + @computeOnce + static get relationshipNames() { let names = { hasMany: [], belongsTo: [], @@ -1798,7 +1646,7 @@ Model.reopenClass({ }); return names; - }), + } /** An array of types directly related to a model. Each type will be @@ -1824,7 +1672,7 @@ Model.reopenClass({ import { get } from '@ember/object'; import Blog from 'app/models/blog'; - let relatedTypes = get(Blog, 'relatedTypes'); + let relatedTypes = Blog.relatedTypes'); //=> [ User, Post ] ``` @@ -1833,7 +1681,27 @@ Model.reopenClass({ @type Ember.Array @readOnly */ - relatedTypes: relatedTypesDescriptor, + @computeOnce + static get relatedTypes() { + let types = []; + + let rels = this.relationshipsObject; + let relationships = Object.keys(rels); + + // create an array of the unique types involved + // in relationships + for (let i = 0; i < relationships.length; i++) { + let name = relationships[i]; + let meta = rels[name]; + let modelName = meta.type; + + if (types.indexOf(modelName) === -1) { + types.push(modelName); + } + } + + return types; + } /** A map whose keys are the relationships of a model and whose values are @@ -1859,7 +1727,7 @@ Model.reopenClass({ import { get } from '@ember/object'; import Blog from 'app/models/blog'; - let relationshipsByName = get(Blog, 'relationshipsByName'); + let relationshipsByName = Blog.relationshipsByName; relationshipsByName.get('users'); //=> { key: 'users', kind: 'hasMany', type: 'user', options: Object, isRelationship: true } relationshipsByName.get('owner'); @@ -1871,9 +1739,36 @@ Model.reopenClass({ @type Map @readOnly */ - relationshipsByName: relationshipsByNameDescriptor, + @computeOnce + static get relationshipsByName() { + let map = new Map(); + let rels = this.relationshipsObject; + let relationships = Object.keys(rels); + + for (let i = 0; i < relationships.length; i++) { + let key = relationships[i]; + let value = rels[key]; - relationshipsObject: relationshipsObjectDescriptor, + map.set(value.key, value); + } + + return map; + } + + @computeOnce + static get relationshipsObject() { + let relationships = Object.create(null); + let modelName = this.modelName; + this.eachComputedProperty((name, meta) => { + if (meta.isRelationship) { + meta.key = name; + meta.name = name; + meta.parentModelName = modelName; + relationships[name] = relationshipFromMeta(meta); + } + }); + return relationships; + } /** A map whose keys are the fields of the model and whose values are strings @@ -1899,7 +1794,7 @@ Model.reopenClass({ import { get } from '@ember/object'; import Blog from 'app/models/blog' - let fields = get(Blog, 'fields'); + let fields = Blog.fields; fields.forEach(function(kind, field) { console.log(field, kind); }); @@ -1916,7 +1811,8 @@ Model.reopenClass({ @type Map @readOnly */ - fields: computed(function() { + @computeOnce + static get fields() { let map = new Map(); this.eachComputedProperty((name, meta) => { @@ -1928,7 +1824,7 @@ Model.reopenClass({ }); return map; - }).readOnly(), + } /** Given a callback, iterates over each of the relationships in the model, @@ -1940,11 +1836,11 @@ Model.reopenClass({ @param {Function} callback the callback to invoke @param {any} binding the value to which the callback's `this` should be bound */ - eachRelationship(callback, binding) { - get(this, 'relationshipsByName').forEach((relationship, name) => { + static eachRelationship(callback, binding) { + this.relationshipsByName.forEach((relationship, name) => { callback.call(binding, name, relationship); }); - }, + } /** Given a callback, iterates over each of the types related to a model, @@ -1957,16 +1853,16 @@ Model.reopenClass({ @param {Function} callback the callback to invoke @param {any} binding the value to which the callback's `this` should be bound */ - eachRelatedType(callback, binding) { - let relationshipTypes = get(this, 'relatedTypes'); + static eachRelatedType(callback, binding) { + let relationshipTypes = this.relatedTypes; for (let i = 0; i < relationshipTypes.length; i++) { let type = relationshipTypes[i]; callback.call(binding, type); } - }, + } - determineRelationshipType(knownSide, store) { + static determineRelationshipType(knownSide, store) { let knownKey = knownSide.key; let knownKind = knownSide.kind; let inverse = this.inverseFor(knownKey, store); @@ -1985,7 +1881,7 @@ Model.reopenClass({ } else { return knownKind === 'belongsTo' ? 'oneToMany' : 'manyToMany'; } - }, + } /** A map whose keys are the attributes of the model (properties @@ -2008,7 +1904,7 @@ Model.reopenClass({ import { get } from '@ember/object'; import Blog from 'app/models/blog' - let attributes = get(Person, 'attributes') + let attributes = Person.attributes attributes.forEach(function(meta, name) { console.log(name, meta); @@ -2025,7 +1921,8 @@ Model.reopenClass({ @type {Map} @readOnly */ - attributes: computed(function() { + @computeOnce + static get attributes() { let map = new Map(); this.eachComputedProperty((name, meta) => { @@ -2042,7 +1939,7 @@ Model.reopenClass({ }); return map; - }).readOnly(), + } /** A map whose keys are the attributes of the model (properties @@ -2066,7 +1963,7 @@ Model.reopenClass({ import { get } from '@ember/object'; import Person from 'app/models/person'; - let transformedAttributes = get(Person, 'transformedAttributes') + let transformedAttributes = Person.transformedAttributes transformedAttributes.forEach(function(field, type) { console.log(field, type); @@ -2082,7 +1979,8 @@ Model.reopenClass({ @type {Map} @readOnly */ - transformedAttributes: computed(function() { + @computeOnce + static get transformedAttributes() { let map = new Map(); this.eachAttribute((key, meta) => { @@ -2092,7 +1990,7 @@ Model.reopenClass({ }); return map; - }).readOnly(), + } /** Iterates through the attributes of the model, calling the passed function on each @@ -2137,11 +2035,11 @@ Model.reopenClass({ @param {Object} [binding] the value to which the callback's `this` should be bound @static */ - eachAttribute(callback, binding) { - get(this, 'attributes').forEach((meta, name) => { + static eachAttribute(callback, binding) { + this.attributes.forEach((meta, name) => { callback.call(binding, name, meta); }); - }, + } /** Iterates through the transformedAttributes of the model, calling @@ -2187,11 +2085,11 @@ Model.reopenClass({ @param {Object} [binding] the value to which the callback's `this` should be bound @static */ - eachTransformedAttribute(callback, binding) { - get(this, 'transformedAttributes').forEach((type, name) => { + static eachTransformedAttribute(callback, binding) { + this.transformedAttributes.forEach((type, name) => { callback.call(binding, name, type); }); - }, + } /** Returns the name of the model class. @@ -2199,9 +2097,217 @@ Model.reopenClass({ @method toString @static */ - toString() { + static toString() { return `model:${get(this, 'modelName')}`; + } +} +Model.prototype._internalModel = null; +Model.prototype.currentState = null; +Model.prototype.store = null; + +const ID_DESCRIPTOR = { + configurable: false, + set(id) { + const normalizedId = coerceId(id); + + if (normalizedId !== null) { + this._internalModel.setId(normalizedId); + } }, -}); + + get() { + // the _internalModel guard exists, because some dev-only deprecation code + // (addListener via validatePropertyInjections) invokes toString before the + // object is real. + if (DEBUG) { + if (!this._internalModel) { + return; + } + } + get(this._internalModel, '_tag'); + return this._internalModel.id; + }, +}; + +Object.defineProperty(Model.prototype, 'id', ID_DESCRIPTOR); + +if (DEPRECATE_EVENTED_API_USAGE) { + /** + Override the default event firing from Ember.Evented to + also call methods with the given name. + + @method trigger + @private + @param {String} name +*/ + Model.reopen(DeprecatedEvented, { + trigger(name) { + if (DEPRECATE_RECORD_LIFECYCLE_EVENT_METHODS) { + let fn = this[name]; + if (typeof fn === 'function') { + let length = arguments.length; + let args = new Array(length - 1); + + for (let i = 1; i < length; i++) { + args[i - 1] = arguments[i]; + } + fn.apply(this, args); + } + } + + const _hasEvent = DEBUG ? this._has(name) : this.has(name); + if (_hasEvent) { + this._super(...arguments); + } + }, + }); +} + +if (DEPRECATE_MODEL_TOJSON) { + /** + Use [JSONSerializer](JSONSerializer.html) to + get the JSON representation of a record. + + `toJSON` takes an optional hash as a parameter, currently + supported options are: + + - `includeId`: `true` if the record's ID should be included in the + JSON representation. + + @method toJSON + @param {Object} options + @return {Object} A JSON representation of the object. + */ + Model.reopen({ + toJSON(options) { + // container is for lazy transform lookups + deprecate( + `Called the built-in \`toJSON\` on the record "${this.constructor.modelName}:${this.id}". The built-in \`toJSON\` method on instances of classes extending \`Model\` is deprecated. For more information see the link below.`, + false, + { + id: 'ember-data:model.toJSON', + until: '4.0', + url: 'https://deprecations.emberjs.com/ember-data/v3.x#toc_record-toJSON', + for: '@ember-data/model', + since: { + available: '3.15', + enabled: '3.15', + }, + } + ); + let serializer = this._internalModel.store.serializerFor('-default'); + let snapshot = this._internalModel.createSnapshot(); + + return serializer.serialize(snapshot, options); + }, + }); +} + +if (DEBUG) { + let lookupDescriptor = function lookupDescriptor(obj, keyName) { + let current = obj; + do { + let descriptor = Object.getOwnPropertyDescriptor(current, keyName); + if (descriptor !== undefined) { + return descriptor; + } + current = Object.getPrototypeOf(current); + } while (current !== null); + return null; + }; + let isBasicDesc = function isBasicDesc(desc) { + return ( + !desc || + (!desc.get && !desc.set && desc.enumerable === true && desc.writable === true && desc.configurable === true) + ); + }; + let isDefaultEmptyDescriptor = function isDefaultEmptyDescriptor(obj, keyName) { + let instanceDesc = lookupDescriptor(obj, keyName); + return isBasicDesc(instanceDesc) && lookupDescriptor(obj.constructor, keyName) === null; + }; + + let lookupDeprecations; + let _deprecatedLifecycleMethods; + + if (DEPRECATE_RECORD_LIFECYCLE_EVENT_METHODS) { + const INSTANCE_DEPRECATIONS = new WeakMap(); + _deprecatedLifecycleMethods = [ + 'becameError', + 'becameInvalid', + 'didCreate', + 'didDelete', + 'didLoad', + 'didUpdate', + 'ready', + 'rolledBack', + ]; + + lookupDeprecations = function lookupInstanceDeprecations(instance) { + let deprecations = INSTANCE_DEPRECATIONS.get(instance); + + if (!deprecations) { + deprecations = new Set(); + INSTANCE_DEPRECATIONS.set(instance, deprecations); + } + + return deprecations; + }; + } + + Model.reopen({ + init() { + this._super(...arguments); + + if (DEPRECATE_EVENTED_API_USAGE) { + this._getDeprecatedEventedInfo = () => `${this._internalModel.modelName}#${this.id}`; + } + + if (!isDefaultEmptyDescriptor(this, '_internalModel') || !(this._internalModel instanceof InternalModel)) { + throw new Error( + `'_internalModel' is a reserved property name on instances of classes extending Model. Please choose a different property name for ${this.constructor.toString()}` + ); + } + + if (!isDefaultEmptyDescriptor(this, 'currentState') || this.currentState !== this._internalModel.currentState) { + throw new Error( + `'currentState' is a reserved property name on instances of classes extending Model. Please choose a different property name for ${this.constructor.toString()}` + ); + } + + let idDesc = lookupDescriptor(this, 'id'); + + if (idDesc.get !== ID_DESCRIPTOR.get) { + throw new EmberError( + `You may not set 'id' as an attribute on your model. Please remove any lines that look like: \`id: attr('')\` from ${this.constructor.toString()}` + ); + } + + if (DEPRECATE_RECORD_LIFECYCLE_EVENT_METHODS) { + let lifecycleDeprecations = lookupDeprecations(this.constructor); + + _deprecatedLifecycleMethods.forEach(methodName => { + if (typeof this[methodName] === 'function' && !lifecycleDeprecations.has(methodName)) { + deprecate( + `You defined a \`${methodName}\` method for ${this.constructor.toString()} but lifecycle events for models have been deprecated.`, + false, + { + id: 'ember-data:record-lifecycle-event-methods', + until: '4.0', + url: 'https://deprecations.emberjs.com/ember-data/v3.x#toc_record-lifecycle-event-methods', + for: '@ember-data/model', + since: { + available: '3.12', + enabled: '3.12', + }, + } + ); + + lifecycleDeprecations.add(methodName); + } + }); + } + }, + }); +} export default Model; diff --git a/packages/model/addon/-private/system/relationships/ext.js b/packages/model/addon/-private/system/relationships/ext.js deleted file mode 100644 index 89eb519cf5f..00000000000 --- a/packages/model/addon/-private/system/relationships/ext.js +++ /dev/null @@ -1,79 +0,0 @@ -import { A } from '@ember/array'; -import { assert } from '@ember/debug'; -import { computed, get } from '@ember/object'; - -import { relationshipFromMeta, typeForRelationshipMeta } from './relationship-meta'; -/** - @module @ember-data/model -*/ - -export const relationshipsDescriptor = computed(function() { - let map = new Map(); - let relationshipsByName = get(this, 'relationshipsByName'); - - // Loop through each computed property on the class - relationshipsByName.forEach(desc => { - let { type } = desc; - - if (!map.has(type)) { - map.set(type, []); - } - - map.get(type).push(desc); - }); - - return map; -}).readOnly(); - -export const relatedTypesDescriptor = computed(function() { - let parentModelName = this.modelName; - let types = A(); - - // Loop through each computed property on the class, - // and create an array of the unique types involved - // in relationships - this.eachComputedProperty((name, meta) => { - if (meta.isRelationship) { - meta.key = name; - let modelName = typeForRelationshipMeta(meta); - - assert(`You specified a hasMany (${meta.type}) on ${parentModelName} but ${meta.type} was not found.`, modelName); - - if (!types.includes(modelName)) { - assert(`Trying to sideload ${name} on ${this.toString()} but the type doesn't exist.`, !!modelName); - types.push(modelName); - } - } - }); - - return types; -}).readOnly(); - -export const relationshipsObjectDescriptor = computed(function() { - let relationships = Object.create(null); - let modelName = this.modelName; - this.eachComputedProperty((name, meta) => { - if (meta.isRelationship) { - meta.key = name; - meta.name = name; - meta.parentModelName = modelName; - relationships[name] = relationshipFromMeta(meta); - } - }); - return relationships; -}); - -export const relationshipsByNameDescriptor = computed(function() { - let map = new Map(); - let rels = get(this, 'relationshipsObject'); - let relationships = Object.keys(rels); - - for (let i = 0; i < relationships.length; i++) { - let key = relationships[i]; - let value = rels[key]; - - map.set(value.key, value); - } - - return map; -}).readOnly(); diff --git a/packages/model/addon/-private/system/relationships/relationship-meta.ts b/packages/model/addon/-private/system/relationships/relationship-meta.ts index f41c94c13ed..50569ee098a 100644 --- a/packages/model/addon/-private/system/relationships/relationship-meta.ts +++ b/packages/model/addon/-private/system/relationships/relationship-meta.ts @@ -11,11 +11,8 @@ type CoreStore = import('@ember-data/store/-private/system/core-store').default; @module @ember-data/store */ -export function typeForRelationshipMeta(meta) { - let modelName; - - modelName = meta.type || meta.key; - modelName = normalizeModelName(modelName); +function typeForRelationshipMeta(meta) { + let modelName = normalizeModelName(meta.type); if (meta.kind === 'hasMany') { modelName = singularize(modelName); diff --git a/packages/model/index.js b/packages/model/index.js index 6c17d597ac9..e9fedafea6b 100644 --- a/packages/model/index.js +++ b/packages/model/index.js @@ -10,19 +10,22 @@ module.exports = Object.assign({}, addonBaseConfig, { shouldRollupPrivate: true, externalDependenciesForPrivateModule() { return [ + '@ember-data/canary-features', + '@ember-data/store', + '@ember-data/store/-private', + '@ember/application', + '@ember/array', + '@ember/array/mutable', + '@ember/array/proxy', '@ember/debug', '@ember/error', - '@ember/utils', '@ember/object', '@ember/object/computed', - '@ember/array', - '@ember/array/proxy', - '@ember/array/mutable', '@ember/polyfills', - '@ember-data/canary-features', - '@ember-data/store', - '@ember-data/store/-private', + '@ember/utils', + + '@glimmer/tracking', 'ember-inflector', 'ember', 'rsvp', diff --git a/packages/store/addon/-private/system/model/internal-model.ts b/packages/store/addon/-private/system/model/internal-model.ts index bf273995def..8a3b7ab8802 100644 --- a/packages/store/addon/-private/system/model/internal-model.ts +++ b/packages/store/addon/-private/system/model/internal-model.ts @@ -6,6 +6,7 @@ import { get, set } from '@ember/object'; import { assign } from '@ember/polyfills'; import { _backburner as emberBackburner, cancel } from '@ember/runloop'; import { DEBUG } from '@glimmer/env'; +import { tracked } from '@glimmer/tracking'; import RSVP, { Promise } from 'rsvp'; @@ -147,6 +148,7 @@ export default class InternalModel { declare isReloading: boolean; declare _doNotDestroy: boolean; declare isDestroying: boolean; + declare _isUpdatingId: boolean; // Not typed yet declare _promiseProxy: any; @@ -165,15 +167,15 @@ export default class InternalModel { declare _retainedManyArrayCache: ConfidentDict; declare _relationshipPromisesCache: ConfidentDict>; declare _relationshipProxyCache: ConfidentDict; - declare currentState: any; declare error: any; constructor(public store: CoreStore | Store, public identifier: StableRecordIdentifier) { if (HAS_MODEL_PACKAGE) { _getModelPackage(); } - this._tag = 0; this._id = identifier.id; + this._tag = 0; + this._isUpdatingId = false; this.modelName = identifier.type; this.clientId = identifier.lid; @@ -194,6 +196,10 @@ export default class InternalModel { this._isDematerializing = false; this._scheduledDestroy = null; + this._record = null; + this.isReloading = false; + this.error = null; + // caches for lazy getters this._modelClass = null; this.__recordArrays = null; @@ -202,7 +208,6 @@ export default class InternalModel { this.isReloading = false; this.error = null; - this.currentState = RootState.empty; // other caches // class fields have [[DEFINE]] semantics which are significantly slower than [[SET]] semantics here @@ -214,6 +219,8 @@ export default class InternalModel { this._deferredTriggers = []; } + @tracked currentState: any = RootState.empty; + get id(): string | null { return this.identifier.id; } @@ -1251,14 +1258,18 @@ export default class InternalModel { return { type: internalModel.modelName, id: internalModel.id }; } - setId(id: string) { + setId(id: string, fromCache: boolean = false) { + if (this._isUpdatingId === true) { + return; + } + this._isUpdatingId = true; let didChange = id !== this._id; - this._id = id; - set(this, '_tag', this._tag + 1); if (didChange && id !== null) { - this.store.setRecordId(this.modelName, id, this.clientId); + if (!fromCache) { + this.store.setRecordId(this.modelName, id, this.clientId); + } // internal set of ID to get it to RecordData from DS.Model // if we are within create we may not have a recordData yet. if (this.__recordData && this._recordData.__setId) { @@ -1270,9 +1281,11 @@ export default class InternalModel { if (CUSTOM_MODEL_CLASS) { this.store._notificationManager.notify(this.identifier, 'identity'); } else { + set(this, '_tag', this._tag + 1); this.notifyPropertyChange('id'); } } + this._isUpdatingId = false; } didError(error) { diff --git a/packages/store/addon/-private/system/store/internal-model-factory.ts b/packages/store/addon/-private/system/store/internal-model-factory.ts index 4b1bd4a48f6..31e41e0a7c1 100644 --- a/packages/store/addon/-private/system/store/internal-model-factory.ts +++ b/packages/store/addon/-private/system/store/internal-model-factory.ts @@ -235,7 +235,7 @@ export default class InternalModelFactory { this.identifierCache.updateRecordIdentifier(identifier, { type, id }); } - internalModel.setId(id); + internalModel.setId(id, true); } peekById(type: string, id: string): InternalModel | null { diff --git a/packages/store/index.js b/packages/store/index.js index 06fb579fb76..22677323e1b 100644 --- a/packages/store/index.js +++ b/packages/store/index.js @@ -10,30 +10,34 @@ module.exports = Object.assign({}, addonBaseConfig, { shouldRollupPrivate: true, externalDependenciesForPrivateModule() { return [ + '@ember-data/canary-features', + '@ember-data/store/-debug', + '@ember/application', + '@ember/array/proxy', + '@ember/array', '@ember/debug', '@ember/error', - '@ember/utils', - '@ember/polyfills', - '@ember/service', - '@ember/runloop', '@ember/object', - '@ember/object/promise-proxy-mixin', '@ember/object/computed', '@ember/object/evented', - '@ember/object/proxy', - '@ember/object/mixin', '@ember/object/internals', - '@ember/array', - '@ember/array/proxy', + '@ember/object/mixin', + '@ember/object/promise-proxy-mixin', + '@ember/object/proxy', + '@ember/polyfills', + '@ember/runloop', + '@ember/service', + '@ember/string', '@ember/test', - '@ember-data/canary-features', + '@ember/utils', + 'ember-inflector', - '@ember-data/store/-debug', 'ember', - 'require', - '@ember/string', 'rsvp', + 'require', + + '@glimmer/tracking', ]; }, }); diff --git a/packages/store/package.json b/packages/store/package.json index a53c20b07eb..9f704a309e5 100644 --- a/packages/store/package.json +++ b/packages/store/package.json @@ -19,6 +19,7 @@ "dependencies": { "@ember-data/canary-features": "3.28.0-alpha.0", "@ember-data/private-build-infra": "3.28.0-alpha.0", + "@glimmer/tracking": "^1.0.4", "@ember/string": "^1.0.0", "ember-cli-babel": "^7.26.3", "ember-cli-path-utils": "^1.0.0", diff --git a/yarn.lock b/yarn.lock index 0fea4e360c0..34582dec9fd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1163,6 +1163,14 @@ "@handlebars/parser" "^1.1.0" simple-html-tokenizer "^0.5.10" +"@glimmer/tracking@^1.0.4": + version "1.0.4" + resolved "https://registry.npmjs.org/@glimmer/tracking/-/tracking-1.0.4.tgz#f1bc1412fe5e2236d0f8d502994a8f88af1bbb21" + integrity sha512-F+oT8I55ba2puSGIzInmVrv/8QA2PcK1VD+GWgFMhF6WC97D+uZX7BFg+a3s/2N4FVBq5KHE+QxZzgazM151Yw== + dependencies: + "@glimmer/env" "^0.1.7" + "@glimmer/validator" "^0.44.0" + "@glimmer/util@0.65.2": version "0.65.2" resolved "https://registry.npmjs.org/@glimmer/util/-/util-0.65.2.tgz#da9c6fa68a117ac1cb74fc79dad3eaa40d9cd4cb" @@ -1190,6 +1198,11 @@ "@glimmer/env" "^0.1.7" "@glimmer/global-context" "0.65.2" +"@glimmer/validator@^0.44.0": + version "0.44.0" + resolved "https://registry.npmjs.org/@glimmer/validator/-/validator-0.44.0.tgz#03d127097dc9cb23052cdb7fcae59d0a9dca53e1" + integrity sha512-i01plR0EgFVz69GDrEuFgq1NheIjZcyTy3c7q+w7d096ddPVeVcRzU3LKaqCfovvLJ+6lJx40j45ecycASUUyw== + "@glimmer/vm-babel-plugins@0.77.5": version "0.77.5" resolved "https://registry.npmjs.org/@glimmer/vm-babel-plugins/-/vm-babel-plugins-0.77.5.tgz#daffb6507aa6b08ec36f69d652897d339fdd0007" From 6fd81efde06fc8f71f37489566fcf191593e4e80 Mon Sep 17 00:00:00 2001 From: Chris Thoburn Date: Tue, 13 Apr 2021 12:08:25 -0700 Subject: [PATCH 02/13] fix notification when feature flag is on --- packages/store/addon/-private/system/model/internal-model.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/store/addon/-private/system/model/internal-model.ts b/packages/store/addon/-private/system/model/internal-model.ts index 8a3b7ab8802..cbd9b3afca3 100644 --- a/packages/store/addon/-private/system/model/internal-model.ts +++ b/packages/store/addon/-private/system/model/internal-model.ts @@ -1278,10 +1278,10 @@ export default class InternalModel { } if (didChange && this.hasRecord) { + set(this, '_tag', this._tag + 1); if (CUSTOM_MODEL_CLASS) { this.store._notificationManager.notify(this.identifier, 'identity'); } else { - set(this, '_tag', this._tag + 1); this.notifyPropertyChange('id'); } } From 32bf2f70738b6373167f307422cf37b06e155438 Mon Sep 17 00:00:00 2001 From: Chris Thoburn Date: Wed, 21 Apr 2021 04:04:52 -0700 Subject: [PATCH 03/13] remove skipped tests --- .../-ember-data/tests/unit/model/attr-test.js | 47 +++++-------------- 1 file changed, 12 insertions(+), 35 deletions(-) diff --git a/packages/-ember-data/tests/unit/model/attr-test.js b/packages/-ember-data/tests/unit/model/attr-test.js index e7726107b82..c7e0b201c13 100644 --- a/packages/-ember-data/tests/unit/model/attr-test.js +++ b/packages/-ember-data/tests/unit/model/attr-test.js @@ -1,4 +1,4 @@ -import { module, skip, test } from 'qunit'; +import { module, test } from 'qunit'; import { setupTest } from 'ember-qunit'; @@ -94,7 +94,8 @@ module('unit/model/attr | attr syntax', function(hooks) { ); }); - skip('attr cannot be used with native syntax prop style', async function(assert) { + test('attr cannot be used with native syntax prop style', async function(assert) { + // TODO it would be nice if this syntax error'd but it currently doesn't class User extends Model { name = attr(); nameWithTransform = attr('string'); @@ -106,10 +107,10 @@ module('unit/model/attr | attr syntax', function(hooks) { let UserModel = store.modelFor('user'); let attrs = UserModel.attributes; - assert.true(attrs.has('name'), 'We have the attr: name'); - assert.true(attrs.has('nameWithTransform'), 'We have the attr: nameWithTransform'); - assert.true(attrs.has('nameWithOptions'), 'We have the attr: nameWithOptions'); - assert.true(attrs.has('nameWithTransformAndOptions'), 'We have the attr: nameWithTransformAndOptions'); + assert.false(attrs.has('name'), 'We have the attr: name'); + assert.false(attrs.has('nameWithTransform'), 'We have the attr: nameWithTransform'); + assert.false(attrs.has('nameWithOptions'), 'We have the attr: nameWithOptions'); + assert.false(attrs.has('nameWithTransformAndOptions'), 'We have the attr: nameWithTransformAndOptions'); let userRecord = store.push({ data: { @@ -124,17 +125,17 @@ module('unit/model/attr | attr syntax', function(hooks) { }, }); - assert.strictEqual(userRecord.name, 'Chris', 'attr is correctly set: name'); - assert.strictEqual(userRecord.nameWithTransform, '@runspired', 'attr is correctly set: nameWithTransform'); - assert.strictEqual(userRecord.nameWithOptions, 'Contributor', 'attr is correctly set: nameWithOptions'); - assert.strictEqual( + assert.notStrictEqual(userRecord.name, 'Chris', 'attr is correctly set: name'); + assert.notStrictEqual(userRecord.nameWithTransform, '@runspired', 'attr is correctly set: nameWithTransform'); + assert.notStrictEqual(userRecord.nameWithOptions, 'Contributor', 'attr is correctly set: nameWithOptions'); + assert.notStrictEqual( userRecord.nameWithTransformAndOptions, '@runspired contribution', 'attr is correctly set: nameWithTransformAndOptions' ); }); - skip('attr can be used with native syntax decorator style without parens', async function(assert) { + test('attr can be used with native syntax decorator style without parens', async function(assert) { class User extends Model { @attr name; } @@ -157,28 +158,4 @@ module('unit/model/attr | attr syntax', function(hooks) { assert.strictEqual(userRecord.name, 'Chris', 'attr is correctly set: name'); }); - - skip('attr can not be used classic syntax without parens', async function(assert) { - const User = Model.extend({ - name: attr, - }); - - owner.register('model:user', User); - - let UserModel = store.modelFor('user'); - let attrs = UserModel.attributes; - assert.true(attrs.has('name'), 'We have the attr: name'); - - let userRecord = store.push({ - data: { - type: 'user', - id: '1', - attributes: { - name: 'Chris', - }, - }, - }); - - assert.strictEqual(userRecord.name, 'Chris', 'attr is correctly set: name'); - }); }); From 4700c601e87afcaf15dbfc3f00e8a60f02db72d1 Mon Sep 17 00:00:00 2001 From: Chris Thoburn Date: Wed, 21 Apr 2021 05:15:58 -0700 Subject: [PATCH 04/13] polish --- packages/model/addon/-private/belongs-to.js | 26 ++++++++++------- packages/model/addon/-private/has-many.js | 12 ++++---- packages/model/addon/-private/model.js | 29 ++++++++++--------- .../system/relationships/relationship-meta.ts | 2 +- .../-private/system/model/internal-model.ts | 28 +++++++++++++++--- 5 files changed, 62 insertions(+), 35 deletions(-) diff --git a/packages/model/addon/-private/belongs-to.js b/packages/model/addon/-private/belongs-to.js index 8ece3938018..66b6f37c6f7 100644 --- a/packages/model/addon/-private/belongs-to.js +++ b/packages/model/addon/-private/belongs-to.js @@ -2,8 +2,6 @@ import { assert, inspect, warn } from '@ember/debug'; import { computed } from '@ember/object'; import { DEBUG } from '@glimmer/env'; -import { normalizeModelName } from '@ember-data/store'; - import { computedMacroWithOptionalParams } from './util'; /** @@ -112,20 +110,28 @@ import { computedMacroWithOptionalParams } from './util'; @return {Ember.computed} relationship */ function belongsTo(modelName, options) { + let opts, userEnteredModelName; + if (typeof modelName === 'object') { + opts = modelName; + userEnteredModelName = undefined; + } else { + opts = options; + userEnteredModelName = modelName; + } + assert( 'The first argument to belongsTo must be a string representing a model type key, not an instance of ' + - inspect(modelName) + + inspect(userEnteredModelName) + ". E.g., to define a relation to the Person model, use belongsTo('person')", - typeof modelName !== 'string' + typeof userEnteredModelName === 'string' || typeof userEnteredModelName === 'undefined' ); - modelName = normalizeModelName(modelName); - options = options || {}; + opts = opts || {}; let meta = { - type: modelName, + type: userEnteredModelName, isRelationship: true, - options, + options: opts, kind: 'belongsTo', name: 'Belongs To', key: null, @@ -139,7 +145,7 @@ function belongsTo(modelName, options) { `'${key}' is a reserved property name on instances of classes extending Model. Please choose a different property name for your belongsTo on ${this.constructor.toString()}` ); } - if (Object.prototype.hasOwnProperty.call(options, 'serialize')) { + if (Object.prototype.hasOwnProperty.call(opts, 'serialize')) { warn( `You provided a serialize option on the "${key}" property in the "${this._internalModel.modelName}" class, this belongs in the serializer. See Serializer and it's implementations https://api.emberjs.com/ember-data/release/classes/Serializer`, false, @@ -149,7 +155,7 @@ function belongsTo(modelName, options) { ); } - if (Object.prototype.hasOwnProperty.call(options, 'embedded')) { + if (Object.prototype.hasOwnProperty.call(opts, 'embedded')) { warn( `You provided an embedded option on the "${key}" property in the "${this._internalModel.modelName}" class, this belongs in the serializer. See EmbeddedRecordsMixin https://api.emberjs.com/ember-data/release/classes/EmbeddedRecordsMixin`, false, diff --git a/packages/model/addon/-private/has-many.js b/packages/model/addon/-private/has-many.js index 8f263521811..9c61011d1ce 100644 --- a/packages/model/addon/-private/has-many.js +++ b/packages/model/addon/-private/has-many.js @@ -5,10 +5,6 @@ import { assert, inspect } from '@ember/debug'; import { computed } from '@ember/object'; import { DEBUG } from '@glimmer/env'; -import { singularize } from 'ember-inflector'; - -import { normalizeModelName } from '@ember-data/store'; - import { computedMacroWithOptionalParams } from './util'; /** @@ -154,15 +150,19 @@ import { computedMacroWithOptionalParams } from './util'; @return {Ember.computed} relationship */ function hasMany(type, options) { + if (typeof type === 'object') { + options = type; + type = undefined; + } + assert( `The first argument to hasMany must be a string representing a model type key, not an instance of ${inspect( type )}. E.g., to define a relation to the Comment model, use hasMany('comment')`, - typeof type !== 'string' + typeof type === 'string' || typeof type === 'undefined' ); options = options || {}; - type = singularize(normalizeModelName(type)); // Metadata about relationships is stored on the meta of // the relationship. This is used for introspection and diff --git a/packages/model/addon/-private/model.js b/packages/model/addon/-private/model.js index 5591964331f..ded56dc6528 100644 --- a/packages/model/addon/-private/model.js +++ b/packages/model/addon/-private/model.js @@ -84,6 +84,11 @@ function findPossibleInverses(type, inverseType, name, relationshipsSoFar) { return possibleRelationships; } +/** + * This decorator allows us to lazily compute + * an expensive getter on first-access and therafter + * never recompute it. + */ function computeOnce(target, key, desc) { const cache = new WeakMap(); let getter = desc.get; @@ -112,16 +117,8 @@ function computeOnce(target, key, desc) { @uses EmberData.DeprecatedEvented */ class Model extends EmberObject { - init() { - super.init(...arguments); - - if (DEBUG) { - if (!this._internalModel) { - throw new EmberError( - 'You should not call `create` on a model. Instead, call `store.createRecord` with the attributes you would like to set.' - ); - } - } + constructor(args) { + super(args); if (RECORD_DATA_ERRORS) { this._invalidRequests = []; @@ -2101,9 +2098,6 @@ class Model extends EmberObject { return `model:${get(this, 'modelName')}`; } } -Model.prototype._internalModel = null; -Model.prototype.currentState = null; -Model.prototype.store = null; const ID_DESCRIPTOR = { configurable: false, @@ -2124,7 +2118,8 @@ const ID_DESCRIPTOR = { return; } } - get(this._internalModel, '_tag'); + // consume the tracked tag + this._internalModel._tag; return this._internalModel.id; }, }; @@ -2258,6 +2253,12 @@ if (DEBUG) { init() { this._super(...arguments); + if (!this._internalModel) { + throw new EmberError( + 'You should not call `create` on a model. Instead, call `store.createRecord` with the attributes you would like to set.' + ); + } + if (DEPRECATE_EVENTED_API_USAGE) { this._getDeprecatedEventedInfo = () => `${this._internalModel.modelName}#${this.id}`; } diff --git a/packages/model/addon/-private/system/relationships/relationship-meta.ts b/packages/model/addon/-private/system/relationships/relationship-meta.ts index 50569ee098a..cbe38260e1b 100644 --- a/packages/model/addon/-private/system/relationships/relationship-meta.ts +++ b/packages/model/addon/-private/system/relationships/relationship-meta.ts @@ -12,7 +12,7 @@ type CoreStore = import('@ember-data/store/-private/system/core-store').default; */ function typeForRelationshipMeta(meta) { - let modelName = normalizeModelName(meta.type); + let modelName = normalizeModelName(meta.type || meta.key); if (meta.kind === 'hasMany') { modelName = singularize(modelName); diff --git a/packages/store/addon/-private/system/model/internal-model.ts b/packages/store/addon/-private/system/model/internal-model.ts index cbd9b3afca3..78d8f11d85b 100644 --- a/packages/store/addon/-private/system/model/internal-model.ts +++ b/packages/store/addon/-private/system/model/internal-model.ts @@ -137,7 +137,6 @@ function extractPivotName(name) { */ export default class InternalModel { declare _id: string | null; - declare _tag: number; declare modelName: string; declare clientId: string; declare __recordData: RecordData | null; @@ -174,7 +173,6 @@ export default class InternalModel { _getModelPackage(); } this._id = identifier.id; - this._tag = 0; this._isUpdatingId = false; this.modelName = identifier.type; this.clientId = identifier.lid; @@ -220,6 +218,13 @@ export default class InternalModel { } @tracked currentState: any = RootState.empty; + /* + A tag which when dirtied allows things tracking a record's ID + to recompute. When we update this we must also flushSyncObservers + for pre-4.0 compat so we still call notifyPropertyChange('id') + on the record + */ + @tracked _tag: string = ''; get id(): string | null { return this.identifier.id; @@ -229,7 +234,7 @@ export default class InternalModel { if (value !== this._id) { let newIdentifier = { type: this.identifier.type, lid: this.identifier.lid, id: value }; identifierCacheFor(this.store).updateRecordIdentifier(this.identifier, newIdentifier); - set(this, '_tag', this._tag + 1); + this._tag = ''; // dirty tag // TODO Show deprecation for private api } } @@ -1258,6 +1263,21 @@ export default class InternalModel { return { type: internalModel.modelName, id: internalModel.id }; } + /** + * calling `store.setRecordId` is necessary to update + * the cache index for this record if we have changed. + * + * However, since the store is not aware of whether the update + * is from us (via user set) or from a push of new data + * it will also call us so that we can notify and update state. + * + * When it does so it calls with `fromCache` so that we can + * short-circuit instead of cycling back. + * + * This differs from the short-circuit in the `_isUpdatingId` + * case in that the the cache can originate the call to setId, + * so on first entry we will still need to do our own update. + */ setId(id: string, fromCache: boolean = false) { if (this._isUpdatingId === true) { return; @@ -1278,7 +1298,7 @@ export default class InternalModel { } if (didChange && this.hasRecord) { - set(this, '_tag', this._tag + 1); + this._tag = ''; // dirty tag if (CUSTOM_MODEL_CLASS) { this.store._notificationManager.notify(this.identifier, 'identity'); } else { From 2a47423c015108c55b96d38c26d0350eba331b22 Mon Sep 17 00:00:00 2001 From: Chris Thoburn Date: Wed, 21 Apr 2021 05:22:52 -0700 Subject: [PATCH 05/13] fix --- packages/model/addon/-private/model.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/model/addon/-private/model.js b/packages/model/addon/-private/model.js index ded56dc6528..78bbf45bc38 100644 --- a/packages/model/addon/-private/model.js +++ b/packages/model/addon/-private/model.js @@ -2099,6 +2099,12 @@ class Model extends EmberObject { } } +// this is required to prevent `init` from passing +// the values initialized during create to `setUnknownProperty` +Model.prototype._internalModel = null; +Model.prototype.currentState = null; +Model.prototype.store = null; + const ID_DESCRIPTOR = { configurable: false, set(id) { From 6bcf40a13893ee12250acf3c74d7d128dcfa07b8 Mon Sep 17 00:00:00 2001 From: Chris Thoburn Date: Wed, 21 Apr 2021 09:10:59 -0700 Subject: [PATCH 06/13] continue to use init --- packages/model/addon/-private/model.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/model/addon/-private/model.js b/packages/model/addon/-private/model.js index 78bbf45bc38..1fd137a3594 100644 --- a/packages/model/addon/-private/model.js +++ b/packages/model/addon/-private/model.js @@ -117,8 +117,8 @@ function computeOnce(target, key, desc) { @uses EmberData.DeprecatedEvented */ class Model extends EmberObject { - constructor(args) { - super(args); + init(...args) { + super.init(...args); if (RECORD_DATA_ERRORS) { this._invalidRequests = []; From 7c95440e9ad632b5d0ea8fb4bc0ac19852b47369 Mon Sep 17 00:00:00 2001 From: Chris Thoburn Date: Wed, 21 Apr 2021 09:26:04 -0700 Subject: [PATCH 07/13] assertion must be in primary init --- packages/model/addon/-private/model.js | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/packages/model/addon/-private/model.js b/packages/model/addon/-private/model.js index 1fd137a3594..4a1d8311496 100644 --- a/packages/model/addon/-private/model.js +++ b/packages/model/addon/-private/model.js @@ -120,6 +120,14 @@ class Model extends EmberObject { init(...args) { super.init(...args); + if (DEBUG) { + if (!this._internalModel) { + throw new EmberError( + 'You should not call `create` on a model. Instead, call `store.createRecord` with the attributes you would like to set.' + ); + } + } + if (RECORD_DATA_ERRORS) { this._invalidRequests = []; } @@ -2259,12 +2267,6 @@ if (DEBUG) { init() { this._super(...arguments); - if (!this._internalModel) { - throw new EmberError( - 'You should not call `create` on a model. Instead, call `store.createRecord` with the attributes you would like to set.' - ); - } - if (DEPRECATE_EVENTED_API_USAGE) { this._getDeprecatedEventedInfo = () => `${this._internalModel.modelName}#${this.id}`; } From 7c9bf3b7fae21fc7f3fb155a2238781d2e872bd1 Mon Sep 17 00:00:00 2001 From: Chris Thoburn Date: Wed, 21 Apr 2021 09:35:01 -0700 Subject: [PATCH 08/13] fix asset-size --show --- .github/workflows/asset-size-check.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/asset-size-check.yml b/.github/workflows/asset-size-check.yml index 55bd43363a1..f79e82a24be 100644 --- a/.github/workflows/asset-size-check.yml +++ b/.github/workflows/asset-size-check.yml @@ -67,15 +67,15 @@ jobs: - name: Analyze ${{github.ref}} Assets (IE11) run: | node ./bin/asset-size-tracking/generate-analysis.js packages/-ember-data/dists/experiment-ie11 ./experiment-ie11-data.json - node ./bin/asset-size-tracking/print-analysis.js ./experiment-ie11-data.json > tmp/asset-sizes/experiment-analysis-ie11.txt + node ./bin/asset-size-tracking/print-analysis.js ./experiment-ie11-data.json -show > tmp/asset-sizes/experiment-analysis-ie11.txt - name: Analyze ${{github.ref}} Assets run: | node ./bin/asset-size-tracking/generate-analysis.js packages/-ember-data/dists/experiment ./experiment-data.json - node ./bin/asset-size-tracking/print-analysis.js ./experiment-data.json > tmp/asset-sizes/experiment-analysis.txt + node ./bin/asset-size-tracking/print-analysis.js ./experiment-data.json -show > tmp/asset-sizes/experiment-analysis.txt - name: Analyze ${{github.ref}} Assets run: | node ./bin/asset-size-tracking/generate-analysis.js packages/-ember-data/dists/experiment-no-rollup ./experiment-data-no-rollup.json - node ./bin/asset-size-tracking/print-analysis.js ./experiment-data-no-rollup.json > tmp/asset-sizes/experiment-analysis-no-rollup.txt + node ./bin/asset-size-tracking/print-analysis.js ./experiment-data-no-rollup.json -show > tmp/asset-sizes/experiment-analysis-no-rollup.txt - name: Test Asset Sizes (IE11) run: | set -o pipefail From 1a6d58d5a29a7ad08b742d143af275a5326e3af5 Mon Sep 17 00:00:00 2001 From: Chris Thoburn Date: Wed, 21 Apr 2021 12:50:54 -0700 Subject: [PATCH 09/13] use dependentKeyCompat (fix modelFragments) --- packages/model/addon/-private/model.js | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/packages/model/addon/-private/model.js b/packages/model/addon/-private/model.js index 4a1d8311496..6b0d7c0e311 100644 --- a/packages/model/addon/-private/model.js +++ b/packages/model/addon/-private/model.js @@ -1,6 +1,7 @@ import { assert, deprecate, warn } from '@ember/debug'; import EmberError from '@ember/error'; import EmberObject, { computed, get } from '@ember/object'; +import { dependentKeyCompat } from '@ember/object/compat'; import { isNone } from '@ember/utils'; import { DEBUG } from '@glimmer/env'; import Ember from 'ember'; @@ -175,6 +176,7 @@ class Model extends EmberObject { @type {Boolean} @readOnly */ + @dependentKeyCompat get isEmpty() { return this._internalModel.currentState.isEmpty; } @@ -189,6 +191,7 @@ class Model extends EmberObject { @type {Boolean} @readOnly */ + @dependentKeyCompat get isLoading() { return this._internalModel.currentState.isLoading; } @@ -213,7 +216,7 @@ class Model extends EmberObject { @type {Boolean} @readOnly */ - + @dependentKeyCompat get isLoaded() { return this._internalModel.currentState.isLoaded; } @@ -242,6 +245,7 @@ class Model extends EmberObject { @type {Boolean} @readOnly */ + @dependentKeyCompat get hasDirtyAttributes() { return this._internalModel.currentState.isDirty; } @@ -268,6 +272,7 @@ class Model extends EmberObject { @type {Boolean} @readOnly */ + @dependentKeyCompat get isSaving() { return this._internalModel.currentState.isSaving; } @@ -309,6 +314,7 @@ class Model extends EmberObject { @type {Boolean} @readOnly */ + @dependentKeyCompat get isDeleted() { if (RECORD_DATA_STATE) { // currently we call notifyPropertyChange from @@ -345,6 +351,7 @@ class Model extends EmberObject { @type {Boolean} @readOnly */ + @dependentKeyCompat get isNew() { if (RECORD_DATA_STATE) { // currently we call notifyPropertyChange from @@ -370,6 +377,7 @@ class Model extends EmberObject { @type {Boolean} @readOnly */ + @dependentKeyCompat get isValid() { if (RECORD_DATA_ERRORS) { return !(this.errors.length > 0); @@ -405,6 +413,7 @@ class Model extends EmberObject { @type {String} @readOnly */ + @dependentKeyCompat get dirtyType() { return this._internalModel.currentState.dirtyType; } @@ -2137,6 +2146,7 @@ const ID_DESCRIPTOR = { return this._internalModel.id; }, }; +dependentKeyCompat(ID_DESCRIPTOR); Object.defineProperty(Model.prototype, 'id', ID_DESCRIPTOR); From c81f581ae3e6eb7b20019e80f76135e4e1edabec Mon Sep 17 00:00:00 2001 From: Chris Thoburn Date: Wed, 21 Apr 2021 12:55:42 -0700 Subject: [PATCH 10/13] strip _debugInfo if not shipping the debug adapter --- packages/model/addon/-private/model.js | 127 +++++++++++++------------ 1 file changed, 65 insertions(+), 62 deletions(-) diff --git a/packages/model/addon/-private/model.js b/packages/model/addon/-private/model.js index 6b0d7c0e311..2a6b337f949 100644 --- a/packages/model/addon/-private/model.js +++ b/packages/model/addon/-private/model.js @@ -7,6 +7,7 @@ import { DEBUG } from '@glimmer/env'; import Ember from 'ember'; import { RECORD_DATA_ERRORS, RECORD_DATA_STATE, REQUEST_SERVICE } from '@ember-data/canary-features'; +import { HAS_DEBUG_PACKAGE } from '@ember-data/private-build-infra'; import { DEPRECATE_EVENTED_API_USAGE, DEPRECATE_MODEL_TOJSON, @@ -1173,68 +1174,6 @@ class Model extends EmberObject { return this._internalModel.referenceFor('hasMany', name); } - /** - Provides info about the model for debugging purposes - by grouping the properties into more semantic groups. - - Meant to be used by debugging tools such as the Chrome Ember Extension. - - - Groups all attributes in "Attributes" group. - - Groups all belongsTo relationships in "Belongs To" group. - - Groups all hasMany relationships in "Has Many" group. - - Groups all flags in "Flags" group. - - Flags relationship CPs as expensive properties. - - @method _debugInfo - @for Model - @private - */ - _debugInfo() { - let attributes = ['id']; - let relationships = {}; - let expensiveProperties = []; - - this.eachAttribute((name, meta) => attributes.push(name)); - - let groups = [ - { - name: 'Attributes', - properties: attributes, - expand: true, - }, - ]; - - this.eachRelationship((name, relationship) => { - let properties = relationships[relationship.kind]; - - if (properties === undefined) { - properties = relationships[relationship.kind] = []; - groups.push({ - name: relationship.kind, - properties, - expand: true, - }); - } - properties.push(name); - expensiveProperties.push(name); - }); - - groups.push({ - name: 'Flags', - properties: ['isLoaded', 'hasDirtyAttributes', 'isSaving', 'isDeleted', 'isError', 'isNew', 'isValid'], - }); - - return { - propertyInfo: { - // include all other mixins / properties (not just the grouped ones) - includeOtherProperties: true, - groups: groups, - // don't pre-calculate unless cached - expensiveProperties: expensiveProperties, - }, - }; - } - notifyBelongsToChange(key) { this.notifyPropertyChange(key); } @@ -2150,6 +2089,70 @@ dependentKeyCompat(ID_DESCRIPTOR); Object.defineProperty(Model.prototype, 'id', ID_DESCRIPTOR); +if (HAS_DEBUG_PACKAGE) { + /** + Provides info about the model for debugging purposes + by grouping the properties into more semantic groups. + + Meant to be used by debugging tools such as the Chrome Ember Extension. + + - Groups all attributes in "Attributes" group. + - Groups all belongsTo relationships in "Belongs To" group. + - Groups all hasMany relationships in "Has Many" group. + - Groups all flags in "Flags" group. + - Flags relationship CPs as expensive properties. + + @method _debugInfo + @for Model + @private + */ + Model.prototype._debugInfo = function() { + let attributes = ['id']; + let relationships = {}; + let expensiveProperties = []; + + this.eachAttribute((name, meta) => attributes.push(name)); + + let groups = [ + { + name: 'Attributes', + properties: attributes, + expand: true, + }, + ]; + + this.eachRelationship((name, relationship) => { + let properties = relationships[relationship.kind]; + + if (properties === undefined) { + properties = relationships[relationship.kind] = []; + groups.push({ + name: relationship.kind, + properties, + expand: true, + }); + } + properties.push(name); + expensiveProperties.push(name); + }); + + groups.push({ + name: 'Flags', + properties: ['isLoaded', 'hasDirtyAttributes', 'isSaving', 'isDeleted', 'isError', 'isNew', 'isValid'], + }); + + return { + propertyInfo: { + // include all other mixins / properties (not just the grouped ones) + includeOtherProperties: true, + groups: groups, + // don't pre-calculate unless cached + expensiveProperties: expensiveProperties, + }, + }; + }; +} + if (DEPRECATE_EVENTED_API_USAGE) { /** Override the default event firing from Ember.Evented to From 04be50cdf1918f47ef59789961e07046cdeacd09 Mon Sep 17 00:00:00 2001 From: Chris Thoburn Date: Wed, 21 Apr 2021 15:05:12 -0700 Subject: [PATCH 11/13] fix beta/canary builds --- packages/model/index.js | 1 + .../store/addon/-private/system/model/internal-model.ts | 9 --------- 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/packages/model/index.js b/packages/model/index.js index e9fedafea6b..bf6284fabac 100644 --- a/packages/model/index.js +++ b/packages/model/index.js @@ -21,6 +21,7 @@ module.exports = Object.assign({}, addonBaseConfig, { '@ember/debug', '@ember/error', '@ember/object', + '@ember/object/compat', '@ember/object/computed', '@ember/polyfills', '@ember/utils', diff --git a/packages/store/addon/-private/system/model/internal-model.ts b/packages/store/addon/-private/system/model/internal-model.ts index 78d8f11d85b..57072d0602e 100644 --- a/packages/store/addon/-private/system/model/internal-model.ts +++ b/packages/store/addon/-private/system/model/internal-model.ts @@ -230,15 +230,6 @@ export default class InternalModel { return this.identifier.id; } - set id(value: string | null) { - if (value !== this._id) { - let newIdentifier = { type: this.identifier.type, lid: this.identifier.lid, id: value }; - identifierCacheFor(this.store).updateRecordIdentifier(this.identifier, newIdentifier); - this._tag = ''; // dirty tag - // TODO Show deprecation for private api - } - } - get modelClass() { if (this.store.modelFor) { return this._modelClass || (this._modelClass = this.store.modelFor(this.modelName)); From 492254a884f1148d05bd56a12a0122c9c1731497 Mon Sep 17 00:00:00 2001 From: Chris Thoburn Date: Wed, 21 Apr 2021 15:45:55 -0700 Subject: [PATCH 12/13] fix ember-m3 --- .../store/addon/-private/system/model/internal-model.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/packages/store/addon/-private/system/model/internal-model.ts b/packages/store/addon/-private/system/model/internal-model.ts index 57072d0602e..5fcc21e8416 100644 --- a/packages/store/addon/-private/system/model/internal-model.ts +++ b/packages/store/addon/-private/system/model/internal-model.ts @@ -229,6 +229,14 @@ export default class InternalModel { get id(): string | null { return this.identifier.id; } + set id(value: string | null) { + if (value !== this._id) { + let newIdentifier = { type: this.identifier.type, lid: this.identifier.lid, id: value }; + identifierCacheFor(this.store).updateRecordIdentifier(this.identifier, newIdentifier); + this._tag = ''; // dirty tag + // TODO Show deprecation for private api, this is currently used by ember-m3 + } + } get modelClass() { if (this.store.modelFor) { From d80193858e7773fbec6d0a3cbbd6180ba79746db Mon Sep 17 00:00:00 2001 From: Chris Thoburn Date: Wed, 21 Apr 2021 16:23:32 -0700 Subject: [PATCH 13/13] make id great again --- packages/model/addon/-private/model.js | 50 ++++++++----------- packages/model/package.json | 2 +- .../-private/system/model/internal-model.ts | 13 ++++- 3 files changed, 34 insertions(+), 31 deletions(-) diff --git a/packages/model/addon/-private/model.js b/packages/model/addon/-private/model.js index 2a6b337f949..074e164ef3c 100644 --- a/packages/model/addon/-private/model.js +++ b/packages/model/addon/-private/model.js @@ -519,6 +519,27 @@ class Model extends EmberObject { @property id @type {String} */ + @dependentKeyCompat + get id() { + // the _internalModel guard exists, because some dev-only deprecation code + // (addListener via validatePropertyInjections) invokes toString before the + // object is real. + if (DEBUG) { + if (!this._internalModel) { + return void 0; + } + } + // consume the tracked tag + this._internalModel._tag; + return this._internalModel.id; + } + set id(id) { + const normalizedId = coerceId(id); + + if (normalizedId !== null) { + this._internalModel.setId(normalizedId); + } + } /** @property currentState @@ -2061,34 +2082,6 @@ Model.prototype._internalModel = null; Model.prototype.currentState = null; Model.prototype.store = null; -const ID_DESCRIPTOR = { - configurable: false, - set(id) { - const normalizedId = coerceId(id); - - if (normalizedId !== null) { - this._internalModel.setId(normalizedId); - } - }, - - get() { - // the _internalModel guard exists, because some dev-only deprecation code - // (addListener via validatePropertyInjections) invokes toString before the - // object is real. - if (DEBUG) { - if (!this._internalModel) { - return; - } - } - // consume the tracked tag - this._internalModel._tag; - return this._internalModel.id; - }, -}; -dependentKeyCompat(ID_DESCRIPTOR); - -Object.defineProperty(Model.prototype, 'id', ID_DESCRIPTOR); - if (HAS_DEBUG_PACKAGE) { /** Provides info about the model for debugging purposes @@ -2296,6 +2289,7 @@ if (DEBUG) { ); } + const ID_DESCRIPTOR = lookupDescriptor(Model.prototype, 'id'); let idDesc = lookupDescriptor(this, 'id'); if (idDesc.get !== ID_DESCRIPTOR.get) { diff --git a/packages/model/package.json b/packages/model/package.json index e78922984e4..52185177fc0 100644 --- a/packages/model/package.json +++ b/packages/model/package.json @@ -63,4 +63,4 @@ "node": "12.16.2", "yarn": "1.22.4" } -} +} \ No newline at end of file diff --git a/packages/store/addon/-private/system/model/internal-model.ts b/packages/store/addon/-private/system/model/internal-model.ts index 5fcc21e8416..855ae719777 100644 --- a/packages/store/addon/-private/system/model/internal-model.ts +++ b/packages/store/addon/-private/system/model/internal-model.ts @@ -2,7 +2,7 @@ import { getOwner, setOwner } from '@ember/application'; import { A, default as EmberArray } from '@ember/array'; import { assert, inspect } from '@ember/debug'; import EmberError from '@ember/error'; -import { get, set } from '@ember/object'; +import { get, notifyPropertyChange, set } from '@ember/object'; import { assign } from '@ember/polyfills'; import { _backburner as emberBackburner, cancel } from '@ember/runloop'; import { DEBUG } from '@glimmer/env'; @@ -63,6 +63,11 @@ function relationshipStateFor(instance: InternalModel, propertyName: string) { return relationshipsFor(instance).get(propertyName); } +const STABLE_UNTRACKED_OBJ = {}; +function flushSyncObservers() { + notifyPropertyChange(STABLE_UNTRACKED_OBJ, '-tracking-prop'); +} + const { hasOwnProperty } = Object.prototype; let ManyArray: ManyArray; @@ -234,6 +239,7 @@ export default class InternalModel { let newIdentifier = { type: this.identifier.type, lid: this.identifier.lid, id: value }; identifierCacheFor(this.store).updateRecordIdentifier(this.identifier, newIdentifier); this._tag = ''; // dirty tag + flushSyncObservers(); // TODO Show deprecation for private api, this is currently used by ember-m3 } } @@ -486,6 +492,7 @@ export default class InternalModel { this.isReloading = true; if (this.hasRecord) { set(this._record, 'isReloading', true); + flushSyncObservers(); } } @@ -493,6 +500,7 @@ export default class InternalModel { this.isReloading = false; if (this.hasRecord) { set(this._record, 'isReloading', false); + flushSyncObservers(); } } @@ -1147,6 +1155,7 @@ export default class InternalModel { this.currentState = state; if (this.hasRecord) { set(this._record, 'currentState', state); + flushSyncObservers(); } for (i = 0, l = setups.length; i < l; i++) { @@ -1301,7 +1310,7 @@ export default class InternalModel { if (CUSTOM_MODEL_CLASS) { this.store._notificationManager.notify(this.identifier, 'identity'); } else { - this.notifyPropertyChange('id'); + flushSyncObservers(); } } this._isUpdatingId = false;