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 {