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 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..c7e0b201c13 --- /dev/null +++ b/packages/-ember-data/tests/unit/model/attr-test.js @@ -0,0 +1,161 @@ +import { module, 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' + ); + }); + + 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'); + nameWithOptions = attr({}); + nameWithTransformAndOptions = attr('string', {}); + } + + owner.register('model:user', User); + + let UserModel = store.modelFor('user'); + let attrs = UserModel.attributes; + 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: { + type: 'user', + id: '1', + attributes: { + name: 'Chris', + nameWithTransform: '@runspired', + nameWithOptions: 'Contributor', + nameWithTransformAndOptions: '@runspired contribution', + }, + }, + }); + + 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' + ); + }); + + test('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'); + }); +}); 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..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'; /** @@ -121,10 +119,6 @@ function belongsTo(modelName, 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) + diff --git a/packages/model/addon/-private/has-many.js b/packages/model/addon/-private/has-many.js index 9d05e740d3b..9c61011d1ce 100644 --- a/packages/model/addon/-private/has-many.js +++ b/packages/model/addon/-private/has-many.js @@ -5,8 +5,6 @@ import { assert, inspect } from '@ember/debug'; import { computed } from '@ember/object'; import { DEBUG } from '@glimmer/env'; -import { normalizeModelName } from '@ember-data/store'; - import { computedMacroWithOptionalParams } from './util'; /** @@ -166,10 +164,6 @@ function hasMany(type, options) { options = options || {}; - if (typeof type === 'string') { - type = normalizeModelName(type); - } - // Metadata about relationships is stored on the meta of // the relationship. This is used for introspection and // serialization. Note that `key` is populated lazily diff --git a/packages/model/addon/-private/model.js b/packages/model/addon/-private/model.js index c7ee9fd6a25..074e164ef3c 100644 --- a/packages/model/addon/-private/model.js +++ b/packages/model/addon/-private/model.js @@ -1,11 +1,13 @@ 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'; 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, @@ -19,16 +21,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 +56,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 +86,30 @@ 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; -} - -let adapterError; -if (REQUEST_SERVICE) { - adapterError = computed(function() { - let request = this._lastError; - if (!request) { - return null; +/** + * 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; + desc.get = function() { + let meta = cache.get(this); + + 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 +118,9 @@ if (REQUEST_SERVICE) { @extends EmberObject @uses EmberData.DeprecatedEvented */ -const Model = EmberObject.extend(DeprecatedEvented, { - init() { - this._super(...arguments); +class Model extends EmberObject { + init(...args) { + super.init(...args); if (DEBUG) { if (!this._internalModel) { @@ -195,15 +154,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 +177,11 @@ const Model = EmberObject.extend(DeprecatedEvented, { @type {Boolean} @readOnly */ - isEmpty: retrieveFromCurrentState, + @dependentKeyCompat + 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 +192,10 @@ const Model = EmberObject.extend(DeprecatedEvented, { @type {Boolean} @readOnly */ - isLoading: retrieveFromCurrentState, + @dependentKeyCompat + 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 +217,11 @@ const Model = EmberObject.extend(DeprecatedEvented, { @type {Boolean} @readOnly */ - isLoaded: retrieveFromCurrentState, + @dependentKeyCompat + 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 +246,11 @@ const Model = EmberObject.extend(DeprecatedEvented, { @type {Boolean} @readOnly */ - hasDirtyAttributes: computed('currentState.isDirty', function() { - return this.get('currentState.isDirty'); - }), + @dependentKeyCompat + 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 +273,11 @@ const Model = EmberObject.extend(DeprecatedEvented, { @type {Boolean} @readOnly */ - isSaving: retrieveFromCurrentState, + @dependentKeyCompat + 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 +315,22 @@ const Model = EmberObject.extend(DeprecatedEvented, { @type {Boolean} @readOnly */ - isDeleted: isDeletedCP, + @dependentKeyCompat + 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 +352,22 @@ const Model = EmberObject.extend(DeprecatedEvented, { @type {Boolean} @readOnly */ - isNew: isNewCP, + @dependentKeyCompat + 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 +378,21 @@ const Model = EmberObject.extend(DeprecatedEvented, { @type {Boolean} @readOnly */ - isValid: isValid, + @dependentKeyCompat + 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 +414,10 @@ const Model = EmberObject.extend(DeprecatedEvented, { @type {String} @readOnly */ - dirtyType: retrieveFromCurrentState, + @dependentKeyCompat + get dirtyType() { + return this._internalModel.currentState.dirtyType; + } /** If `true` the adapter reported that it was unable to save local @@ -422,13 +438,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 +480,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 @@ -466,32 +519,43 @@ const Model = EmberObject.extend(DeprecatedEvented, { @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 @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 +608,8 @@ const Model = EmberObject.extend(DeprecatedEvented, { @property errors @type {Errors} */ - errors: computed(function() { + @computeOnce + get errors() { let errors = Errors.create(); errors._registerHandlers( @@ -571,7 +636,7 @@ const Model = EmberObject.extend(DeprecatedEvented, { } } return errors; - }).readOnly(), + } invalidErrorsChanged(jsonApiErrors) { if (RECORD_DATA_ERRORS) { @@ -584,7 +649,7 @@ const Model = EmberObject.extend(DeprecatedEvented, { errors._add(errorKeys[i], newErrors[errorKeys[i]]); } } - }, + } /** This property holds the `AdapterError` object with which @@ -593,7 +658,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 +694,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 +702,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 +754,7 @@ const Model = EmberObject.extend(DeprecatedEvented, { */ send(name, context) { return this._internalModel.send(name, context); - }, + } /** @method transitionTo @@ -688,7 +763,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 +799,7 @@ const Model = EmberObject.extend(DeprecatedEvented, { */ deleteRecord() { this._internalModel.deleteRecord(); - }, + } /** Same as `deleteRecord`, but saves the record immediately. @@ -773,7 +848,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 +861,7 @@ const Model = EmberObject.extend(DeprecatedEvented, { return; } this._internalModel.unloadRecord(); - }, + } /** @method _notifyProperties @@ -803,7 +878,7 @@ const Model = EmberObject.extend(DeprecatedEvented, { this.notifyPropertyChange(key); } }); - }, + } /** Returns an object, whose keys are changed properties, and value is @@ -852,7 +927,7 @@ const Model = EmberObject.extend(DeprecatedEvented, { */ changedAttributes() { return this._internalModel.changedAttributes(); - }, + } /** If the model `hasDirtyAttributes` this function will discard any unsaved @@ -879,7 +954,7 @@ const Model = EmberObject.extend(DeprecatedEvented, { if (REQUEST_SERVICE) { this._markErrorRequestAsClean(); } - }, + } /* @method _createSnapshot @@ -887,14 +962,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 +1015,7 @@ const Model = EmberObject.extend(DeprecatedEvented, { return PromiseObject.create({ promise: this._internalModel.save(options).then(() => this), }); - }, + } /** Reload the record from the adapter. @@ -982,14 +1057,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 +1131,7 @@ const Model = EmberObject.extend(DeprecatedEvented, { */ belongsTo(name) { return this._internalModel.referenceFor('belongsTo', name); - }, + } /** Get the reference for the specified hasMany relationship. @@ -1118,73 +1193,11 @@ const Model = EmberObject.extend(DeprecatedEvented, { */ hasMany(name) { 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); - }, + } /** 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,261 +1252,51 @@ 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. - - @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); - }, - }); -} - -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 (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' - ``` + ```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: @@ -1513,7 +1316,7 @@ Model.reopenClass({ @readonly @static */ - modelName: null, + static modelName = null; /* These class methods below provide relationship @@ -1553,14 +1356,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 +1399,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 +1408,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 +1429,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 +1510,7 @@ Model.reopenClass({ kind: inverseKind, options: inverseOptions, }; - }, + } /** The model's relationships as a map, keyed on the type of the @@ -1735,7 +1539,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 +1553,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 +1594,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 +1606,8 @@ Model.reopenClass({ @type Object @readOnly */ - relationshipNames: computed(function() { + @computeOnce + static get relationshipNames() { let names = { hasMany: [], belongsTo: [], @@ -1798,7 +1620,7 @@ Model.reopenClass({ }); return names; - }), + } /** An array of types directly related to a model. Each type will be @@ -1824,7 +1646,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 +1655,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 +1701,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 +1713,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]; + + map.set(value.key, value); + } - relationshipsObject: relationshipsObjectDescriptor, + 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 +1768,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 +1785,8 @@ Model.reopenClass({ @type Map @readOnly */ - fields: computed(function() { + @computeOnce + static get fields() { let map = new Map(); this.eachComputedProperty((name, meta) => { @@ -1928,7 +1798,7 @@ Model.reopenClass({ }); return map; - }).readOnly(), + } /** Given a callback, iterates over each of the relationships in the model, @@ -1940,11 +1810,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 +1827,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 +1855,7 @@ Model.reopenClass({ } else { return knownKind === 'belongsTo' ? 'oneToMany' : 'manyToMany'; } - }, + } /** A map whose keys are the attributes of the model (properties @@ -2008,7 +1878,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 +1895,8 @@ Model.reopenClass({ @type {Map} @readOnly */ - attributes: computed(function() { + @computeOnce + static get attributes() { let map = new Map(); this.eachComputedProperty((name, meta) => { @@ -2042,7 +1913,7 @@ Model.reopenClass({ }); return map; - }).readOnly(), + } /** A map whose keys are the attributes of the model (properties @@ -2066,7 +1937,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 +1953,8 @@ Model.reopenClass({ @type {Map} @readOnly */ - transformedAttributes: computed(function() { + @computeOnce + static get transformedAttributes() { let map = new Map(); this.eachAttribute((key, meta) => { @@ -2092,7 +1964,7 @@ Model.reopenClass({ }); return map; - }).readOnly(), + } /** Iterates through the attributes of the model, calling the passed function on each @@ -2137,11 +2009,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 +2059,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 +2071,259 @@ Model.reopenClass({ @method toString @static */ - toString() { + static toString() { return `model:${get(this, 'modelName')}`; - }, -}); + } +} + +// 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; + +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 + 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()}` + ); + } + + const ID_DESCRIPTOR = lookupDescriptor(Model.prototype, 'id'); + 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..cbe38260e1b 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 || meta.key); if (meta.kind === 'hasMany') { modelName = singularize(modelName); diff --git a/packages/model/index.js b/packages/model/index.js index 6c17d597ac9..bf6284fabac 100644 --- a/packages/model/index.js +++ b/packages/model/index.js @@ -10,19 +10,23 @@ 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/compat', '@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/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 bf273995def..855ae719777 100644 --- a/packages/store/addon/-private/system/model/internal-model.ts +++ b/packages/store/addon/-private/system/model/internal-model.ts @@ -2,10 +2,11 @@ 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'; +import { tracked } from '@glimmer/tracking'; import RSVP, { Promise } from 'rsvp'; @@ -62,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; @@ -136,7 +142,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; @@ -147,6 +152,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 +171,14 @@ 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._isUpdatingId = false; this.modelName = identifier.type; this.clientId = identifier.lid; @@ -194,6 +199,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 +211,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,16 +222,25 @@ export default class InternalModel { this._deferredTriggers = []; } + @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; } - 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); - set(this, '_tag', this._tag + 1); - // TODO Show deprecation for private api + this._tag = ''; // dirty tag + flushSyncObservers(); + // TODO Show deprecation for private api, this is currently used by ember-m3 } } @@ -475,6 +492,7 @@ export default class InternalModel { this.isReloading = true; if (this.hasRecord) { set(this._record, 'isReloading', true); + flushSyncObservers(); } } @@ -482,6 +500,7 @@ export default class InternalModel { this.isReloading = false; if (this.hasRecord) { set(this._record, 'isReloading', false); + flushSyncObservers(); } } @@ -1136,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++) { @@ -1251,14 +1271,33 @@ export default class InternalModel { return { type: internalModel.modelName, id: internalModel.id }; } - setId(id: string) { + /** + * 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; + } + 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) { @@ -1267,12 +1306,14 @@ export default class InternalModel { } if (didChange && this.hasRecord) { + this._tag = ''; // dirty tag if (CUSTOM_MODEL_CLASS) { this.store._notificationManager.notify(this.identifier, 'identity'); } else { - this.notifyPropertyChange('id'); + flushSyncObservers(); } } + 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"