From 95cc6f592c0deeeb501868e24aad430722d1b080 Mon Sep 17 00:00:00 2001 From: Chris Thoburn Date: Sat, 23 Jul 2022 18:26:11 -0700 Subject: [PATCH 01/16] eliminate a few store methods --- .../tests/integration/records/load-test.js | 2 +- .../tests/integration/snapshot-test.js | 6 +++--- .../tests/unit/store/adapter-interop-test.js | 12 ++++++------ .../addon/-private/system/promise-belongs-to.ts | 4 ++-- .../store/addon/-private/system/core-store.ts | 15 --------------- 5 files changed, 12 insertions(+), 27 deletions(-) diff --git a/packages/-ember-data/tests/integration/records/load-test.js b/packages/-ember-data/tests/integration/records/load-test.js index 7b5441f0472..e0de0e567ed 100644 --- a/packages/-ember-data/tests/integration/records/load-test.js +++ b/packages/-ember-data/tests/integration/records/load-test.js @@ -143,7 +143,7 @@ module('integration/load - Loading Records', function (hooks) { }) ); - let internalModel = store._internalModelForId('person', '1'); + let internalModel = store._internalModelForResource({ type: 'person', id: '1' }); // test that our initial state is correct assert.true(internalModel.isEmpty, 'We begin in the empty state'); diff --git a/packages/-ember-data/tests/integration/snapshot-test.js b/packages/-ember-data/tests/integration/snapshot-test.js index 53fb32b1dff..4e4591779b0 100644 --- a/packages/-ember-data/tests/integration/snapshot-test.js +++ b/packages/-ember-data/tests/integration/snapshot-test.js @@ -127,7 +127,7 @@ module('integration/snapshot - Snapshot', function (hooks) { }, }, }); - let postInternalModel = store._internalModelForId('post', 1); + let postInternalModel = store._internalModelForResource({ type: 'post', id: '1' }); let snapshot = await postInternalModel.createSnapshot(); assert.false(postClassLoaded, 'model class is not eagerly loaded'); @@ -167,7 +167,7 @@ module('integration/snapshot - Snapshot', function (hooks) { }, }); - let postInternalModel = store._internalModelForId('post', 1); + let postInternalModel = store._internalModelForResource({ type: 'post', id: '1' }); let snapshot = postInternalModel.createSnapshot(); let expected = { author: undefined, @@ -192,7 +192,7 @@ module('integration/snapshot - Snapshot', function (hooks) { }, }); - let postInternalModel = store._internalModelForId('post', 1); + let postInternalModel = store._internalModelForResource({ type: 'post', id: '1' }); let snapshot = postInternalModel.createSnapshot(); let expected = { author: undefined, diff --git a/packages/-ember-data/tests/unit/store/adapter-interop-test.js b/packages/-ember-data/tests/unit/store/adapter-interop-test.js index aa8b999d46d..302a73fb6ad 100644 --- a/packages/-ember-data/tests/unit/store/adapter-interop-test.js +++ b/packages/-ember-data/tests/unit/store/adapter-interop-test.js @@ -747,9 +747,9 @@ module('unit/store/adapter-interop - Store working with a Adapter', function (ho store.createRecord('test'); let internalModels = [ - store._internalModelForId('test', 10), - store._internalModelForId('phone', 20), - store._internalModelForId('phone', 21), + store._internalModelForResource({ type: 'test', id: '10' }), + store._internalModelForResource({ type: 'phone', id: '20' }), + store._internalModelForResource({ type: 'phone', id: '21' }), ]; return run(() => { @@ -790,9 +790,9 @@ module('unit/store/adapter-interop - Store working with a Adapter', function (ho let store = this.owner.lookup('service:store'); let internalModels = [ - store._internalModelForId('test', 10), - store._internalModelForId('test', 20), - store._internalModelForId('test', 21), + store._internalModelForResource({ type: 'test', id: '10' }), + store._internalModelForResource({ type: 'test', id: '20' }), + store._internalModelForResource({ type: 'test', id: '21' }), ]; return run(() => { diff --git a/packages/model/addon/-private/system/promise-belongs-to.ts b/packages/model/addon/-private/system/promise-belongs-to.ts index 48d4f089910..4d944f7fd1d 100644 --- a/packages/model/addon/-private/system/promise-belongs-to.ts +++ b/packages/model/addon/-private/system/promise-belongs-to.ts @@ -63,8 +63,8 @@ class PromiseBelongsTo extends Extended { async reload(options: Dict): Promise { assert('You are trying to reload an async belongsTo before it has been created', this.content !== undefined); - let { key, store, originatingInternalModel } = this._belongsToState; - await store.reloadBelongsTo(this, originatingInternalModel, key, options); + let { key, originatingInternalModel } = this._belongsToState; + await originatingInternalModel.reloadBelongsTo(key, options); return this; } } diff --git a/packages/store/addon/-private/system/core-store.ts b/packages/store/addon/-private/system/core-store.ts index 7c4a1b7a1d3..db8be50b1ed 100644 --- a/packages/store/addon/-private/system/core-store.ts +++ b/packages/store/addon/-private/system/core-store.ts @@ -2815,25 +2815,10 @@ abstract class CoreStore extends Service { serializer.pushPayload(this, payload); } - reloadBelongsTo(belongsToProxy, internalModel, key, options) { - return internalModel.reloadBelongsTo(key, options); - } - _internalModelForResource(resource: ResourceIdentifierObject): InternalModel { return internalModelFactoryFor(this).getByResource(resource); } - /** - * TODO Only needed temporarily for test support - * - * @method _internalModelForId - * @internal - */ - _internalModelForId(type: string, id: string | null, lid: string | null): InternalModel { - const resource = constructResource(type, id, lid); - return internalModelFactoryFor(this).lookup(resource); - } - serializeRecord(record: RecordInstance, options?: Dict): unknown { let identifier = recordIdentifierFor(record); let internalModel = internalModelFactoryFor(this).peek(identifier); From 52fe67f505e6e9092cf6268aafa26f3761607d8b Mon Sep 17 00:00:00 2001 From: Chris Thoburn Date: Sat, 23 Jul 2022 18:44:18 -0700 Subject: [PATCH 02/16] cleanup some unneeded code --- .eslintrc.js | 1 - .../custom-class-model-test.ts | 22 +-- packages/store/addon/-private/index.ts | 2 +- .../store/addon/-private/system/core-store.ts | 97 +++++++++++-- .../addon/-private/system/ds-model-store.ts | 136 ------------------ .../addon/-private/system/errors-utils.js | 2 +- .../system/schema-definition-service.ts | 2 +- tsconfig.json | 1 - 8 files changed, 100 insertions(+), 163 deletions(-) delete mode 100644 packages/store/addon/-private/system/ds-model-store.ts diff --git a/.eslintrc.js b/.eslintrc.js index 4693db8440e..fb3f3663bfc 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -213,7 +213,6 @@ module.exports = { 'packages/store/addon/-private/system/internal-model-map.ts', 'packages/store/addon/-private/system/identity-map.ts', 'packages/store/addon/-private/system/fetch-manager.ts', - 'packages/store/addon/-private/system/ds-model-store.ts', 'packages/store/addon/-private/system/core-store.ts', 'packages/store/addon/-private/system/coerce-id.ts', 'packages/store/addon/-private/index.ts', diff --git a/packages/-ember-data/tests/unit/custom-class-support/custom-class-model-test.ts b/packages/-ember-data/tests/unit/custom-class-support/custom-class-model-test.ts index 102a0f1eb97..777460b9e9e 100644 --- a/packages/-ember-data/tests/unit/custom-class-support/custom-class-model-test.ts +++ b/packages/-ember-data/tests/unit/custom-class-support/custom-class-model-test.ts @@ -81,12 +81,12 @@ module('unit/model - Custom Class Model', function (hooks) { let notificationCount = 0; let identifier; let recordData; - let CreationStore = CustomStore.extend({ + class CreationStore extends CustomStore { createRecordDataFor() { let rd = this._super(...arguments); recordData = rd; return rd; - }, + } instantiateRecord( id: StableRecordIdentifier, createRecordArgs, @@ -106,8 +106,8 @@ module('unit/model - Custom Class Model', function (hooks) { } }); return { hi: 'igor' }; - }, - }); + } + } this.owner.register('service:store', CreationStore); store = this.owner.lookup('service:store') as Store; store.push({ data: { id: '1', type: 'person', attributes: { name: 'chris' } } }); @@ -123,17 +123,17 @@ module('unit/model - Custom Class Model', function (hooks) { test('record creation and teardown', function (assert) { assert.expect(5); let returnValue; - let CreationStore = CustomStore.extend({ + class CreationStore extends CustomStore { instantiateRecord(identifier, createRecordArgs, recordDataFor, notificationManager) { assert.strictEqual(identifier.type, 'person', 'Identifier type passed in correctly'); assert.deepEqual(createRecordArgs, { otherProp: 'unk' }, 'createRecordArg passed in'); returnValue = {}; return returnValue; - }, + } teardownRecord(record) { assert.strictEqual(record, person, 'Passed in person to teardown'); - }, - }); + } + } this.owner.register('service:store', CreationStore); store = this.owner.lookup('service:store') as Store; let person = store.createRecord('person', { name: 'chris', otherProp: 'unk' }); @@ -144,13 +144,13 @@ module('unit/model - Custom Class Model', function (hooks) { test('recordData lookup', function (assert) { assert.expect(1); let rd; - let CreationStore = CustomStore.extend({ + class CreationStore extends CustomStore { instantiateRecord(identifier, createRecordArgs, recordDataFor, notificationManager) { rd = recordDataFor(identifier); assert.strictEqual(rd.getAttr('name'), 'chris', 'Can look up record data from recordDataFor'); return {}; - }, - }); + } + } this.owner.register('service:store', CreationStore); store = this.owner.lookup('service:store') as Store; let schema: SchemaDefinitionService = { diff --git a/packages/store/addon/-private/index.ts b/packages/store/addon/-private/index.ts index cfdb58033c0..7f9c894d093 100644 --- a/packages/store/addon/-private/index.ts +++ b/packages/store/addon/-private/index.ts @@ -2,7 +2,7 @@ @module @ember-data/store */ -export { default as Store } from './system/ds-model-store'; +export { default as Store } from './system/core-store'; export { recordIdentifierFor } from './system/store/internal-model-factory'; diff --git a/packages/store/addon/-private/system/core-store.ts b/packages/store/addon/-private/system/core-store.ts index db8be50b1ed..b8b2692c687 100644 --- a/packages/store/addon/-private/system/core-store.ts +++ b/packages/store/addon/-private/system/core-store.ts @@ -1,9 +1,10 @@ /** @module @ember-data/store */ -import { getOwner } from '@ember/application'; +import { getOwner, setOwner } from '@ember/application'; import { A } from '@ember/array'; import { assert, inspect, warn } from '@ember/debug'; +import EmberError from '@ember/error'; import { set } from '@ember/object'; import { _backburner as emberBackburner } from '@ember/runloop'; import type { Backburner } from '@ember/runloop/-private/backburner'; @@ -16,11 +17,13 @@ import Ember from 'ember'; import { importSync } from '@embroider/macros'; import { all, default as RSVP, resolve } from 'rsvp'; +import type DSModelClass from '@ember-data/model'; import { HAS_RECORD_DATA_PACKAGE } from '@ember-data/private-build-infra'; import type { ManyRelationship, RecordData as RecordDataClass } from '@ember-data/record-data/-private'; import type { RelationshipState } from '@ember-data/record-data/-private/graph/-state'; import { IdentifierCache } from '../identifiers/cache'; +import type { DSModel } from '../ts-interfaces/ds-model'; import type { CollectionResourceDocument, EmptyResourceDocument, @@ -67,6 +70,7 @@ import NotificationManager from './record-notification-manager'; import type { BelongsToReference, HasManyReference } from './references'; import { RecordReference } from './references'; import type RequestCache from './request-cache'; +import { DSModelSchemaDefinitionService, getModelFactory } from './schema-definition-service'; import { _findAll, _findBelongsTo, _findHasMany, _query, _queryRecord } from './store/finders'; import { internalModelFactoryFor, @@ -172,7 +176,7 @@ export interface CreateRecordProperties { @extends Ember.Service */ -abstract class CoreStore extends Service { +class CoreStore extends Service { /** * Ember Data uses several specialized micro-queues for organizing and coalescing similar async work. @@ -195,6 +199,10 @@ abstract class CoreStore extends Service { declare _fetchManager: FetchManager; declare _schemaDefinitionService: SchemaDefinitionService; + declare _modelFactoryCache; + declare _relationshipsDefCache; + declare _attributesDefCache; + // DEBUG-only properties declare _trackedAsyncRequests: AsyncTrackingToken[]; declare generateStackTracesForTrackedRequests: boolean; @@ -254,6 +262,10 @@ abstract class CoreStore extends Service { this._backburner = edBackburner; this.recordArrayManager = new RecordArrayManager({ store: this }); + this._modelFactoryCache = Object.create(null); + this._relationshipsDefCache = Object.create(null); + this._attributesDefCache = Object.create(null); + RECORD_REFERENCES._generator = (identifier) => { return new RecordReference(this, identifier); }; @@ -379,14 +391,43 @@ abstract class CoreStore extends Service { return record; } - abstract instantiateRecord( + instantiateRecord( identifier: StableRecordIdentifier, - createRecordArgs: { [key: string]: unknown }, // args passed in to store.createRecord() and processed by recordData to be set on creation - recordDataFor: (identifier: RecordIdentifier) => RecordDataRecordWrapper, + createRecordArgs: { [key: string]: unknown }, + recordDataFor: (identifier: StableRecordIdentifier) => RecordDataRecordWrapper, notificationManager: NotificationManager - ): RecordInstance; - abstract teardownRecord(record: RecordInstance): void; - abstract getSchemaDefinitionService(): SchemaDefinitionService; + ): DSModel | RecordInstance { + let modelName = identifier.type; + + let internalModel = this._internalModelForResource(identifier); + let createOptions: any = { + store: this, + _internalModel: internalModel, + // TODO deprecate allowing unknown args setting + _createProps: createRecordArgs, + container: null, + }; + + // ensure that `getOwner(this)` works inside a model instance + setOwner(createOptions, getOwner(this)); + + delete createOptions.container; + let record = this._modelFactoryFor(modelName).create(createOptions); + return record; + } + teardownRecord(record: DSModel | RecordInstance): void { + assert( + `expected to receive an instance of DSModel. If using a custom model make sure you implement teardownRecord`, + 'destroy' in record + ); + (record as DSModel).destroy(); + } + getSchemaDefinitionService(): SchemaDefinitionService { + if (!this._schemaDefinitionService) { + this._schemaDefinitionService = new DSModelSchemaDefinitionService(this); + } + return this._schemaDefinitionService; + } _attributesDefinitionFor(identifier: RecordIdentifier | { type: string }): AttributesSchema { return this.getSchemaDefinitionService().attributesDefinitionFor(identifier); @@ -420,12 +461,43 @@ abstract class CoreStore extends Service { @param {String} modelName @return {subclass of Model | ShimModelClass} */ - modelFor(modelName: string): ShimModelClass { + modelFor(modelName: string): ShimModelClass | DSModelClass { if (DEBUG) { assertDestroyedStoreOnly(this, 'modelFor'); } + assert(`You need to pass a model name to the store's modelFor method`, isPresent(modelName)); + assert( + `Passing classes to store methods has been removed. Please pass a dasherized string instead of ${modelName}`, + typeof modelName === 'string' + ); + + let maybeFactory = this._modelFactoryFor(modelName); + + // for factorFor factory/class split + let klass = maybeFactory && maybeFactory.class ? maybeFactory.class : maybeFactory; + if (!klass || !klass.isModel) { + if (!this.getSchemaDefinitionService().doesTypeExist(modelName)) { + throw new EmberError(`No model was found for '${modelName}' and no schema handles the type`); + } + return getShimClass(this, modelName); + } else { + return klass; + } + } + + _modelFactoryFor(modelName: string): DSModelClass { + if (DEBUG) { + assertDestroyedStoreOnly(this, '_modelFactoryFor'); + } + assert(`You need to pass a model name to the store's _modelFactoryFor method`, isPresent(modelName)); + assert( + `Passing classes to store methods has been removed. Please pass a dasherized string instead of ${modelName}`, + typeof modelName === 'string' + ); + let normalizedModelName = normalizeModelName(modelName); + let factory = getModelFactory(this, this._modelFactoryCache, normalizedModelName); - return getShimClass(this, modelName); + return factory; } // Feature Flagged in DSModelStore @@ -441,7 +513,10 @@ abstract class CoreStore extends Service { @method _hasModelFor @private */ - _hasModelFor(modelName: string): boolean { + _hasModelFor(modelName) { + if (DEBUG) { + assertDestroyingStore(this, '_hasModelFor'); + } assert(`You need to pass a model name to the store's hasModelFor method`, isPresent(modelName)); assert( `Passing classes to store methods has been removed. Please pass a dasherized string instead of ${modelName}`, diff --git a/packages/store/addon/-private/system/ds-model-store.ts b/packages/store/addon/-private/system/ds-model-store.ts deleted file mode 100644 index 6b6375293f0..00000000000 --- a/packages/store/addon/-private/system/ds-model-store.ts +++ /dev/null @@ -1,136 +0,0 @@ -import { getOwner, setOwner } from '@ember/application'; -import { assert } from '@ember/debug'; -import EmberError from '@ember/error'; -import { isPresent } from '@ember/utils'; -import { DEBUG } from '@glimmer/env'; - -import type DSModelClass from '@ember-data/model'; - -import type { DSModel } from '../ts-interfaces/ds-model'; -import type { StableRecordIdentifier } from '../ts-interfaces/identifier'; -import type { RecordDataRecordWrapper } from '../ts-interfaces/record-data-record-wrapper'; -import type { SchemaDefinitionService } from '../ts-interfaces/schema-definition-service'; -import CoreStore from './core-store'; -import type ShimModelClass from './model/shim-model-class'; -import { getShimClass } from './model/shim-model-class'; -import normalizeModelName from './normalize-model-name'; -import type NotificationManager from './record-notification-manager'; -import { DSModelSchemaDefinitionService, getModelFactory } from './schema-definition-service'; - -class Store extends CoreStore { - public _modelFactoryCache = Object.create(null); - private _relationshipsDefCache = Object.create(null); - private _attributesDefCache = Object.create(null); - - instantiateRecord( - identifier: StableRecordIdentifier, - createRecordArgs: { [key: string]: any }, - recordDataFor: (identifier: StableRecordIdentifier) => RecordDataRecordWrapper, - notificationManager: NotificationManager - ): DSModel { - let modelName = identifier.type; - - let internalModel = this._internalModelForResource(identifier); - let createOptions: any = { - store: this, - _internalModel: internalModel, - // TODO deprecate allowing unknown args setting - _createProps: createRecordArgs, - container: null, - }; - - // ensure that `getOwner(this)` works inside a model instance - setOwner(createOptions, getOwner(this)); - - delete createOptions.container; - let record = this._modelFactoryFor(modelName).create(createOptions); - return record; - } - - teardownRecord(record: DSModel) { - record.destroy(); - } - - modelFor(modelName: string): ShimModelClass | DSModelClass { - if (DEBUG) { - assertDestroyedStoreOnly(this, 'modelFor'); - } - assert(`You need to pass a model name to the store's modelFor method`, isPresent(modelName)); - assert( - `Passing classes to store methods has been removed. Please pass a dasherized string instead of ${modelName}`, - typeof modelName === 'string' - ); - - let maybeFactory = this._modelFactoryFor(modelName); - - // for factorFor factory/class split - let klass = maybeFactory && maybeFactory.class ? maybeFactory.class : maybeFactory; - if (!klass || !klass.isModel) { - if (!this.getSchemaDefinitionService().doesTypeExist(modelName)) { - throw new EmberError(`No model was found for '${modelName}' and no schema handles the type`); - } - return getShimClass(this, modelName); - } else { - return klass; - } - } - - _modelFactoryFor(modelName: string): DSModelClass { - if (DEBUG) { - assertDestroyedStoreOnly(this, '_modelFactoryFor'); - } - assert(`You need to pass a model name to the store's _modelFactoryFor method`, isPresent(modelName)); - assert( - `Passing classes to store methods has been removed. Please pass a dasherized string instead of ${modelName}`, - typeof modelName === 'string' - ); - let normalizedModelName = normalizeModelName(modelName); - let factory = getModelFactory(this, this._modelFactoryCache, normalizedModelName); - - return factory; - } - - _hasModelFor(modelName) { - if (DEBUG) { - assertDestroyingStore(this, '_hasModelFor'); - } - assert(`You need to pass a model name to the store's hasModelFor method`, isPresent(modelName)); - assert( - `Passing classes to store methods has been removed. Please pass a dasherized string instead of ${modelName}`, - typeof modelName === 'string' - ); - - return this.getSchemaDefinitionService().doesTypeExist(modelName); - } - - _relationshipMetaFor(modelName: string, id: string | null, key: string) { - return this._relationshipsDefinitionFor({ type: modelName })[key]; - } - - getSchemaDefinitionService(): SchemaDefinitionService { - if (!this._schemaDefinitionService) { - this._schemaDefinitionService = new DSModelSchemaDefinitionService(this); - } - return this._schemaDefinitionService; - } -} - -let assertDestroyingStore: Function; -let assertDestroyedStoreOnly: Function; - -if (DEBUG) { - assertDestroyingStore = function assertDestroyedStore(store, method) { - assert( - `Attempted to call store.${method}(), but the store instance has already been destroyed.`, - !(store.isDestroying || store.isDestroyed) - ); - }; - assertDestroyedStoreOnly = function assertDestroyedStoreOnly(store, method) { - assert( - `Attempted to call store.${method}(), but the store instance has already been destroyed.`, - !store.isDestroyed - ); - }; -} - -export default Store; diff --git a/packages/store/addon/-private/system/errors-utils.js b/packages/store/addon/-private/system/errors-utils.js index 05fb07f5869..f9c7611565e 100644 --- a/packages/store/addon/-private/system/errors-utils.js +++ b/packages/store/addon/-private/system/errors-utils.js @@ -15,7 +15,7 @@ const PRIMARY_ATTRIBUTE_KEY = 'base'; import DS from 'ember-data'; const { errorsHashToArray } = DS; - + let errors = { base: 'Invalid attributes on saving this record', name: 'Must be present', diff --git a/packages/store/addon/-private/system/schema-definition-service.ts b/packages/store/addon/-private/system/schema-definition-service.ts index fcd6e66fcb7..9b1a36170d0 100644 --- a/packages/store/addon/-private/system/schema-definition-service.ts +++ b/packages/store/addon/-private/system/schema-definition-service.ts @@ -8,7 +8,7 @@ import { HAS_MODEL_PACKAGE } from '@ember-data/private-build-infra'; import type { RecordIdentifier } from '../ts-interfaces/identifier'; import type { AttributesSchema, RelationshipsSchema } from '../ts-interfaces/record-data-schemas'; -import type Store from './ds-model-store'; +import type Store from './core-store'; import normalizeModelName from './normalize-model-name'; type ModelForMixin = (store: Store, normalizedModelName: string) => Model | null; diff --git a/tsconfig.json b/tsconfig.json index 5936d79031e..9cb12d4b7bd 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -71,7 +71,6 @@ "packages/store/addon/-private/system/internal-model-map.ts", "packages/store/addon/-private/system/identity-map.ts", "packages/store/addon/-private/system/fetch-manager.ts", - "packages/store/addon/-private/system/ds-model-store.ts", "packages/store/addon/-private/system/core-store.ts", "packages/store/addon/-private/system/coerce-id.ts", "packages/store/addon/-private/index.ts", From 7c64e9fc8612388e62e91393f7e198b912cf5449 Mon Sep 17 00:00:00 2001 From: Chris Thoburn Date: Sat, 23 Jul 2022 19:17:34 -0700 Subject: [PATCH 03/16] introduce InstanceCache for eliminating InternalModel --- .../tests/unit/store/adapter-interop-test.js | 14 +++---- .../store/addon/-private/instance-cache.ts | 41 +++++++++++++++++++ .../store/addon/-private/system/core-store.ts | 14 +++---- .../-private/system/model/internal-model.ts | 16 +------- 4 files changed, 55 insertions(+), 30 deletions(-) create mode 100644 packages/store/addon/-private/instance-cache.ts diff --git a/packages/-ember-data/tests/unit/store/adapter-interop-test.js b/packages/-ember-data/tests/unit/store/adapter-interop-test.js index 302a73fb6ad..64daa1e96db 100644 --- a/packages/-ember-data/tests/unit/store/adapter-interop-test.js +++ b/packages/-ember-data/tests/unit/store/adapter-interop-test.js @@ -714,7 +714,7 @@ module('unit/store/adapter-interop - Store working with a Adapter', function (ho }); }); - test('store._scheduleFetchMany should not resolve until all the records are resolved', function (assert) { + test('store._scheduleFetchMany should not resolve until all the records are resolved', async function (assert) { assert.expect(1); const ApplicationAdapter = Adapter.extend({ @@ -752,13 +752,13 @@ module('unit/store/adapter-interop - Store working with a Adapter', function (ho store._internalModelForResource({ type: 'phone', id: '21' }), ]; - return run(() => { - return store._scheduleFetchMany(internalModels).then(() => { - let unloadedRecords = A(internalModels.map((r) => r.getRecord())).filterBy('isEmpty'); + await store._scheduleFetchMany(internalModels); - assert.strictEqual(get(unloadedRecords, 'length'), 0, 'All unloaded records should be loaded'); - }); - }); + const records = [store.peekRecord('test', '10'), store.peekRecord('phone', '20'), store.peekRecord('phone', '21')]; + + let unloadedRecords = records.filter((record) => record === null || record.isEmpty); + + assert.strictEqual(unloadedRecords.length, 0, 'All unloaded records should be loaded'); }); test('the store calls adapter.findMany according to groupings returned by adapter.groupRecordsForFindMany', function (assert) { diff --git a/packages/store/addon/-private/instance-cache.ts b/packages/store/addon/-private/instance-cache.ts new file mode 100644 index 00000000000..ed31f860623 --- /dev/null +++ b/packages/store/addon/-private/instance-cache.ts @@ -0,0 +1,41 @@ +import type { CreateRecordProperties } from './system/core-store'; +import CoreStore from './system/core-store'; +import type { StableRecordIdentifier } from './ts-interfaces/identifier'; +import type { RecordInstance } from './ts-interfaces/record-instance'; + +export class InstanceCache { + declare store: CoreStore; + + #records = new WeakMap(); + + constructor(store: CoreStore) { + this.store = store; + } + + getRecord(identifier: StableRecordIdentifier, properties?: CreateRecordProperties): RecordInstance { + let record = this.#records.get(identifier); + + // TODO how to handle dematerializing + + if (!record) { + if (properties && properties.id) { + this.getInternalModel(identifier).setId(properties.id); + } + + record = this.store._instantiateRecord(this.getRecordData(identifier), identifier, properties); + this.#records.set(identifier, record); + } + + return record; + } + + // TODO move RecordData Cache into InstanceCache + getRecordData(identifier: StableRecordIdentifier) { + return this.getInternalModel(identifier)._recordData; + } + + // TODO move InternalModel cache into InstanceCache + getInternalModel(identifier: StableRecordIdentifier) { + return this.store._internalModelForResource(identifier); + } +} diff --git a/packages/store/addon/-private/system/core-store.ts b/packages/store/addon/-private/system/core-store.ts index b8b2692c687..26a0f07da71 100644 --- a/packages/store/addon/-private/system/core-store.ts +++ b/packages/store/addon/-private/system/core-store.ts @@ -23,6 +23,7 @@ import type { ManyRelationship, RecordData as RecordDataClass } from '@ember-dat import type { RelationshipState } from '@ember-data/record-data/-private/graph/-state'; import { IdentifierCache } from '../identifiers/cache'; +import { InstanceCache } from '../instance-cache'; import type { DSModel } from '../ts-interfaces/ds-model'; import type { CollectionResourceDocument, @@ -198,6 +199,7 @@ class CoreStore extends Service { declare _storeWrapper: RecordDataStoreWrapper; declare _fetchManager: FetchManager; declare _schemaDefinitionService: SchemaDefinitionService; + declare _instanceCache: InstanceCache; declare _modelFactoryCache; declare _relationshipsDefCache; @@ -261,6 +263,7 @@ class CoreStore extends Service { this._storeWrapper = new RecordDataStoreWrapper(this); this._backburner = edBackburner; this.recordArrayManager = new RecordArrayManager({ store: this }); + this._instanceCache = new InstanceCache(this); this._modelFactoryCache = Object.create(null); this._relationshipsDefCache = Object.create(null); @@ -338,11 +341,9 @@ class CoreStore extends Service { } _instantiateRecord( - internalModel: InternalModel, - modelName: string, recordData: RecordData, identifier: StableRecordIdentifier, - properties?: { [key: string]: any } + properties?: { [key: string]: unknown } ) { // assert here if (properties !== undefined) { @@ -351,12 +352,10 @@ class CoreStore extends Service { typeof properties === 'object' && properties !== null ); - if ('id' in properties) { - internalModel.setId(properties.id); - } + const { type } = identifier; // convert relationship Records to RecordDatas before passing to RecordData - let defs = this._relationshipsDefinitionFor({ type: modelName }); + let defs = this._relationshipsDefinitionFor({ type }); if (defs !== null) { let keys = Object.keys(properties); @@ -387,7 +386,6 @@ class CoreStore extends Service { //TODO Igor pass a wrapper instead of RD let record = this.instantiateRecord(identifier, createOptions, this.__recordDataFor, this._notificationManager); setRecordIdentifier(record, identifier); - //recordToInternalModelMap.set(record, internalModel); return record; } diff --git a/packages/store/addon/-private/system/model/internal-model.ts b/packages/store/addon/-private/system/model/internal-model.ts index ee82acf222b..ba26c5fa2cd 100644 --- a/packages/store/addon/-private/system/model/internal-model.ts +++ b/packages/store/addon/-private/system/model/internal-model.ts @@ -285,26 +285,12 @@ export default class InternalModel { } getRecord(properties?: CreateRecordProperties): RecordInstance { - let record = this._record; - if (this._isDematerializing) { // TODO we should assert here instead of this return. return null as unknown as RecordInstance; } - if (!record) { - let { store } = this; - - record = this._record = store._instantiateRecord( - this, - this.modelName, - this._recordData, - this.identifier, - properties - ); - } - - return record; + return this.store._instanceCache.getRecord(this.identifier, properties); } dematerializeRecord() { From bf645f4ebfa5f0c192a842c3416e6824fedd9da7 Mon Sep 17 00:00:00 2001 From: Chris Thoburn Date: Sat, 23 Jul 2022 19:25:18 -0700 Subject: [PATCH 04/16] fix failing test --- .../custom-class-support/custom-class-model-test.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/packages/-ember-data/tests/unit/custom-class-support/custom-class-model-test.ts b/packages/-ember-data/tests/unit/custom-class-support/custom-class-model-test.ts index 777460b9e9e..e373d6fe697 100644 --- a/packages/-ember-data/tests/unit/custom-class-support/custom-class-model-test.ts +++ b/packages/-ember-data/tests/unit/custom-class-support/custom-class-model-test.ts @@ -8,7 +8,7 @@ import { setupTest } from 'ember-qunit'; import JSONAPIAdapter from '@ember-data/adapter/json-api'; import JSONAPISerializer from '@ember-data/serializer/json-api'; import Store, { recordIdentifierFor } from '@ember-data/store'; -import type { Snapshot } from '@ember-data/store/-private'; +import type { RecordDataStoreWrapper, Snapshot } from '@ember-data/store/-private'; import type CoreStore from '@ember-data/store/-private/system/core-store'; import type NotificationManager from '@ember-data/store/-private/system/record-notification-manager'; import type { RecordIdentifier, StableRecordIdentifier } from '@ember-data/store/-private/ts-interfaces/identifier'; @@ -82,8 +82,13 @@ module('unit/model - Custom Class Model', function (hooks) { let identifier; let recordData; class CreationStore extends CustomStore { - createRecordDataFor() { - let rd = this._super(...arguments); + createRecordDataFor( + modelName: string, + id: string | null, + clientId: string, + storeWrapper: RecordDataStoreWrapper + ) { + let rd = super.createRecordDataFor(modelName, id, clientId, storeWrapper); recordData = rd; return rd; } From 9bb4c0273f6fba6faa1b51c90f28f06bb7a4c746 Mon Sep 17 00:00:00 2001 From: Chris Thoburn Date: Sat, 23 Jul 2022 20:29:21 -0700 Subject: [PATCH 05/16] more cleanup --- .../node-tests/fixtures/expected.js | 1 - .../record-data/store-wrapper-test.ts | 3 +- .../tests/integration/records/unload-test.js | 34 +++--- .../store/addon/-private/instance-cache.ts | 42 ++++++- .../store/addon/-private/system/core-store.ts | 107 ++++++++---------- .../-private/system/model/internal-model.ts | 44 +++---- .../system/store/record-data-store-wrapper.ts | 8 +- 7 files changed, 130 insertions(+), 109 deletions(-) diff --git a/packages/-ember-data/node-tests/fixtures/expected.js b/packages/-ember-data/node-tests/fixtures/expected.js index e5e8e7e8687..faefc75d65c 100644 --- a/packages/-ember-data/node-tests/fixtures/expected.js +++ b/packages/-ember-data/node-tests/fixtures/expected.js @@ -88,7 +88,6 @@ module.exports = { '(private) @ember-data/store SnapshotRecordArray#constructor', '(private) @ember-data/store Store#_backburner', '(private) @ember-data/store Store#_fetchAll', - '(private) @ember-data/store Store#_generateId', '(private) @ember-data/store Store#_hasModelFor', '(private) @ember-data/store Store#_load', '(private) @ember-data/store Store#_push', diff --git a/packages/-ember-data/tests/integration/record-data/store-wrapper-test.ts b/packages/-ember-data/tests/integration/record-data/store-wrapper-test.ts index 369dd00ecf1..6c8313e35d0 100644 --- a/packages/-ember-data/tests/integration/record-data/store-wrapper-test.ts +++ b/packages/-ember-data/tests/integration/record-data/store-wrapper-test.ts @@ -383,8 +383,7 @@ module('integration/store-wrapper - RecordData StoreWrapper tests', function (ho super(); if (!id) { assert.true(storeWrapper.isRecordInUse('house', '1'), 'house 1 is in use'); - // TODO isRecordInUse should coorce to false rather than null - assert.strictEqual(storeWrapper.isRecordInUse('house', '2'), null, 'house 2 is not in use'); + assert.false(storeWrapper.isRecordInUse('house', '2'), 'house 2 is not in use'); } } } diff --git a/packages/-ember-data/tests/integration/records/unload-test.js b/packages/-ember-data/tests/integration/records/unload-test.js index 2bb675ba2ec..45d3343a7d8 100644 --- a/packages/-ember-data/tests/integration/records/unload-test.js +++ b/packages/-ember-data/tests/integration/records/unload-test.js @@ -2136,7 +2136,7 @@ module('integration/unload - Unloading Records', function (hooks) { ); }); - test('1 async : many sync unload async side', function (assert) { + test('1 async : many sync unload async side', async function (assert) { let findRecordCalls = 0; adapter.coalesceFindRequests = true; @@ -2190,29 +2190,27 @@ module('integration/unload - Unloading Records', function (hooks) { let spoon3 = store.peekRecord('spoon', 3); let spoons = person.get('favoriteSpoons'); - return run(() => { - assert.deepEqual(person.get('favoriteSpoons').mapBy('id'), ['2', '3'], 'initially relationship established lhs'); - assert.strictEqual(spoon2.belongsTo('person').id(), '1', 'initially relationship established rhs'); - assert.strictEqual(spoon3.belongsTo('person').id(), '1', 'initially relationship established rhs'); + assert.deepEqual(person.get('favoriteSpoons').mapBy('id'), ['2', '3'], 'initially relationship established lhs'); + assert.strictEqual(spoon2.belongsTo('person').id(), '1', 'initially relationship established rhs'); + assert.strictEqual(spoon3.belongsTo('person').id(), '1', 'initially relationship established rhs'); - assert.false(spoons.isDestroyed, 'ManyArray is not destroyed'); + assert.false(spoons.isDestroyed, 'ManyArray is not destroyed'); - run(() => person.unloadRecord()); + run(() => person.unloadRecord()); - assert.false(spoons.isDestroyed, 'ManyArray is not destroyed when 1 side is unloaded'); - assert.strictEqual(spoon2.belongsTo('person').id(), '1', 'unload async is not treated as delete'); - assert.strictEqual(spoon3.belongsTo('person').id(), '1', 'unload async is not treated as delete'); + assert.false(spoons.isDestroyed, 'ManyArray is not destroyed when 1 side is unloaded'); + assert.strictEqual(spoon2.belongsTo('person').id(), '1', 'unload async is not treated as delete'); + assert.strictEqual(spoon3.belongsTo('person').id(), '1', 'unload async is not treated as delete'); - return spoon2.get('person'); - }).then((refetchedPerson) => { - assert.notEqual(person, refetchedPerson, 'the previously loaded record is not reused'); + const refetchedPerson = await spoon2.get('person'); - assert.deepEqual(person.get('favoriteSpoons').mapBy('id'), ['2', '3'], 'unload async is not treated as delete'); - assert.strictEqual(spoon2.belongsTo('person').id(), '1', 'unload async is not treated as delete'); - assert.strictEqual(spoon3.belongsTo('person').id(), '1', 'unload async is not treated as delete'); + assert.notEqual(person, refetchedPerson, 'the previously loaded record is not reused'); - assert.strictEqual(findRecordCalls, 1, 'findRecord called as expected'); - }); + assert.deepEqual(person.get('favoriteSpoons').mapBy('id'), ['2', '3'], 'unload async is not treated as delete'); + assert.strictEqual(spoon2.belongsTo('person').id(), '1', 'unload async is not treated as delete'); + assert.strictEqual(spoon3.belongsTo('person').id(), '1', 'unload async is not treated as delete'); + + assert.strictEqual(findRecordCalls, 1, 'findRecord called as expected'); }); test('1 sync : many async unload async side', async function (assert) { diff --git a/packages/store/addon/-private/instance-cache.ts b/packages/store/addon/-private/instance-cache.ts index ed31f860623..1ae0d17d2fb 100644 --- a/packages/store/addon/-private/instance-cache.ts +++ b/packages/store/addon/-private/instance-cache.ts @@ -1,34 +1,68 @@ +import { assert } from '@ember/debug'; + import type { CreateRecordProperties } from './system/core-store'; import CoreStore from './system/core-store'; import type { StableRecordIdentifier } from './ts-interfaces/identifier'; import type { RecordInstance } from './ts-interfaces/record-instance'; +type Caches = { + record: WeakMap; +}; export class InstanceCache { declare store: CoreStore; - #records = new WeakMap(); + #instances: Caches = { + record: new WeakMap(), + }; constructor(store: CoreStore) { this.store = store; } + peek({ identifier, bucket }: { identifier: StableRecordIdentifier; bucket: 'record' }): RecordInstance | undefined { + return this.#instances[bucket].get(identifier); + } + set({ + identifier, + bucket, + value, + }: { + identifier: StableRecordIdentifier; + bucket: 'record'; + value: RecordInstance; + }): void { + this.#instances[bucket].set(identifier, value); + } + getRecord(identifier: StableRecordIdentifier, properties?: CreateRecordProperties): RecordInstance { - let record = this.#records.get(identifier); + let record = this.peek({ identifier, bucket: 'record' }); // TODO how to handle dematerializing if (!record) { - if (properties && properties.id) { + if (properties && 'id' in properties) { + assert(`expected id to be a string or null`, properties.id !== undefined); this.getInternalModel(identifier).setId(properties.id); } record = this.store._instantiateRecord(this.getRecordData(identifier), identifier, properties); - this.#records.set(identifier, record); + this.set({ identifier, bucket: 'record', value: record }); } return record; } + removeRecord(identifier: StableRecordIdentifier): boolean { + let record = this.peek({ identifier, bucket: 'record' }); + + if (record) { + this.#instances.record.delete(identifier); + this.store.teardownRecord(record); + } + + return !!record; + } + // TODO move RecordData Cache into InstanceCache getRecordData(identifier: StableRecordIdentifier) { return this.getInternalModel(identifier)._recordData; diff --git a/packages/store/addon/-private/system/core-store.ts b/packages/store/addon/-private/system/core-store.ts index 26a0f07da71..df8670bbd1b 100644 --- a/packages/store/addon/-private/system/core-store.ts +++ b/packages/store/addon/-private/system/core-store.ts @@ -4,13 +4,10 @@ import { getOwner, setOwner } from '@ember/application'; import { A } from '@ember/array'; import { assert, inspect, warn } from '@ember/debug'; -import EmberError from '@ember/error'; -import { set } from '@ember/object'; import { _backburner as emberBackburner } from '@ember/runloop'; import type { Backburner } from '@ember/runloop/-private/backburner'; import Service from '@ember/service'; import { registerWaiter, unregisterWaiter } from '@ember/test'; -import { isNone, isPresent, typeOf } from '@ember/utils'; import { DEBUG } from '@glimmer/env'; import Ember from 'ember'; @@ -463,7 +460,7 @@ class CoreStore extends Service { if (DEBUG) { assertDestroyedStoreOnly(this, 'modelFor'); } - assert(`You need to pass a model name to the store's modelFor method`, isPresent(modelName)); + assert(`You need to pass a model name to the store's modelFor method`, modelName); assert( `Passing classes to store methods has been removed. Please pass a dasherized string instead of ${modelName}`, typeof modelName === 'string' @@ -474,9 +471,11 @@ class CoreStore extends Service { // for factorFor factory/class split let klass = maybeFactory && maybeFactory.class ? maybeFactory.class : maybeFactory; if (!klass || !klass.isModel) { - if (!this.getSchemaDefinitionService().doesTypeExist(modelName)) { - throw new EmberError(`No model was found for '${modelName}' and no schema handles the type`); - } + assert( + `No model was found for '${modelName}' and no schema handles the type`, + this.getSchemaDefinitionService().doesTypeExist(modelName) + ); + return getShimClass(this, modelName); } else { return klass; @@ -487,7 +486,7 @@ class CoreStore extends Service { if (DEBUG) { assertDestroyedStoreOnly(this, '_modelFactoryFor'); } - assert(`You need to pass a model name to the store's _modelFactoryFor method`, isPresent(modelName)); + assert(`You need to pass a model name to the store's _modelFactoryFor method`, modelName); assert( `Passing classes to store methods has been removed. Please pass a dasherized string instead of ${modelName}`, typeof modelName === 'string' @@ -515,7 +514,7 @@ class CoreStore extends Service { if (DEBUG) { assertDestroyingStore(this, '_hasModelFor'); } - assert(`You need to pass a model name to the store's hasModelFor method`, isPresent(modelName)); + assert(`You need to pass a model name to the store's hasModelFor method`, modelName); assert( `Passing classes to store methods has been removed. Please pass a dasherized string instead of ${modelName}`, typeof modelName === 'string' @@ -561,7 +560,7 @@ class CoreStore extends Service { if (DEBUG) { assertDestroyingStore(this, 'createRecord'); } - assert(`You need to pass a model name to the store's createRecord method`, isPresent(modelName)); + assert(`You need to pass a model name to the store's createRecord method`, modelName); assert( `Passing classes to store methods has been removed. Please pass a dasherized string instead of ${modelName}`, typeof modelName === 'string' @@ -582,8 +581,14 @@ class CoreStore extends Service { // client-side ID generators will use something like uuid.js // to avoid conflicts. - if (isNone(properties.id)) { - properties.id = this._generateId(normalizedModelName, properties); + if (properties.id === null || properties.id === undefined) { + let adapter = this.adapterFor(modelName); + + if (adapter && adapter.generateIdForRecord) { + properties.id = adapter.generateIdForRecord(this, modelName, properties); + } else { + properties.id = null; + } } // Coerce ID to a string @@ -600,26 +605,6 @@ class CoreStore extends Service { }); } - /** - If possible, this method asks the adapter to generate an ID for - a newly created record. - - @method _generateId - @private - @param {String} modelName - @param {Object} properties from the new record - @return {String} if the adapter can generate one, an ID - */ - _generateId(modelName: string, properties: CreateRecordProperties): string | null { - let adapter = this.adapterFor(modelName); - - if (adapter && adapter.generateIdForRecord) { - return adapter.generateIdForRecord(this, modelName, properties); - } - - return null; - } - // ................. // . DELETE RECORD . // ................. @@ -697,6 +682,7 @@ class CoreStore extends Service { @return {Promise} promise @private */ + // TODO @runspired @deprecate find(modelName: string, id: string | number, options?): PromiseObject { if (DEBUG) { assertDestroyingStore(this, 'find'); @@ -1109,7 +1095,7 @@ class CoreStore extends Service { assert( `You need to pass a modelName or resource identifier as the first argument to the store's findRecord method`, - isPresent(resource) + resource ); if (isMaybeIdentifier(resource)) { options = id as FindOptions | undefined; @@ -1208,11 +1194,12 @@ class CoreStore extends Service { @param {Array} ids @return {Promise} promise */ + // TODO @runspired @deprecate findByIds(modelName, ids) { if (DEBUG) { assertDestroyingStore(this, 'findByIds'); } - assert(`You need to pass a model name to the store's findByIds method`, isPresent(modelName)); + assert(`You need to pass a model name to the store's findByIds method`, modelName); assert( `Passing classes to store methods has been removed. Please pass a dasherized string instead of ${modelName}`, typeof modelName === 'string' @@ -1397,7 +1384,7 @@ class CoreStore extends Service { assertDestroyingStore(this, 'peekRecord'); } - assert(`You need to pass a model name to the store's peekRecord method`, isPresent(identifier)); + assert(`You need to pass a model name to the store's peekRecord method`, identifier); assert( `Passing classes to store methods has been removed. Please pass a dasherized string instead of ${identifier}`, typeof identifier === 'string' @@ -1462,11 +1449,12 @@ class CoreStore extends Service { @param {(String|Integer)} id @return {Boolean} */ + // TODO @runspired @deprecate hasRecordForId(modelName: string, id: string | number): boolean { if (DEBUG) { assertDestroyingStore(this, 'hasRecordForId'); } - assert(`You need to pass a model name to the store's hasRecordForId method`, isPresent(modelName)); + assert(`You need to pass a model name to the store's hasRecordForId method`, modelName); assert( `Passing classes to store methods has been removed. Please pass a dasherized string instead of ${modelName}`, typeof modelName === 'string' @@ -1492,11 +1480,12 @@ class CoreStore extends Service { @param {(String|Integer)} id @return {Model} record */ + // TODO @runspired @deprecate recordForId(modelName: string, id: string | number): RecordInstance { if (DEBUG) { assertDestroyingStore(this, 'recordForId'); } - assert(`You need to pass a model name to the store's recordForId method`, isPresent(modelName)); + assert(`You need to pass a model name to the store's recordForId method`, modelName); assert( `Passing classes to store methods has been removed. Please pass a dasherized string instead of ${modelName}`, typeof modelName === 'string' @@ -1513,6 +1502,7 @@ class CoreStore extends Service { @param {Array} internalModels @return {Promise} promise */ + // TODO @runspired @deprecate findMany(internalModels, options) { if (DEBUG) { assertDestroyingStore(this, 'findMany'); @@ -1784,7 +1774,7 @@ class CoreStore extends Service { if (DEBUG) { assertDestroyingStore(this, 'query'); } - assert(`You need to pass a model name to the store's query method`, isPresent(modelName)); + assert(`You need to pass a model name to the store's query method`, modelName); assert(`You need to pass a query hash to the store's query method`, query); assert( `Passing classes to store methods has been removed. Please pass a dasherized string instead of ${modelName}`, @@ -1802,7 +1792,7 @@ class CoreStore extends Service { } _query(modelName: string, query, array, options): Promise { - assert(`You need to pass a model name to the store's query method`, isPresent(modelName)); + assert(`You need to pass a model name to the store's query method`, modelName); assert(`You need to pass a query hash to the store's query method`, query); assert( `Passing classes to store methods has been removed. Please pass a dasherized string instead of ${modelName}`, @@ -1922,7 +1912,7 @@ class CoreStore extends Service { if (DEBUG) { assertDestroyingStore(this, 'queryRecord'); } - assert(`You need to pass a model name to the store's queryRecord method`, isPresent(modelName)); + assert(`You need to pass a model name to the store's queryRecord method`, modelName); assert(`You need to pass a query hash to the store's queryRecord method`, query); assert( `Passing classes to store methods has been removed. Please pass a dasherized string instead of ${modelName}`, @@ -2153,7 +2143,7 @@ class CoreStore extends Service { if (DEBUG) { assertDestroyingStore(this, 'findAll'); } - assert(`You need to pass a model name to the store's findAll method`, isPresent(modelName)); + assert(`You need to pass a model name to the store's findAll method`, modelName); assert( `Passing classes to store methods has been removed. Please pass a dasherized string instead of ${modelName}`, typeof modelName === 'string' @@ -2186,7 +2176,7 @@ class CoreStore extends Service { ); if (options.reload) { - set(array, 'isUpdating', true); + array.isUpdating = true; return _findAll(adapter, this, modelName, options); } @@ -2197,7 +2187,7 @@ class CoreStore extends Service { (adapter.shouldReloadAll && adapter.shouldReloadAll(this, snapshotArray)) || (!adapter.shouldReloadAll && snapshotArray.length === 0) ) { - set(array, 'isUpdating', true); + array.isUpdating = true; return _findAll(adapter, this, modelName, options); } } @@ -2211,7 +2201,7 @@ class CoreStore extends Service { !adapter.shouldBackgroundReloadAll || adapter.shouldBackgroundReloadAll(this, snapshotArray) ) { - set(array, 'isUpdating', true); + array.isUpdating = true; _findAll(adapter, this, modelName, options); } @@ -2247,7 +2237,7 @@ class CoreStore extends Service { if (DEBUG) { assertDestroyingStore(this, 'peekAll'); } - assert(`You need to pass a model name to the store's peekAll method`, isPresent(modelName)); + assert(`You need to pass a model name to the store's peekAll method`, modelName); assert( `Passing classes to store methods has been removed. Please pass a dasherized string instead of ${modelName}`, typeof modelName === 'string' @@ -2741,10 +2731,10 @@ class CoreStore extends Service { } assert( - `Expected an object in the 'data' property in a call to 'push' for ${jsonApiDoc.type}, but was ${typeOf( - jsonApiDoc.data - )}`, - typeOf(jsonApiDoc.data) === 'object' + `Expected an object in the 'data' property in a call to 'push' for ${ + jsonApiDoc.type + }, but was ${typeof jsonApiDoc.data}`, + typeof jsonApiDoc.data === 'object' ); return this._pushInternalModel(jsonApiDoc.data); @@ -2770,6 +2760,7 @@ class CoreStore extends Service { // If ENV.DS_WARN_ON_UNKNOWN_KEYS is set to true and the payload // contains unknown attributes or relationships, log a warning. + // TODO @runspired @deprecate in favor of a build-time config not in ENV if (ENV.DS_WARN_ON_UNKNOWN_KEYS) { let unknownAttributes, unknownRelationships; let relationships = this.getSchemaDefinitionService().relationshipsDefinitionFor(modelName); @@ -3019,7 +3010,7 @@ class CoreStore extends Service { if (DEBUG) { assertDestroyingStore(this, 'normalize'); } - assert(`You need to pass a model name to the store's normalize method`, isPresent(modelName)); + assert(`You need to pass a model name to the store's normalize method`, modelName); assert( `Passing classes to store methods has been removed. Please pass a dasherized string instead of ${inspect( modelName @@ -3078,7 +3069,7 @@ class CoreStore extends Service { if (DEBUG) { assertDestroyingStore(this, 'adapterFor'); } - assert(`You need to pass a model name to the store's adapterFor method`, isPresent(modelName)); + assert(`You need to pass a model name to the store's adapterFor method`, modelName); assert( `Passing classes to store.adapterFor has been removed. Please pass a dasherized string instead of ${modelName}`, typeof modelName === 'string' @@ -3096,7 +3087,8 @@ class CoreStore extends Service { // name specific adapter adapter = owner.lookup(`adapter:${normalizedModelName}`); if (adapter !== undefined) { - set(adapter, 'store', this); + // TODO @runspired @deprecate store auto-inject + adapter.store = this; _adapterCache[normalizedModelName] = adapter; return adapter; } @@ -3104,7 +3096,7 @@ class CoreStore extends Service { // no adapter found for the specific name, fallback and check for application adapter adapter = _adapterCache.application || owner.lookup('adapter:application'); if (adapter !== undefined) { - set(adapter, 'store', this); + adapter.store = this; _adapterCache[normalizedModelName] = adapter; _adapterCache.application = adapter; return adapter; @@ -3118,7 +3110,7 @@ class CoreStore extends Service { `No adapter was found for '${modelName}' and no 'application' adapter was found as a fallback.`, adapter !== undefined ); - set(adapter, 'store', this); + adapter.store = this; _adapterCache[normalizedModelName] = adapter; _adapterCache['-json-api'] = adapter; return adapter; @@ -3149,7 +3141,7 @@ class CoreStore extends Service { if (DEBUG) { assertDestroyingStore(this, 'serializerFor'); } - assert(`You need to pass a model name to the store's serializerFor method`, isPresent(modelName)); + assert(`You need to pass a model name to the store's serializerFor method`, modelName); assert( `Passing classes to store.serializerFor has been removed. Please pass a dasherized string instead of ${modelName}`, typeof modelName === 'string' @@ -3167,7 +3159,8 @@ class CoreStore extends Service { // by name serializer = owner.lookup(`serializer:${normalizedModelName}`); if (serializer !== undefined) { - set(serializer, 'store', this); + // TODO @runspired @deprecate store auto-inject + serializer.store = this; _serializerCache[normalizedModelName] = serializer; return serializer; } @@ -3175,7 +3168,7 @@ class CoreStore extends Service { // no serializer found for the specific model, fallback and check for application serializer serializer = _serializerCache.application || owner.lookup('serializer:application'); if (serializer !== undefined) { - set(serializer, 'store', this); + serializer.store = this; _serializerCache[normalizedModelName] = serializer; _serializerCache.application = serializer; return serializer; diff --git a/packages/store/addon/-private/system/model/internal-model.ts b/packages/store/addon/-private/system/model/internal-model.ts index ba26c5fa2cd..b8c7974c744 100644 --- a/packages/store/addon/-private/system/model/internal-model.ts +++ b/packages/store/addon/-private/system/model/internal-model.ts @@ -105,7 +105,6 @@ export default class InternalModel { declare _deletedRecordWasNew: boolean; // Not typed yet - declare _record: RecordInstance | null; declare _scheduledDestroy: any; declare _modelClass: any; declare __recordArrays: any; @@ -118,6 +117,7 @@ export default class InternalModel { declare error: any; declare store: CoreStore; declare identifier: StableRecordIdentifier; + declare hasRecord: boolean; constructor(store: CoreStore, identifier: StableRecordIdentifier) { if (HAS_MODEL_PACKAGE) { @@ -129,6 +129,7 @@ export default class InternalModel { this._isUpdatingId = false; this.modelName = identifier.type; this.clientId = identifier.lid; + this.hasRecord = false; this.__recordData = null; @@ -145,7 +146,6 @@ export default class InternalModel { this._isDematerializing = false; this._scheduledDestroy = null; - this._record = null; this.error = null; // caches for lazy getters @@ -290,6 +290,7 @@ export default class InternalModel { return null as unknown as RecordInstance; } + this.hasRecord = true; return this.store._instanceCache.getRecord(this.identifier, properties); } @@ -301,9 +302,8 @@ export default class InternalModel { this._doNotDestroy = false; // this has to occur before the internal model is removed // for legacy compat. - if (this._record) { - this.store.teardownRecord(this._record); - } + const { identifier } = this; + let hadRecord = this.store._instanceCache.removeRecord(identifier); // move to an empty never-loaded state // ensure any record notifications happen prior to us @@ -313,7 +313,7 @@ export default class InternalModel { this._recordData.unloadRecord(); }); - if (this._record) { + if (hadRecord) { let keys = Object.keys(this._relationshipProxyCache); keys.forEach((key) => { let proxy = this._relationshipProxyCache[key]!; @@ -324,7 +324,7 @@ export default class InternalModel { }); } - this._record = null; + this.hasRecord = false; // this must occur after reltionship removal this.error = null; this.store.recordArrayManager.recordDidChange(this.identifier); } @@ -685,9 +685,10 @@ export default class InternalModel { } destroy() { + let record = this.store._instanceCache.peek({ identifier: this.identifier, bucket: 'record' }); assert( 'Cannot destroy an internalModel while its record is materialized', - !this._record || this._record.isDestroyed || this._record.isDestroying + !record || record.isDestroyed || record.isDestroying ); this.isDestroying = true; if (this._recordReference) { @@ -754,8 +755,9 @@ export default class InternalModel { let currentValue = this._recordData.getAttr(key); if (currentValue !== value) { this._recordData.setDirtyAttribute(key, value); - if (this.hasRecord && isDSModel(this._record)) { - this._record.errors.remove(key); + let record = this.store._instanceCache.peek({ identifier: this.identifier, bucket: 'record' }); + if (record && isDSModel(record)) { + record.errors.remove(key); } } @@ -766,10 +768,6 @@ export default class InternalModel { return this._isDestroyed; } - get hasRecord(): boolean { - return !!this._record; - } - createSnapshot(options: FindOptions = {}): Snapshot { return new Snapshot(options, this.identifier, this.store); } @@ -792,8 +790,9 @@ export default class InternalModel { adapterWillCommit(): void { this._recordData.willCommit(); - if (this.hasRecord && isDSModel(this._record)) { - this._record.errors.clear(); + let record = this.store._instanceCache.peek({ identifier: this.identifier, bucket: 'record' }); + if (record && isDSModel(record)) { + record.errors.clear(); } } @@ -840,8 +839,10 @@ export default class InternalModel { rollbackAttributes() { this.store._backburner.join(() => { let dirtyKeys = this._recordData.rollbackAttributes(); - if (this.hasRecord && isDSModel(this._record)) { - this._record.errors.clear(); + + let record = this.store._instanceCache.peek({ identifier: this.identifier, bucket: 'record' }); + if (record && isDSModel(record)) { + record.errors.clear(); } if (this.hasRecord && dirtyKeys && dirtyKeys.length > 0) { @@ -932,7 +933,7 @@ export default class InternalModel { * 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) { + setId(id: string | null, fromCache: boolean = false) { if (this._isUpdatingId === true) { return; } @@ -974,11 +975,12 @@ export default class InternalModel { if (this._recordData.getErrors) { return this._recordData.getErrors(this.identifier).length > 0; } else { + let record = this.store._instanceCache.peek({ identifier: this.identifier, bucket: 'record' }); // we can't have errors if we never tried loading - if (!this._record) { + if (!record) { return false; } - let errors = (this._record as DSModel).errors; + let errors = (record as DSModel).errors; return errors.length > 0; } } diff --git a/packages/store/addon/-private/system/store/record-data-store-wrapper.ts b/packages/store/addon/-private/system/store/record-data-store-wrapper.ts index 75daffbe2a9..292da79e3ef 100644 --- a/packages/store/addon/-private/system/store/record-data-store-wrapper.ts +++ b/packages/store/addon/-private/system/store/record-data-store-wrapper.ts @@ -219,14 +219,10 @@ export default class RecordDataStoreWrapper implements StoreWrapper { isRecordInUse(type: string, id: string | null, lid?: string | null): boolean { const resource = constructResource(type, id, lid); const identifier = this.identifierCache.getOrCreateRecordIdentifier(resource); - const internalModel = internalModelFactoryFor(this._store).peek(identifier); - if (!internalModel) { - return false; - } + const record = this._store._instanceCache.peek({ identifier, bucket: 'record' }); - const record = internalModel._record as RecordInstance; - return record && !(record.isDestroyed || record.isDestroying); + return record ? !(record.isDestroyed || record.isDestroying) : false; } disconnectRecord(type: string, id: string | null, lid: string): void; From ee9c591800290c78c0e779b26e04902f375f75de Mon Sep 17 00:00:00 2001 From: Chris Thoburn Date: Sat, 23 Jul 2022 21:11:13 -0700 Subject: [PATCH 06/16] fix types --- packages/-ember-data/tests/unit/store/adapter-interop-test.js | 1 - .../addon/-private/system/store/record-data-store-wrapper.ts | 1 - 2 files changed, 2 deletions(-) diff --git a/packages/-ember-data/tests/unit/store/adapter-interop-test.js b/packages/-ember-data/tests/unit/store/adapter-interop-test.js index 64daa1e96db..4198f594803 100644 --- a/packages/-ember-data/tests/unit/store/adapter-interop-test.js +++ b/packages/-ember-data/tests/unit/store/adapter-interop-test.js @@ -1,4 +1,3 @@ -import { A } from '@ember/array'; import { get, set } from '@ember/object'; import { later, run } from '@ember/runloop'; diff --git a/packages/store/addon/-private/system/store/record-data-store-wrapper.ts b/packages/store/addon/-private/system/store/record-data-store-wrapper.ts index 292da79e3ef..f28b98aa210 100644 --- a/packages/store/addon/-private/system/store/record-data-store-wrapper.ts +++ b/packages/store/addon/-private/system/store/record-data-store-wrapper.ts @@ -12,7 +12,6 @@ import type { RelationshipsSchema, } from '../../ts-interfaces/record-data-schemas'; import type { RecordDataStoreWrapper as StoreWrapper } from '../../ts-interfaces/record-data-store-wrapper'; -import { RecordInstance } from '../../ts-interfaces/record-instance'; import constructResource from '../../utils/construct-resource'; import type CoreStore from '../core-store'; import { internalModelFactoryFor } from './internal-model-factory'; From 74103cc28476a6ac4c0f02f37fb84f0c3b3d133e Mon Sep 17 00:00:00 2001 From: Chris Thoburn Date: Sat, 23 Jul 2022 21:14:43 -0700 Subject: [PATCH 07/16] remove deprecated setEdition usage --- packages/-ember-data/.ember-cli.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/-ember-data/.ember-cli.js b/packages/-ember-data/.ember-cli.js index 79999737931..070f4c00b22 100644 --- a/packages/-ember-data/.ember-cli.js +++ b/packages/-ember-data/.ember-cli.js @@ -1,7 +1,3 @@ -const { setEdition } = require('@ember/edition-utils'); - -setEdition('octane'); - module.exports = { /** Ember CLI sends analytics information by default. The data is completely From 11bb8232488817a730e3f1c38090b370e3bc369a Mon Sep 17 00:00:00 2001 From: Chris Thoburn Date: Sun, 24 Jul 2022 15:56:02 -0700 Subject: [PATCH 08/16] that was a deep rabbit hole --- .../node-tests/fixtures/expected.js | 11 - packages/-ember-data/package.json | 3 + .../relationships/belongs-to-test.js | 6 +- .../adapter/client-side-delete-test.js | 6 +- .../tests/integration/adapter/find-test.js | 2 +- .../integration/adapter/rest-adapter-test.js | 10 +- .../integration/adapter/store-adapter-test.js | 2 +- .../tests/integration/multiple-stores-test.js | 32 +- .../tests/integration/record-array-test.js | 60 -- .../record-data/store-wrapper-test.ts | 10 +- .../tests/integration/records/load-test.js | 2 +- .../integration/records/rematerialize-test.js | 12 +- .../tests/integration/records/save-test.js | 10 +- .../tests/integration/records/unload-test.js | 46 +- .../relationships/has-many-test.js | 18 +- .../integration/request-state-service-test.ts | 3 +- .../embedded-records-mixin-test.js | 3 +- .../tests/integration/snapshot-test.js | 66 +- .../tests/integration/store-test.js | 10 +- .../custom-class-model-test.ts | 27 - packages/-ember-data/tests/unit/model-test.js | 35 +- .../unit/model/relationships/has-many-test.js | 14 +- .../unit/record-arrays/record-array-test.js | 8 +- .../tests/unit/store/adapter-interop-test.js | 42 +- .../tests/unit/store/asserts-test.js | 12 - .../tests/unit/store/create-record-test.js | 83 +-- .../tests/unit/store/has-model-for-test.js | 19 - .../unit/store/has-record-for-id-test.js | 75 -- .../tests/unit/store/unload-test.js | 2 +- .../unit/system/snapshot-record-array-test.js | 49 +- packages/adapter/addon/index.ts | 5 +- packages/model/addon/-private/model.js | 65 +- packages/model/addon/-private/record-state.ts | 8 +- packages/model/index.js | 1 + .../addon/current-deprecations.ts | 4 + .../private-build-infra/addon/deprecations.ts | 4 + .../addon/-private/graph/-utils.ts | 2 +- .../graph/operations/update-relationship.ts | 4 +- .../addon/-private/embedded-records-mixin.js | 15 +- packages/serializer/addon/index.ts | 12 +- packages/serializer/addon/json-api.js | 27 +- packages/serializer/addon/json.js | 12 +- packages/serializer/addon/rest.js | 6 +- packages/store/addon/-private/index.ts | 2 +- .../store/addon/-private/instance-cache.ts | 2 +- .../store/addon/-private/system/core-store.ts | 654 +++++++----------- .../addon/-private/system/errors-utils.js | 11 +- .../addon/-private/system/fetch-manager.ts | 80 ++- .../-private/system/model/internal-model.ts | 58 +- .../addon/-private/system/promise-proxies.ts | 2 +- .../system/record-arrays/record-array.ts | 37 +- .../-private/system/snapshot-record-array.ts | 29 +- .../store/addon/-private/system/snapshot.ts | 27 +- .../system/store/internal-model-factory.ts | 3 +- .../addon/-private/ts-interfaces/store.ts | 1 - .../addon/-private/utils/promise-record.ts | 8 +- packages/store/addon/index.ts | 1 + 57 files changed, 724 insertions(+), 1034 deletions(-) delete mode 100644 packages/-ember-data/tests/unit/store/has-model-for-test.js delete mode 100644 packages/-ember-data/tests/unit/store/has-record-for-id-test.js diff --git a/packages/-ember-data/node-tests/fixtures/expected.js b/packages/-ember-data/node-tests/fixtures/expected.js index faefc75d65c..c262e6d0b8c 100644 --- a/packages/-ember-data/node-tests/fixtures/expected.js +++ b/packages/-ember-data/node-tests/fixtures/expected.js @@ -87,22 +87,11 @@ module.exports = { '(private) @ember-data/store SnapshotRecordArray#_snapshots', '(private) @ember-data/store SnapshotRecordArray#constructor', '(private) @ember-data/store Store#_backburner', - '(private) @ember-data/store Store#_fetchAll', - '(private) @ember-data/store Store#_hasModelFor', '(private) @ember-data/store Store#_load', '(private) @ember-data/store Store#_push', - '(private) @ember-data/store Store#_reloadRecord', - '(private) @ember-data/store Store#defaultAdapter', '(private) @ember-data/store Store#didSaveRecord', '(private) @ember-data/store Store#find', - '(private) @ember-data/store Store#findBelongsTo', - '(private) @ember-data/store Store#findByIds', - '(private) @ember-data/store Store#findHasMany', - '(private) @ember-data/store Store#findMany', - '(private) @ember-data/store Store#flushPendingSave', '(private) @ember-data/store Store#init', - '(private) @ember-data/store Store#recordForId', - '(private) @ember-data/store Store#recordWasError', '(private) @ember-data/store Store#recordWasInvalid', '(private) @ember-data/store Store#scheduleSave', '(private) @ember-data/store Store#setRecordId', diff --git a/packages/-ember-data/package.json b/packages/-ember-data/package.json index 52d53de17e9..d0efb1aff2b 100644 --- a/packages/-ember-data/package.json +++ b/packages/-ember-data/package.json @@ -96,6 +96,9 @@ "ember-addon": { "configPath": "tests/dummy/config" }, + "ember": { + "edition": "octane" + }, "volta": { "node": "16.16.0", "yarn": "1.22.19" diff --git a/packages/-ember-data/tests/acceptance/relationships/belongs-to-test.js b/packages/-ember-data/tests/acceptance/relationships/belongs-to-test.js index ae872e52bdb..bfb8c20a72c 100644 --- a/packages/-ember-data/tests/acceptance/relationships/belongs-to-test.js +++ b/packages/-ember-data/tests/acceptance/relationships/belongs-to-test.js @@ -487,7 +487,9 @@ module('async belongs-to rendering tests', function (hooks) { data: people.dict['5:has-parent-no-children'], }); - adapter.setupPayloads(assert, [new ServerError([], 'hard error while finding 5:has-parent-no-children')]); + adapter.setupPayloads(assert, [ + new ServerError([], 'hard error while finding 5:has-parent-no-children.parent'), + ]); // render this.set('sedona', sedona); @@ -500,7 +502,7 @@ module('async belongs-to rendering tests', function (hooks) { assert.ok(true, 'Children promise did reject'); assert.strictEqual( e.message, - 'hard error while finding 5:has-parent-no-children', + 'hard error while finding 5:has-parent-no-children.parent', 'Rejection has the correct message' ); } else { diff --git a/packages/-ember-data/tests/integration/adapter/client-side-delete-test.js b/packages/-ember-data/tests/integration/adapter/client-side-delete-test.js index 8991296ee98..249356e200f 100644 --- a/packages/-ember-data/tests/integration/adapter/client-side-delete-test.js +++ b/packages/-ember-data/tests/integration/adapter/client-side-delete-test.js @@ -77,11 +77,7 @@ module('integration/adapter/store-adapter - client-side delete', function (hooks await book2.destroyRecord({ adapterOptions: { clientSideDelete: true } }); - book2.unloadRecord(); - - await settled(); - - assert.false(store.hasRecordForId('book', '2'), 'book 2 unloaded'); + assert.strictEqual(store.peekRecord('book', '2'), null, 'book 2 unloaded'); assert.deepEqual(bookstore.books.mapBy('id'), ['1'], 'one book client-side deleted'); store.push({ diff --git a/packages/-ember-data/tests/integration/adapter/find-test.js b/packages/-ember-data/tests/integration/adapter/find-test.js index b2256511ef3..098badd1ddc 100644 --- a/packages/-ember-data/tests/integration/adapter/find-test.js +++ b/packages/-ember-data/tests/integration/adapter/find-test.js @@ -169,7 +169,7 @@ module('integration/adapter/find - Finding Records', function (hooks) { assert.ok(false, 'We expected to throw but did not'); } catch (e) { assert.ok(true, 'The rejection handler was called'); - assert.notOk(store.hasRecordForId('person', '1'), 'The record has been unloaded'); + assert.strictEqual(store.peekRecord('person', '1'), null, 'The record has been unloaded'); } }); diff --git a/packages/-ember-data/tests/integration/adapter/rest-adapter-test.js b/packages/-ember-data/tests/integration/adapter/rest-adapter-test.js index 1ec15c9f1e4..23f87d21636 100644 --- a/packages/-ember-data/tests/integration/adapter/rest-adapter-test.js +++ b/packages/-ember-data/tests/integration/adapter/rest-adapter-test.js @@ -1940,7 +1940,7 @@ module('integration/adapter/rest_adapter - REST Adapter', function (hooks) { ajaxError('error', 401); try { - await store.find('post', '1'); + await store.findRecord('post', '1'); } catch (reason) { assert.ok(true, 'promise should be rejected'); assert.ok(reason instanceof DS.UnauthorizedError, 'reason should be an instance of DS.UnauthorizedError'); @@ -1949,7 +1949,7 @@ module('integration/adapter/rest_adapter - REST Adapter', function (hooks) { ajaxError('error', 403); try { - await store.find('post', '1'); + await store.findRecord('post', '1'); } catch (reason) { assert.ok(true, 'promise should be rejected'); assert.ok(reason instanceof DS.ForbiddenError, 'reason should be an instance of DS.ForbiddenError'); @@ -1958,7 +1958,7 @@ module('integration/adapter/rest_adapter - REST Adapter', function (hooks) { ajaxError('error', 404); try { - await store.find('post', '1'); + await store.findRecord('post', '1'); } catch (reason) { assert.ok(true, 'promise should be rejected'); assert.ok(reason instanceof DS.NotFoundError, 'reason should be an instance of DS.NotFoundError'); @@ -1967,7 +1967,7 @@ module('integration/adapter/rest_adapter - REST Adapter', function (hooks) { ajaxError('error', 409); try { - await store.find('post', '1'); + await store.findRecord('post', '1'); } catch (reason) { assert.ok(true, 'promise should be rejected'); assert.ok(reason instanceof DS.ConflictError, 'reason should be an instance of DS.ConflictError'); @@ -1976,7 +1976,7 @@ module('integration/adapter/rest_adapter - REST Adapter', function (hooks) { ajaxError('error', 500); try { - await store.find('post', '1'); + await store.findRecord('post', '1'); } catch (reason) { assert.ok(true, 'promise should be rejected'); assert.ok(reason instanceof DS.ServerError, 'reason should be an instance of DS.ServerError'); 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 af2074f20c3..2a3a2b901da 100644 --- a/packages/-ember-data/tests/integration/adapter/store-adapter-test.js +++ b/packages/-ember-data/tests/integration/adapter/store-adapter-test.js @@ -22,7 +22,7 @@ function moveRecordOutOfInFlight(record) { // TODO this would be made nicer by a cancellation API let pending = store.getRequestStateService().getPendingRequestsForRecord(_internalModel.identifier); pending.splice(0, pending.length); // release the requests - store.recordWasError(_internalModel, new Error()); + _internalModel.adapterDidError(new Error()); } module('integration/adapter/store-adapter - DS.Store and DS.Adapter integration test', function (hooks) { diff --git a/packages/-ember-data/tests/integration/multiple-stores-test.js b/packages/-ember-data/tests/integration/multiple-stores-test.js index 2ac3a606e75..8545b0efb3f 100644 --- a/packages/-ember-data/tests/integration/multiple-stores-test.js +++ b/packages/-ember-data/tests/integration/multiple-stores-test.js @@ -135,7 +135,11 @@ module('integration/multiple_stores - Multiple Stores Tests', function (hooks) { ); andromedaStore.push(normalizedAndromedaPayload); - assert.true(andromedaStore.hasRecordForId('super-villain', '1'), 'superVillain should exist in service:store'); + assert.notStrictEqual( + andromedaStore.peekRecord('super-villain', '1'), + null, + 'superVillain should exist in service:store' + ); const normalizedCartWheelPayload = cartwheelSerializer.normalizeResponse( cartwheelStore, @@ -146,7 +150,11 @@ module('integration/multiple_stores - Multiple Stores Tests', function (hooks) { ); cartwheelStore.push(normalizedCartWheelPayload); - assert.true(cartwheelStore.hasRecordForId('super-villain', '1'), 'superVillain should exist in store:store-a'); + assert.notStrictEqual( + cartwheelStore.peekRecord('super-villain', '1'), + null, + 'superVillain should exist in store:store-a' + ); const normalizedCigarPayload = cigarSerializer.normalizeResponse( cigarStore, @@ -157,7 +165,11 @@ module('integration/multiple_stores - Multiple Stores Tests', function (hooks) { ); cigarStore.push(normalizedCigarPayload); - assert.true(cigarStore.hasRecordForId('super-villain', '1'), 'superVillain should exist in store:store-b'); + assert.notStrictEqual( + cigarStore.peekRecord('super-villain', '1'), + null, + 'superVillain should exist in store:store-b' + ); }); test('each store should have a unique instance of the serializers', function (assert) { @@ -169,13 +181,11 @@ module('integration/multiple_stores - Multiple Stores Tests', function (hooks) { const andromedaSerializer = andromedaStore.serializerFor('home-planet'); const cigarSerializer = cigarStore.serializerFor('home-planet'); - assert.strictEqual( - andromedaSerializer.store, - andromedaStore, - "andromedaSerializer's store prop should be andromedaStore" + assert.notStrictEqual( + andromedaSerializer, + cigarSerializer, + 'andromedaStore and cigarStore should be unique instances' ); - assert.strictEqual(cigarSerializer.store, cigarStore, "cigarSerializer's store prop should be cigarStore"); - assert.notEqual(andromedaSerializer, cigarSerializer, 'andromedaStore and cigarStore should be unique instances'); }); test('each store should have a unique instance of the adapters', function (assert) { @@ -187,8 +197,6 @@ module('integration/multiple_stores - Multiple Stores Tests', function (hooks) { const andromedaAdapter = andromedaStore.adapterFor('home-planet'); const cigarAdapter = cigarStore.adapterFor('home-planet'); - assert.strictEqual(andromedaAdapter.store, andromedaStore); - assert.strictEqual(cigarAdapter.store, cigarStore); - assert.notEqual(andromedaAdapter, cigarAdapter); + assert.notStrictEqual(andromedaAdapter, cigarAdapter, 'the adapters are unique'); }); }); diff --git a/packages/-ember-data/tests/integration/record-array-test.js b/packages/-ember-data/tests/integration/record-array-test.js index 45d8b36b1cd..4ca856cf62e 100644 --- a/packages/-ember-data/tests/integration/record-array-test.js +++ b/packages/-ember-data/tests/integration/record-array-test.js @@ -44,66 +44,6 @@ module('unit/record-array - RecordArray', function (hooks) { store = owner.lookup('service:store'); }); - test('a record array is backed by records', async function (assert) { - assert.expect(3); - this.owner.register( - 'adapter:application', - Adapter.extend({ - shouldBackgroundReloadRecord() { - return false; - }, - }) - ); - - store.push({ - data: [ - { - type: 'person', - id: '1', - attributes: { - name: 'Scumbag Dale', - }, - }, - { - type: 'person', - id: '2', - attributes: { - name: 'Scumbag Katz', - }, - }, - { - type: 'person', - id: '3', - attributes: { - name: 'Scumbag Bryn', - }, - }, - ], - }); - - let records = await store.findByIds('person', [1, 2, 3]); - let expectedResults = { - data: [ - { id: '1', type: 'person', attributes: { name: 'Scumbag Dale' } }, - { id: '2', type: 'person', attributes: { name: 'Scumbag Katz' } }, - { id: '3', type: 'person', attributes: { name: 'Scumbag Bryn' } }, - ], - }; - - for (let i = 0, l = expectedResults.data.length; i < l; i++) { - let { - id, - attributes: { name }, - } = expectedResults.data[i]; - - assert.deepEqual( - records[i].getProperties('id', 'name'), - { id, name }, - 'a record array materializes objects on demand' - ); - } - }); - test('acts as a live query', async function (assert) { let recordArray = store.peekAll('person'); diff --git a/packages/-ember-data/tests/integration/record-data/store-wrapper-test.ts b/packages/-ember-data/tests/integration/record-data/store-wrapper-test.ts index 6c8313e35d0..994e754170a 100644 --- a/packages/-ember-data/tests/integration/record-data/store-wrapper-test.ts +++ b/packages/-ember-data/tests/integration/record-data/store-wrapper-test.ts @@ -36,8 +36,11 @@ class House extends Model { // TODO: this should work // class TestRecordData implements RecordData class TestRecordData { + _isNew = false; pushData(data, calculateChange?: boolean) {} - clientDidCreate() {} + clientDidCreate() { + this._isNew = true; + } willCommit() {} @@ -78,6 +81,9 @@ class TestRecordData { isAttrDirty(key: string) { return false; } + isNew() { + return this._isNew; + } removeFromInverseRelationships() {} _initRecordCreateOptions(options) {} @@ -443,6 +449,6 @@ module('integration/store-wrapper - RecordData StoreWrapper tests', function (ho included: [houseHash], }); wrapper.disconnectRecord('house', '1'); - assert.false(store.hasRecordForId('house', '1'), 'record was removed from id map'); + assert.strictEqual(store.peekRecord('house', '1'), null, 'record was removed from id map'); }); }); diff --git a/packages/-ember-data/tests/integration/records/load-test.js b/packages/-ember-data/tests/integration/records/load-test.js index e0de0e567ed..f630be4ad95 100644 --- a/packages/-ember-data/tests/integration/records/load-test.js +++ b/packages/-ember-data/tests/integration/records/load-test.js @@ -41,7 +41,7 @@ module('integration/load - Loading Records', function (hooks) { ); await store.findRecord('person', '1').catch(() => { - assert.false(store.hasRecordForId('person', '1')); + assert.strictEqual(store.peekRecord('person', '1'), null); }); }); diff --git a/packages/-ember-data/tests/integration/records/rematerialize-test.js b/packages/-ember-data/tests/integration/records/rematerialize-test.js index 7d2dbe0b510..9759b9b1373 100644 --- a/packages/-ember-data/tests/integration/records/rematerialize-test.js +++ b/packages/-ember-data/tests/integration/records/rematerialize-test.js @@ -84,7 +84,7 @@ module('integration/unload - Rematerializing Unloaded Records', function (hooks) let person = store.peekRecord('person', 1); assert.strictEqual(person.get('cars.length'), 1, 'The inital length of cars is correct'); - assert.true(store.hasRecordForId('person', 1), 'The person is in the store'); + assert.notStrictEqual(store.peekRecord('person', '1'), null, 'The person is in the store'); assert.true( store._internalModelsFor('person').has('@ember-data:lid-person-1'), 'The person internalModel is loaded' @@ -92,7 +92,7 @@ module('integration/unload - Rematerializing Unloaded Records', function (hooks) run(() => person.unloadRecord()); - assert.false(store.hasRecordForId('person', 1), 'The person is unloaded'); + assert.strictEqual(store.peekRecord('person', '1'), null, 'The person is unloaded'); assert.false( store._internalModelsFor('person').has('@ember-data:lid-person-1'), 'The person internalModel is freed' @@ -213,12 +213,12 @@ module('integration/unload - Rematerializing Unloaded Records', function (hooks) }); // assert our initial cache state - assert.true(store.hasRecordForId('person', '1'), 'The person is in the store'); + assert.notStrictEqual(store.peekRecord('person', '1'), null, 'The person is in the store'); assert.true( store._internalModelsFor('person').has('@ember-data:lid-person-1'), 'The person internalModel is loaded' ); - assert.true(store.hasRecordForId('boat', '1'), 'The boat is in the store'); + assert.notStrictEqual(store.peekRecord('boat', '1'), null, 'The boat is in the store'); assert.true(store._internalModelsFor('boat').has('@ember-data:lid-boat-1'), 'The boat internalModel is loaded'); let boats = await adam.get('boats'); @@ -228,7 +228,7 @@ module('integration/unload - Rematerializing Unloaded Records', function (hooks) assert.strictEqual(boats.get('length'), 1, 'after unloading boats.length is correct'); // assert our new cache state - assert.false(store.hasRecordForId('boat', '1'), 'The boat is unloaded'); + assert.strictEqual(store.peekRecord('boat', '1'), null, 'The boat is unloaded'); assert.true(store._internalModelsFor('boat').has('@ember-data:lid-boat-1'), 'The boat internalModel is retained'); // cause a rematerialization, this should also cause us to fetch boat '1' again @@ -241,7 +241,7 @@ module('integration/unload - Rematerializing Unloaded Records', function (hooks) assert.strictEqual(rematerializedBoaty.get('name'), 'Boaty McBoatface', 'Rematerialized boat has the right name'); assert.notStrictEqual(rematerializedBoaty, boaty, 'the boat is rematerialized, not recycled'); - assert.true(store.hasRecordForId('boat', '1'), 'The boat is loaded'); + assert.notStrictEqual(store.peekRecord('boat', '1'), null, 'The boat is loaded'); assert.true(store._internalModelsFor('boat').has('@ember-data:lid-boat-1'), 'The boat internalModel is retained'); }); }); diff --git a/packages/-ember-data/tests/integration/records/save-test.js b/packages/-ember-data/tests/integration/records/save-test.js index 3f01275984b..531e83eaf26 100644 --- a/packages/-ember-data/tests/integration/records/save-test.js +++ b/packages/-ember-data/tests/integration/records/save-test.js @@ -230,7 +230,10 @@ module('integration/records/save - Save Record', function (hooks) { store.unloadAll('post'); }); - await assert.expectAssertion(() => post.save(), 'Cannot initiate a save request for an unloaded record'); + await assert.expectAssertion( + () => post.save(), + 'A record in a disconnected state cannot utilize the store. This typically means the record has been destroyed, most commonly by unloading it.' + ); }); test('Will error when saving after unloading record', async function (assert) { @@ -253,6 +256,9 @@ module('integration/records/save - Save Record', function (hooks) { post.unloadRecord(); }); - await assert.expectAssertion(() => post.save(), 'Cannot initiate a save request for an unloaded record'); + await assert.expectAssertion( + () => post.save(), + 'A record in a disconnected state cannot utilize the store. This typically means the record has been destroyed, most commonly by unloading it.' + ); }); }); diff --git a/packages/-ember-data/tests/integration/records/unload-test.js b/packages/-ember-data/tests/integration/records/unload-test.js index 45d3343a7d8..c309ece37bf 100644 --- a/packages/-ember-data/tests/integration/records/unload-test.js +++ b/packages/-ember-data/tests/integration/records/unload-test.js @@ -455,8 +455,8 @@ module('integration/unload - Unloading Records', function (hooks) { // ensure we loaded the people and boats assert.strictEqual(knownPeople.models.length, 1, 'one person record is loaded'); assert.strictEqual(knownBoats.models.length, 1, 'one boat record is loaded'); - assert.true(store.hasRecordForId('person', '1')); - assert.true(store.hasRecordForId('boat', '1')); + assert.notStrictEqual(store.peekRecord('person', '1'), null); + assert.notStrictEqual(store.peekRecord('boat', '1'), null); // ensure the relationship was established (we reach through the async proxy here) let peopleBoats = await person.get('boats'); @@ -533,8 +533,8 @@ module('integration/unload - Unloading Records', function (hooks) { // ensure we loaded the people and boats assert.strictEqual(knownPeople.models.length, 1, 'one person record is loaded'); assert.strictEqual(knownBoats.models.length, 1, 'one boat record is loaded'); - assert.true(store.hasRecordForId('person', '1')); - assert.true(store.hasRecordForId('boat', '1')); + assert.notStrictEqual(store.peekRecord('person', '1'), null); + assert.notStrictEqual(store.peekRecord('boat', '1'), null); // ensure the relationship was established (we reach through the async proxy here) let peopleBoats = run(() => person.get('boats.content')); @@ -605,8 +605,8 @@ module('integration/unload - Unloading Records', function (hooks) { ['1'], 'one boat record is loaded' ); - assert.true(store.hasRecordForId('person', '1')); - assert.true(store.hasRecordForId('boat', '1')); + assert.notStrictEqual(store.peekRecord('person', '1'), null); + assert.notStrictEqual(store.peekRecord('boat', '1'), null); // ensure the relationship was established (we reach through the async proxy here) let peopleBoats = run(() => person.get('boats.content')); @@ -736,9 +736,9 @@ module('integration/unload - Unloading Records', function (hooks) { assert.strictEqual(store._internalModelsFor('person').models.length, 1, 'one person record is loaded'); assert.strictEqual(store._internalModelsFor('boat').models.length, 2, 'two boat records are loaded'); - assert.true(store.hasRecordForId('person', 1)); - assert.true(store.hasRecordForId('boat', 1)); - assert.true(store.hasRecordForId('boat', 2)); + assert.notStrictEqual(store.peekRecord('person', '1'), null); + assert.notStrictEqual(store.peekRecord('boat', '1'), null); + assert.notStrictEqual(store.peekRecord('boat', '2'), null); let checkOrphanCalls = 0; let cleanupOrphanCalls = 0; @@ -1197,7 +1197,7 @@ module('integration/unload - Unloading Records', function (hooks) { run(() => house.unloadRecord()); assert.strictEqual(person.get('house'), null, 'unloading acts as a delete for sync relationships'); - assert.false(store.hasRecordForId('house', 2), 'unloaded record gone from store'); + assert.strictEqual(store.peekRecord('house', '2'), null, 'unloaded record gone from store'); house = run(() => store.push({ @@ -1208,7 +1208,7 @@ module('integration/unload - Unloading Records', function (hooks) { }) ); - assert.true(store.hasRecordForId('house', 2), 'unloaded record can be restored'); + assert.notStrictEqual(store.peekRecord('house', '2'), null, 'unloaded record can be restored'); assert.strictEqual(person.get('house'), null, 'restoring unloaded record does not restore relationship'); assert.strictEqual(house.get('person'), null, 'restoring unloaded record does not restore relationship'); @@ -1279,7 +1279,7 @@ module('integration/unload - Unloading Records', function (hooks) { run(() => person.unloadRecord()); - assert.false(store.hasRecordForId('person', 1), 'unloaded record gone from store'); + assert.strictEqual(store.peekRecord('person', '1'), null, 'unloaded record gone from store'); assert.strictEqual(car2.get('person'), null, 'unloading acts as delete for sync relationships'); assert.strictEqual(car3.get('person'), null, 'unloading acts as delete for sync relationships'); @@ -1294,7 +1294,7 @@ module('integration/unload - Unloading Records', function (hooks) { }) ); - assert.true(store.hasRecordForId('person', 1), 'unloaded record can be restored'); + assert.notStrictEqual(store.peekRecord('person', '1'), null, 'unloaded record can be restored'); assert.deepEqual(person.get('cars').mapBy('id'), [], 'restoring unloaded record does not restore relationship'); assert.strictEqual(car2.get('person'), null, 'restoring unloaded record does not restore relationship'); assert.strictEqual(car3.get('person'), null, 'restoring unloaded record does not restore relationship'); @@ -1373,7 +1373,7 @@ module('integration/unload - Unloading Records', function (hooks) { run(() => car2.unloadRecord()); - assert.false(store.hasRecordForId('car', 2), 'unloaded record gone from store'); + assert.strictEqual(store.peekRecord('car', '2'), null, 'unloaded record gone from store'); assert.false(cars.isDestroyed, 'ManyArray not destroyed'); assert.deepEqual(person.get('cars').mapBy('id'), ['3'], 'unload sync relationship acts as delete'); @@ -1388,7 +1388,7 @@ module('integration/unload - Unloading Records', function (hooks) { }) ); - assert.true(store.hasRecordForId('car', 2), 'unloaded record can be restored'); + assert.notStrictEqual(store.peekRecord('car', '2'), null, 'unloaded record can be restored'); assert.deepEqual(person.get('cars').mapBy('id'), ['3'], 'restoring unloaded record does not restore relationship'); assert.strictEqual(car2.get('person'), null, 'restoring unloaded record does not restore relationship'); @@ -1501,7 +1501,7 @@ module('integration/unload - Unloading Records', function (hooks) { assert.deepEqual(group3.get('people').mapBy('id'), ['1'], 'unloading acts as delete for sync relationships'); assert.deepEqual(group4.get('people').mapBy('id'), ['1'], 'unloading acts as delete for sync relationships'); - assert.false(store.hasRecordForId('person', 2), 'unloading removes record from store'); + assert.strictEqual(store.peekRecord('person', '2'), null, 'unloading removes record from store'); person2 = run(() => store.push({ @@ -1512,7 +1512,7 @@ module('integration/unload - Unloading Records', function (hooks) { }) ); - assert.true(store.hasRecordForId('person', 2), 'unloaded record can be restored'); + assert.notStrictEqual(store.peekRecord('person', '2'), null, 'unloaded record can be restored'); assert.deepEqual(person2.get('groups').mapBy('id'), [], 'restoring unloaded record does not restore relationship'); assert.deepEqual( group3.get('people').mapBy('id'), @@ -1922,7 +1922,7 @@ module('integration/unload - Unloading Records', function (hooks) { run(() => book.unloadRecord()); assert.strictEqual(person.get('book'), undefined, 'unloading acts as a delete for sync relationships'); - assert.false(store.hasRecordForId('book', 2), 'unloaded record gone from store'); + assert.strictEqual(store.peekRecord('book', '2'), null, 'unloaded record gone from store'); book = run(() => store.push({ @@ -1933,7 +1933,7 @@ module('integration/unload - Unloading Records', function (hooks) { }) ); - assert.true(store.hasRecordForId('book', 2), 'unloaded record can be restored'); + assert.notStrictEqual(store.peekRecord('book', '2'), null, 'unloaded record can be restored'); assert.strictEqual(person.get('book'), undefined, 'restoring unloaded record does not restore relationship'); assert.strictEqual( book.belongsTo('person').id(), @@ -2074,7 +2074,7 @@ module('integration/unload - Unloading Records', function (hooks) { run(() => spoon2.unloadRecord()); - assert.false(store.hasRecordForId('spoon', 2), 'unloaded record gone from store'); + assert.strictEqual(store.peekRecord('spoon', '2'), null, 'unloaded record gone from store'); assert.false(spoons.isDestroyed, 'ManyArray not destroyed'); assert.deepEqual(person.get('favoriteSpoons').mapBy('id'), ['3'], 'unload sync relationship acts as delete'); @@ -2093,7 +2093,7 @@ module('integration/unload - Unloading Records', function (hooks) { }) ); - assert.true(store.hasRecordForId('spoon', 2), 'unloaded record can be restored'); + assert.notStrictEqual(store.peekRecord('spoon', '2'), null, 'unloaded record can be restored'); assert.deepEqual( person.get('favoriteSpoons').mapBy('id'), ['3'], @@ -2347,7 +2347,7 @@ module('integration/unload - Unloading Records', function (hooks) { run(() => person.unloadRecord()); - assert.false(store.hasRecordForId('person', 1), 'unloaded record gone from store'); + assert.strictEqual(store.peekRecord('person', '1'), null, 'unloaded record gone from store'); assert.true(shows.isDestroyed, 'previous manyarray immediately destroyed'); assert.strictEqual(show2.get('person.id'), undefined, 'unloading acts as delete for sync relationships'); @@ -2362,7 +2362,7 @@ module('integration/unload - Unloading Records', function (hooks) { }) ); - assert.true(store.hasRecordForId('person', 1), 'unloaded record can be restored'); + assert.notStrictEqual(store.peekRecord('person', '1'), null, 'unloaded record can be restored'); assert.deepEqual( person.hasMany('favoriteShows').ids(), [], diff --git a/packages/-ember-data/tests/integration/relationships/has-many-test.js b/packages/-ember-data/tests/integration/relationships/has-many-test.js index cda3bd62b36..b9a898456ff 100644 --- a/packages/-ember-data/tests/integration/relationships/has-many-test.js +++ b/packages/-ember-data/tests/integration/relationships/has-many-test.js @@ -1967,7 +1967,7 @@ module('integration/relationships/has_many - Has-Many Relationships', function ( }, ], }); - post.set('comments', store.peekAll('comment')); + post.set('comments', store.peekAll('comment').toArray()); }); assert.strictEqual(get(post, 'comments.length'), 2, 'we can set HM relationship'); @@ -2003,7 +2003,7 @@ module('integration/relationships/has_many - Has-Many Relationships', function ( }, ], }); - post.set('comments', store.peekAll('comment')); + post.set('comments', store.peekAll('comment').toArray()); }); return post.get('comments').then((comments) => { @@ -3773,24 +3773,14 @@ module('integration/relationships/has_many - Has-Many Relationships', function ( ['post-1', 'post-2', 'post-3', 'post-4', 'post-5'] ); - await store - .peekRecord('post', 'post-2') - .destroyRecord() - .then((record) => { - return store.unloadRecord(record); - }); + await store.peekRecord('post', 'post-2').destroyRecord(); assert.deepEqual( posts.map((x) => x.get('id')), ['post-1', 'post-3', 'post-4', 'post-5'] ); - await store - .peekRecord('post', 'post-3') - .destroyRecord() - .then((record) => { - return store.unloadRecord(record); - }); + await store.peekRecord('post', 'post-3').destroyRecord(); assert.deepEqual( posts.map((x) => x.get('id')), diff --git a/packages/-ember-data/tests/integration/request-state-service-test.ts b/packages/-ember-data/tests/integration/request-state-service-test.ts index 6cf25c3b8ca..5f4c3deab9b 100644 --- a/packages/-ember-data/tests/integration/request-state-service-test.ts +++ b/packages/-ember-data/tests/integration/request-state-service-test.ts @@ -39,7 +39,7 @@ module('integration/request-state-service - Request State Service', function (ho data: { type: 'person', id: '1', - lid: '', + lid: '@ember-data:lid-person-1', attributes: { name: 'Scumbag Dale', }, @@ -142,6 +142,7 @@ module('integration/request-state-service - Request State Service', function (ho data: { type: 'person', id: '1', + lid: '@ember-data:lid-person-1', attributes: { name: 'Scumbag Dale', }, diff --git a/packages/-ember-data/tests/integration/serializers/embedded-records-mixin-test.js b/packages/-ember-data/tests/integration/serializers/embedded-records-mixin-test.js index 88d0bde54cf..561b3727019 100644 --- a/packages/-ember-data/tests/integration/serializers/embedded-records-mixin-test.js +++ b/packages/-ember-data/tests/integration/serializers/embedded-records-mixin-test.js @@ -1642,7 +1642,8 @@ module('integration/embedded-records-mixin', function (hooks) { calledSerializeHasMany = true; let key = relationship.key; let payloadKey = this.keyForRelationship ? this.keyForRelationship(key, 'hasMany') : key; - let relationshipType = snapshot.type.determineRelationshipType(relationship, store); + let schema = this.store.modelFor(snapshot.modelName); + let relationshipType = schema.determineRelationshipType(relationship, store); // "manyToOne" not supported in ActiveModelSerializer.prototype.serializeHasMany let relationshipTypes = ['manyToNone', 'manyToMany', 'manyToOne']; if (relationshipTypes.indexOf(relationshipType) > -1) { diff --git a/packages/-ember-data/tests/integration/snapshot-test.js b/packages/-ember-data/tests/integration/snapshot-test.js index 4e4591779b0..442ddffcbc8 100644 --- a/packages/-ember-data/tests/integration/snapshot-test.js +++ b/packages/-ember-data/tests/integration/snapshot-test.js @@ -7,6 +7,7 @@ import JSONAPIAdapter from '@ember-data/adapter/json-api'; import Model, { attr, belongsTo, hasMany } from '@ember-data/model'; import JSONAPISerializer from '@ember-data/serializer/json-api'; import { Snapshot } from '@ember-data/store/-private'; +import { deprecatedTest } from '@ember-data/unpublished-test-infra/test-support/deprecated-test'; let owner, store, _Post; @@ -86,8 +87,8 @@ module('integration/snapshot - Snapshot', function (hooks) { assert.ok(snapshot instanceof Snapshot, 'snapshot is an instance of Snapshot'); }); - test('snapshot.id, snapshot.type and snapshot.modelName returns correctly', function (assert) { - assert.expect(3); + test('snapshot.id, and snapshot.modelName returns correctly', function (assert) { + assert.expect(2); store.push({ data: { @@ -102,38 +103,45 @@ module('integration/snapshot - Snapshot', function (hooks) { let snapshot = post._createSnapshot(); assert.strictEqual(snapshot.id, '1', 'id is correct'); - assert.ok(Model.detect(snapshot.type), 'type is correct'); assert.strictEqual(snapshot.modelName, 'post', 'modelName is correct'); }); - test('snapshot.type loads the class lazily', async function (assert) { - assert.expect(3); - - let postClassLoaded = false; - let modelFactoryFor = store._modelFactoryFor; - store._modelFactoryFor = (name) => { - if (name === 'post') { - postClassLoaded = true; - } - return modelFactoryFor.call(store, name); - }; - - await store._push({ - data: { - type: 'post', - id: '1', - attributes: { - title: 'Hello World', + deprecatedTest( + 'snapshot.type loads the class lazily', + { + id: 'ember-data:deprecate-snapshot-model-class-access', + count: 1, + until: '5.0', + }, + async function (assert) { + assert.expect(3); + + let postClassLoaded = false; + let modelFactoryFor = store._modelFactoryFor; + store._modelFactoryFor = (name) => { + if (name === 'post') { + postClassLoaded = true; + } + return modelFactoryFor.call(store, name); + }; + + await store._push({ + data: { + type: 'post', + id: '1', + attributes: { + title: 'Hello World', + }, }, - }, - }); - let postInternalModel = store._internalModelForResource({ type: 'post', id: '1' }); - let snapshot = await postInternalModel.createSnapshot(); + }); + let postInternalModel = store._internalModelForResource({ type: 'post', id: '1' }); + let snapshot = await postInternalModel.createSnapshot(); - assert.false(postClassLoaded, 'model class is not eagerly loaded'); - assert.strictEqual(snapshot.type, _Post, 'type is correct'); - assert.true(postClassLoaded, 'model class is loaded'); - }); + assert.false(postClassLoaded, 'model class is not eagerly loaded'); + assert.strictEqual(snapshot.type, _Post, 'type is correct'); + assert.true(postClassLoaded, 'model class is loaded'); + } + ); test('an initial findRecord call has no record for internal-model when a snapshot is generated', function (assert) { assert.expect(2); diff --git a/packages/-ember-data/tests/integration/store-test.js b/packages/-ember-data/tests/integration/store-test.js index ab6911f7319..62c98a8ce67 100644 --- a/packages/-ember-data/tests/integration/store-test.js +++ b/packages/-ember-data/tests/integration/store-test.js @@ -307,7 +307,7 @@ module('integration/store - findRecord', function (hooks) { ], }); - let cachedRecordIsPresent = store.hasRecordForId('car', '20'); + let cachedRecordIsPresent = store.peekRecord('car', '20') !== null; assert.notOk(cachedRecordIsPresent, 'Car with id=20 should not exist'); @@ -1074,11 +1074,9 @@ module('integration/store - findAll', function (hooks) { }, }); - let car = store.recordForId('car', '20'); + assert.strictEqual(store.peekRecord('car', '20'), null, 'the car is not loaded'); - assert.true(car.isEmpty, 'Car with id=20 should be empty'); - - car = await store.findRecord('car', '20', { reload: true }); + let car = await store.findRecord('car', '20'); assert.strictEqual(car.make, 'BMCW', 'Car with id=20 is now loaded'); }); @@ -1145,7 +1143,7 @@ module('integration/store - deleteRecord', function (hooks) { person = store.peekRecord('person', '1'); - assert.ok(store.hasRecordForId('person', '1'), 'expected the record to be in the store'); + assert.notStrictEqual(store.peekRecord('person', '1'), null, 'expected the record to be in the store'); store.deleteRecord(person); assert.ok(person.isDeleted, 'expect person to be isDeleted'); diff --git a/packages/-ember-data/tests/unit/custom-class-support/custom-class-model-test.ts b/packages/-ember-data/tests/unit/custom-class-support/custom-class-model-test.ts index e373d6fe697..76bea106696 100644 --- a/packages/-ember-data/tests/unit/custom-class-support/custom-class-model-test.ts +++ b/packages/-ember-data/tests/unit/custom-class-support/custom-class-model-test.ts @@ -298,33 +298,6 @@ module('unit/model - Custom Class Model', function (hooks) { await (person as unknown as Person).save(); }); - test('hasModelFor with custom schema definition', async function (assert) { - assert.expect(4); - this.owner.register('service:store', CustomStore); - store = this.owner.lookup('service:store') as Store; - let count = 0; - let schema = { - attributesDefinitionFor() { - return {}; - }, - relationshipsDefinitionFor() { - return {}; - }, - doesTypeExist(modelName: string) { - if (count === 0) { - assert.strictEqual(modelName, 'person', 'type passed in to the schema hooks'); - } else if (count === 1) { - assert.strictEqual(modelName, 'boat', 'type passed in to the schema hooks'); - } - count++; - return modelName === 'person'; - }, - }; - store.registerSchemaDefinitionService(schema); - assert.true(store._hasModelFor('person'), 'hasModelFor matches schema hook when true'); - assert.false(store._hasModelFor('boat'), 'hasModelFor matches schema hook when false'); - }); - test('store.saveRecord', async function (assert) { assert.expect(1); this.owner.register( diff --git a/packages/-ember-data/tests/unit/model-test.js b/packages/-ember-data/tests/unit/model-test.js index b3689652d62..641edefd923 100644 --- a/packages/-ember-data/tests/unit/model-test.js +++ b/packages/-ember-data/tests/unit/model-test.js @@ -1,6 +1,5 @@ import { computed, get, observer, set } from '@ember/object'; import { guidFor } from '@ember/object/internals'; -import { DEBUG } from '@glimmer/env'; import { module, test } from 'qunit'; import { reject, resolve } from 'rsvp'; @@ -116,7 +115,7 @@ module('unit/model - Model', function (hooks) { assert.true(get(record, 'isArchived'), 'The record reflects the update to canonical state'); }); - test('Does not support dirtying in root.deleted.saved', async function (assert) { + testInDebug('Does not support dirtying in root.deleted.saved', async function (assert) { adapter.deleteRecord = () => { return resolve({ data: null }); }; @@ -144,23 +143,13 @@ module('unit/model - Model', function (hooks) { 'the deleted person is not removed from store (no unload called)' ); - if (DEBUG) { - assert.throws( - () => { - set(record, 'isArchived', true); - }, - /Attempted to set 'isArchived' to 'true' on the deleted record /, - 'Assertion includes more context when in DEBUG' - ); - } else { - assert.throws( - () => { - set(record, 'isArchived', true); - }, - /Attempted to set 'isArchived' on the deleted record /, - "Assertion does not leak the 'value'" - ); - } + assert.expectAssertion( + () => { + record.isArchived = true; + }, + /Attempted to set 'isArchived' on the deleted record /, + "Assertion does not leak the 'value'" + ); currentState = record.currentState; @@ -300,10 +289,10 @@ module('unit/model - Model', function (hooks) { ], }); - assert.true(store.hasRecordForId('person', 1), 'should have person with id 1'); - assert.true(store.hasRecordForId('person', 1), 'should have person with id 1'); - assert.false(store.hasRecordForId('person', 4), 'should not have person with id 4'); - assert.false(store.hasRecordForId('person', 4), 'should not have person with id 4'); + assert.notStrictEqual(store.peekRecord('person', '1'), null, 'should have person with id 1'); + assert.notStrictEqual(store.peekRecord('person', '1'), null, 'should have person with id 1'); + assert.strictEqual(store.peekRecord('person', '4'), null, 'should not have person with id 4'); + assert.strictEqual(store.peekRecord('person', '4'), null, 'should not have person with id 4'); }); test('setting the id during createRecord should correctly update the id', async function (assert) { diff --git a/packages/-ember-data/tests/unit/model/relationships/has-many-test.js b/packages/-ember-data/tests/unit/model/relationships/has-many-test.js index 03a8ad0aa9b..f8502dc58ed 100644 --- a/packages/-ember-data/tests/unit/model/relationships/has-many-test.js +++ b/packages/-ember-data/tests/unit/model/relationships/has-many-test.js @@ -1298,9 +1298,7 @@ module('unit/model/relationships - DS.hasMany', function (hooks) { ); run(() => { - return shen.destroyRecord({}).then(() => { - shen.unloadRecord(); - }); + return shen.destroyRecord({}); }); assert.deepEqual( @@ -1637,8 +1635,6 @@ module('unit/model/relationships - DS.hasMany', function (hooks) { assert.strictEqual(get(petsProxy, 'length'), 3, 'precond2 - proxy now reflects three pets'); return shen.destroyRecord({}).then(() => { - shen.unloadRecord(); - assert.deepEqual( pets.map((p) => get(p, 'id')), ['2', '3'], @@ -1723,8 +1719,6 @@ module('unit/model/relationships - DS.hasMany', function (hooks) { return run(() => { return shen.destroyRecord({}).then(() => { - shen.unloadRecord(); - dog = person.get('dog'); assert.strictEqual(dog, rambo, 'The currentState of the belongsTo was preserved after the delete'); }); @@ -1803,8 +1797,6 @@ module('unit/model/relationships - DS.hasMany', function (hooks) { assert.strictEqual(dog, rambo, 'precond2 - relationship was updated'); return shen.destroyRecord({}).then(() => { - shen.unloadRecord(); - dog = person.get('dog.content'); assert.strictEqual(dog, rambo, 'The currentState of the belongsTo was preserved after the delete'); }); @@ -1885,8 +1877,6 @@ module('unit/model/relationships - DS.hasMany', function (hooks) { return run(() => { return rambo.destroyRecord({}).then(() => { - rambo.unloadRecord(); - dog = person.get('dog'); assert.strictEqual(dog, null, 'The current state of the belongsTo was clearer'); }); @@ -2076,8 +2066,6 @@ module('unit/model/relationships - DS.hasMany', function (hooks) { return run(() => { return shen.destroyRecord({}).then(() => { - shen.unloadRecord(); - // were ember-data to now preserve local edits during a relationship push, this would be '2' assert.deepEqual( pets.map((p) => get(p, 'id')), diff --git a/packages/-ember-data/tests/unit/record-arrays/record-array-test.js b/packages/-ember-data/tests/unit/record-arrays/record-array-test.js index 2a3f46bb892..955949f6c15 100644 --- a/packages/-ember-data/tests/unit/record-arrays/record-array-test.js +++ b/packages/-ember-data/tests/unit/record-arrays/record-array-test.js @@ -305,10 +305,6 @@ module('unit/record-arrays/record-array - DS.RecordArray', function (hooks) { let model2 = { id: '2', type: 'tag', - save() { - model2Saved++; - return this; - }, }; let [record1, record2] = store.push({ @@ -319,11 +315,11 @@ module('unit/record-arrays/record-array - DS.RecordArray', function (hooks) { content: identifiers, store, }); - record1._internalModel.save = () => { + record1.save = () => { model1Saved++; return resolve(this); }; - record2._internalModel.save = () => { + record2.save = () => { model2Saved++; return resolve(this); }; diff --git a/packages/-ember-data/tests/unit/store/adapter-interop-test.js b/packages/-ember-data/tests/unit/store/adapter-interop-test.js index 4198f594803..26d5a975084 100644 --- a/packages/-ember-data/tests/unit/store/adapter-interop-test.js +++ b/packages/-ember-data/tests/unit/store/adapter-interop-test.js @@ -745,13 +745,13 @@ module('unit/store/adapter-interop - Store working with a Adapter', function (ho store.createRecord('test'); - let internalModels = [ - store._internalModelForResource({ type: 'test', id: '10' }), - store._internalModelForResource({ type: 'phone', id: '20' }), - store._internalModelForResource({ type: 'phone', id: '21' }), + let identifiers = [ + store.identifierCache.getOrCreateRecordIdentifier({ type: 'test', id: '10' }), + store.identifierCache.getOrCreateRecordIdentifier({ type: 'phone', id: '20' }), + store.identifierCache.getOrCreateRecordIdentifier({ type: 'phone', id: '21' }), ]; - await store._scheduleFetchMany(internalModels); + await store._scheduleFetchMany(identifiers); const records = [store.peekRecord('test', '10'), store.peekRecord('phone', '20'), store.peekRecord('phone', '21')]; @@ -760,7 +760,7 @@ module('unit/store/adapter-interop - Store working with a Adapter', function (ho assert.strictEqual(unloadedRecords.length, 0, 'All unloaded records should be loaded'); }); - test('the store calls adapter.findMany according to groupings returned by adapter.groupRecordsForFindMany', function (assert) { + test('the store calls adapter.findMany according to groupings returned by adapter.groupRecordsForFindMany', async function (assert) { assert.expect(3); const ApplicationAdapter = Adapter.extend({ @@ -788,18 +788,16 @@ module('unit/store/adapter-interop - Store working with a Adapter', function (ho let store = this.owner.lookup('service:store'); - let internalModels = [ - store._internalModelForResource({ type: 'test', id: '10' }), - store._internalModelForResource({ type: 'test', id: '20' }), - store._internalModelForResource({ type: 'test', id: '21' }), + let identifiers = [ + store.identifierCache.getOrCreateRecordIdentifier({ type: 'test', id: '10' }), + store.identifierCache.getOrCreateRecordIdentifier({ type: 'test', id: '20' }), + store.identifierCache.getOrCreateRecordIdentifier({ type: 'test', id: '21' }), ]; - return run(() => { - return store._scheduleFetchMany(internalModels).then(() => { - let ids = internalModels.map((x) => x.id); - assert.deepEqual(ids, ['10', '20', '21'], 'The promise fulfills with the records'); - }); - }); + const result = await store._scheduleFetchMany(identifiers); + + let ids = result.map((x) => x.id); + assert.deepEqual(ids, ['10', '20', '21'], 'The promise fulfills with the identifiers'); }); test('the promise returned by `_scheduleFetch`, when it resolves, does not depend on the promises returned to other calls to `_scheduleFetch` that are in the same run loop, but different groups', function (assert) { @@ -1344,18 +1342,6 @@ module('unit/store/adapter-interop - Store working with a Adapter', function (ho return done; }); - testInDebug('store should assert of the user tries to call store.filter', function (assert) { - assert.expect(1); - - this.owner.register('model:person', Model.extend()); - - let store = this.owner.lookup('service:store'); - - assert.expectAssertion(() => { - run(() => store.filter('person', {})); - }, /The filter API has been moved to a plugin/); - }); - testInDebug('Calling adapterFor with a model class should assert', function (assert) { let Person = Model.extend(); diff --git a/packages/-ember-data/tests/unit/store/asserts-test.js b/packages/-ember-data/tests/unit/store/asserts-test.js index 956d3f6853b..464dfb5ef71 100644 --- a/packages/-ember-data/tests/unit/store/asserts-test.js +++ b/packages/-ember-data/tests/unit/store/asserts-test.js @@ -22,10 +22,7 @@ module('unit/store/asserts - DS.Store methods produce useful assertion messages' const MODEL_NAME_METHODS = [ 'createRecord', 'findRecord', - 'findByIds', 'peekRecord', - 'hasRecordForId', - 'recordForId', 'query', 'queryRecord', 'findAll', @@ -58,25 +55,16 @@ module('unit/store/asserts - DS.Store methods produce useful assertion messages' 'unloadRecord', 'find', 'findRecord', - 'findByIds', 'getReference', 'peekRecord', - 'hasRecordForId', - 'recordForId', - 'findMany', - 'findHasMany', - 'findBelongsTo', 'query', 'queryRecord', 'findAll', 'peekAll', 'unloadAll', 'didSaveRecord', - 'recordWasInvalid', - 'recordWasError', 'modelFor', '_modelFactoryFor', - '_hasModelFor', 'push', '_push', 'pushPayload', diff --git a/packages/-ember-data/tests/unit/store/create-record-test.js b/packages/-ember-data/tests/unit/store/create-record-test.js index 60f29f0e0c5..ae9ef6c08b0 100644 --- a/packages/-ember-data/tests/unit/store/create-record-test.js +++ b/packages/-ember-data/tests/unit/store/create-record-test.js @@ -1,6 +1,3 @@ -import { A } from '@ember/array'; -import { run } from '@ember/runloop'; - import { module, test } from 'qunit'; import { setupTest } from 'ember-qunit'; @@ -32,28 +29,23 @@ module('unit/store/createRecord - Store creating records', function (hooks) { this.owner.register('model:author', Author); let store = this.owner.lookup('service:store'); - - let comment, author; - - run(() => { - comment = store.push({ - data: { - type: 'comment', - id: '1', - attributes: { - text: 'Hello darkness my old friend', - }, + let comment = store.push({ + data: { + type: 'comment', + id: '1', + attributes: { + text: 'Hello darkness my old friend', }, - }); - author = store.push({ - data: { - type: 'author', - id: '1', - attributes: { - name: '@runspired', - }, + }, + }); + let author = store.push({ + data: { + type: 'author', + id: '1', + attributes: { + name: '@runspired', }, - }); + }, }); let properties = { @@ -88,41 +80,38 @@ module('unit/store/createRecord - Store creating records', function (hooks) { this.owner.register('model:storage', Storage); let store = this.owner.lookup('service:store'); - let records, storage; - - run(() => { - store.push({ - data: [ - { - type: 'record', - id: '1', - attributes: { - title: "it's a beautiful day", - }, + + store.push({ + data: [ + { + type: 'record', + id: '1', + attributes: { + title: "it's a beautiful day", }, - { - type: 'record', - id: '2', - attributes: { - title: "it's a beautiful day", - }, + }, + { + type: 'record', + id: '2', + attributes: { + title: "it's a beautiful day", }, - ], - }); - - records = store.peekAll('record'); - storage = store.createRecord('storage', { name: 'Great store', records: records }); + }, + ], }); + let records = store.peekAll('record').toArray(); + let storage = store.createRecord('storage', { name: 'Great store', records: records }); + assert.strictEqual(storage.get('name'), 'Great store', 'The attribute is well defined'); assert.strictEqual( storage.get('records').findBy('id', '1'), - A(records).findBy('id', '1'), + records.find((r) => r.id === '1'), 'Defined relationships are allowed in createRecord' ); assert.strictEqual( storage.get('records').findBy('id', '2'), - A(records).findBy('id', '2'), + records.find((r) => r.id === '2'), 'Defined relationships are allowed in createRecord' ); }); diff --git a/packages/-ember-data/tests/unit/store/has-model-for-test.js b/packages/-ember-data/tests/unit/store/has-model-for-test.js deleted file mode 100644 index 843b3f74956..00000000000 --- a/packages/-ember-data/tests/unit/store/has-model-for-test.js +++ /dev/null @@ -1,19 +0,0 @@ -import { module, test } from 'qunit'; - -import { setupTest } from 'ember-qunit'; - -import Model from '@ember-data/model'; - -module('unit/store/has-model-For', function (hooks) { - setupTest(hooks); - - test(`hasModelFor correctly normalizes`, function (assert) { - this.owner.register('model:one-foo', Model.extend({})); - this.owner.register('model:two-foo', Model.extend({})); - - let store = this.owner.lookup('service:store'); - - assert.true(store._hasModelFor('oneFoo')); - assert.true(store._hasModelFor('twoFoo')); - }); -}); diff --git a/packages/-ember-data/tests/unit/store/has-record-for-id-test.js b/packages/-ember-data/tests/unit/store/has-record-for-id-test.js deleted file mode 100644 index 90bef4b59c8..00000000000 --- a/packages/-ember-data/tests/unit/store/has-record-for-id-test.js +++ /dev/null @@ -1,75 +0,0 @@ -import { run } from '@ember/runloop'; - -import { module, test } from 'qunit'; - -import { setupTest } from 'ember-qunit'; - -import Model, { attr, belongsTo, hasMany } from '@ember-data/model'; - -module('unit/store/hasRecordForId - Store hasRecordForId', function (hooks) { - setupTest(hooks); - - hooks.beforeEach(function () { - const Person = Model.extend({ - firstName: attr('string'), - lastName: attr('string'), - phoneNumbers: hasMany('phone-number', { async: false }), - }); - - const PhoneNumber = Model.extend({ - number: attr('string'), - person: belongsTo('person', { async: false }), - }); - - this.owner.register('model:person', Person); - this.owner.register('model:phone-number', PhoneNumber); - }); - - test('hasRecordForId should return false for records in the empty state ', function (assert) { - let store = this.owner.lookup('service:store'); - - run(() => { - store.push({ - data: { - type: 'person', - id: '1', - attributes: { - firstName: 'Yehuda', - lastName: 'Katz', - }, - relationships: { - phoneNumbers: { - data: [{ type: 'phone-number', id: '1' }], - }, - }, - }, - }); - - assert.false(store.hasRecordForId('phone-number', 1), 'hasRecordForId only returns true for loaded records'); - }); - }); - - test('hasRecordForId should return true for records in the loaded state ', function (assert) { - let store = this.owner.lookup('service:store'); - - run(() => { - store.push({ - data: { - type: 'person', - id: '1', - attributes: { - firstName: 'Yehuda', - lastName: 'Katz', - }, - relationships: { - phoneNumbers: { - data: [{ type: 'phone-number', id: '1' }], - }, - }, - }, - }); - - assert.true(store.hasRecordForId('person', 1), 'hasRecordForId returns true for records loaded into the store'); - }); - }); -}); diff --git a/packages/-ember-data/tests/unit/store/unload-test.js b/packages/-ember-data/tests/unit/store/unload-test.js index 8c1002fa12e..3317b2d6d13 100644 --- a/packages/-ember-data/tests/unit/store/unload-test.js +++ b/packages/-ember-data/tests/unit/store/unload-test.js @@ -116,7 +116,7 @@ module('unit/store/unload - Store unloading records', function (hooks) { test('unload followed by create of the same type + id', function (assert) { let record = store.createRecord('record', { id: 1 }); - assert.strictEqual(store.recordForId('record', 1), record, 'record should exactly equal'); + assert.strictEqual(store.peekRecord('record', 1), record, 'record should exactly equal'); return run(() => { record.unloadRecord(); diff --git a/packages/-ember-data/tests/unit/system/snapshot-record-array-test.js b/packages/-ember-data/tests/unit/system/snapshot-record-array-test.js index da746a31024..d2c4a21d8f1 100644 --- a/packages/-ember-data/tests/unit/system/snapshot-record-array-test.js +++ b/packages/-ember-data/tests/unit/system/snapshot-record-array-test.js @@ -3,11 +3,11 @@ import { A } from '@ember/array'; import { module, test } from 'qunit'; import { SnapshotRecordArray } from '@ember-data/store/-private'; +import { deprecatedTest } from '@ember-data/unpublished-test-infra/test-support/deprecated-test'; module('Unit - snapshot-record-array', function () { test('constructor', function (assert) { let array = A([1, 2]); - array.type = 'some type'; let meta = {}; let options = { adapterOptions: 'some options', @@ -18,7 +18,6 @@ module('Unit - snapshot-record-array', function () { assert.strictEqual(snapshot.length, 2); assert.strictEqual(snapshot.meta, meta); - assert.strictEqual(snapshot.type, 'some type'); assert.strictEqual(snapshot.adapterOptions, 'some options'); assert.strictEqual(snapshot.include, 'include me'); }); @@ -49,27 +48,35 @@ module('Unit - snapshot-record-array', function () { assert.strictEqual(didTakeSnapshot, 1, 'still only one snapshot should have been taken'); }); - test('SnapshotRecordArray.type loads the class lazily', function (assert) { - let array = A([1, 2]); - let typeLoaded = false; + deprecatedTest( + 'SnapshotRecordArray.type loads the class lazily', + { + id: 'ember-data:deprecate-snapshot-model-class-access', + count: 1, + until: '5.0', + }, + function (assert) { + let array = A([1, 2]); + let typeLoaded = false; - Object.defineProperty(array, 'type', { - get() { - typeLoaded = true; - return 'some type'; - }, - }); + Object.defineProperty(array, 'type', { + get() { + typeLoaded = true; + return 'some type'; + }, + }); - let meta = {}; - let options = { - adapterOptions: 'some options', - include: 'include me', - }; + let meta = {}; + let options = { + adapterOptions: 'some options', + include: 'include me', + }; - let snapshot = new SnapshotRecordArray(array, meta, options); + let snapshot = new SnapshotRecordArray(array, meta, options); - assert.false(typeLoaded, 'model class is not eager loaded'); - assert.strictEqual(snapshot.type, 'some type'); - assert.true(typeLoaded, 'model class is loaded'); - }); + assert.false(typeLoaded, 'model class is not eager loaded'); + assert.strictEqual(snapshot.type, 'some type'); + assert.true(typeLoaded, 'model class is loaded'); + } + ); }); diff --git a/packages/adapter/addon/index.ts b/packages/adapter/addon/index.ts index 60ee934fb42..22a9e13d1b0 100644 --- a/packages/adapter/addon/index.ts +++ b/packages/adapter/addon/index.ts @@ -136,12 +136,13 @@ */ import EmberObject from '@ember/object'; +import { inject as service } from '@ember/service'; import { DEBUG } from '@glimmer/env'; import { Promise as RSVPPromise } from 'rsvp'; +import type Store from '@ember-data/store'; import type { Snapshot } from '@ember-data/store/-private'; -import type Store from '@ember-data/store/-private/system/core-store'; import type ShimModelClass from '@ember-data/store/-private/system/model/shim-model-class'; import type SnapshotRecordArray from '@ember-data/store/-private/system/snapshot-record-array'; import type { @@ -206,6 +207,8 @@ import type { Dict } from '@ember-data/store/-private/ts-interfaces/utils'; @extends Ember.EmberObject */ export default class Adapter extends EmberObject implements MinimumAdapterInterface { + @service declare store: Store; + declare _coalesceFindRequests: boolean; /** diff --git a/packages/model/addon/-private/model.js b/packages/model/addon/-private/model.js index d5cb8f81f9f..1ecb316d4ef 100644 --- a/packages/model/addon/-private/model.js +++ b/packages/model/addon/-private/model.js @@ -7,19 +7,22 @@ import EmberError from '@ember/error'; import EmberObject, { get } from '@ember/object'; import { dependentKeyCompat } from '@ember/object/compat'; import { run } from '@ember/runloop'; +import { inject as service } from '@ember/service'; import { isNone } from '@ember/utils'; import { DEBUG } from '@glimmer/env'; import { tracked } from '@glimmer/tracking'; import Ember from 'ember'; +import { resolve } from 'rsvp'; + import { HAS_DEBUG_PACKAGE } from '@ember-data/private-build-infra'; import { DEPRECATE_SAVE_PROMISE_ACCESS } from '@ember-data/private-build-infra/deprecations'; +import { recordIdentifierFor, storeFor } from '@ember-data/store'; import { coerceId, deprecatedPromiseObject, errorsArrayToHash, InternalModel, - PromiseObject, recordDataFor, } from '@ember-data/store/-private'; @@ -104,6 +107,8 @@ function computeOnce(target, key, desc) { @extends Ember.EmberObject */ class Model extends EmberObject { + @service store; + init(options = {}) { const createProps = options._createProps; delete options._createProps; @@ -389,9 +394,9 @@ class Model extends EmberObject { Example ```javascript - record.get('isReloading'); // false + record.isReloading; // false record.reload(); - record.get('isReloading'); // true + record.isReloading; // true ``` @property isReloading @@ -626,7 +631,7 @@ class Model extends EmberObject { @public */ deleteRecord() { - this.store.deleteRecord(this); + storeFor(this).deleteRecord(this); } /** @@ -675,7 +680,11 @@ class Model extends EmberObject { successfully or rejected if the adapter returns with an error. */ destroyRecord(options) { + const { isNew } = this.currentState; this.deleteRecord(); + if (isNew) { + return resolve(this); + } return this.save(options).then((_) => { run(() => { this.unloadRecord(); @@ -692,10 +701,10 @@ class Model extends EmberObject { @public */ unloadRecord() { - if (this.isDestroyed) { + if (this.isNew && (this.isDestroyed || this.isDestroying)) { return; } - this.store.unloadRecord(this); + storeFor(this).unloadRecord(this); } /** @@ -845,7 +854,14 @@ class Model extends EmberObject { successfully or rejected if the adapter returns with an error. */ save(options) { - const promise = this._internalModel.save(options).then(() => this); + let promise; + + if (this.currentState.isNew && this.currentState.isDeleted) { + promise = resolve(this); + } else { + promise = storeFor(this).saveRecord(this, options); + } + if (DEPRECATE_SAVE_PROMISE_ACCESS) { return deprecatedPromiseObject(promise); } @@ -882,24 +898,28 @@ class Model extends EmberObject { adapter returns successfully or rejected if the adapter returns with an error. */ - reload(options) { - let wrappedAdapterOptions; + reload(_options) { + let options = {}; - if (typeof options === 'object' && options !== null && options.adapterOptions) { - wrappedAdapterOptions = { - adapterOptions: options.adapterOptions, - }; + if (typeof _options === 'object' && _options !== null && _options.adapterOptions) { + options.adapterOptions = _options.adapterOptions; } + options.isReloading = true; + let identifier = recordIdentifierFor(this); + assert(`You cannot reload a record without an ID`, identifier.id); this.isReloading = true; - return PromiseObject.create({ - promise: this._internalModel - .reload(wrappedAdapterOptions) - .then(() => this) - .finally(() => { - this.isReloading = false; - }), - }); + const promise = storeFor(this) + ._fetchManager.scheduleFetch(identifier, options) + .then(() => this) + .finally(() => { + this.isReloading = false; + }); + + if (DEPRECATE_SAVE_PROMISE_ACCESS) { + return deprecatedPromiseObject(promise); + } + return promise; } attr() { @@ -1102,7 +1122,7 @@ class Model extends EmberObject { } inverseFor(key) { - return this.constructor.inverseFor(key, this._internalModel.store); + return this.constructor.inverseFor(key, storeFor(this)); } eachAttribute(callback, binding) { @@ -1929,7 +1949,6 @@ 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.store = null; Model.prototype._createProps = null; if (HAS_DEBUG_PACKAGE) { diff --git a/packages/model/addon/-private/record-state.ts b/packages/model/addon/-private/record-state.ts index 4d66c533812..cbe3a64f0b3 100644 --- a/packages/model/addon/-private/record-state.ts +++ b/packages/model/addon/-private/record-state.ts @@ -152,9 +152,7 @@ export default class RecordState { declare _lastError: any; constructor(record: Model) { - const { store } = record; - - let id = record._internalModel.identifier; + const { store, identifier: identity } = record._internalModel; this.record = record; this.recordData = record._internalModel._recordData; @@ -169,7 +167,7 @@ export default class RecordState { let requests = store.getRequestStateService(); let notifications = store._notificationManager; - requests.subscribeForRecord(id, (req) => { + requests.subscribeForRecord(identity, (req) => { if (req.type === 'mutation') { switch (req.state) { case 'pending': @@ -220,7 +218,7 @@ export default class RecordState { } }); - notifications.subscribe(id, (identifier: StableRecordIdentifier, type: NotificationType, key?: string) => { + notifications.subscribe(identity, (identifier: StableRecordIdentifier, type: NotificationType, key?: string) => { notifyChanges(identifier, type, key, record, store); switch (type) { case 'state': diff --git a/packages/model/index.js b/packages/model/index.js index 6a2b988678b..8dfe1640708 100644 --- a/packages/model/index.js +++ b/packages/model/index.js @@ -29,6 +29,7 @@ module.exports = Object.assign({}, addonBaseConfig, { '@ember/polyfills', '@ember/runloop', '@ember/utils', + '@ember/service', '@glimmer/tracking/primitives/cache', '@glimmer/tracking', diff --git a/packages/private-build-infra/addon/current-deprecations.ts b/packages/private-build-infra/addon/current-deprecations.ts index f1e16247b49..391c760edef 100644 --- a/packages/private-build-infra/addon/current-deprecations.ts +++ b/packages/private-build-infra/addon/current-deprecations.ts @@ -41,4 +41,8 @@ export default { DEPRECATE_3_12: '3.12', DEPRECATE_RSVP_PROMISE: '4.4', DEPRECATE_SAVE_PROMISE_ACCESS: '4.4', + DEPRECATE_SNAPSHOT_MODEL_CLASS_ACCESS: '4.5', + DEPRECATE_STORE_FIND: '4.5', + DEPRECATE_HAS_RECORD: '4.5', + DEPRECATE_RECORD_WAS_INVALID: '4.5', }; diff --git a/packages/private-build-infra/addon/deprecations.ts b/packages/private-build-infra/addon/deprecations.ts index f9c7aa21e62..43a90b4868b 100644 --- a/packages/private-build-infra/addon/deprecations.ts +++ b/packages/private-build-infra/addon/deprecations.ts @@ -10,3 +10,7 @@ export const DEPRECATE_CATCH_ALL = deprecationState('DEPRECATE_CATCH_ALL'); export const DEPRECATE_3_12 = deprecationState('DEPRECATE_3_12'); export const DEPRECATE_SAVE_PROMISE_ACCESS = deprecationState('DEPRECATE_SAVE_PROMISE_ACCESS'); export const DEPRECATE_RSVP_PROMISE = deprecationState('DEPRECATE_RSVP_PROMISE'); +export const DEPRECATE_SNAPSHOT_MODEL_CLASS_ACCESS = deprecationState('DEPRECATE_SNAPSHOT_MODEL_CLASS_ACCESS'); +export const DEPRECATE_STORE_FIND = deprecationState('DEPRECATE_STORE_FIND'); +export const DEPRECATE_HAS_RECORD = deprecationState('DEPRECATE_HAS_RECORD'); +export const DEPRECATE_RECORD_WAS_INVALID = deprecationState('DEPRECATE_RECORD_WAS_INVALID'); diff --git a/packages/record-data/addon/-private/graph/-utils.ts b/packages/record-data/addon/-private/graph/-utils.ts index ade84cdbd4e..331ff7951aa 100644 --- a/packages/record-data/addon/-private/graph/-utils.ts +++ b/packages/record-data/addon/-private/graph/-utils.ts @@ -123,6 +123,6 @@ export function assertRelationshipData(store, identifier, data, meta) { ); assert( `Encountered a relationship identifier with type '${data.type}' for the ${meta.kind} relationship '${meta.key}' on <${identifier.type}:${identifier.id}>, Expected a json-api identifier with type '${meta.type}'. No model was found for '${data.type}'.`, - data === null || !data.type || store._hasModelFor(data.type) + data === null || !data.type || store.getSchemaDefinitionService().doesTypeExist(data.type) ); } diff --git a/packages/record-data/addon/-private/graph/operations/update-relationship.ts b/packages/record-data/addon/-private/graph/operations/update-relationship.ts index 0a85556ac9b..acc997cab46 100644 --- a/packages/record-data/addon/-private/graph/operations/update-relationship.ts +++ b/packages/record-data/addon/-private/graph/operations/update-relationship.ts @@ -111,7 +111,7 @@ export default function updateRelationshipOperation(graph: Graph, op: UpdateRela /* Data being pushed into the relationship might contain only data or links, or a combination of both. - + IF contains only data IF contains both links and data state.isEmpty -> true if is empty array (has-many) or is null (belongs-to) @@ -119,7 +119,7 @@ export default function updateRelationshipOperation(graph: Graph, op: UpdateRela hasDematerializedInverse -> false state.isStale -> false allInverseRecordsAreLoaded -> run-check-to-determine - + IF contains only links state.isStale -> true */ diff --git a/packages/serializer/addon/-private/embedded-records-mixin.js b/packages/serializer/addon/-private/embedded-records-mixin.js index 037d011a8da..afe2bd864e8 100644 --- a/packages/serializer/addon/-private/embedded-records-mixin.js +++ b/packages/serializer/addon/-private/embedded-records-mixin.js @@ -212,7 +212,8 @@ export default Mixin.create({ let includeRecords = this.hasSerializeRecordsOption(attr); let embeddedSnapshot = snapshot.belongsTo(attr); if (includeIds) { - let serializedKey = this._getMappedKey(relationship.key, snapshot.type); + let schema = this.store.modelFor(snapshot.modelName); + let serializedKey = this._getMappedKey(relationship.key, schema); if (serializedKey === relationship.key && this.keyForRelationship) { serializedKey = this.keyForRelationship(relationship.key, relationship.kind, 'serialize'); } @@ -233,7 +234,8 @@ export default Mixin.create({ _serializeEmbeddedBelongsTo(snapshot, json, relationship) { let embeddedSnapshot = snapshot.belongsTo(relationship.key); - let serializedKey = this._getMappedKey(relationship.key, snapshot.type); + let schema = this.store.modelFor(snapshot.modelName); + let serializedKey = this._getMappedKey(relationship.key, schema); if (serializedKey === relationship.key && this.keyForRelationship) { serializedKey = this.keyForRelationship(relationship.key, relationship.kind, 'serialize'); } @@ -396,7 +398,8 @@ export default Mixin.create({ } if (this.hasSerializeIdsOption(attr)) { - let serializedKey = this._getMappedKey(relationship.key, snapshot.type); + let schema = this.store.modelFor(snapshot.modelName); + let serializedKey = this._getMappedKey(relationship.key, schema); if (serializedKey === relationship.key && this.keyForRelationship) { serializedKey = this.keyForRelationship(relationship.key, relationship.kind, 'serialize'); } @@ -433,7 +436,8 @@ export default Mixin.create({ }, _serializeEmbeddedHasMany(snapshot, json, relationship) { - let serializedKey = this._getMappedKey(relationship.key, snapshot.type); + let schema = this.store.modelFor(snapshot.modelName); + let serializedKey = this._getMappedKey(relationship.key, schema); if (serializedKey === relationship.key && this.keyForRelationship) { serializedKey = this.keyForRelationship(relationship.key, relationship.kind, 'serialize'); } @@ -484,7 +488,8 @@ export default Mixin.create({ */ removeEmbeddedForeignKey(snapshot, embeddedSnapshot, relationship, json) { if (relationship.kind === 'belongsTo') { - let parentRecord = snapshot.type.inverseFor(relationship.key, this.store); + let schema = this.store.modelFor(snapshot.modelName); + let parentRecord = schema.inverseFor(relationship.key, this.store); if (parentRecord) { let name = parentRecord.name; let embeddedSerializer = this.store.serializerFor(embeddedSnapshot.modelName); diff --git a/packages/serializer/addon/index.ts b/packages/serializer/addon/index.ts index a589a7e4804..e8804a2c768 100644 --- a/packages/serializer/addon/index.ts +++ b/packages/serializer/addon/index.ts @@ -100,6 +100,9 @@ */ import EmberObject from '@ember/object'; +import { inject as service } from '@ember/service'; + +import type Store from '@ember-data/store'; /** `Serializer` is an abstract base class that you should override in your @@ -121,7 +124,8 @@ import EmberObject from '@ember/object'; @extends Ember.EmberObject */ -export default EmberObject.extend({ +export default class extends EmberObject { + @service declare store: Store; /** The `store` property is the application's `store` that contains all records. It can be used to look up serializers for other model @@ -179,7 +183,6 @@ export default EmberObject.extend({ @param {String} requestType @return {Object} JSON-API Document */ - normalizeResponse: null, /** The `serialize` method is used when a record is saved in order to convert @@ -222,7 +225,6 @@ export default EmberObject.extend({ @param {Object} [options] @return {Object} */ - serialize: null, /** The `normalize` method is used to convert a payload received from your @@ -253,5 +255,5 @@ export default EmberObject.extend({ */ normalize(typeClass, hash) { return hash; - }, -}); + } +} diff --git a/packages/serializer/addon/json-api.js b/packages/serializer/addon/json-api.js index 4d46d13e177..79939dbd14f 100644 --- a/packages/serializer/addon/json-api.js +++ b/packages/serializer/addon/json-api.js @@ -187,16 +187,14 @@ const JSONAPISerializer = JSONSerializer.extend({ @private */ _normalizeResourceHelper(resourceHash) { - assert(this.warnMessageForUndefinedType(), !isNone(resourceHash.type), { - id: 'ds.serializer.type-is-undefined', - }); + assert(this.warnMessageForUndefinedType(), !isNone(resourceHash.type)); let modelName, usedLookup; modelName = this.modelNameFromPayloadKey(resourceHash.type); usedLookup = 'modelNameFromPayloadKey'; - if (!this.store._hasModelFor(modelName)) { + if (!this.store.getSchemaDefinitionService().doesTypeExist(modelName)) { warn(this.warnMessageNoModelForType(modelName, resourceHash.type, usedLookup), false, { id: 'ds.serializer.model-for-type-missing', }); @@ -243,10 +241,7 @@ const JSONAPISerializer = JSONSerializer.extend({ assert( 'Expected the primary data returned by the serializer for a `queryRecord` response to be a single object but instead it was an array.', - !Array.isArray(normalized.data), - { - id: 'ds.serializer.json-api.queryRecord-array-response', - } + !Array.isArray(normalized.data) ); return normalized; @@ -536,7 +531,7 @@ const JSONAPISerializer = JSONSerializer.extend({ return data formatted to match your API's expectations, or override the invoked adapter method and do the serialization in the adapter directly by using the provided snapshot. - + If your API's format differs greatly from the JSON:API spec, you should consider authoring your own adapter and serializer instead of extending this class. @@ -659,7 +654,8 @@ const JSONAPISerializer = JSONSerializer.extend({ value = transform.serialize(value, attribute.options); } - let payloadKey = this._getMappedKey(key, snapshot.type); + let schema = this.store.modelFor(snapshot.modelName); + let payloadKey = this._getMappedKey(key, schema); if (payloadKey === key) { payloadKey = this.keyForAttribute(key, 'serialize'); @@ -679,7 +675,8 @@ const JSONAPISerializer = JSONSerializer.extend({ if (belongsTo === null || belongsToIsNotNew) { json.relationships = json.relationships || {}; - let payloadKey = this._getMappedKey(key, snapshot.type); + let schema = this.store.modelFor(snapshot.modelName); + let payloadKey = this._getMappedKey(key, schema); if (payloadKey === key) { payloadKey = this.keyForRelationship(key, 'belongsTo', 'serialize'); } @@ -707,7 +704,8 @@ const JSONAPISerializer = JSONSerializer.extend({ if (hasMany !== undefined) { json.relationships = json.relationships || {}; - let payloadKey = this._getMappedKey(key, snapshot.type); + let schema = this.store.modelFor(snapshot.modelName); + let payloadKey = this._getMappedKey(key, schema); if (payloadKey === key && this.keyForRelationship) { payloadKey = this.keyForRelationship(key, 'hasMany', 'serialize'); } @@ -739,10 +737,7 @@ if (DEBUG) { assert( `You've used the EmbeddedRecordsMixin in ${this.toString()} which is not fully compatible with the JSON:API specification. Please confirm that this works for your specific API and add \`this.isEmbeddedRecordsMixinCompatible = true\` to your serializer.`, - !this.isEmbeddedRecordsMixin || this.isEmbeddedRecordsMixinCompatible === true, - { - id: 'ds.serializer.embedded-records-mixin-not-supported', - } + !this.isEmbeddedRecordsMixin || this.isEmbeddedRecordsMixinCompatible === true ); let constructor = this.constructor; diff --git a/packages/serializer/addon/json.js b/packages/serializer/addon/json.js index 749719adef6..fc75eabef7c 100644 --- a/packages/serializer/addon/json.js +++ b/packages/serializer/addon/json.js @@ -938,7 +938,8 @@ const JSONSerializer = Serializer.extend({ @return {boolean} true if the hasMany relationship should be serialized */ shouldSerializeHasMany(snapshot, key, relationship) { - let relationshipType = snapshot.type.determineRelationshipType(relationship, this.store); + const schema = this.store.modelFor(snapshot.modelName); + let relationshipType = schema.determineRelationshipType(relationship, this.store); if (this._mustSerialize(key)) { return true; } @@ -1194,7 +1195,8 @@ const JSONSerializer = Serializer.extend({ // if provided, use the mapping provided by `attrs` in // the serializer - let payloadKey = this._getMappedKey(key, snapshot.type); + let schema = this.store.modelFor(snapshot.modelName); + let payloadKey = this._getMappedKey(key, schema); if (payloadKey === key && this.keyForAttribute) { payloadKey = this.keyForAttribute(key, 'serialize'); @@ -1240,7 +1242,8 @@ const JSONSerializer = Serializer.extend({ // if provided, use the mapping provided by `attrs` in // the serializer - let payloadKey = this._getMappedKey(key, snapshot.type); + let schema = this.store.modelFor(snapshot.modelName); + let payloadKey = this._getMappedKey(key, schema); if (payloadKey === key && this.keyForRelationship) { payloadKey = this.keyForRelationship(key, 'belongsTo', 'serialize'); } @@ -1293,7 +1296,8 @@ const JSONSerializer = Serializer.extend({ if (hasMany !== undefined) { // if provided, use the mapping provided by `attrs` in // the serializer - let payloadKey = this._getMappedKey(key, snapshot.type); + let schema = this.store.modelFor(snapshot.modelName); + let payloadKey = this._getMappedKey(key, schema); if (payloadKey === key && this.keyForRelationship) { payloadKey = this.keyForRelationship(key, 'hasMany', 'serialize'); } diff --git a/packages/serializer/addon/rest.js b/packages/serializer/addon/rest.js index 4f122c8ef22..079841bdc98 100644 --- a/packages/serializer/addon/rest.js +++ b/packages/serializer/addon/rest.js @@ -202,7 +202,7 @@ const RESTSerializer = JSONSerializer.extend({ // Support polymorphic records in async relationships let modelName = this.modelNameFromPayloadKey(hash.type); - if (store._hasModelFor(modelName)) { + if (store.getSchemaDefinitionService().doesTypeExist(modelName)) { serializer = store.serializerFor(modelName); modelClass = store.modelFor(modelName); } @@ -270,7 +270,7 @@ const RESTSerializer = JSONSerializer.extend({ } var typeName = this.modelNameFromPayloadKey(modelName); - if (!store._hasModelFor(typeName)) { + if (!store.getSchemaDefinitionService().doesTypeExist(typeName)) { warn(this.warnMessageNoModelForKey(modelName, typeName), false, { id: 'ds.serializer.model-for-key-missing', }); @@ -393,7 +393,7 @@ const RESTSerializer = JSONSerializer.extend({ for (var prop in payload) { var modelName = this.modelNameFromPayloadKey(prop); - if (!store._hasModelFor(modelName)) { + if (!store.getSchemaDefinitionService().doesTypeExist(modelName)) { warn(this.warnMessageNoModelForKey(prop, modelName), false, { id: 'ds.serializer.model-for-key-missing', }); diff --git a/packages/store/addon/-private/index.ts b/packages/store/addon/-private/index.ts index 7f9c894d093..72ed97cac43 100644 --- a/packages/store/addon/-private/index.ts +++ b/packages/store/addon/-private/index.ts @@ -2,7 +2,7 @@ @module @ember-data/store */ -export { default as Store } from './system/core-store'; +export { default as Store, storeFor } from './system/core-store'; export { recordIdentifierFor } from './system/store/internal-model-factory'; diff --git a/packages/store/addon/-private/instance-cache.ts b/packages/store/addon/-private/instance-cache.ts index 1ae0d17d2fb..2b7f698f69b 100644 --- a/packages/store/addon/-private/instance-cache.ts +++ b/packages/store/addon/-private/instance-cache.ts @@ -57,7 +57,7 @@ export class InstanceCache { if (record) { this.#instances.record.delete(identifier); - this.store.teardownRecord(record); + this.store._teardownRecord(record); } return !!record; diff --git a/packages/store/addon/-private/system/core-store.ts b/packages/store/addon/-private/system/core-store.ts index df8670bbd1b..dd4460cb900 100644 --- a/packages/store/addon/-private/system/core-store.ts +++ b/packages/store/addon/-private/system/core-store.ts @@ -2,8 +2,7 @@ @module @ember-data/store */ import { getOwner, setOwner } from '@ember/application'; -import { A } from '@ember/array'; -import { assert, inspect, warn } from '@ember/debug'; +import { assert, deprecate, inspect, warn } from '@ember/debug'; import { _backburner as emberBackburner } from '@ember/runloop'; import type { Backburner } from '@ember/runloop/-private/backburner'; import Service from '@ember/service'; @@ -12,10 +11,15 @@ import { DEBUG } from '@glimmer/env'; import Ember from 'ember'; import { importSync } from '@embroider/macros'; -import { all, default as RSVP, resolve } from 'rsvp'; +import { all, default as RSVP, reject, resolve } from 'rsvp'; import type DSModelClass from '@ember-data/model'; import { HAS_RECORD_DATA_PACKAGE } from '@ember-data/private-build-infra'; +import { + DEPRECATE_HAS_RECORD, + DEPRECATE_RECORD_WAS_INVALID, + DEPRECATE_STORE_FIND, +} from '@ember-data/private-build-infra/deprecations'; import type { ManyRelationship, RecordData as RecordDataClass } from '@ember-data/record-data/-private'; import type { RelationshipState } from '@ember-data/record-data/-private/graph/-state'; @@ -86,6 +90,16 @@ const { ENV } = Ember; type AsyncTrackingToken = Readonly<{ label: string; trace: Error | string }>; const RECORD_REFERENCES = new WeakCache(DEBUG ? 'reference' : ''); +const StoreMap = new WeakCache(DEBUG ? 'store' : ''); + +export function storeFor(record: RecordInstance): CoreStore | undefined { + const store = StoreMap.get(record); + assert( + `A record in a disconnected state cannot utilize the store. This typically means the record has been destroyed, most commonly by unloading it.`, + store + ); + return store; +} function freeze(obj: T): T { if (typeof Object.freeze === 'function') { @@ -233,22 +247,6 @@ class CoreStore extends Service { @type {String} */ - /** - This property returns the adapter, after resolving a possible - string key. - - If the supplied `adapter` was a class, or a String property - path resolved to a class, this property will instantiate the - class. - - This property is cacheable, so the same instance of a specified - adapter class should be used for the lifetime of the store. - - @property defaultAdapter - @private - @return Adapter -*/ - /** @method init @private @@ -383,6 +381,7 @@ class CoreStore extends Service { //TODO Igor pass a wrapper instead of RD let record = this.instantiateRecord(identifier, createOptions, this.__recordDataFor, this._notificationManager); setRecordIdentifier(record, identifier); + StoreMap.set(record, this); return record; } @@ -396,20 +395,24 @@ class CoreStore extends Service { let internalModel = this._internalModelForResource(identifier); let createOptions: any = { - store: this, _internalModel: internalModel, // TODO deprecate allowing unknown args setting _createProps: createRecordArgs, - container: null, + container: null, // necessary hack for setOwner? }; // ensure that `getOwner(this)` works inside a model instance setOwner(createOptions, getOwner(this)); - delete createOptions.container; + let record = this._modelFactoryFor(modelName).create(createOptions); return record; } + _teardownRecord(record: DSModel | RecordInstance) { + StoreMap.delete(record); + // TODO remove identifier + this.teardownRecord(record); + } teardownRecord(record: DSModel | RecordInstance): void { assert( `expected to receive an instance of DSModel. If using a custom model make sure you implement teardownRecord`, @@ -497,32 +500,6 @@ class CoreStore extends Service { return factory; } - // Feature Flagged in DSModelStore - /** - Returns whether a ModelClass exists for a given modelName - This exists for legacy support for the RESTSerializer, - which due to how it must guess whether a key is a model - must query for whether a match exists. - - We should investigate an RFC to make this public or removing - this requirement. - - @method _hasModelFor - @private - */ - _hasModelFor(modelName) { - if (DEBUG) { - assertDestroyingStore(this, '_hasModelFor'); - } - assert(`You need to pass a model name to the store's hasModelFor method`, modelName); - assert( - `Passing classes to store methods has been removed. Please pass a dasherized string instead of ${modelName}`, - typeof modelName === 'string' - ); - - return this.getSchemaDefinitionService().doesTypeExist(modelName); - } - // ..................... // . CREATE NEW RECORD . // ..................... @@ -680,9 +657,9 @@ class CoreStore extends Service { @param {String|Integer} id @param {Object} options @return {Promise} promise + @deprecated @private */ - // TODO @runspired @deprecate find(modelName: string, id: string | number, options?): PromiseObject { if (DEBUG) { assertDestroyingStore(this, 'find'); @@ -712,7 +689,20 @@ class CoreStore extends Service { typeof modelName === 'string' ); - return this.findRecord(modelName, id); + if (DEPRECATE_STORE_FIND) { + deprecate( + `Using store.find is deprecated, use store.findRecord instead. Likely this means you are relying on the implicit store fetching behavior of routes unknowingly.`, + false, + { + id: 'ember-data:deprecate-store-find', + since: { available: '4.5', enabled: '4.5' }, + for: 'ember-data', + until: '5.0', + } + ); + return this.findRecord(modelName, id); + } + assert(`store.find has been removed. Use store.findRecord instead.`); } /** @@ -1114,20 +1104,24 @@ class CoreStore extends Service { if (!internalModel.isLoaded) { return promiseRecord( - this._findByInternalModel(internalModel, options), + this, + this._fetchDataIfNeededForIdentifier(internalModel.identifier, options), `DS: Store#findRecord ${internalModel.identifier}` ); } - let fetchedInternalModel = this._findRecord(internalModel, options); + let fetchedIdentifier = this._findRecord(internalModel, options); - return promiseRecord(fetchedInternalModel, `DS: Store#findRecord ${internalModel.identifier}`); + return promiseRecord(this, fetchedIdentifier, `DS: Store#findRecord ${internalModel.identifier}`); } - _findRecord(internalModel: InternalModel, options: FindOptions): Promise { + _findRecord(internalModel: InternalModel, options: FindOptions): Promise { + const { identifier } = internalModel; + // Refetch if the reload option is passed if (options.reload) { - return this._scheduleFetch(internalModel, options); + assertIdentifierHasId(identifier); + return this._fetchManager.scheduleFetch(identifier, options); } let snapshot = internalModel.createSnapshot(options); @@ -1139,11 +1133,12 @@ class CoreStore extends Service { adapter.shouldReloadRecord && adapter.shouldReloadRecord(this, snapshot) ) { - return this._scheduleFetch(internalModel, options); + assertIdentifierHasId(identifier); + return this._fetchManager.scheduleFetch(identifier, options); } if (options.backgroundReload === false) { - return resolve(internalModel); + return resolve(internalModel.identifier); } // Trigger the background refetch if backgroundReload option is passed @@ -1152,16 +1147,24 @@ class CoreStore extends Service { !adapter.shouldBackgroundReloadRecord || adapter.shouldBackgroundReloadRecord(this, snapshot) ) { - this._scheduleFetch(internalModel, options); + assertIdentifierHasId(identifier); + this._fetchManager.scheduleFetch(identifier, options); } // Return the cached record - return resolve(internalModel); + return resolve(internalModel.identifier); } - _findByInternalModel(internalModel: InternalModel, options: FindOptions = {}): Promise { - // pre-loading will change this value - const { isEmpty } = internalModel; + _fetchDataIfNeededForIdentifier( + identifier: StableRecordIdentifier, + options: FindOptions = {} + ): Promise { + const cache = this._instanceCache; + const internalModel = cache.getInternalModel(identifier); + + // pre-loading will change the isEmpty value + // TODO stpre this state somewhere other than InternalModel + const { isEmpty, isLoading } = internalModel; if (options.preload) { this._backburner.join(() => { @@ -1169,95 +1172,37 @@ class CoreStore extends Service { }); } + let promise; if (isEmpty) { - return this._scheduleFetch(internalModel, options); - } - - if (internalModel.isLoading) { - let pendingRequest = this._fetchManager.getPendingFetch(internalModel.identifier, options); - if (pendingRequest) { - return pendingRequest.then(() => resolve(internalModel)); - } - return this._scheduleFetch(internalModel, options); - } + assertIdentifierHasId(identifier); - return resolve(internalModel); - } - - /** - This method makes a series of requests to the adapter's `find` method - and returns a promise that resolves once they are all loaded. - - @private - @method findByIds - @param {String} modelName - @param {Array} ids - @return {Promise} promise - */ - // TODO @runspired @deprecate - findByIds(modelName, ids) { - if (DEBUG) { - assertDestroyingStore(this, 'findByIds'); - } - assert(`You need to pass a model name to the store's findByIds method`, modelName); - assert( - `Passing classes to store methods has been removed. Please pass a dasherized string instead of ${modelName}`, - typeof modelName === 'string' - ); - - let promises = new Array(ids.length); - - let normalizedModelName = normalizeModelName(modelName); - - for (let i = 0; i < ids.length; i++) { - promises[i] = this.findRecord(normalizedModelName, ids[i]); + promise = this._fetchManager.scheduleFetch(identifier, options); + } else if (isLoading) { + promise = this._fetchManager.getPendingFetch(identifier, options); + assert(`Expected to find a pending request for a record in the loading state, but found none`, promise); + } else { + promise = resolve(identifier); } - return promiseArray(all(promises).then(A, null, `DS: Store#findByIds of ${normalizedModelName} complete`)); + return promise; } - _scheduleFetchMany(internalModels, options) { - let fetches = new Array(internalModels.length); + _scheduleFetchMany( + identifiers: StableRecordIdentifier[], + options: FindOptions = {} + ): Promise { + let fetches = new Array(identifiers.length); + const manager = this._fetchManager; - for (let i = 0; i < internalModels.length; i++) { - fetches[i] = this._scheduleFetch(internalModels[i], options); + for (let i = 0; i < identifiers.length; i++) { + let identifier = identifiers[i]; + assertIdentifierHasId(identifier); + fetches[i] = manager.scheduleFetch(identifier, options); } return all(fetches); } - _scheduleFetch(internalModel: InternalModel, options = {}): Promise { - let generateStackTrace = this.generateStackTracesForTrackedRequests; - let identifier = internalModel.identifier; - - assertIdentifierHasId(identifier); - - let promise = this._fetchManager.scheduleFetch(identifier, options, generateStackTrace); - const isLoading = internalModel.isLoading; - return promise.then( - (payload) => { - // ensure that regardless of id returned we assign to the correct record - if (payload.data && !Array.isArray(payload.data)) { - payload.data.lid = identifier.lid; - } - - // Returning this._push here, breaks typing but not any tests, investigate potential missing tests - let potentiallyNewIm = this._push(payload); - if (potentiallyNewIm && !Array.isArray(potentiallyNewIm)) { - return potentiallyNewIm; - } else { - return internalModel; - } - }, - (error) => { - if (internalModel.isEmpty || isLoading) { - internalModel.unloadRecord(); - } - throw error; - } - ); - } - /** Get the reference for the specified record. @@ -1392,41 +1337,11 @@ class CoreStore extends Service { const type = normalizeModelName(identifier); const normalizedId = ensureStringId(id); + const resource = { type, id: normalizedId }; + const stableIdentifier = this.identifierCache.peekRecordIdentifier(resource); + const internalModel = stableIdentifier && internalModelFactoryFor(this).peek(stableIdentifier); - if (this.hasRecordForId(type, normalizedId)) { - const resource = constructResource(type, normalizedId); - return internalModelFactoryFor(this).lookup(resource).getRecord(); - } else { - return null; - } - } - - /** - This method is called by the record's `reload` method. - - This method calls the adapter's `find` method, which returns a promise. When - **that** promise resolves, `_reloadRecord` will resolve the promise returned - by the record's `reload`. - - @method _reloadRecord - @private - @param {Model} internalModel - @param options optional to include adapterOptions - @return {Promise} promise - */ - _reloadRecord(internalModel: InternalModel, options: FindOptions): Promise { - options.isReloading = true; - let { id, modelName } = internalModel; - let adapter = this.adapterFor(modelName); - - assert(`You cannot reload a record without an ID`, id); - assert(`You tried to reload a record but you have no adapter (for ${modelName})`, adapter); - assert( - `You tried to reload a record but your adapter does not implement 'findRecord'`, - typeof adapter.findRecord === 'function' - ); - - return this._scheduleFetch(internalModel, options); + return internalModel && internalModel.isLoaded ? internalModel.getRecord() : null; } /** @@ -1449,114 +1364,40 @@ class CoreStore extends Service { @param {(String|Integer)} id @return {Boolean} */ - // TODO @runspired @deprecate hasRecordForId(modelName: string, id: string | number): boolean { - if (DEBUG) { - assertDestroyingStore(this, 'hasRecordForId'); - } - assert(`You need to pass a model name to the store's hasRecordForId method`, modelName); - assert( - `Passing classes to store methods has been removed. Please pass a dasherized string instead of ${modelName}`, - typeof modelName === 'string' - ); - - const type = normalizeModelName(modelName); - const trueId = ensureStringId(id); - const resource = { type, id: trueId }; - - const identifier = this.identifierCache.peekRecordIdentifier(resource); - const internalModel = identifier && internalModelFactoryFor(this).peek(identifier); - - return !!internalModel && internalModel.isLoaded; - } - - /** - Returns id record for a given type and ID. If one isn't already loaded, - it builds a new record and leaves it in the `empty` state. - - @method recordForId - @private - @param {String} modelName - @param {(String|Integer)} id - @return {Model} record - */ - // TODO @runspired @deprecate - recordForId(modelName: string, id: string | number): RecordInstance { - if (DEBUG) { - assertDestroyingStore(this, 'recordForId'); - } - assert(`You need to pass a model name to the store's recordForId method`, modelName); - assert( - `Passing classes to store methods has been removed. Please pass a dasherized string instead of ${modelName}`, - typeof modelName === 'string' - ); - - const resource = constructResource(modelName, ensureStringId(id)); - - return internalModelFactoryFor(this).lookup(resource).getRecord(); - } - - /** - @method findMany - @private - @param {Array} internalModels - @return {Promise} promise - */ - // TODO @runspired @deprecate - findMany(internalModels, options) { - if (DEBUG) { - assertDestroyingStore(this, 'findMany'); - } - let finds = new Array(internalModels.length); - - for (let i = 0; i < internalModels.length; i++) { - finds[i] = this._findByInternalModel(internalModels[i], options); - } - - return all(finds); - } - - /** - If a relationship was originally populated by the adapter as a link - (as opposed to a list of IDs), this method is called when the - relationship is fetched. + if (DEPRECATE_HAS_RECORD) { + deprecate(`store.hasRecordForId has been deprecated in favor of store.peekRecord`, false, { + id: 'ember-data:deprecate-has-record-for-id', + since: { available: '4.5', enabled: '4.5' }, + until: '5.0', + for: 'ember-data', + }); + if (DEBUG) { + assertDestroyingStore(this, 'hasRecordForId'); + } + assert(`You need to pass a model name to the store's hasRecordForId method`, modelName); + assert( + `Passing classes to store methods has been removed. Please pass a dasherized string instead of ${modelName}`, + typeof modelName === 'string' + ); - The link (which is usually a URL) is passed through unchanged, so the - adapter can make whatever request it wants. + const type = normalizeModelName(modelName); + const trueId = ensureStringId(id); + const resource = { type, id: trueId }; - The usual use-case is for the server to register a URL as a link, and - then use that URL in the future to make a request for the relationship. + const identifier = this.identifierCache.peekRecordIdentifier(resource); + const internalModel = identifier && internalModelFactoryFor(this).peek(identifier); - @method findHasMany - @private - @param {InternalModel} internalModel - @param {any} link - @param {(Relationship)} relationship - @return {Promise} promise - */ - findHasMany(internalModel, link, relationship, options) { - if (DEBUG) { - assertDestroyingStore(this, 'findHasMany'); + return !!internalModel && internalModel.isLoaded; } - let adapter = this.adapterFor(internalModel.modelName); - - assert( - `You tried to load a hasMany relationship but you have no adapter (for ${internalModel.modelName})`, - adapter - ); - assert( - `You tried to load a hasMany relationship from a specified 'link' in the original payload but your adapter does not implement 'findHasMany'`, - typeof adapter.findHasMany === 'function' - ); - - return _findHasMany(adapter, this, internalModel, link, relationship, options); + assert(`store.hasRecordForId has been removed`); } _findHasManyByJsonApiResource( resource, parentInternalModel: InternalModel, relationship: ManyRelationship, - options?: Dict + options?: FindOptions ): Promise { if (HAS_RECORD_DATA_PACKAGE) { if (!resource) { @@ -1579,7 +1420,29 @@ class CoreStore extends Service { // findHasMany, although not public, does not need to care about our upgrade relationship definitions // and can stick with the public definition API for now. const relationshipMeta = this._storeWrapper.relationshipsDefinitionFor(definition.inverseType)[definition.key]; - return this.findHasMany(parentInternalModel, resource.links.related, relationshipMeta, options); + let adapter = this.adapterFor(parentInternalModel.modelName); + + /* + If a relationship was originally populated by the adapter as a link + (as opposed to a list of IDs), this method is called when the + relationship is fetched. + + The link (which is usually a URL) is passed through unchanged, so the + adapter can make whatever request it wants. + + The usual use-case is for the server to register a URL as a link, and + then use that URL in the future to make a request for the relationship. + */ + assert( + `You tried to load a hasMany relationship but you have no adapter (for ${parentInternalModel.modelName})`, + adapter + ); + assert( + `You tried to load a hasMany relationship from a specified 'link' in the original payload but your adapter does not implement 'findHasMany'`, + typeof adapter.findHasMany === 'function' + ); + + return _findHasMany(adapter, this, parentInternalModel, resource.links.related, relationshipMeta, options); } let preferLocalCache = hasReceivedData && !isEmpty; @@ -1589,18 +1452,22 @@ class CoreStore extends Service { // fetch using data, pulling from local cache if possible if (!shouldForceReload && !isStale && (preferLocalCache || hasLocalPartialData)) { - let internalModels = resource.data.map((json) => this._internalModelForResource(json)); + let finds = new Array(resource.data.length); + for (let i = 0; i < resource.data.length; i++) { + let identifier = this.identifierCache.getOrCreateRecordIdentifier(resource.data[i]); + finds[i] = this._fetchDataIfNeededForIdentifier(identifier, options); + } - return this.findMany(internalModels, options); + return all(finds); } let hasData = hasReceivedData && !isEmpty; // fetch by data if (hasData || hasLocalPartialData) { - let internalModels = resource.data.map((json) => this._internalModelForResource(json)); + let identifiers = resource.data.map((json) => this.identifierCache.getOrCreateRecordIdentifier(json)); - return this._scheduleFetchMany(internalModels, options); + return this._scheduleFetchMany(identifiers, options); } // we were explicitly told we have no data and no links. @@ -1610,22 +1477,24 @@ class CoreStore extends Service { assert(`hasMany only works with the @ember-data/record-data package`); } - /** - @method findBelongsTo - @private - @param {InternalModel} internalModel - @param {any} link - @param {Relationship} relationship - @return {Promise} promise - */ - findBelongsTo(internalModel, link, relationship, options): Promise { + _fetchBelongsToLinkFromResource( + resource, + parentInternalModel: InternalModel, + relationshipMeta, + options + ): Promise { if (DEBUG) { - assertDestroyingStore(this, 'findBelongsTo'); + assertDestroyingStore(this, '_fetchBelongsToLinkFromResource'); + } + if (!resource || !resource.links || !resource.links.related) { + // should we warn here, not sure cause its an internal method + return resolve(null); } - let adapter = this.adapterFor(internalModel.modelName); + + let adapter = this.adapterFor(parentInternalModel.modelName); assert( - `You tried to load a belongsTo relationship but you have no adapter (for ${internalModel.modelName})`, + `You tried to load a belongsTo relationship but you have no adapter (for ${parentInternalModel.modelName})`, adapter ); assert( @@ -1633,28 +1502,15 @@ class CoreStore extends Service { typeof adapter.findBelongsTo === 'function' ); - return _findBelongsTo(adapter, this, internalModel, link, relationship, options); - } - - _fetchBelongsToLinkFromResource( - resource, - parentInternalModel: InternalModel, - relationshipMeta, - options - ): Promise { - if (!resource || !resource.links || !resource.links.related) { - // should we warn here, not sure cause its an internal method - return resolve(null); - } - return this.findBelongsTo(parentInternalModel, resource.links.related, relationshipMeta, options); + return _findBelongsTo(adapter, this, parentInternalModel, resource.links.related, relationshipMeta, options); } _findBelongsToByJsonApiResource( resource, parentInternalModel: InternalModel, relationshipMeta, - options - ): Promise { + options: FindOptions = {} + ): Promise { if (!resource) { return resolve(null); } @@ -1673,13 +1529,15 @@ class CoreStore extends Service { // short circuit if we are already loading let pendingRequest = this._fetchManager.getPendingFetch(internalModel.identifier, options); if (pendingRequest) { - return pendingRequest.then(() => internalModel); + return pendingRequest; } } // fetch via link if (shouldFindViaLink) { - return this._fetchBelongsToLinkFromResource(resource, parentInternalModel, relationshipMeta, options); + return this._fetchBelongsToLinkFromResource(resource, parentInternalModel, relationshipMeta, options).then((im) => + im ? im.identifier : null + ); } let preferLocalCache = hasReceivedData && allInverseRecordsAreLoaded && !isEmpty; @@ -1700,18 +1558,21 @@ class CoreStore extends Service { assert(`No InternalModel found for ${resource.lid}`, internalModel); } - return this._findByInternalModel(internalModel, options); + return this._fetchDataIfNeededForIdentifier(internalModel.identifier, options); } let resourceIsLocal = !localDataIsEmpty && resource.data.id === null; if (internalModel && resourceIsLocal) { - return resolve(internalModel); + return resolve(internalModel.identifier); } // fetch by data if (internalModel && !localDataIsEmpty) { - return this._scheduleFetch(internalModel, options); + let identifier = internalModel.identifier; + assertIdentifierHasId(identifier); + + return this._fetchManager.scheduleFetch(identifier, options); } // we were explicitly told we have no data and no links. @@ -2150,26 +2011,12 @@ class CoreStore extends Service { ); let normalizedModelName = normalizeModelName(modelName); - let fetch = this._fetchAll(normalizedModelName, this.peekAll(normalizedModelName), options); - - return promiseArray(fetch); - } + let array = this.peekAll(normalizedModelName); + let fetch; - /** - @method _fetchAll - @private - @param {Model} modelName - @param {RecordArray} array - @return {Promise} promise - */ - _fetchAll( - modelName: string, - array: RecordArray, - options: { reload?: boolean; backgroundReload?: boolean } - ): Promise { - let adapter = this.adapterFor(modelName); + let adapter = this.adapterFor(normalizedModelName); - assert(`You tried to load all records but you have no adapter (for ${modelName})`, adapter); + assert(`You tried to load all records but you have no adapter (for ${normalizedModelName})`, adapter); assert( `You tried to load all records but your adapter does not implement 'findAll'`, typeof adapter.findAll === 'function' @@ -2177,35 +2024,37 @@ class CoreStore extends Service { if (options.reload) { array.isUpdating = true; - return _findAll(adapter, this, modelName, options); - } - - let snapshotArray = array._createSnapshot(options); - - if (options.reload !== false) { - if ( - (adapter.shouldReloadAll && adapter.shouldReloadAll(this, snapshotArray)) || - (!adapter.shouldReloadAll && snapshotArray.length === 0) - ) { - array.isUpdating = true; - return _findAll(adapter, this, modelName, options); + fetch = _findAll(adapter, this, normalizedModelName, options); + } else { + let snapshotArray = array._createSnapshot(options); + + if (options.reload !== false) { + if ( + (adapter.shouldReloadAll && adapter.shouldReloadAll(this, snapshotArray)) || + (!adapter.shouldReloadAll && snapshotArray.length === 0) + ) { + array.isUpdating = true; + fetch = _findAll(adapter, this, modelName, options); + } } - } - if (options.backgroundReload === false) { - return resolve(array); - } + if (!fetch) { + if (options.backgroundReload === false) { + fetch = resolve(array); + } else if ( + options.backgroundReload || + !adapter.shouldBackgroundReloadAll || + adapter.shouldBackgroundReloadAll(this, snapshotArray) + ) { + array.isUpdating = true; + _findAll(adapter, this, modelName, options); + } - if ( - options.backgroundReload || - !adapter.shouldBackgroundReloadAll || - adapter.shouldBackgroundReloadAll(this, snapshotArray) - ) { - array.isUpdating = true; - _findAll(adapter, this, modelName, options); + fetch = resolve(array); + } } - return resolve(array); + return promiseArray(fetch); } /** @@ -2280,13 +2129,6 @@ class CoreStore extends Service { } } - filter() { - assert( - 'The filter API has been moved to a plugin. To enable store.filter using an environment flag, or to use an alternative, you can visit the ember-data-filter addon page. https://github.com/ember-data/ember-data-filter', - false - ); - } - // .............. // . PERSISTING . // .............. @@ -2303,11 +2145,7 @@ class CoreStore extends Service { @param {Resolver} resolver @param {Object} options */ - scheduleSave( - internalModel: InternalModel, - resolver: RSVP.Deferred, - options: FindOptions - ): void | Promise { + scheduleSave(internalModel: InternalModel, resolver: RSVP.Deferred, options: FindOptions): Promise { assert( `Cannot initiate a save request for an unloaded record: ${internalModel.identifier}`, !internalModel.isEmpty && !internalModel.isDestroyed @@ -2358,7 +2196,7 @@ class CoreStore extends Service { throw e; } const { error, parsedErrors } = e; - this.recordWasInvalid(internalModel, parsedErrors, error); + internalModel.adapterDidInvalidate(parsedErrors, error); throw error; } ); @@ -2366,18 +2204,6 @@ class CoreStore extends Service { return promise; } - /** - This method is called at the end of the run loop, and - flushes any records passed into `scheduleSave` - - @method flushPendingSave - @private - */ - flushPendingSave() { - // assert here - return; - } - /** This method is called once the promise returned by an adapter's `createRecord`, `updateRecord` or `deleteRecord` @@ -2426,31 +2252,28 @@ class CoreStore extends Service { @method recordWasInvalid @private + @deprecated @param {InternalModel} internalModel @param {Object} errors */ recordWasInvalid(internalModel, parsedErrors, error) { - if (DEBUG) { - assertDestroyingStore(this, 'recordWasInvalid'); - } - internalModel.adapterDidInvalidate(parsedErrors, error); - } - - /** - This method is called once the promise returned by an - adapter's `createRecord`, `updateRecord` or `deleteRecord` - is rejected (with anything other than a `InvalidError`). - - @method recordWasError - @private - @param {InternalModel} internalModel - @param {Error} error - */ - recordWasError(internalModel, error) { - if (DEBUG) { - assertDestroyingStore(this, 'recordWasError'); + if (DEPRECATE_RECORD_WAS_INVALID) { + deprecate( + `The private API recordWasInvalid will be removed in an upcoming release. Use record.errors add/remove instead if the intent was to move the record into an invalid state manually.`, + false, + { + id: 'ember-data:deprecate-record-was-invalid', + for: 'ember-data', + until: '5.0', + since: { enabled: '4.5', available: '4.5' }, + } + ); + if (DEBUG) { + assertDestroyingStore(this, 'recordWasInvalid'); + } + internalModel.adapterDidInvalidate(parsedErrors, error); } - internalModel.adapterDidError(error); + assert(`store.recordWasInvalid has been removed`); } /** @@ -2753,7 +2576,7 @@ class CoreStore extends Service { ); assert( `You tried to push data with a type '${modelName}' but no model could be found with that name.`, - this._hasModelFor(modelName) + this.getSchemaDefinitionService().doesTypeExist(modelName) ); if (DEBUG) { @@ -2883,6 +2706,7 @@ class CoreStore extends Service { return internalModelFactoryFor(this).getByResource(resource); } + // TODO @runspired @deprecate records should implement their own serialization if desired serializeRecord(record: RecordInstance, options?: Dict): unknown { let identifier = recordIdentifierFor(record); let internalModel = internalModelFactoryFor(this).peek(identifier); @@ -2890,13 +2714,24 @@ class CoreStore extends Service { return internalModel!.createSnapshot(options).serialize(options); } - saveRecord(record: RecordInstance, options?: Dict): Promise { + saveRecord(record: RecordInstance, options: Dict = {}): Promise { + assert(`Unable to initate save for a record in a disconnected state`, storeFor(record)); let identifier = recordIdentifierFor(record); - let internalModel = internalModelFactoryFor(this).peek(identifier); + let internalModel = identifier && internalModelFactoryFor(this).peek(identifier)!; + + if (!internalModel) { + // this commonly means we're disconnected + // but just in case we reject here to prevent bad things. + return reject(`Record Is Disconnected`); + } // TODO we used to check if the record was destroyed here // Casting can be removed once REQUEST_SERVICE ff is turned on // because a `Record` is provided there will always be a matching internalModel - return (internalModel!.save(options) as Promise).then(() => record); + + let promiseLabel = 'DS: Model#save ' + this; + let resolver = RSVP.defer(promiseLabel); + + return this.scheduleSave(internalModel, resolver, options).then(() => record); } relationshipReferenceFor(identifier: RecordIdentifier, key: string): BelongsToReference | HasManyReference { @@ -3006,6 +2841,7 @@ class CoreStore extends Service { @param {Object} payload @return {Object} The normalized payload */ + // TODO @runspired @deprecate users should call normalize on the associated serializer directly normalize(modelName: string, payload) { if (DEBUG) { assertDestroyingStore(this, 'normalize'); @@ -3027,10 +2863,6 @@ class CoreStore extends Service { return serializer.normalize(model, payload); } - newClientId() { - assert(`Private API Removed`, false); - } - // ............... // . DESTRUCTION . // ............... @@ -3051,15 +2883,12 @@ class CoreStore extends Service { /** Returns an instance of the adapter for a given type. For example, `adapterFor('person')` will return an instance of - `App.PersonAdapter`. + the adapter located at `app/adapters/person.js` - If no `App.PersonAdapter` is found, this method will look - for an `App.ApplicationAdapter` (the default adapter for + If no `person` adapter is found, this method will look + for an `application` adapter (the default adapter for your entire application). - If no `App.ApplicationAdapter` is found, it will return - the value of the `defaultAdapter`. - @method adapterFor @public @param {String} modelName @@ -3087,8 +2916,6 @@ class CoreStore extends Service { // name specific adapter adapter = owner.lookup(`adapter:${normalizedModelName}`); if (adapter !== undefined) { - // TODO @runspired @deprecate store auto-inject - adapter.store = this; _adapterCache[normalizedModelName] = adapter; return adapter; } @@ -3096,7 +2923,6 @@ class CoreStore extends Service { // no adapter found for the specific name, fallback and check for application adapter adapter = _adapterCache.application || owner.lookup('adapter:application'); if (adapter !== undefined) { - adapter.store = this; _adapterCache[normalizedModelName] = adapter; _adapterCache.application = adapter; return adapter; @@ -3110,7 +2936,6 @@ class CoreStore extends Service { `No adapter was found for '${modelName}' and no 'application' adapter was found as a fallback.`, adapter !== undefined ); - adapter.store = this; _adapterCache[normalizedModelName] = adapter; _adapterCache['-json-api'] = adapter; return adapter; @@ -3159,8 +2984,6 @@ class CoreStore extends Service { // by name serializer = owner.lookup(`serializer:${normalizedModelName}`); if (serializer !== undefined) { - // TODO @runspired @deprecate store auto-inject - serializer.store = this; _serializerCache[normalizedModelName] = serializer; return serializer; } @@ -3168,7 +2991,6 @@ class CoreStore extends Service { // no serializer found for the specific model, fallback and check for application serializer serializer = _serializerCache.application || owner.lookup('serializer:application'); if (serializer !== undefined) { - serializer.store = this; _serializerCache[normalizedModelName] = serializer; _serializerCache.application = serializer; return serializer; diff --git a/packages/store/addon/-private/system/errors-utils.js b/packages/store/addon/-private/system/errors-utils.js index f9c7611565e..3e805fb71cc 100644 --- a/packages/store/addon/-private/system/errors-utils.js +++ b/packages/store/addon/-private/system/errors-utils.js @@ -1,6 +1,3 @@ -import { makeArray } from '@ember/array'; -import { isPresent } from '@ember/utils'; - /** @module @ember-data/adapter/error */ @@ -9,6 +6,10 @@ const SOURCE_POINTER_REGEXP = /^\/?data\/(attributes|relationships)\/(.*)/; const SOURCE_POINTER_PRIMARY_REGEXP = /^\/?data/; const PRIMARY_ATTRIBUTE_KEY = 'base'; +function makeArray(value) { + return Array.isArray(value) ? value : [value]; +} + /** Convert an hash of errors into an array with errors in JSON-API format. ```javascript @@ -55,7 +56,7 @@ const PRIMARY_ATTRIBUTE_KEY = 'base'; export function errorsHashToArray(errors) { let out = []; - if (isPresent(errors)) { + if (errors) { Object.keys(errors).forEach((key) => { let messages = makeArray(errors[key]); for (let i = 0; i < messages.length; i++) { @@ -122,7 +123,7 @@ export function errorsHashToArray(errors) { export function errorsArrayToHash(errors) { let out = {}; - if (isPresent(errors)) { + if (errors) { errors.forEach((error) => { if (error.source && error.source.pointer) { let key = error.source.pointer.match(SOURCE_POINTER_REGEXP); diff --git a/packages/store/addon/-private/system/fetch-manager.ts b/packages/store/addon/-private/system/fetch-manager.ts index 7545daa57d3..d838c46dcb9 100644 --- a/packages/store/addon/-private/system/fetch-manager.ts +++ b/packages/store/addon/-private/system/fetch-manager.ts @@ -1,7 +1,6 @@ /** * @module @ember-data/store */ -import { A } from '@ember/array'; import { assert, deprecate, warn } from '@ember/debug'; import { _backburner as emberBackburner } from '@ember/runloop'; import { DEBUG } from '@glimmer/env'; @@ -12,7 +11,11 @@ import { DEPRECATE_RSVP_PROMISE } from '@ember-data/private-build-infra/deprecat import type { CollectionResourceDocument, SingleResourceDocument } from '../ts-interfaces/ember-data-json-api'; import type { FindRecordQuery, Request, SaveRecordMutation } from '../ts-interfaces/fetch-manager'; -import type { ExistingRecordIdentifier, RecordIdentifier, StableRecordIdentifier } from '../ts-interfaces/identifier'; +import type { + RecordIdentifier, + StableExistingRecordIdentifier, + StableRecordIdentifier, +} from '../ts-interfaces/identifier'; import type { MinimumSerializerInterface } from '../ts-interfaces/minimum-serializer-interface'; import { FindOptions } from '../ts-interfaces/store'; import type { Dict } from '../ts-interfaces/utils'; @@ -45,11 +48,12 @@ export const SaveOp: unique symbol = Symbol('SaveOp'); export type FetchMutationOptions = FindOptions & { [SaveOp]: 'createRecord' | 'deleteRecord' | 'updateRecord' }; interface PendingFetchItem { - identifier: ExistingRecordIdentifier; + identifier: StableExistingRecordIdentifier; queryRequest: Request; resolver: RSVP.Deferred; - options: { [k: string]: unknown }; + options: FindOptions; trace?: any; + promise: Promise; } interface PendingSaveItem { @@ -207,8 +211,9 @@ export default class FetchManager { } } - scheduleFetch(identifier: ExistingRecordIdentifier, options: any, shouldTrace: boolean): Promise { + scheduleFetch(identifier: StableExistingRecordIdentifier, options: FindOptions): Promise { // TODO Probably the store should pass in the query object + let shouldTrace = DEBUG && this._store.generateStackTracesForTrackedRequests; let query: FindRecordQuery = { op: 'findRecord', @@ -220,26 +225,21 @@ export default class FetchManager { data: [query], }; - let pendingFetches = this._pendingFetch.get(identifier.type); - - // We already have a pending fetch for this - if (pendingFetches) { - let matchingPendingFetch = pendingFetches.find((fetch) => fetch.identifier === identifier); - if (matchingPendingFetch) { - return matchingPendingFetch.resolver.promise; - } + let pendingFetch = this.getPendingFetch(identifier, options); + if (pendingFetch) { + return pendingFetch; } let id = identifier.id; let modelName = identifier.type; - let resolver = RSVP.defer(`Fetching ${modelName}' with id: ${id}`); + let resolver = RSVP.defer(`Fetching ${modelName}' with id: ${id}`); let pendingFetchItem: PendingFetchItem = { identifier, resolver, options, queryRequest, - }; + } as PendingFetchItem; if (DEBUG) { if (shouldTrace) { @@ -259,7 +259,36 @@ export default class FetchManager { } } - let promise = resolver.promise; + let resolverPromise = resolver.promise; + + // TODO replace with some form of record state cache + const store = this._store; + const internalModel = store._instanceCache.getInternalModel(identifier); + const isLoading = !internalModel.isLoaded; // we don't use isLoading directly because we are the request + + const promise = resolverPromise.then( + (payload) => { + // ensure that regardless of id returned we assign to the correct record + if (payload.data && !Array.isArray(payload.data)) { + payload.data.lid = identifier.lid; + } + + // additional data received in the payload + // may result in the merging of identifiers (and thus records) + let potentiallyNewIm = store._push(payload); + if (potentiallyNewIm && !Array.isArray(potentiallyNewIm)) { + return potentiallyNewIm.identifier; + } + + return identifier; + }, + (error) => { + if (internalModel.isEmpty || isLoading) { + internalModel.unloadRecord(); + } + throw error; + } + ); if (this._pendingFetch.size === 0) { emberBackburner.schedule('actions', this, this.flushAllPendingFetches); @@ -273,7 +302,8 @@ export default class FetchManager { (fetches.get(modelName) as PendingFetchItem[]).push(pendingFetchItem); - this.requestCache.enqueue(promise, pendingFetchItem.queryRequest); + pendingFetchItem.promise = promise; + this.requestCache.enqueue(resolverPromise, pendingFetchItem.queryRequest); return promise; } @@ -418,7 +448,7 @@ export default class FetchManager { ) { let modelClass = store.modelFor(modelName); // `adapter.findMany` gets the modelClass still let ids = snapshots.map((s) => s.id); - let promise = adapter.findMany(store, modelClass, ids, A(snapshots)); + let promise = adapter.findMany(store, modelClass, ids, snapshots); let label = `DS: Handle Adapter#findMany of '${modelName}'`; if (promise === undefined) { @@ -483,7 +513,7 @@ export default class FetchManager { let identifiers = new Array(totalItems); let seeking: { [id: string]: PendingFetchItem } = Object.create(null); - let optionsMap = new WeakCache>(DEBUG ? 'fetch-options' : ''); + let optionsMap = new WeakCache(DEBUG ? 'fetch-options' : ''); for (let i = 0; i < totalItems; i++) { let pendingItem = pendingFetchItems[i]; @@ -530,12 +560,14 @@ export default class FetchManager { } getPendingFetch(identifier: StableRecordIdentifier, options) { - let pendingRequest = this.requestCache.getPendingRequestsForRecord(identifier).find((req) => { - return req.type === 'query' && isSameRequest(options, req.request.data[0].options); - }); + let pendingFetches = this._pendingFetch.get(identifier.type); - if (pendingRequest) { - return pendingRequest[RequestPromise]; + // We already have a pending fetch for this + if (pendingFetches) { + let matchingPendingFetch = pendingFetches.find((fetch) => fetch.identifier === identifier); + if (matchingPendingFetch) { + return matchingPendingFetch.promise; + } } } diff --git a/packages/store/addon/-private/system/model/internal-model.ts b/packages/store/addon/-private/system/model/internal-model.ts index b8c7974c744..85aadd12dbc 100644 --- a/packages/store/addon/-private/system/model/internal-model.ts +++ b/packages/store/addon/-private/system/model/internal-model.ts @@ -1,12 +1,8 @@ -import { A, default as EmberArray } from '@ember/array'; -import { assert, inspect } from '@ember/debug'; -import EmberError from '@ember/error'; -import { get } from '@ember/object'; +import { assert } from '@ember/debug'; import { _backburner as emberBackburner, cancel, run } from '@ember/runloop'; import { DEBUG } from '@glimmer/env'; import { importSync } from '@embroider/macros'; -import RSVP, { resolve } from 'rsvp'; import type { ManyArray } from '@ember-data/model/-private'; import type { ManyArrayCreateArgs } from '@ember-data/model/-private/system/many-array'; @@ -55,8 +51,6 @@ type PrivateModelModule = { @module @ember-data/store */ -const { hasOwnProperty } = Object.prototype; - let _ManyArray: PrivateModelModule['ManyArray']; let _PromiseBelongsTo: PrivateModelModule['PromiseBelongsTo']; let _PromiseManyArray: PrivateModelModule['PromiseManyArray']; @@ -346,21 +340,6 @@ export default class InternalModel { }); } - save(options: FindOptions = {}): Promise { - if (this._deletedRecordWasNew) { - return resolve(); - } - let promiseLabel = 'DS: Model#save ' + this; - let resolver = RSVP.defer(promiseLabel); - - // Casting to promise to narrow due to the feature flag paths inside scheduleSave - return this.store.scheduleSave(this, resolver, options) as Promise; - } - - reload(options: Dict = {}): Promise { - return this.store._reloadRecord(this, options); - } - /* Unload the record for this internal model. This will cause the record to be destroyed and freed up for garbage collection. It will also do a check @@ -451,7 +430,8 @@ export default class InternalModel { // TODO @runspired follow up if parent isNew then we should not be attempting load here // TODO @runspired follow up on whether this should be in the relationship requests cache return this.store._findBelongsToByJsonApiResource(resource, this, relationshipMeta, options).then( - (internalModel) => handleCompletedRelationshipRequest(this, key, resource._relationship, internalModel), + (identifier: StableRecordIdentifier | null) => + handleCompletedRelationshipRequest(this, key, resource._relationship, identifier), (e) => handleCompletedRelationshipRequest(this, key, resource._relationship, null, e) ); } @@ -744,13 +724,7 @@ export default class InternalModel { } setDirtyAttribute(key: string, value: T): T { - if (this.isDeleted()) { - if (DEBUG) { - throw new EmberError(`Attempted to set '${key}' to '${value}' on the deleted record ${this}`); - } else { - throw new EmberError(`Attempted to set '${key}' on the deleted record ${this}`); - } - } + assert(`Attempted to set '${key}' on the deleted record ${this}`, !this.isDeleted()); let currentValue = this._recordData.getAttr(key); if (currentValue !== value) { @@ -874,7 +848,7 @@ export default class InternalModel { let jsonPayload: JsonApiResource = {}; //TODO(Igor) consider the polymorphic case Object.keys(preload).forEach((key) => { - let preloadValue = get(preload, key); + let preloadValue = preload[key]; let relationshipMeta = this.modelClass.metaForProperty(key); if (relationshipMeta.isRelationship) { if (!jsonPayload.relationships) { @@ -997,7 +971,7 @@ export default class InternalModel { let record = this.getRecord() as DSModel; let errors = record.errors; for (attribute in parsedErrors) { - if (hasOwnProperty.call(parsedErrors, attribute)) { + if (Object.prototype.hasOwnProperty.call(parsedErrors, attribute)) { errors.add(attribute, parsedErrors[attribute]); } } @@ -1069,7 +1043,7 @@ function handleCompletedRelationshipRequest( internalModel: InternalModel, key: string, relationship: BelongsToRelationship, - value: InternalModel | null + value: StableRecordIdentifier | null ): RecordInstance | null; function handleCompletedRelationshipRequest( internalModel: InternalModel, @@ -1095,7 +1069,7 @@ function handleCompletedRelationshipRequest( internalModel: InternalModel, key: string, relationship: BelongsToRelationship | ManyRelationship, - value: ManyArray | InternalModel | null, + value: ManyArray | StableRecordIdentifier | null, error?: Error ): ManyArray | RecordInstance | null { delete internalModel._relationshipPromisesCache[key]; @@ -1137,19 +1111,19 @@ function handleCompletedRelationshipRequest( // only set to not stale if no error is thrown relationship.state.isStale = false; - return isHasMany || !value ? (value as ManyArray | null) : (value as InternalModel).getRecord(); + return isHasMany || !value + ? (value as ManyArray | null) + : internalModel.store.peekRecord(value as StableRecordIdentifier); } export function assertRecordsPassedToHasMany(records) { - // TODO only allow native arrays - assert( - `You must pass an array of records to set a hasMany relationship`, - Array.isArray(records) || EmberArray.detect(records) - ); + assert(`You must pass an array of records to set a hasMany relationship`, Array.isArray(records)); assert( - `All elements of a hasMany relationship must be instances of Model, you passed ${inspect(records)}`, + `All elements of a hasMany relationship must be instances of Model, you passed ${records + .map((r) => `${typeof r}`) + .join(', ')}`, (function () { - return A(records).every((record) => hasOwnProperty.call(record, '_internalModel') === true); + return records.every((record) => Object.prototype.hasOwnProperty.call(record, '_internalModel') === true); })() ); } diff --git a/packages/store/addon/-private/system/promise-proxies.ts b/packages/store/addon/-private/system/promise-proxies.ts index c8b5d1026b3..b5cfb5120ec 100644 --- a/packages/store/addon/-private/system/promise-proxies.ts +++ b/packages/store/addon/-private/system/promise-proxies.ts @@ -111,7 +111,7 @@ export function deprecatedPromiseObject(promise: Promise): PromiseObjectPr get(target: object, prop: string, receiver?: object): unknown { if (!ALLOWABLE_METHODS.includes(prop)) { deprecate( - `Accessing ${prop} is deprecated. Only available methods to access on a promise returned from model.save() are .then, .catch and .finally`, + `Accessing ${prop} is deprecated. The return type is being changed fomr PromiseObjectProxy to a Promise. The only available methods to access on this promise are .then, .catch and .finally`, false, { id: 'ember-data:model-save-promise', diff --git a/packages/store/addon/-private/system/record-arrays/record-array.ts b/packages/store/addon/-private/system/record-arrays/record-array.ts index 8f5c8f09e14..5d0dae72f10 100644 --- a/packages/store/addon/-private/system/record-arrays/record-array.ts +++ b/packages/store/addon/-private/system/record-arrays/record-array.ts @@ -3,14 +3,16 @@ */ import type NativeArray from '@ember/array/-private/native-array'; import ArrayProxy from '@ember/array/proxy'; -import { assert } from '@ember/debug'; -import { computed, get, set } from '@ember/object'; +import { assert, deprecate } from '@ember/debug'; +import { get, set } from '@ember/object'; import { tracked } from '@glimmer/tracking'; import { Promise } from 'rsvp'; import type { RecordArrayManager, Snapshot } from 'ember-data/-private'; +import { DEPRECATE_SNAPSHOT_MODEL_CLASS_ACCESS } from '@ember-data/private-build-infra/deprecations'; + import type { StableRecordIdentifier } from '../../ts-interfaces/identifier'; import type { RecordInstance } from '../../ts-interfaces/record-instance'; import type { FindOptions } from '../../ts-interfaces/store'; @@ -121,15 +123,9 @@ export default class RecordArray extends ArrayProxy internalModelFactoryFor(this.store).lookup(identifier).createSnapshot()); } } + +if (DEPRECATE_SNAPSHOT_MODEL_CLASS_ACCESS) { + Object.defineProperty(RecordArray.prototype, 'type', { + get() { + deprecate( + `Using RecordArray.type to access the ModelClass for a record is deprecated. Use store.modelFor() instead.`, + false, + { + id: 'ember-data:deprecate-snapshot-model-class-access', + until: '5.0', + for: 'ember-data', + since: { available: '4.5.0', enabled: '4.5.0' }, + } + ); + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + if (!this.modelName) { + return null; + } + // eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call + return this.store.modelFor(this.modelName); + }, + }); +} diff --git a/packages/store/addon/-private/system/snapshot-record-array.ts b/packages/store/addon/-private/system/snapshot-record-array.ts index 233410ec675..f18ecfdbbb5 100644 --- a/packages/store/addon/-private/system/snapshot-record-array.ts +++ b/packages/store/addon/-private/system/snapshot-record-array.ts @@ -2,6 +2,10 @@ @module @ember-data/store */ +import { deprecate } from '@ember/debug'; + +import { DEPRECATE_SNAPSHOT_MODEL_CLASS_ACCESS } from '@ember-data/private-build-infra/deprecations'; + import type { ModelSchema } from '../ts-interfaces/ds-model'; import { FindOptions } from '../ts-interfaces/store'; import type { Dict } from '../ts-interfaces/utils'; @@ -75,8 +79,6 @@ export default class SnapshotRecordArray { */ this.length = recordArray.get('length'); - this._type = null; - /** Meta objects for the record array. @@ -151,12 +153,11 @@ export default class SnapshotRecordArray { /** The type of the underlying records for the snapshots in the array, as a Model @property type + @deprecated @public @type {Model} */ - get type() { - return this._type || (this._type = this._recordArray.get('type')); - } + /** The modelName of the underlying records for the snapshots in the array, as a Model @property modelName @@ -205,3 +206,21 @@ export default class SnapshotRecordArray { return this._snapshots; } } + +if (DEPRECATE_SNAPSHOT_MODEL_CLASS_ACCESS) { + Object.defineProperty(SnapshotRecordArray.prototype, 'type', { + get() { + deprecate( + `Using SnapshotRecordArray.type to access the ModelClass for a record is deprecated. Use store.modelFor() instead.`, + false, + { + id: 'ember-data:deprecate-snapshot-model-class-access', + until: '5.0', + for: 'ember-data', + since: { available: '4.5.0', enabled: '4.5.0' }, + } + ); + return this._recordArray.get('type'); + }, + }); +} diff --git a/packages/store/addon/-private/system/snapshot.ts b/packages/store/addon/-private/system/snapshot.ts index 2fe0cb7a13c..ce1352832e4 100644 --- a/packages/store/addon/-private/system/snapshot.ts +++ b/packages/store/addon/-private/system/snapshot.ts @@ -1,12 +1,13 @@ /** @module @ember-data/store */ -import { assert } from '@ember/debug'; +import { assert, deprecate } from '@ember/debug'; import { get } from '@ember/object'; import { importSync } from '@embroider/macros'; import { HAS_RECORD_DATA_PACKAGE } from '@ember-data/private-build-infra'; +import { DEPRECATE_SNAPSHOT_MODEL_CLASS_ACCESS } from '@ember-data/private-build-infra/deprecations'; import type BelongsToRelationship from '@ember-data/record-data/addon/-private/relationships/state/belongs-to'; import type ManyRelationship from '@ember-data/record-data/addon/-private/relationships/state/has-many'; import type { @@ -166,7 +167,7 @@ export default class Snapshot implements Snapshot { let attributes = (this.__attributes = Object.create(null)); let attrs = Object.keys(this._store._attributesDefinitionFor(this.identifier)); attrs.forEach((keyName) => { - if (schemaIsDSModel(this.type)) { + if (schemaIsDSModel(this._internalModel.modelClass)) { // if the schema is for a DSModel then the instance is too attributes[keyName] = get(record as DSModel, keyName); } else { @@ -182,11 +183,9 @@ export default class Snapshot implements Snapshot { @property type @public + @deprecated @type {Model} */ - get type(): ModelSchema { - return this._internalModel.modelClass; - } get isNew(): boolean { return this._internalModel.isNew(); @@ -555,3 +554,21 @@ export default class Snapshot implements Snapshot { return serializer.serialize(this, options); } } + +if (DEPRECATE_SNAPSHOT_MODEL_CLASS_ACCESS) { + Object.defineProperty(Snapshot.prototype, 'type', { + get() { + deprecate( + `Using Snapshot.type to access the ModelClass for a record is deprecated. Use store.modelFor() instead.`, + false, + { + id: 'ember-data:deprecate-snapshot-model-class-access', + until: '5.0', + for: 'ember-data', + since: { available: '4.5.0', enabled: '4.5.0' }, + } + ); + return this._internalModel.modelClass; + }, + }); +} 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 3ed86ee39e5..4e75c1ec3b0 100644 --- a/packages/store/addon/-private/system/store/internal-model-factory.ts +++ b/packages/store/addon/-private/system/store/internal-model-factory.ts @@ -1,5 +1,4 @@ import { assert, warn } from '@ember/debug'; -import { isNone } from '@ember/utils'; import { DEBUG } from '@glimmer/env'; import type { IdentifierCache } from '../../identifiers/cache'; @@ -268,7 +267,7 @@ export default class InternalModelFactory { assert( `'${modelName}' was saved to the server, but the response returned the new id '${id}', which has already been used with another record.'`, - isNone(existingInternalModel) || existingInternalModel === internalModel + !existingInternalModel || existingInternalModel === internalModel ); if (identifier.id === null) { diff --git a/packages/store/addon/-private/ts-interfaces/store.ts b/packages/store/addon/-private/ts-interfaces/store.ts index c3bddf9ba61..a2c01ab4b29 100644 --- a/packages/store/addon/-private/ts-interfaces/store.ts +++ b/packages/store/addon/-private/ts-interfaces/store.ts @@ -6,5 +6,4 @@ export interface FindOptions { include?: string; adapterOptions?: Dict; preload?: Dict; - isReloading?: boolean; } diff --git a/packages/store/addon/-private/utils/promise-record.ts b/packages/store/addon/-private/utils/promise-record.ts index 759d7984531..a79c4d1bfc1 100644 --- a/packages/store/addon/-private/utils/promise-record.ts +++ b/packages/store/addon/-private/utils/promise-record.ts @@ -1,6 +1,7 @@ -import type InternalModel from '../system/model/internal-model'; +import CoreStore from '../system/core-store'; import type { PromiseObject } from '../system/promise-proxies'; import { promiseObject } from '../system/promise-proxies'; +import type { StableRecordIdentifier } from '../ts-interfaces/identifier'; import type { RecordInstance } from '../ts-interfaces/record-instance'; /** @@ -16,10 +17,11 @@ import type { RecordInstance } from '../ts-interfaces/record-instance'; * @internal */ export default function promiseRecord( - internalModelPromise: Promise, + store: CoreStore, + promise: Promise, label: string ): PromiseObject { - let toReturn = internalModelPromise.then((internalModel) => internalModel.getRecord()); + let toReturn = promise.then((identifier: StableRecordIdentifier) => store.peekRecord(identifier)!); return promiseObject(toReturn, label); } diff --git a/packages/store/addon/index.ts b/packages/store/addon/index.ts index e67d1c70207..74e75e7eac2 100644 --- a/packages/store/addon/index.ts +++ b/packages/store/addon/index.ts @@ -10,4 +10,5 @@ export { setIdentifierForgetMethod, setIdentifierResetMethod, recordIdentifierFor, + storeFor, } from './-private'; From b6b81f4682c2408bdf4f6ddc9c009cb0a92bde97 Mon Sep 17 00:00:00 2001 From: Chris Thoburn Date: Sun, 24 Jul 2022 16:01:46 -0700 Subject: [PATCH 09/16] fixup --- .../tests/integration/adapter/client-side-delete-test.js | 2 -- packages/store/addon/-private/system/fetch-manager.ts | 5 +++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/-ember-data/tests/integration/adapter/client-side-delete-test.js b/packages/-ember-data/tests/integration/adapter/client-side-delete-test.js index 249356e200f..6a8d6b56f6c 100644 --- a/packages/-ember-data/tests/integration/adapter/client-side-delete-test.js +++ b/packages/-ember-data/tests/integration/adapter/client-side-delete-test.js @@ -1,5 +1,3 @@ -import { settled } from '@ember/test-helpers'; - import { module, test } from 'qunit'; import { resolve } from 'rsvp'; diff --git a/packages/store/addon/-private/system/fetch-manager.ts b/packages/store/addon/-private/system/fetch-manager.ts index d838c46dcb9..640fd0a4492 100644 --- a/packages/store/addon/-private/system/fetch-manager.ts +++ b/packages/store/addon/-private/system/fetch-manager.ts @@ -23,7 +23,7 @@ import coerceId from './coerce-id'; import type CoreStore from './core-store'; import { errorsArrayToHash } from './errors-utils'; import ShimModelClass from './model/shim-model-class'; -import RequestCache, { RequestPromise } from './request-cache'; +import RequestCache from './request-cache'; import type { PrivateSnapshot } from './snapshot'; import Snapshot from './snapshot'; import { _bind, _guard, _objectIsAlive, guardDestroyedStore } from './store/common'; @@ -565,7 +565,7 @@ export default class FetchManager { // We already have a pending fetch for this if (pendingFetches) { let matchingPendingFetch = pendingFetches.find((fetch) => fetch.identifier === identifier); - if (matchingPendingFetch) { + if (matchingPendingFetch && isSameRequest(options, matchingPendingFetch.options)) { return matchingPendingFetch.promise; } } @@ -594,6 +594,7 @@ function assertIsString(id: string | null): asserts id is string { } // this function helps resolve whether we have a pending request that we should use instead +// TODO @runspired @needsTest removing this did not cause any test failures function isSameRequest(options: Dict = {}, reqOptions: Dict = {}) { return options.include === reqOptions.include; } From 6b4617e4a2f1d7028bb0b6247b19c881423f0b3c Mon Sep 17 00:00:00 2001 From: Chris Thoburn Date: Sun, 24 Jul 2022 16:02:26 -0700 Subject: [PATCH 10/16] cleanup types --- packages/store/addon/-private/system/fetch-manager.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/store/addon/-private/system/fetch-manager.ts b/packages/store/addon/-private/system/fetch-manager.ts index 640fd0a4492..a5fd856a793 100644 --- a/packages/store/addon/-private/system/fetch-manager.ts +++ b/packages/store/addon/-private/system/fetch-manager.ts @@ -559,7 +559,7 @@ export default class FetchManager { } } - getPendingFetch(identifier: StableRecordIdentifier, options) { + getPendingFetch(identifier: StableRecordIdentifier, options: FindOptions) { let pendingFetches = this._pendingFetch.get(identifier.type); // We already have a pending fetch for this @@ -595,6 +595,6 @@ function assertIsString(id: string | null): asserts id is string { // this function helps resolve whether we have a pending request that we should use instead // TODO @runspired @needsTest removing this did not cause any test failures -function isSameRequest(options: Dict = {}, reqOptions: Dict = {}) { +function isSameRequest(options: FindOptions = {}, reqOptions: FindOptions = {}) { return options.include === reqOptions.include; } From 91f2671beb7311f98f7df2d87aced320fa268e01 Mon Sep 17 00:00:00 2001 From: Chris Thoburn Date: Sun, 24 Jul 2022 20:39:46 -0700 Subject: [PATCH 11/16] slowly the world burns --- .../node-tests/fixtures/expected.js | 3 - .../relationships/belongs-to-test.js | 8 +- .../adapter/record-persistence-test.js | 12 +- .../integration/adapter/rest-adapter-test.js | 4 - .../integration/adapter/store-adapter-test.js | 2 +- .../tests/integration/store-test.js | 39 +- .../unit/record-arrays/record-array-test.js | 112 +--- .../tests/unit/store/asserts-test.js | 1 - .../-ember-data/tests/unit/store/push-test.js | 26 +- .../model/addon/-private/system/many-array.ts | 36 +- .../addon/current-deprecations.ts | 1 + .../private-build-infra/addon/deprecations.ts | 1 + .../record-data/addon/-private/record-data.ts | 6 +- .../store/addon/-private/instance-cache.ts | 25 +- .../store/addon/-private/system/core-store.ts | 505 +++++++----------- .../addon/-private/system/fetch-manager.ts | 2 +- .../-private/system/model/internal-model.ts | 26 +- .../system/record-arrays/record-array.ts | 5 +- .../-private/system/references/belongs-to.ts | 9 +- .../-private/system/references/record.ts | 9 +- .../system/schema-definition-service.ts | 26 +- .../store/addon/-private/system/snapshot.ts | 6 +- .../addon/-private/system/store/finders.js | 91 ++-- .../addon/-private/utils/promise-record.ts | 2 +- 24 files changed, 399 insertions(+), 558 deletions(-) diff --git a/packages/-ember-data/node-tests/fixtures/expected.js b/packages/-ember-data/node-tests/fixtures/expected.js index c262e6d0b8c..65e980fc6ee 100644 --- a/packages/-ember-data/node-tests/fixtures/expected.js +++ b/packages/-ember-data/node-tests/fixtures/expected.js @@ -89,11 +89,9 @@ module.exports = { '(private) @ember-data/store Store#_backburner', '(private) @ember-data/store Store#_load', '(private) @ember-data/store Store#_push', - '(private) @ember-data/store Store#didSaveRecord', '(private) @ember-data/store Store#find', '(private) @ember-data/store Store#init', '(private) @ember-data/store Store#recordWasInvalid', - '(private) @ember-data/store Store#scheduleSave', '(private) @ember-data/store Store#setRecordId', '(public) @ember-data/adapter Adapter#coalesceFindRequests', '(public) @ember-data/adapter Adapter#createRecord', @@ -350,7 +348,6 @@ module.exports = { '(public) @ember-data/store StableRecordIdentifier#id', '(public) @ember-data/store StableRecordIdentifier#lid', '(public) @ember-data/store StableRecordIdentifier#type', - '(public) @ember-data/store Store#adapter', '(public) @ember-data/store Store#adapterFor', '(public) @ember-data/store Store#createRecord', '(public) @ember-data/store Store#createRecordDataFor', diff --git a/packages/-ember-data/tests/acceptance/relationships/belongs-to-test.js b/packages/-ember-data/tests/acceptance/relationships/belongs-to-test.js index bfb8c20a72c..8d677cd2e82 100644 --- a/packages/-ember-data/tests/acceptance/relationships/belongs-to-test.js +++ b/packages/-ember-data/tests/acceptance/relationships/belongs-to-test.js @@ -442,12 +442,10 @@ module('async belongs-to rendering tests', function (hooks) {

{{this.sedona.parent.name}}

`); - let parent = await sedona.get('parent'); + let parent = await sedona.parent; await parent.destroyRecord(); - - let newParent = await sedona.get('parent'); - - await settled(); + // TODO for some reason parent isn't notified via the destroy above :thinking_face: + let newParent = await sedona.parent; assert.strictEqual(newParent, null, 'We no longer have a parent'); assert.strictEqual( diff --git a/packages/-ember-data/tests/integration/adapter/record-persistence-test.js b/packages/-ember-data/tests/integration/adapter/record-persistence-test.js index 69e34ecd11f..a65c9da3573 100644 --- a/packages/-ember-data/tests/integration/adapter/record-persistence-test.js +++ b/packages/-ember-data/tests/integration/adapter/record-persistence-test.js @@ -171,7 +171,7 @@ module('integration/adapter/record_persistence - Persisting Records', function ( assert.true(tom.isDeleted, 'record is marked as deleted'); }); - test('An adapter can notify the store that a record was updated and provide new data by calling `didSaveRecord`.', async function (assert) { + test('An adapter can notify the store that a record was updated and provide new data.', async function (assert) { class Person extends Model { @attr('string') updatedAt; @attr('string') name; @@ -250,25 +250,25 @@ module('integration/adapter/record_persistence - Persisting Records', function ( assert.strictEqual(savedYehuda, yehuda, 'The record is correct'); assert.false(tom.hasDirtyAttributes, 'Tom is not dirty after saving record'); assert.false(yehuda.hasDirtyAttributes, 'Yehuda is not dirty after dsaving record'); - assert.strictEqual(tom.name, 'Tom Dale', 'name attribute should reflect value of hash passed to didSaveRecords'); + assert.strictEqual(tom.name, 'Tom Dale', 'name attribute should reflect value of hash returned from the request'); assert.strictEqual( tom.updatedAt, 'now', - 'updatedAt attribute should reflect value of hash passed to didSaveRecords' + 'updatedAt attribute should reflect value of hash returned from the request' ); assert.strictEqual( yehuda.name, 'Yehuda Katz', - 'name attribute should reflect value of hash passed to didSaveRecords' + 'name attribute should reflect value of hash returned from the request' ); assert.strictEqual( yehuda.updatedAt, 'now!', - 'updatedAt attribute should reflect value of hash passed to didSaveRecords' + 'updatedAt attribute should reflect value of hash returned from the request' ); }); - test('An adapter can notify the store that records were deleted by calling `didSaveRecords`.', async function (assert) { + test('An adapter can notify the store that records were deleted', async function (assert) { class Person extends Model { @attr('string') updatedAt; @attr('string') name; diff --git a/packages/-ember-data/tests/integration/adapter/rest-adapter-test.js b/packages/-ember-data/tests/integration/adapter/rest-adapter-test.js index 23f87d21636..b1a677cf684 100644 --- a/packages/-ember-data/tests/integration/adapter/rest-adapter-test.js +++ b/packages/-ember-data/tests/integration/adapter/rest-adapter-test.js @@ -2091,10 +2091,6 @@ module('integration/adapter/rest_adapter - REST Adapter', function (hooks) { }, (reason) => { assert.ok(/saved to the server/.test(reason.message)); - // Workaround for #7371 to get the record a correct state before teardown - let identifier = recordIdentifierFor(post); - let im = store._internalModelForResource(identifier); - store.didSaveRecord(im, { data: { id: '1', type: 'post' } }, 'createRecord'); } ); } 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 2a3a2b901da..a897b0572df 100644 --- a/packages/-ember-data/tests/integration/adapter/store-adapter-test.js +++ b/packages/-ember-data/tests/integration/adapter/store-adapter-test.js @@ -210,7 +210,7 @@ module('integration/adapter/store-adapter - DS.Store and DS.Adapter integration }); }); - test('calling store.didSaveRecord can provide an optional hash', async function (assert) { + test('additional new values can be returned on store save', async function (assert) { let store = this.owner.lookup('service:store'); let adapter = store.adapterFor('application'); let Person = store.modelFor('person'); diff --git a/packages/-ember-data/tests/integration/store-test.js b/packages/-ember-data/tests/integration/store-test.js index 62c98a8ce67..f90d003469b 100644 --- a/packages/-ember-data/tests/integration/store-test.js +++ b/packages/-ember-data/tests/integration/store-test.js @@ -1182,32 +1182,29 @@ module('integration/store - deleteRecord', function (hooks) { }, /expected the primary data returned from a 'findRecord' response to be an object but instead it found an array/); }); - testInDebug( - 'store#didSaveRecord should assert when the response to a save does not include the id', - async function (assert) { - this.owner.register('model:car', Car); - this.owner.register('adapter:application', RESTAdapter.extend()); - this.owner.register('serializer:application', RESTSerializer.extend()); + testInDebug('saveRecord should assert when the response to a save does not include the id', async function (assert) { + this.owner.register('model:car', Car); + this.owner.register('adapter:application', RESTAdapter.extend()); + this.owner.register('serializer:application', RESTSerializer.extend()); - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + let store = this.owner.lookup('service:store'); + let adapter = store.adapterFor('application'); - adapter.createRecord = function () { - return {}; - }; + adapter.createRecord = function () { + return {}; + }; - let car = store.createRecord('car'); + let car = store.createRecord('car'); - await assert.expectAssertion(async () => { - await car.save(); - }, /Your car record was saved to the server, but the response does not have an id and no id has been set client side. Records must have ids. Please update the server response to provide an id in the response or generate the id on the client side either before saving the record or while normalizing the response./); + await assert.expectAssertion(async () => { + await car.save(); + }, /Your car record was saved to the server, but the response does not have an id and no id has been set client side. Records must have ids. Please update the server response to provide an id in the response or generate the id on the client side either before saving the record or while normalizing the response./); - // This is here to transition the model out of the inFlight state to avoid - // throwing another error when the test context is torn down, which tries - // to unload the record, which is not allowed when record is inFlight. - // car._internalModel.transitionTo('loaded.saved'); - } - ); + // This is here to transition the model out of the inFlight state to avoid + // throwing another error when the test context is torn down, which tries + // to unload the record, which is not allowed when record is inFlight. + // car._internalModel.transitionTo('loaded.saved'); + }); }); module('integration/store - queryRecord', function (hooks) { diff --git a/packages/-ember-data/tests/unit/record-arrays/record-array-test.js b/packages/-ember-data/tests/unit/record-arrays/record-array-test.js index 955949f6c15..a672cb64cad 100644 --- a/packages/-ember-data/tests/unit/record-arrays/record-array-test.js +++ b/packages/-ember-data/tests/unit/record-arrays/record-array-test.js @@ -173,52 +173,26 @@ module('unit/record-arrays/record-array - DS.RecordArray', function (hooks) { content, }); - let model1 = { - id: 1, - identifier: { lid: '@ember-data:lid-model-1' }, - getRecord() { - return this; - }, - }; - let model2 = { - id: 2, - identifier: { lid: '@ember-data:lid-model-2' }, - getRecord() { - return this; - }, - }; - let model3 = { - id: 3, - identifier: { lid: '@ember-data:lid-model-3' }, - getRecord() { - return this; - }, - }; + let model1 = { lid: '@ember-data:lid-model-1' }; + let model2 = { lid: '@ember-data:lid-model-2' }; + let model3 = { lid: '@ember-data:lid-model-3' }; - assert.strictEqual( - recordArray._pushIdentifiers([model1.identifier]), - undefined, - '_pushIdentifiers has no return value' - ); - assert.deepEqual(recordArray.get('content'), [model1.identifier], 'now contains model1'); + assert.strictEqual(recordArray._pushIdentifiers([model1]), undefined, '_pushIdentifiers has no return value'); + assert.deepEqual(recordArray.get('content'), [model1], 'now contains model1'); - recordArray._pushIdentifiers([model1.identifier]); + recordArray._pushIdentifiers([model1]); assert.deepEqual( recordArray.get('content'), - [model1.identifier, model1.identifier], + [model1, model1], 'allows duplicates, because record-array-manager ensures no duplicates, this layer should not double check' ); - recordArray._removeIdentifiers([model1.identifier]); - recordArray._pushIdentifiers([model1.identifier]); + recordArray._removeIdentifiers([model1]); + recordArray._pushIdentifiers([model1]); // can add multiple models at once - recordArray._pushIdentifiers([model2.identifier, model3.identifier]); - assert.deepEqual( - recordArray.get('content'), - [model1.identifier, model2.identifier, model3.identifier], - 'now contains model1, model2, model3' - ); + recordArray._pushIdentifiers([model2, model3]); + assert.deepEqual(recordArray.get('content'), [model1, model2, model3], 'now contains model1, model2, model3'); }); test('#_removeIdentifiers', async function (assert) { @@ -227,70 +201,32 @@ module('unit/record-arrays/record-array - DS.RecordArray', function (hooks) { content, }); - let model1 = { - id: 1, - identifier: { lid: '@ember-data:lid-model-1' }, - getRecord() { - return 'model-1'; - }, - }; - let model2 = { - id: 2, - identifier: { lid: '@ember-data:lid-model-2' }, - getRecord() { - return 'model-2'; - }, - }; - let model3 = { - id: 3, - identifier: { lid: '@ember-data:lid-model-3' }, - getRecord() { - return 'model-3'; - }, - }; + let model1 = { lid: '@ember-data:lid-model-1' }; + let model2 = { lid: '@ember-data:lid-model-2' }; + let model3 = { lid: '@ember-data:lid-model-3' }; assert.strictEqual(recordArray.get('content').length, 0); - assert.strictEqual( - recordArray._removeIdentifiers([model1.identifier]), - undefined, - '_removeIdentifiers has no return value' - ); + assert.strictEqual(recordArray._removeIdentifiers([model1]), undefined, '_removeIdentifiers has no return value'); assert.deepEqual(recordArray.get('content'), [], 'now contains no models'); - recordArray._pushIdentifiers([model1.identifier, model2.identifier]); + recordArray._pushIdentifiers([model1, model2]); - assert.deepEqual( - recordArray.get('content'), - [model1.identifier, model2.identifier], - 'now contains model1, model2,' - ); - assert.strictEqual( - recordArray._removeIdentifiers([model1.identifier]), - undefined, - '_removeIdentifiers has no return value' - ); - assert.deepEqual(recordArray.get('content'), [model2.identifier], 'now only contains model2'); - assert.strictEqual( - recordArray._removeIdentifiers([model2.identifier]), - undefined, - '_removeIdentifiers has no return value' - ); + assert.deepEqual(recordArray.get('content'), [model1, model2], 'now contains model1, model2,'); + assert.strictEqual(recordArray._removeIdentifiers([model1]), undefined, '_removeIdentifiers has no return value'); + assert.deepEqual(recordArray.get('content'), [model2], 'now only contains model2'); + assert.strictEqual(recordArray._removeIdentifiers([model2]), undefined, '_removeIdentifiers has no return value'); assert.deepEqual(recordArray.get('content'), [], 'now contains no models'); - recordArray._pushIdentifiers([model1.identifier, model2.identifier, model3.identifier]); + recordArray._pushIdentifiers([model1, model2, model3]); assert.strictEqual( - recordArray._removeIdentifiers([model1.identifier, model3.identifier]), + recordArray._removeIdentifiers([model1, model3]), undefined, '_removeIdentifiers has no return value' ); - assert.deepEqual(recordArray.get('content'), [model2.identifier], 'now contains model2'); - assert.strictEqual( - recordArray._removeIdentifiers([model2.identifier]), - undefined, - '_removeIdentifiers has no return value' - ); + assert.deepEqual(recordArray.get('content'), [model2], 'now contains model2'); + assert.strictEqual(recordArray._removeIdentifiers([model2]), undefined, '_removeIdentifiers has no return value'); assert.deepEqual(recordArray.get('content'), [], 'now contains no models'); }); diff --git a/packages/-ember-data/tests/unit/store/asserts-test.js b/packages/-ember-data/tests/unit/store/asserts-test.js index 464dfb5ef71..8988e21dbf1 100644 --- a/packages/-ember-data/tests/unit/store/asserts-test.js +++ b/packages/-ember-data/tests/unit/store/asserts-test.js @@ -62,7 +62,6 @@ module('unit/store/asserts - DS.Store methods produce useful assertion messages' 'findAll', 'peekAll', 'unloadAll', - 'didSaveRecord', 'modelFor', '_modelFactoryFor', 'push', diff --git a/packages/-ember-data/tests/unit/store/push-test.js b/packages/-ember-data/tests/unit/store/push-test.js index dec129c89ad..be984b9eb6f 100644 --- a/packages/-ember-data/tests/unit/store/push-test.js +++ b/packages/-ember-data/tests/unit/store/push-test.js @@ -895,20 +895,16 @@ module('unit/store/push - DS.Store#push', function (hooks) { }, /The payload for 'person' contains these unknown .*: .* Make sure they've been defined in your model./); }); - test('_push returns an instance of InternalModel if an object is pushed', function (assert) { - let pushResult; - - run(() => { - pushResult = store._push({ - data: { - id: 1, - type: 'person', - }, - }); + test('_push returns an identifier if an object is pushed', function (assert) { + let pushResult = store._push({ + data: { + id: 1, + type: 'person', + }, }); - assert.ok(pushResult instanceof DS.InternalModel); - assert.notOk(pushResult.record, 'InternalModel is not materialized'); + assert.strictEqual(pushResult, store.identifierCache.getOrCreateRecordIdentifier({ type: 'person', id: '1' })); + assert.notOk(store._instanceCache.peek(pushResult, { bucket: 'record' }), 'record is not materialized'); }); test('_push does not require a modelName to resolve to a modelClass', function (assert) { @@ -930,7 +926,7 @@ module('unit/store/push - DS.Store#push', function (hooks) { assert.ok('We made it'); }); - test('_push returns an array of InternalModels if an array is pushed', function (assert) { + test('_push returns an array of identifiers if an array is pushed', function (assert) { let pushResult; run(() => { @@ -945,8 +941,8 @@ module('unit/store/push - DS.Store#push', function (hooks) { }); assert.ok(pushResult instanceof Array); - assert.ok(pushResult[0] instanceof DS.InternalModel); - assert.notOk(pushResult[0].record, 'InternalModel is not materialized'); + assert.strictEqual(pushResult[0], store.identifierCache.getOrCreateRecordIdentifier({ type: 'person', id: '1' })); + assert.notOk(store._instanceCache.peek(pushResult[0], { bucket: 'record' }), 'record is not materialized'); }); test('_push returns null if no data is pushed', function (assert) { diff --git a/packages/model/addon/-private/system/many-array.ts b/packages/model/addon/-private/system/many-array.ts index cafb259b5ea..5f9c34ad214 100644 --- a/packages/model/addon/-private/system/many-array.ts +++ b/packages/model/addon/-private/system/many-array.ts @@ -16,6 +16,7 @@ import type { CreateRecordProperties } from '@ember-data/store/-private/system/c import ShimModelClass from '@ember-data/store/-private/system/model/shim-model-class'; import type { DSModelSchema } from '@ember-data/store/-private/ts-interfaces/ds-model'; import type { Links, PaginationLinks } from '@ember-data/store/-private/ts-interfaces/ember-data-json-api'; +import { StableRecordIdentifier } from '@ember-data/store/-private/ts-interfaces/identifier'; import type { RecordInstance } from '@ember-data/store/-private/ts-interfaces/record-instance'; import type { Dict } from '@ember-data/store/-private/ts-interfaces/utils'; @@ -83,7 +84,7 @@ export interface ManyArrayCreateArgs { @extends Ember.EmberObject @uses Ember.MutableArray */ -export default class ManyArray extends MutableArrayWithObject { +export default class ManyArray extends MutableArrayWithObject { declare isAsync: boolean; declare isLoaded: boolean; declare isPolymorphic: boolean; @@ -95,7 +96,7 @@ export default class ManyArray extends MutableArrayWithObject | null; declare _links: Links | PaginationLinks | null; - declare currentState: InternalModel[]; + declare currentState: StableRecordIdentifier[]; declare recordData: RelationshipRecordData; declare internalModel: InternalModel; declare store: CoreStore; @@ -260,23 +261,25 @@ export default class ManyArray extends MutableArrayWithObject { - let internalModels: InternalModel[]; + const { store } = this; + store._backburner.join(() => { + let identifiers: StableRecordIdentifier[]; if (amt > 0) { - internalModels = this.currentState.slice(idx, idx + amt); + identifiers = this.currentState.slice(idx, idx + amt); this.recordData.removeFromHasMany( this.key, - internalModels.map((im) => recordDataFor(im)) + // TODO RecordData V2: recordData should take identifiers not RecordDatas + identifiers.map((identifier) => store._instanceCache.getRecordData(identifier)) ); } if (objects) { @@ -303,14 +306,15 @@ export default class ManyArray extends MutableArrayWithObject; @@ -20,7 +21,7 @@ export class InstanceCache { } peek({ identifier, bucket }: { identifier: StableRecordIdentifier; bucket: 'record' }): RecordInstance | undefined { - return this.#instances[bucket].get(identifier); + return this.#instances[bucket]?.get(identifier); } set({ identifier, @@ -37,9 +38,22 @@ export class InstanceCache { getRecord(identifier: StableRecordIdentifier, properties?: CreateRecordProperties): RecordInstance { let record = this.peek({ identifier, bucket: 'record' }); - // TODO how to handle dematerializing - if (!record) { + // TODO store this state somewhere better + const internalModel = this.getInternalModel(identifier); + + if (internalModel._isDematerializing) { + // TODO this should be an assertion, this likely means + // we have a bug to find wherein our own store is calling this + // with an identifier that should have already been disconnected. + // the destroy + fetch again case is likely either preserving the + // identifier for re-use or failing to unload it. + return null as unknown as RecordInstance; + } + + // TODO store this state somewhere better + internalModel.hasRecord = true; + if (properties && 'id' in properties) { assert(`expected id to be a string or null`, properties.id !== undefined); this.getInternalModel(identifier).setId(properties.id); @@ -72,4 +86,9 @@ export class InstanceCache { getInternalModel(identifier: StableRecordIdentifier) { return this.store._internalModelForResource(identifier); } + + // TODO move InternalModel cache into InstanceCache + createSnapshot(identifier: StableRecordIdentifier, options?: FindOptions) { + return this.getInternalModel(identifier).createSnapshot(options); + } } diff --git a/packages/store/addon/-private/system/core-store.ts b/packages/store/addon/-private/system/core-store.ts index dd4460cb900..446a8f2d7e4 100644 --- a/packages/store/addon/-private/system/core-store.ts +++ b/packages/store/addon/-private/system/core-store.ts @@ -11,7 +11,7 @@ import { DEBUG } from '@glimmer/env'; import Ember from 'ember'; import { importSync } from '@embroider/macros'; -import { all, default as RSVP, reject, resolve } from 'rsvp'; +import { all, reject, resolve } from 'rsvp'; import type DSModelClass from '@ember-data/model'; import { HAS_RECORD_DATA_PACKAGE } from '@ember-data/private-build-infra'; @@ -63,8 +63,7 @@ import { import type ShimModelClass from './model/shim-model-class'; import { getShimClass } from './model/shim-model-class'; import normalizeModelName from './normalize-model-name'; -import type { PromiseArray, PromiseObject } from './promise-proxies'; -import { promiseArray, promiseObject } from './promise-proxies'; +import { PromiseArray, promiseArray, PromiseObject, promiseObject } from './promise-proxies'; import RecordArrayManager from './record-array-manager'; import { AdapterPopulatedRecordArray, RecordArray } from './record-arrays'; import { setRecordDataFor } from './record-data-for'; @@ -223,30 +222,6 @@ class CoreStore extends Service { declare _trackAsyncRequestEnd: (token: AsyncTrackingToken) => void; declare __asyncWaiter: () => boolean; - /** - The default adapter to use to communicate to a backend server or - other persistence layer. This will be overridden by an application - adapter if present. - - If you want to specify `app/adapters/custom.js` as a string, do: - - ```js - import Store from '@ember-data/store'; - - export default Store.extend({ - constructor() { - super(...arguments); - this.adapter = 'custom'; - } - } - ``` - - @property adapter - @public - @default '-json-api' - @type {String} - */ - /** @method init @private @@ -335,6 +310,7 @@ class CoreStore extends Service { return this._fetchManager.requestCache; } + // TODO move this to InstanceCache _instantiateRecord( recordData: RecordData, identifier: StableRecordIdentifier, @@ -408,9 +384,11 @@ class CoreStore extends Service { let record = this._modelFactoryFor(modelName).create(createOptions); return record; } + + // TODO move this to InstanceCache _teardownRecord(record: DSModel | RecordInstance) { StoreMap.delete(record); - // TODO remove identifier + // TODO remove identifier:record cache link this.teardownRecord(record); } teardownRecord(record: DSModel | RecordInstance): void { @@ -420,6 +398,7 @@ class CoreStore extends Service { ); (record as DSModel).destroy(); } + getSchemaDefinitionService(): SchemaDefinitionService { if (!this._schemaDefinitionService) { this._schemaDefinitionService = new DSModelSchemaDefinitionService(this); @@ -573,11 +552,12 @@ class CoreStore extends Service { const factory = internalModelFactoryFor(this); const internalModel = factory.build({ type: normalizedModelName, id: properties.id }); + const { identifier } = internalModel; - internalModel._recordData.clientDidCreate(); - this.recordArrayManager.recordDidChange(internalModel.identifier); + this._instanceCache.getRecordData(identifier).clientDidCreate(); + this.recordArrayManager.recordDidChange(identifier); - return internalModel.getRecord(properties); + return this._instanceCache.getRecord(identifier, properties); }); }); } @@ -1100,59 +1080,48 @@ class CoreStore extends Service { } const internalModel = internalModelFactoryFor(this).lookup(resource); + const { identifier } = internalModel; + let promise; options = options || {}; + // if not loaded start loading if (!internalModel.isLoaded) { - return promiseRecord( - this, - this._fetchDataIfNeededForIdentifier(internalModel.identifier, options), - `DS: Store#findRecord ${internalModel.identifier}` - ); - } + promise = this._fetchDataIfNeededForIdentifier(identifier, options); - let fetchedIdentifier = this._findRecord(internalModel, options); - - return promiseRecord(this, fetchedIdentifier, `DS: Store#findRecord ${internalModel.identifier}`); - } - - _findRecord(internalModel: InternalModel, options: FindOptions): Promise { - const { identifier } = internalModel; - - // Refetch if the reload option is passed - if (options.reload) { - assertIdentifierHasId(identifier); - return this._fetchManager.scheduleFetch(identifier, options); - } - - let snapshot = internalModel.createSnapshot(options); - let adapter = this.adapterFor(internalModel.modelName); - - // Refetch the record if the adapter thinks the record is stale - if ( - typeof options.reload === 'undefined' && - adapter.shouldReloadRecord && - adapter.shouldReloadRecord(this, snapshot) - ) { + // Refetch if the reload option is passed + } else if (options.reload) { assertIdentifierHasId(identifier); - return this._fetchManager.scheduleFetch(identifier, options); - } - - if (options.backgroundReload === false) { - return resolve(internalModel.identifier); - } + promise = this._fetchManager.scheduleFetch(identifier, options); + } else { + let snapshot = this._instanceCache.createSnapshot(identifier, options); + let adapter = this.adapterFor(identifier.type); + + // Refetch the record if the adapter thinks the record is stale + if ( + typeof options.reload === 'undefined' && + adapter.shouldReloadRecord && + adapter.shouldReloadRecord(this, snapshot) + ) { + assertIdentifierHasId(identifier); + promise = this._fetchManager.scheduleFetch(identifier, options); + } else { + // Trigger the background refetch if backgroundReload option is passed + if ( + options.backgroundReload !== false && + (options.backgroundReload || + !adapter.shouldBackgroundReloadRecord || + adapter.shouldBackgroundReloadRecord(this, snapshot)) + ) { + assertIdentifierHasId(identifier); + this._fetchManager.scheduleFetch(identifier, options); + } - // Trigger the background refetch if backgroundReload option is passed - if ( - options.backgroundReload || - !adapter.shouldBackgroundReloadRecord || - adapter.shouldBackgroundReloadRecord(this, snapshot) - ) { - assertIdentifierHasId(identifier); - this._fetchManager.scheduleFetch(identifier, options); + // Return the cached record + promise = resolve(identifier); + } } - // Return the cached record - return resolve(internalModel.identifier); + return promiseRecord(this, promise, `DS: Store#findRecord ${identifier}`); } _fetchDataIfNeededForIdentifier( @@ -1318,11 +1287,11 @@ class CoreStore extends Service { peekRecord(identifier: ResourceIdentifierObject): RecordInstance | null; peekRecord(identifier: ResourceIdentifierObject | string, id?: string | number): RecordInstance | null { if (arguments.length === 1 && isMaybeIdentifier(identifier)) { - let stableIdentifier = this.identifierCache.peekRecordIdentifier(identifier); - if (stableIdentifier) { - return internalModelFactoryFor(this).peek(stableIdentifier)?.getRecord() || null; - } - return null; + const stableIdentifier = this.identifierCache.peekRecordIdentifier(identifier); + const internalModel = stableIdentifier && internalModelFactoryFor(this).peek(stableIdentifier); + // TODO come up with a better mechanism for determining if we have data and could peek. + // this is basically an "are we not empty" query. + return internalModel && internalModel.isLoaded ? this._instanceCache.getRecord(stableIdentifier) : null; } if (DEBUG) { @@ -1341,7 +1310,7 @@ class CoreStore extends Service { const stableIdentifier = this.identifierCache.peekRecordIdentifier(resource); const internalModel = stableIdentifier && internalModelFactoryFor(this).peek(stableIdentifier); - return internalModel && internalModel.isLoaded ? internalModel.getRecord() : null; + return internalModel && internalModel.isLoaded ? this._instanceCache.getRecord(stableIdentifier) : null; } /** @@ -1395,7 +1364,7 @@ class CoreStore extends Service { _findHasManyByJsonApiResource( resource, - parentInternalModel: InternalModel, + parentIdentifier: StableRecordIdentifier, relationship: ManyRelationship, options?: FindOptions ): Promise { @@ -1420,7 +1389,7 @@ class CoreStore extends Service { // findHasMany, although not public, does not need to care about our upgrade relationship definitions // and can stick with the public definition API for now. const relationshipMeta = this._storeWrapper.relationshipsDefinitionFor(definition.inverseType)[definition.key]; - let adapter = this.adapterFor(parentInternalModel.modelName); + let adapter = this.adapterFor(parentIdentifier.type); /* If a relationship was originally populated by the adapter as a link @@ -1434,7 +1403,7 @@ class CoreStore extends Service { then use that URL in the future to make a request for the relationship. */ assert( - `You tried to load a hasMany relationship but you have no adapter (for ${parentInternalModel.modelName})`, + `You tried to load a hasMany relationship but you have no adapter (for ${parentIdentifier.type})`, adapter ); assert( @@ -1442,7 +1411,7 @@ class CoreStore extends Service { typeof adapter.findHasMany === 'function' ); - return _findHasMany(adapter, this, parentInternalModel, resource.links.related, relationshipMeta, options); + return _findHasMany(adapter, this, parentIdentifier, resource.links.related, relationshipMeta, options); } let preferLocalCache = hasReceivedData && !isEmpty; @@ -1477,40 +1446,15 @@ class CoreStore extends Service { assert(`hasMany only works with the @ember-data/record-data package`); } - _fetchBelongsToLinkFromResource( - resource, - parentInternalModel: InternalModel, - relationshipMeta, - options - ): Promise { - if (DEBUG) { - assertDestroyingStore(this, '_fetchBelongsToLinkFromResource'); - } - if (!resource || !resource.links || !resource.links.related) { - // should we warn here, not sure cause its an internal method - return resolve(null); - } - - let adapter = this.adapterFor(parentInternalModel.modelName); - - assert( - `You tried to load a belongsTo relationship but you have no adapter (for ${parentInternalModel.modelName})`, - adapter - ); - assert( - `You tried to load a belongsTo relationship from a specified 'link' in the original payload but your adapter does not implement 'findBelongsTo'`, - typeof adapter.findBelongsTo === 'function' - ); - - return _findBelongsTo(adapter, this, parentInternalModel, resource.links.related, relationshipMeta, options); - } - _findBelongsToByJsonApiResource( resource, - parentInternalModel: InternalModel, + parentIdentifier: StableRecordIdentifier, relationshipMeta, options: FindOptions = {} ): Promise { + if (DEBUG) { + assertDestroyingStore(this, '_findBelongsToByJsonApiResource'); + } if (!resource) { return resolve(null); } @@ -1535,9 +1479,7 @@ class CoreStore extends Service { // fetch via link if (shouldFindViaLink) { - return this._fetchBelongsToLinkFromResource(resource, parentInternalModel, relationshipMeta, options).then((im) => - im ? im.identifier : null - ); + return _findBelongsTo(this, parentIdentifier, resource.links.related, relationshipMeta, options); } let preferLocalCache = hasReceivedData && allInverseRecordsAreLoaded && !isEmpty; @@ -1794,19 +1736,15 @@ class CoreStore extends Service { typeof adapter.queryRecord === 'function' ); - const promise: Promise = _queryRecord( + const promise: Promise = _queryRecord( adapter, this, normalizedModelName, query, adapterOptionsWrapper - ) as Promise; + ) as Promise; - return promiseObject( - promise.then((internalModel: InternalModel | null) => { - return internalModel ? internalModel.getRecord() : null; - }) - ); + return promiseObject(promise.then((identifier) => identifier && this.peekRecord(identifier))); } /** @@ -2133,118 +2071,6 @@ class CoreStore extends Service { // . PERSISTING . // .............. - /** - This method is called by `record.save`, and gets passed a - resolver for the promise that `record.save` returns. - - It schedules saving to happen at the end of the run loop. - - @method scheduleSave - @private - @param {InternalModel} internalModel - @param {Resolver} resolver - @param {Object} options - */ - scheduleSave(internalModel: InternalModel, resolver: RSVP.Deferred, options: FindOptions): Promise { - assert( - `Cannot initiate a save request for an unloaded record: ${internalModel.identifier}`, - !internalModel.isEmpty && !internalModel.isDestroyed - ); - if (internalModel._isRecordFullyDeleted()) { - resolver.resolve(); - return resolver.promise; - } - - internalModel.adapterWillCommit(); - - if (!options) { - options = {}; - } - let recordData = internalModel._recordData; - let operation: 'createRecord' | 'deleteRecord' | 'updateRecord' = 'updateRecord'; - - // TODO handle missing isNew - if (recordData.isNew && recordData.isNew()) { - operation = 'createRecord'; - } else if (recordData.isDeleted && recordData.isDeleted()) { - operation = 'deleteRecord'; - } - - const saveOptions = Object.assign({ [SaveOp]: operation }, options); - let fetchManagerPromise = this._fetchManager.scheduleSave(internalModel.identifier, saveOptions); - let promise = fetchManagerPromise.then( - (payload) => { - /* - Note to future spelunkers hoping to optimize. - We rely on this `run` to create a run loop if needed - that `store._push` and `store.didSaveRecord` will both share. - - We use `join` because it is often the case that we - have an outer run loop available still from the first - call to `store._push`; - */ - this._backburner.join(() => { - let data = payload && payload.data; - this.didSaveRecord(internalModel, { data }, operation); - if (payload && payload.included) { - this._push({ data: null, included: payload.included }); - } - }); - }, - (e) => { - if (typeof e === 'string') { - throw e; - } - const { error, parsedErrors } = e; - internalModel.adapterDidInvalidate(parsedErrors, error); - throw error; - } - ); - - return promise; - } - - /** - This method is called once the promise returned by an - adapter's `createRecord`, `updateRecord` or `deleteRecord` - is resolved. - - If the data provides a server-generated ID, it will - update the record and the store's indexes. - - @method didSaveRecord - @private - @param {InternalModel} internalModel the in-flight internal model - @param {Object} data optional data (see above) - @param {string} op the adapter operation that was committed - */ - didSaveRecord(internalModel, dataArg, op: 'createRecord' | 'updateRecord' | 'deleteRecord') { - if (DEBUG) { - assertDestroyingStore(this, 'didSaveRecord'); - } - let data; - if (dataArg) { - data = dataArg.data; - } - if (!data) { - assert( - `Your ${internalModel.modelName} record was saved to the server, but the response does not have an id and no id has been set client side. Records must have ids. Please update the server response to provide an id in the response or generate the id on the client side either before saving the record or while normalizing the response.`, - internalModel.id - ); - } - - const cache = this.identifierCache; - const identifier = internalModel.identifier; - - if (op !== 'deleteRecord' && data) { - cache.updateRecordIdentifier(identifier, data); - } - - //We first make sure the primary data has been updated - //TODO try to move notification to the user to the end of the runloop - internalModel.adapterDidCommit(data); - } - /** This method is called once the promise returned by an adapter's `createRecord`, `updateRecord` or `deleteRecord` @@ -2286,6 +2112,7 @@ class CoreStore extends Service { @param {string} newId @param {string} clientId */ + // TODO move this into one of the caches setRecordId(modelName: string, newId: string, clientId: string) { if (DEBUG) { assertDestroyingStore(this, 'setRecordId'); @@ -2304,7 +2131,48 @@ class CoreStore extends Service { @private @param {Object} data */ - _load(data: ExistingResourceObject) { + _load(data: ExistingResourceObject): StableExistingRecordIdentifier { + // TODO type should be pulled from the identifier for debug + let modelName = data.type; + assert( + `You must include an 'id' for ${modelName} in an object passed to 'push'`, + data.id !== null && data.id !== undefined && data.id !== '' + ); + assert( + `You tried to push data with a type '${modelName}' but no model could be found with that name.`, + this.getSchemaDefinitionService().doesTypeExist(modelName) + ); + + if (DEBUG) { + // If ENV.DS_WARN_ON_UNKNOWN_KEYS is set to true and the payload + // contains unknown attributes or relationships, log a warning. + + // TODO @runspired @deprecate in favor of a build-time config not in ENV + if (ENV.DS_WARN_ON_UNKNOWN_KEYS) { + let unknownAttributes, unknownRelationships; + let relationships = this.getSchemaDefinitionService().relationshipsDefinitionFor({ type: modelName }); + let attributes = this.getSchemaDefinitionService().attributesDefinitionFor({ type: modelName }); + // Check unknown attributes + unknownAttributes = Object.keys(data.attributes || {}).filter((key) => { + return !attributes[key]; + }); + + // Check unknown relationships + unknownRelationships = Object.keys(data.relationships || {}).filter((key) => { + return !relationships[key]; + }); + let unknownAttributesMessage = `The payload for '${modelName}' contains these unknown attributes: ${unknownAttributes}. Make sure they've been defined in your model.`; + warn(unknownAttributesMessage, unknownAttributes.length === 0, { + id: 'ds.store.unknown-keys-in-payload', + }); + + let unknownRelationshipsMessage = `The payload for '${modelName}' contains these unknown relationships: ${unknownRelationships}. Make sure they've been defined in your model.`; + warn(unknownRelationshipsMessage, unknownRelationships.length === 0, { + id: 'ds.store.unknown-keys-in-payload', + }); + } + } + // TODO this should determine identifier via the cache before making assumptions const resource = constructResource(normalizeModelName(data.type), ensureStringId(data.id), coerceId(data.lid)); const maybeIdentifier = this.identifierCache.peekRecordIdentifier(resource); @@ -2340,7 +2208,7 @@ class CoreStore extends Service { this.recordArrayManager.recordDidChange(identifier); } - return internalModel; + return internalModel.identifier as StableExistingRecordIdentifier; } /** @@ -2504,7 +2372,7 @@ class CoreStore extends Service { let pushed = this._push(data); if (Array.isArray(pushed)) { - let records = pushed.map((internalModel) => internalModel.getRecord()); + let records = pushed.map((identifier) => this._instanceCache.getRecord(identifier)); return records; } @@ -2512,8 +2380,7 @@ class CoreStore extends Service { return null; } - let record = pushed.getRecord(); - return record; + return this._instanceCache.getRecord(pushed); } /** @@ -2525,28 +2392,28 @@ class CoreStore extends Service { @param {Object} jsonApiDoc @return {InternalModel|Array} pushed InternalModel(s) */ - _push(jsonApiDoc): InternalModel | InternalModel[] | null { + _push(jsonApiDoc): StableExistingRecordIdentifier | StableExistingRecordIdentifier[] | null { if (DEBUG) { assertDestroyingStore(this, '_push'); } - let internalModelOrModels = this._backburner.join(() => { + let identifiers = this._backburner.join(() => { let included = jsonApiDoc.included; let i, length; if (included) { for (i = 0, length = included.length; i < length; i++) { - this._pushInternalModel(included[i]); + this._load(included[i]); } } if (Array.isArray(jsonApiDoc.data)) { length = jsonApiDoc.data.length; - let internalModels = new Array(length); + let identifiers = new Array(length); for (i = 0; i < length; i++) { - internalModels[i] = this._pushInternalModel(jsonApiDoc.data[i]); + identifiers[i] = this._load(jsonApiDoc.data[i]); } - return internalModels; + return identifiers; } if (jsonApiDoc.data === null) { @@ -2560,61 +2427,11 @@ class CoreStore extends Service { typeof jsonApiDoc.data === 'object' ); - return this._pushInternalModel(jsonApiDoc.data); + return this._load(jsonApiDoc.data); }); // this typecast is necessary because `backburner.join` is mistyped to return void - return internalModelOrModels as unknown as InternalModel | InternalModel[]; - } - - _pushInternalModel(data) { - // TODO type should be pulled from the identifier for debug - let modelName = data.type; - assert( - `You must include an 'id' for ${modelName} in an object passed to 'push'`, - data.id !== null && data.id !== undefined && data.id !== '' - ); - assert( - `You tried to push data with a type '${modelName}' but no model could be found with that name.`, - this.getSchemaDefinitionService().doesTypeExist(modelName) - ); - - if (DEBUG) { - // If ENV.DS_WARN_ON_UNKNOWN_KEYS is set to true and the payload - // contains unknown attributes or relationships, log a warning. - - // TODO @runspired @deprecate in favor of a build-time config not in ENV - if (ENV.DS_WARN_ON_UNKNOWN_KEYS) { - let unknownAttributes, unknownRelationships; - let relationships = this.getSchemaDefinitionService().relationshipsDefinitionFor(modelName); - let attributes = this.getSchemaDefinitionService().attributesDefinitionFor(modelName); - // Check unknown attributes - unknownAttributes = Object.keys(data.attributes || {}).filter((key) => { - return !attributes[key]; - }); - - // Check unknown relationships - unknownRelationships = Object.keys(data.relationships || {}).filter((key) => { - return !relationships[key]; - }); - let unknownAttributesMessage = `The payload for '${modelName}' contains these unknown attributes: ${unknownAttributes}. Make sure they've been defined in your model.`; - warn(unknownAttributesMessage, unknownAttributes.length === 0, { - id: 'ds.store.unknown-keys-in-payload', - }); - - let unknownRelationshipsMessage = `The payload for '${modelName}' contains these unknown relationships: ${unknownRelationships}. Make sure they've been defined in your model.`; - warn(unknownRelationshipsMessage, unknownRelationships.length === 0, { - id: 'ds.store.unknown-keys-in-payload', - }); - } - } - - // Actually load the record into the store. - let internalModel = this._load(data); - - // this._setupRelationshipsForModel(internalModel, data); - - return internalModel; + return identifiers; } /** @@ -2673,6 +2490,7 @@ class CoreStore extends Service { @param {String} modelName Optionally, a model type used to determine which serializer will be used @param {Object} inputPayload */ + // TODO @runspired @deprecate pushPayload in favor of looking up the serializer pushPayload(modelName, inputPayload) { if (DEBUG) { assertDestroyingStore(this, 'pushPayload'); @@ -2702,6 +2520,7 @@ class CoreStore extends Service { serializer.pushPayload(this, payload); } + // TODO string candidate for early elimination _internalModelForResource(resource: ResourceIdentifierObject): InternalModel { return internalModelFactoryFor(this).getByResource(resource); } @@ -2714,6 +2533,7 @@ class CoreStore extends Service { return internalModel!.createSnapshot(options).serialize(options); } + // todo @runspired this should likely be publicly documented for custom records saveRecord(record: RecordInstance, options: Dict = {}): Promise { assert(`Unable to initate save for a record in a disconnected state`, storeFor(record)); let identifier = recordIdentifierFor(record); @@ -2728,12 +2548,87 @@ class CoreStore extends Service { // Casting can be removed once REQUEST_SERVICE ff is turned on // because a `Record` is provided there will always be a matching internalModel - let promiseLabel = 'DS: Model#save ' + this; - let resolver = RSVP.defer(promiseLabel); + assert( + `Cannot initiate a save request for an unloaded record: ${internalModel.identifier}`, + !internalModel.isEmpty && !internalModel.isDestroyed + ); + if (internalModel._isRecordFullyDeleted()) { + return resolve(record); + } + + internalModel.adapterWillCommit(); + + if (!options) { + options = {}; + } + let recordData = internalModel._recordData; + let operation: 'createRecord' | 'deleteRecord' | 'updateRecord' = 'updateRecord'; + + // TODO handle missing isNew + if (recordData.isNew && recordData.isNew()) { + operation = 'createRecord'; + } else if (recordData.isDeleted && recordData.isDeleted()) { + operation = 'deleteRecord'; + } + + const saveOptions = Object.assign({ [SaveOp]: operation }, options); + let fetchManagerPromise = this._fetchManager.scheduleSave(internalModel.identifier, saveOptions); + return fetchManagerPromise.then( + (payload) => { + /* + // TODO @runspired re-evaluate the below claim now that + // the save request pipeline is more streamlined. + + Note to future spelunkers hoping to optimize. + We rely on this `run` to create a run loop if needed + that `store._push` and `store.saveRecord` will both share. + + We use `join` because it is often the case that we + have an outer run loop available still from the first + call to `store._push`; + */ + this._backburner.join(() => { + if (DEBUG) { + assertDestroyingStore(this, 'saveRecord'); + } + + let data = payload && payload.data; + if (!data) { + assert( + `Your ${internalModel.modelName} record was saved to the server, but the response does not have an id and no id has been set client side. Records must have ids. Please update the server response to provide an id in the response or generate the id on the client side either before saving the record or while normalizing the response.`, + internalModel.id + ); + } - return this.scheduleSave(internalModel, resolver, options).then(() => record); + const cache = this.identifierCache; + const identifier = internalModel.identifier; + + if (operation !== 'deleteRecord' && data) { + cache.updateRecordIdentifier(identifier, data); + } + + //We first make sure the primary data has been updated + //TODO try to move notification to the user to the end of the runloop + internalModel.adapterDidCommit(data); + + if (payload && payload.included) { + this._push({ data: null, included: payload.included }); + } + }); + return record; + }, + (e) => { + if (typeof e === 'string') { + throw e; + } + const { error, parsedErrors } = e; + internalModel.adapterDidInvalidate(parsedErrors, error); + throw error; + } + ); } + // TODO move this to InstanceCache relationshipReferenceFor(identifier: RecordIdentifier, key: string): BelongsToReference | HasManyReference { let stableIdentifier = this.identifierCache.getOrCreateRecordIdentifier(identifier); let internalModel = internalModelFactoryFor(this).peek(stableIdentifier); @@ -2747,6 +2642,7 @@ class CoreStore extends Service { * @method _createRecordData * @internal */ + // TODO move this to InstanceCache _createRecordData(identifier: StableRecordIdentifier): RecordData { const recordData = this.createRecordDataFor(identifier.type, identifier.id, identifier.lid, this._storeWrapper); setRecordDataFor(identifier, recordData); @@ -2800,6 +2696,8 @@ class CoreStore extends Service { /** * @internal */ + + // TODO move this to InstanceCache private property __recordDataFor(resource: RecordIdentifier) { const identifier = this.identifierCache.getOrCreateRecordIdentifier(resource); return this.recordDataFor(identifier, false); @@ -2808,6 +2706,7 @@ class CoreStore extends Service { /** * @internal */ + // TODO move this to InstanceCache recordDataFor(identifier: StableRecordIdentifier | { type: string }, isCreate: boolean): RecordData { let internalModel: InternalModel; if (isCreate === true) { diff --git a/packages/store/addon/-private/system/fetch-manager.ts b/packages/store/addon/-private/system/fetch-manager.ts index a5fd856a793..cdb01958679 100644 --- a/packages/store/addon/-private/system/fetch-manager.ts +++ b/packages/store/addon/-private/system/fetch-manager.ts @@ -277,7 +277,7 @@ export default class FetchManager { // may result in the merging of identifiers (and thus records) let potentiallyNewIm = store._push(payload); if (potentiallyNewIm && !Array.isArray(potentiallyNewIm)) { - return potentiallyNewIm.identifier; + return potentiallyNewIm; } return identifier; diff --git a/packages/store/addon/-private/system/model/internal-model.ts b/packages/store/addon/-private/system/model/internal-model.ts index 85aadd12dbc..5024587c7d9 100644 --- a/packages/store/addon/-private/system/model/internal-model.ts +++ b/packages/store/addon/-private/system/model/internal-model.ts @@ -34,7 +34,6 @@ import type { RecordInstance } from '../../ts-interfaces/record-instance'; import type { FindOptions } from '../../ts-interfaces/store'; import type { Dict } from '../../ts-interfaces/utils'; import type CoreStore from '../core-store'; -import type { CreateRecordProperties } from '../core-store'; import { errorsHashToArray } from '../errors-utils'; import recordDataFor from '../record-data-for'; import { BelongsToReference, HasManyReference, RecordReference } from '../references'; @@ -278,16 +277,6 @@ export default class InternalModel { return !this.isEmpty; } - getRecord(properties?: CreateRecordProperties): RecordInstance { - if (this._isDematerializing) { - // TODO we should assert here instead of this return. - return null as unknown as RecordInstance; - } - - this.hasRecord = true; - return this.store._instanceCache.getRecord(this.identifier, properties); - } - dematerializeRecord() { this._isDematerializing = true; @@ -429,7 +418,7 @@ export default class InternalModel { ): Promise { // TODO @runspired follow up if parent isNew then we should not be attempting load here // TODO @runspired follow up on whether this should be in the relationship requests cache - return this.store._findBelongsToByJsonApiResource(resource, this, relationshipMeta, options).then( + return this.store._findBelongsToByJsonApiResource(resource, this.identifier, relationshipMeta, options).then( (identifier: StableRecordIdentifier | null) => handleCompletedRelationshipRequest(this, key, resource._relationship, identifier), (e) => handleCompletedRelationshipRequest(this, key, resource._relationship, null, e) @@ -455,8 +444,6 @@ export default class InternalModel { }; if (isAsync) { - let internalModel = identifier !== null ? store._internalModelForResource(identifier) : null; - if (resource._relationship.state.hasFailedLoadAttempt) { return this._relationshipProxyCache[key] as PromiseBelongsTo; } @@ -465,15 +452,14 @@ export default class InternalModel { return this._updatePromiseProxyFor('belongsTo', key, { promise, - content: internalModel ? internalModel.getRecord() : null, + content: identifier ? store._instanceCache.getRecord(identifier) : null, _belongsToState, }); } else { if (identifier === null) { return null; } else { - let internalModel = store._internalModelForResource(identifier); - let toReturn = internalModel.getRecord(); + let toReturn = store._instanceCache.getRecord(identifier); assert( "You looked up the '" + key + @@ -482,7 +468,7 @@ export default class InternalModel { "' with id " + parentInternalModel.id + ' but some of the associated records were not loaded. Either make sure they are all loaded together with the parent record, or specify that the relationship is async (`belongsTo({ async: true })`)', - toReturn === null || !internalModel.isEmpty + toReturn === null || !store._instanceCache.getInternalModel(identifier).isEmpty ); return toReturn; } @@ -531,7 +517,7 @@ export default class InternalModel { const jsonApi = this._recordData.getHasMany(key); - loadingPromise = this.store._findHasManyByJsonApiResource(jsonApi, this, relationship, options).then( + loadingPromise = this.store._findHasManyByJsonApiResource(jsonApi, this.identifier, relationship, options).then( () => handleCompletedRelationshipRequest(this, key, relationship, manyArray), (e) => handleCompletedRelationshipRequest(this, key, relationship, manyArray, e) ); @@ -968,7 +954,7 @@ export default class InternalModel { if (error && parsedErrors) { // TODO add assertion forcing consuming RecordData's to implement getErrors if (!this._recordData.getErrors) { - let record = this.getRecord() as DSModel; + let record = this.store._instanceCache.getRecord(this.identifier) as DSModel; let errors = record.errors; for (attribute in parsedErrors) { if (Object.prototype.hasOwnProperty.call(parsedErrors, attribute)) { diff --git a/packages/store/addon/-private/system/record-arrays/record-array.ts b/packages/store/addon/-private/system/record-arrays/record-array.ts index 5d0dae72f10..cc1bd439fd1 100644 --- a/packages/store/addon/-private/system/record-arrays/record-array.ts +++ b/packages/store/addon/-private/system/record-arrays/record-array.ts @@ -20,10 +20,9 @@ import type CoreStore from '../core-store'; import type { PromiseArray } from '../promise-proxies'; import { promiseArray } from '../promise-proxies'; import SnapshotRecordArray from '../snapshot-record-array'; -import { internalModelFactoryFor } from '../store/internal-model-factory'; function recordForIdentifier(store: CoreStore, identifier: StableRecordIdentifier): RecordInstance { - return internalModelFactoryFor(store).lookup(identifier).getRecord(); + return store._instanceCache.getRecord(identifier); } export interface RecordArrayCreateArgs { @@ -291,7 +290,7 @@ export default class RecordArray extends ArrayProxy internalModelFactoryFor(this.store).lookup(identifier).createSnapshot()); + return this.content.map((identifier) => this.store._instanceCache.createSnapshot(identifier)); } } diff --git a/packages/store/addon/-private/system/references/belongs-to.ts b/packages/store/addon/-private/system/references/belongs-to.ts index dbfef5755d4..15f6b1ee9a6 100644 --- a/packages/store/addon/-private/system/references/belongs-to.ts +++ b/packages/store/addon/-private/system/references/belongs-to.ts @@ -270,14 +270,7 @@ export default class BelongsToReference extends Reference { */ value(): RecordInstance | null { let resource = this._resource(); - if (resource && resource.data) { - let inverseInternalModel = this.store._internalModelForResource(resource.data); - if (inverseInternalModel && inverseInternalModel.isLoaded) { - return inverseInternalModel.getRecord(); - } - } - - return null; + return resource && resource.data ? this.store.peekRecord(resource.data) : null; } /** diff --git a/packages/store/addon/-private/system/references/record.ts b/packages/store/addon/-private/system/references/record.ts index fba6d5c03c2..9e0b8557877 100644 --- a/packages/store/addon/-private/system/references/record.ts +++ b/packages/store/addon/-private/system/references/record.ts @@ -8,7 +8,6 @@ import type { StableRecordIdentifier } from '../../ts-interfaces/identifier'; import type { RecordInstance } from '../../ts-interfaces/record-instance'; import type CoreStore from '../core-store'; import { NotificationType, unsubscribe } from '../record-notification-manager'; -import { internalModelFactoryFor } from '../store/internal-model-factory'; import Reference from './reference'; /** @module @ember-data/store @@ -190,13 +189,7 @@ export default class RecordReference extends Reference { @return {Model} the record for this RecordReference */ value(): RecordInstance | null { - if (this.id() !== null) { - let internalModel = internalModelFactoryFor(this.store).peek(this.#identifier); - if (internalModel && internalModel.isLoaded) { - return internalModel.getRecord(); - } - } - return null; + return this.store.peekRecord(this.#identifier); } /** diff --git a/packages/store/addon/-private/system/schema-definition-service.ts b/packages/store/addon/-private/system/schema-definition-service.ts index 9b1a36170d0..9c586d7621b 100644 --- a/packages/store/addon/-private/system/schema-definition-service.ts +++ b/packages/store/addon/-private/system/schema-definition-service.ts @@ -1,10 +1,12 @@ import { getOwner } from '@ember/application'; +import { deprecate } from '@ember/debug'; import { get } from '@ember/object'; import { importSync } from '@embroider/macros'; import type Model from '@ember-data/model'; import { HAS_MODEL_PACKAGE } from '@ember-data/private-build-infra'; +import { DEPRECATE_STRING_ARG_SCHEMAS } from '@ember-data/private-build-infra/deprecations'; import type { RecordIdentifier } from '../ts-interfaces/identifier'; import type { AttributesSchema, RelationshipsSchema } from '../ts-interfaces/record-data-schemas'; @@ -34,7 +36,17 @@ export class DSModelSchemaDefinitionService { // Following the existing RD implementation attributesDefinitionFor(identifier: RecordIdentifier | { type: string }): AttributesSchema { let modelName, attributes; - if (typeof identifier === 'string') { + if (DEPRECATE_STRING_ARG_SCHEMAS && typeof identifier === 'string') { + deprecate( + `attributesDefinitionFor expects either a record identifier or an argument of shape { type: string }, received a string.`, + false, + { + id: 'ember-data:deprecate-string-arg-schemas', + for: 'ember-data', + until: '5.0', + since: { enabled: '4.5', available: '4.5' }, + } + ); modelName = identifier; } else { modelName = identifier.type; @@ -57,7 +69,17 @@ export class DSModelSchemaDefinitionService { // Following the existing RD implementation relationshipsDefinitionFor(identifier: RecordIdentifier | { type: string }): RelationshipsSchema { let modelName, relationships; - if (typeof identifier === 'string') { + if (DEPRECATE_STRING_ARG_SCHEMAS && typeof identifier === 'string') { + deprecate( + `relationshipsDefinitionFor expects either a record identifier or an argument of shape { type: string }, received a string.`, + false, + { + id: 'ember-data:deprecate-string-arg-schemas', + for: 'ember-data', + until: '5.0', + since: { enabled: '4.5', available: '4.5' }, + } + ); modelName = identifier; } else { modelName = identifier.type; diff --git a/packages/store/addon/-private/system/snapshot.ts b/packages/store/addon/-private/system/snapshot.ts index ce1352832e4..ae9657f5968 100644 --- a/packages/store/addon/-private/system/snapshot.ts +++ b/packages/store/addon/-private/system/snapshot.ts @@ -84,7 +84,7 @@ export default class Snapshot implements Snapshot { this.identifier = identifier; /* - If the internalModel does not yet have a record, then we are + If the we do not yet have a record, then we are likely a snapshot being provided to a find request, so we populate __attributes lazily. Else, to preserve the "moment in time" in which a snapshot is created, we greedily grab @@ -135,7 +135,7 @@ export default class Snapshot implements Snapshot { @type {String} @public */ - this.modelName = internalModel.modelName; + this.modelName = identifier.type; if (internalModel.hasRecord) { this._changedAttributes = recordDataFor(internalModel).changedAttributes(); } @@ -156,7 +156,7 @@ export default class Snapshot implements Snapshot { @public */ get record(): RecordInstance { - return this._internalModel.getRecord(); + return this._store._instanceCache.getRecord(this.identifier); } get _attributes(): Dict { diff --git a/packages/store/addon/-private/system/store/finders.js b/packages/store/addon/-private/system/store/finders.js index c551b1fdd3f..7c1cff6e486 100644 --- a/packages/store/addon/-private/system/store/finders.js +++ b/packages/store/addon/-private/system/store/finders.js @@ -34,13 +34,13 @@ function iterateData(data, fn) { // assert that record.relationships[inverse] is either undefined (so we can fix it) // or provide a data: {id, type} that matches the record that requested it // return the relationship data for the parent -function syncRelationshipDataFromLink(store, payload, parentInternalModel, relationship) { +function syncRelationshipDataFromLink(store, payload, parentIdentifier, relationship) { // ensure the right hand side (incoming payload) points to the parent record that // requested this relationship let relationshipData = payload.data ? iterateData(payload.data, (data, index) => { const { id, type } = data; - ensureRelationshipIsSetToParent(data, parentInternalModel, store, relationship, index); + ensureRelationshipIsSetToParent(data, parentIdentifier, store, relationship, index); return { id, type }; }) : null; @@ -60,8 +60,8 @@ function syncRelationshipDataFromLink(store, payload, parentInternalModel, relat // now, push the left hand side (the parent record) to ensure things are in sync, since // the payload will be pushed with store._push const parentPayload = { - id: parentInternalModel.id, - type: parentInternalModel.modelName, + id: parentIdentifier.id, + type: parentIdentifier.type, relationships: { [relationship.key]: relatedDataHash, }, @@ -75,7 +75,7 @@ function syncRelationshipDataFromLink(store, payload, parentInternalModel, relat return payload; } -function ensureRelationshipIsSetToParent(payload, parentInternalModel, store, parentRelationship, index) { +function ensureRelationshipIsSetToParent(payload, parentIdentifier, store, parentRelationship, index) { let { id, type } = payload; if (!payload.relationships) { @@ -83,7 +83,7 @@ function ensureRelationshipIsSetToParent(payload, parentInternalModel, store, pa } let { relationships } = payload; - let inverse = getInverse(store, parentInternalModel, parentRelationship, type); + let inverse = getInverse(store, parentIdentifier, parentRelationship, type); if (inverse) { let { inverseKey, kind } = inverse; @@ -92,7 +92,7 @@ function ensureRelationshipIsSetToParent(payload, parentInternalModel, store, pa if ( DEBUG && typeof relationshipData !== 'undefined' && - !relationshipDataPointsToParent(relationshipData, parentInternalModel) + !relationshipDataPointsToParent(relationshipData, parentIdentifier) ) { let inspect = function inspect(thing) { return `'${JSON.stringify(thing)}'`; @@ -100,10 +100,10 @@ function ensureRelationshipIsSetToParent(payload, parentInternalModel, store, pa let quotedType = inspect(type); let quotedInverse = inspect(inverseKey); let expected = inspect({ - id: parentInternalModel.id, - type: parentInternalModel.modelName, + id: parentIdentifier.id, + type: parentIdentifier.type, }); - let expectedModel = `${parentInternalModel.modelName}:${parentInternalModel.id}`; + let expectedModel = `${parentIdentifier.type}:${parentIdentifier.id}`; let got = inspect(relationshipData); let prefix = typeof index === 'number' ? `data[${index}]` : `data`; let path = `${prefix}.relationships.${inverseKey}.data`; @@ -113,7 +113,7 @@ function ensureRelationshipIsSetToParent(payload, parentInternalModel, store, pa let message = [ `Encountered mismatched relationship: Ember Data expected ${path} in the payload from ${relationshipFetched} to include ${expected} but got ${got} instead.\n`, `The ${includedRecord} record loaded at ${prefix} in the payload specified ${other} as its ${quotedInverse}, but should have specified ${expectedModel} (the record the relationship is being loaded from) as its ${quotedInverse} instead.`, - `This could mean that the response for ${relationshipFetched} may have accidentally returned ${quotedType} records that aren't related to ${expectedModel} and could be related to a different ${parentInternalModel.modelName} record instead.`, + `This could mean that the response for ${relationshipFetched} may have accidentally returned ${quotedType} records that aren't related to ${expectedModel} and could be related to a different ${parentIdentifier.type} record instead.`, `Ember Data has corrected the ${includedRecord} record's ${quotedInverse} relationship to ${expectedModel} so that ${relationshipFetched} will include ${includedRecord}.`, `Please update the response from the server or change your serializer to either ensure that the response for only includes ${quotedType} records that specify ${expectedModel} as their ${quotedInverse}, or omit the ${quotedInverse} relationship from the response.`, ].join('\n'); @@ -123,7 +123,7 @@ function ensureRelationshipIsSetToParent(payload, parentInternalModel, store, pa if (kind !== 'hasMany' || typeof relationshipData !== 'undefined') { relationships[inverseKey] = relationships[inverseKey] || {}; - relationships[inverseKey].data = fixRelationshipData(relationshipData, kind, parentInternalModel); + relationships[inverseKey].data = fixRelationshipData(relationshipData, kind, parentIdentifier); } } } @@ -132,10 +132,10 @@ function getInverse(store, parentInternalModel, parentRelationship, type) { return recordDataFindInverseRelationshipInfo(store, parentInternalModel, parentRelationship, type); } -function recordDataFindInverseRelationshipInfo({ _storeWrapper }, parentInternalModel, parentRelationship, type) { +function recordDataFindInverseRelationshipInfo({ _storeWrapper }, parentIdentifier, parentRelationship, type) { let { name: lhs_relationshipName } = parentRelationship; - let { modelName } = parentInternalModel; - let inverseKey = _storeWrapper.inverseForRelationship(modelName, lhs_relationshipName); + let { type: parentType } = parentIdentifier; + let inverseKey = _storeWrapper.inverseForRelationship(parentType, lhs_relationshipName); if (inverseKey) { let { @@ -148,7 +148,7 @@ function recordDataFindInverseRelationshipInfo({ _storeWrapper }, parentInternal } } -function relationshipDataPointsToParent(relationshipData, internalModel) { +function relationshipDataPointsToParent(relationshipData, identifier) { if (relationshipData === null) { return false; } @@ -159,21 +159,21 @@ function relationshipDataPointsToParent(relationshipData, internalModel) { } for (let i = 0; i < relationshipData.length; i++) { let entry = relationshipData[i]; - if (validateRelationshipEntry(entry, internalModel)) { + if (validateRelationshipEntry(entry, identifier)) { return true; } } } else { - return validateRelationshipEntry(relationshipData, internalModel); + return validateRelationshipEntry(relationshipData, identifier); } return false; } -function fixRelationshipData(relationshipData, relationshipKind, { id, modelName }) { +function fixRelationshipData(relationshipData, relationshipKind, { id, type }) { let parentRelationshipData = { id, - type: modelName, + type, }; let payload; @@ -205,18 +205,18 @@ function validateRelationshipEntry({ id }, { id: parentModelID }) { return id && id.toString() === parentModelID; } -export function _findHasMany(adapter, store, internalModel, link, relationship, options) { - let snapshot = internalModel.createSnapshot(options); +export function _findHasMany(adapter, store, identifier, link, relationship, options) { + const snapshot = store._instanceCache.createSnapshot(identifier, options); let modelClass = store.modelFor(relationship.type); let useLink = !link || typeof link === 'string'; let relatedLink = useLink ? link : link.href; let promise = adapter.findHasMany(store, snapshot, relatedLink, relationship); - let label = `DS: Handle Adapter#findHasMany of '${internalModel.modelName}' : '${relationship.type}'`; + let label = `DS: Handle Adapter#findHasMany of '${identifier.type}' : '${relationship.type}'`; promise = guardDestroyedStore(promise, store, label); promise = promise.then( (adapterPayload) => { - if (!_objectIsAlive(internalModel)) { + if (!_objectIsAlive(store._instanceCache.getInternalModel(identifier))) { if (DEPRECATE_RSVP_PROMISE) { deprecate( `A Promise for fetching ${relationship.type} did not resolve by the time your model was destroyed. This will error in a future release.`, @@ -235,47 +235,53 @@ export function _findHasMany(adapter, store, internalModel, link, relationship, } assert( - `You made a 'findHasMany' request for a ${internalModel.modelName}'s '${relationship.key}' relationship, using link '${link}' , but the adapter's response did not have any data`, + `You made a 'findHasMany' request for a ${identifier.type}'s '${relationship.key}' relationship, using link '${link}' , but the adapter's response did not have any data`, payloadIsNotBlank(adapterPayload) ); let serializer = store.serializerFor(relationship.type); let payload = normalizeResponseHelper(serializer, store, modelClass, adapterPayload, null, 'findHasMany'); assert( - `fetched the hasMany relationship '${relationship.name}' for ${internalModel.modelName}:${internalModel.id} with link '${link}', but no data member is present in the response. If no data exists, the response should set { data: [] }`, + `fetched the hasMany relationship '${relationship.name}' for ${identifier.type}:${identifier.id} with link '${link}', but no data member is present in the response. If no data exists, the response should set { data: [] }`, 'data' in payload && Array.isArray(payload.data) ); - payload = syncRelationshipDataFromLink(store, payload, internalModel, relationship); + payload = syncRelationshipDataFromLink(store, payload, identifier, relationship); - let internalModelArray = store._push(payload); - return internalModelArray; + return store._push(payload); }, null, - `DS: Extract payload of '${internalModel.modelName}' : hasMany '${relationship.type}'` + `DS: Extract payload of '${identifier.type}' : hasMany '${relationship.type}'` ); if (DEPRECATE_RSVP_PROMISE) { - promise = _guard(promise, _bind(_objectIsAlive, internalModel)); + promise = _guard(promise, _bind(_objectIsAlive, store._instanceCache.getInternalModel(identifier))); } return promise; } -export function _findBelongsTo(adapter, store, internalModel, link, relationship, options) { - let snapshot = internalModel.createSnapshot(options); +export function _findBelongsTo(store, identifier, link, relationship, options) { + let adapter = store.adapterFor(identifier.type); + + assert(`You tried to load a belongsTo relationship but you have no adapter (for ${identifier.type})`, adapter); + assert( + `You tried to load a belongsTo relationship from a specified 'link' in the original payload but your adapter does not implement 'findBelongsTo'`, + typeof adapter.findBelongsTo === 'function' + ); + let snapshot = store._instanceCache.createSnapshot(identifier, options); let modelClass = store.modelFor(relationship.type); let useLink = !link || typeof link === 'string'; let relatedLink = useLink ? link : link.href; let promise = adapter.findBelongsTo(store, snapshot, relatedLink, relationship); - let label = `DS: Handle Adapter#findBelongsTo of ${internalModel.modelName} : ${relationship.type}`; + let label = `DS: Handle Adapter#findBelongsTo of ${identifier.type} : ${relationship.type}`; promise = guardDestroyedStore(promise, store, label); - promise = _guard(promise, _bind(_objectIsAlive, internalModel)); + promise = _guard(promise, _bind(_objectIsAlive, store._instanceCache.getInternalModel(identifier))); promise = promise.then( (adapterPayload) => { - if (!_objectIsAlive(internalModel)) { + if (!_objectIsAlive(store._instanceCache.getInternalModel(identifier))) { if (DEPRECATE_RSVP_PROMISE) { deprecate( `A Promise for fetching ${relationship.type} did not resolve by the time your model was destroyed. This will error in a future release.`, @@ -297,7 +303,7 @@ export function _findBelongsTo(adapter, store, internalModel, link, relationship let payload = normalizeResponseHelper(serializer, store, modelClass, adapterPayload, null, 'findBelongsTo'); assert( - `fetched the belongsTo relationship '${relationship.name}' for ${internalModel.modelName}:${internalModel.id} with link '${link}', but no data member is present in the response. If no data exists, the response should set { data: null }`, + `fetched the belongsTo relationship '${relationship.name}' for ${identifier.type}:${identifier.id} with link '${link}', but no data member is present in the response. If no data exists, the response should set { data: null }`, 'data' in payload && (payload.data === null || (typeof payload.data === 'object' && !Array.isArray(payload.data))) ); @@ -306,16 +312,16 @@ export function _findBelongsTo(adapter, store, internalModel, link, relationship return null; } - payload = syncRelationshipDataFromLink(store, payload, internalModel, relationship); + payload = syncRelationshipDataFromLink(store, payload, identifier, relationship); return store._push(payload); }, null, - `DS: Extract payload of ${internalModel.modelName} : ${relationship.type}` + `DS: Extract payload of ${identifier.type} : ${relationship.type}` ); if (DEPRECATE_RSVP_PROMISE) { - promise = _guard(promise, _bind(_objectIsAlive, internalModel)); + promise = _guard(promise, _bind(_objectIsAlive, store._instanceCache.getInternalModel(identifier))); } return promise; @@ -362,13 +368,12 @@ export function _query(adapter, store, modelName, query, recordArray, options) { (adapterPayload) => { let serializer = store.serializerFor(modelName); let payload = normalizeResponseHelper(serializer, store, modelClass, adapterPayload, null, 'query'); - let internalModels = store._push(payload); + let identifiers = store._push(payload); assert( 'The response to store.query is expected to be an array but it was a single record. Please wrap your response in an array or use `store.queryRecord` to query for a single record.', - Array.isArray(internalModels) + Array.isArray(identifiers) ); - let identifiers = internalModels.map((im) => im.identifier); if (recordArray) { recordArray._setIdentifiers(identifiers, payload); } else { diff --git a/packages/store/addon/-private/utils/promise-record.ts b/packages/store/addon/-private/utils/promise-record.ts index a79c4d1bfc1..9ee2d9f635d 100644 --- a/packages/store/addon/-private/utils/promise-record.ts +++ b/packages/store/addon/-private/utils/promise-record.ts @@ -19,7 +19,7 @@ import type { RecordInstance } from '../ts-interfaces/record-instance'; export default function promiseRecord( store: CoreStore, promise: Promise, - label: string + label?: string ): PromiseObject { let toReturn = promise.then((identifier: StableRecordIdentifier) => store.peekRecord(identifier)!); From 3b7631fa2d197048e07bb6cddc6f13500ac304bd Mon Sep 17 00:00:00 2001 From: Chris Thoburn Date: Sun, 24 Jul 2022 20:55:04 -0700 Subject: [PATCH 12/16] fixup tests --- .../tests/integration/adapter/rest-adapter-test.js | 1 - .../tests/integration/store/adapter-for-test.js | 14 -------------- .../-ember-data/tests/unit/store/model-for-test.js | 3 ++- 3 files changed, 2 insertions(+), 16 deletions(-) diff --git a/packages/-ember-data/tests/integration/adapter/rest-adapter-test.js b/packages/-ember-data/tests/integration/adapter/rest-adapter-test.js index b1a677cf684..31f11606c16 100644 --- a/packages/-ember-data/tests/integration/adapter/rest-adapter-test.js +++ b/packages/-ember-data/tests/integration/adapter/rest-adapter-test.js @@ -12,7 +12,6 @@ import { setupTest } from 'ember-qunit'; import RESTAdapter from '@ember-data/adapter/rest'; import Model, { belongsTo, hasMany } from '@ember-data/model'; import RESTSerializer from '@ember-data/serializer/rest'; -import { recordIdentifierFor } from '@ember-data/store'; import deepCopy from '@ember-data/unpublished-test-infra/test-support/deep-copy'; import testInDebug from '@ember-data/unpublished-test-infra/test-support/test-in-debug'; diff --git a/packages/-ember-data/tests/integration/store/adapter-for-test.js b/packages/-ember-data/tests/integration/store/adapter-for-test.js index ab815822074..2b3942f28a4 100644 --- a/packages/-ember-data/tests/integration/store/adapter-for-test.js +++ b/packages/-ember-data/tests/integration/store/adapter-for-test.js @@ -29,20 +29,6 @@ module('integration/store - adapterFor', function (hooks) { }); test('when no adapter is available we throw an error', async function (assert) { - let { owner } = this; - /* - adapter:-json-api is the "last chance" fallback and is - the json-api adapter which is re-exported as app/adapters/-json-api. - here we override to ensure adapterFor will return `undefined`. - */ - const lookup = owner.lookup; - owner.lookup = (registrationName) => { - if (registrationName === 'adapter:-json-api') { - return undefined; - } - return lookup.call(owner, registrationName); - }; - assert.expectAssertion(() => { store.adapterFor('person'); }, /Assertion Failed: No adapter was found for 'person' and no 'application' adapter was found as a fallback/); diff --git a/packages/-ember-data/tests/unit/store/model-for-test.js b/packages/-ember-data/tests/unit/store/model-for-test.js index 7f6138f9330..53bae338b56 100644 --- a/packages/-ember-data/tests/unit/store/model-for-test.js +++ b/packages/-ember-data/tests/unit/store/model-for-test.js @@ -5,6 +5,7 @@ import { module, test } from 'qunit'; import { setupTest } from 'ember-qunit'; import Model from '@ember-data/model'; +import testInDebug from '@ember-data/unpublished-test-infra/test-support/test-in-debug'; module('unit/store/model_for - DS.Store#modelFor', function (hooks) { setupTest(hooks); @@ -34,7 +35,7 @@ module('unit/store/model_for - DS.Store#modelFor', function (hooks) { assert.strictEqual(store.modelFor('blog.post').modelName, 'blog.post', 'modelName is normalized to dasherized'); }); - test(`when fetching something that doesn't exist, throws error`, function (assert) { + testInDebug(`when fetching something that doesn't exist, throws error`, function (assert) { let store = this.owner.lookup('service:store'); assert.throws(() => { From 15ac3eeb7420067c16655aa790100293953127dd Mon Sep 17 00:00:00 2001 From: Chris Thoburn Date: Mon, 25 Jul 2022 13:35:13 -0700 Subject: [PATCH 13/16] various fixes --- .../tests/dummy/app/adapters/application.ts | 1 + .../tests/integration/records/reload-test.js | 2 +- .../integration/references/belongs-to-test.js | 42 ++++---- .../inverse-relationships-test.js | 4 +- .../tests/integration/snapshot-test.js | 6 +- .../integration/store/adapter-for-test.js | 100 ++++++++++++------ .../group-records-for-find-many-test.js | 3 +- packages/-ember-data/tests/unit/model-test.js | 2 +- packages/model/addon/-private/attr.js | 20 ++-- packages/model/addon/-private/model.js | 55 +++++++--- .../model/addon/-private/notify-changes.ts | 4 +- packages/model/addon/-private/record-state.ts | 33 ++++-- .../addon/current-deprecations.ts | 1 + .../private-build-infra/addon/deprecations.ts | 1 + .../record-data/addon/-private/record-data.ts | 2 - .../store/addon/-private/instance-cache.ts | 32 ++++-- .../store/addon/-private/system/core-store.ts | 79 +++++++++----- .../-private/system/model/internal-model.ts | 52 ++------- .../addon/-private/system/record-data-for.ts | 18 +++- .../store/addon/-private/system/snapshot.ts | 10 +- .../system/store/internal-model-factory.ts | 9 +- .../store/addon/-private/system/weak-cache.ts | 2 +- 22 files changed, 294 insertions(+), 184 deletions(-) create mode 100644 packages/-ember-data/tests/dummy/app/adapters/application.ts diff --git a/packages/-ember-data/tests/dummy/app/adapters/application.ts b/packages/-ember-data/tests/dummy/app/adapters/application.ts new file mode 100644 index 00000000000..2cbb7cd7057 --- /dev/null +++ b/packages/-ember-data/tests/dummy/app/adapters/application.ts @@ -0,0 +1 @@ +export { default } from '@ember-data/adapter/json-api'; diff --git a/packages/-ember-data/tests/integration/records/reload-test.js b/packages/-ember-data/tests/integration/records/reload-test.js index e1b9711bcfc..6f688b4257c 100644 --- a/packages/-ember-data/tests/integration/records/reload-test.js +++ b/packages/-ember-data/tests/integration/records/reload-test.js @@ -110,7 +110,7 @@ module('integration/reload - Reloading Records', function (hooks) { 'adapter:application', JSONAPIAdapter.extend({ shouldBackgroundReloadRecord() { - return true; + return false; }, findRecord() { diff --git a/packages/-ember-data/tests/integration/references/belongs-to-test.js b/packages/-ember-data/tests/integration/references/belongs-to-test.js index 6ffd87e0184..82747ae1901 100644 --- a/packages/-ember-data/tests/integration/references/belongs-to-test.js +++ b/packages/-ember-data/tests/integration/references/belongs-to-test.js @@ -374,36 +374,32 @@ module('integration/references/belongs-to', function (hooks) { test('value() returns the referenced record when loaded even if links are present', function (assert) { let store = this.owner.lookup('service:store'); - - var person, family; - run(function () { - person = store.push({ - data: { - type: 'person', - id: 1, - relationships: { - family: { - data: { type: 'family', id: 1 }, - }, + let person = store.push({ + data: { + type: 'person', + id: 1, + relationships: { + family: { + data: { type: 'family', id: 1 }, }, }, - }); - family = store.push({ - data: { - type: 'family', - id: 1, - relationships: { - persons: { - links: { - related: '/this/should/not/matter', - }, + }, + }); + let family = store.push({ + data: { + type: 'family', + id: 1, + relationships: { + persons: { + links: { + related: '/this/should/not/matter', }, }, }, - }); + }, }); - var familyReference = person.belongsTo('family'); + const familyReference = person.belongsTo('family'); assert.strictEqual(familyReference.value(), family); }); diff --git a/packages/-ember-data/tests/integration/relationships/inverse-relationships-test.js b/packages/-ember-data/tests/integration/relationships/inverse-relationships-test.js index 24ad7084eee..ee518ad9859 100644 --- a/packages/-ember-data/tests/integration/relationships/inverse-relationships-test.js +++ b/packages/-ember-data/tests/integration/relationships/inverse-relationships-test.js @@ -4,6 +4,7 @@ import { setupTest } from 'ember-qunit'; import Model, { attr, belongsTo, hasMany } from '@ember-data/model'; import { graphFor } from '@ember-data/record-data/-private'; +import { recordDataFor } from '@ember-data/store/-private'; import testInDebug from '@ember-data/unpublished-test-infra/test-support/test-in-debug'; module('integration/relationships/inverse_relationships - Inverse Relationships', function (hooks) { @@ -668,6 +669,7 @@ module('integration/relationships/inverse_relationships - Inverse Relationships' register('model:comment', Comment); const comment = store.createRecord('comment'); + const recordData = recordDataFor(comment); const post = store.createRecord('post'); post.get('comments').pushObject(comment); @@ -677,6 +679,6 @@ module('integration/relationships/inverse_relationships - Inverse Relationships' const identifier = comment._internalModel.identifier; assert.false(graphFor(store._storeWrapper).identifiers.has(identifier), 'relationships are cleared'); - assert.ok(comment._internalModel.__recordData.isDestroyed, 'recordData is destroyed'); + assert.ok(recordData.isDestroyed, 'recordData is destroyed'); }); }); diff --git a/packages/-ember-data/tests/integration/snapshot-test.js b/packages/-ember-data/tests/integration/snapshot-test.js index 442ddffcbc8..bd02c388d5d 100644 --- a/packages/-ember-data/tests/integration/snapshot-test.js +++ b/packages/-ember-data/tests/integration/snapshot-test.js @@ -135,7 +135,7 @@ module('integration/snapshot - Snapshot', function (hooks) { }, }); let postInternalModel = store._internalModelForResource({ type: 'post', id: '1' }); - let snapshot = await postInternalModel.createSnapshot(); + let snapshot = await store._instanceCache.createSnapshot(postInternalModel.identifier); assert.false(postClassLoaded, 'model class is not eagerly loaded'); assert.strictEqual(snapshot.type, _Post, 'type is correct'); @@ -176,7 +176,7 @@ module('integration/snapshot - Snapshot', function (hooks) { }); let postInternalModel = store._internalModelForResource({ type: 'post', id: '1' }); - let snapshot = postInternalModel.createSnapshot(); + let snapshot = store._instanceCache.createSnapshot(postInternalModel.identifier); let expected = { author: undefined, title: 'Hello World', @@ -201,7 +201,7 @@ module('integration/snapshot - Snapshot', function (hooks) { }); let postInternalModel = store._internalModelForResource({ type: 'post', id: '1' }); - let snapshot = postInternalModel.createSnapshot(); + let snapshot = store._instanceCache.createSnapshot(postInternalModel.identifier); let expected = { author: undefined, title: 'Hello World', diff --git a/packages/-ember-data/tests/integration/store/adapter-for-test.js b/packages/-ember-data/tests/integration/store/adapter-for-test.js index 2b3942f28a4..65f97e167cc 100644 --- a/packages/-ember-data/tests/integration/store/adapter-for-test.js +++ b/packages/-ember-data/tests/integration/store/adapter-for-test.js @@ -5,6 +5,7 @@ import { module, test } from 'qunit'; import { setupTest } from 'ember-qunit'; import Store from '@ember-data/store'; +import { deprecatedTest } from '@ember-data/unpublished-test-infra/test-support/deprecated-test'; class TestAdapter { constructor(args) { @@ -30,6 +31,22 @@ module('integration/store - adapterFor', function (hooks) { test('when no adapter is available we throw an error', async function (assert) { assert.expectAssertion(() => { + let { owner } = this; + /* + adapter:-json-api is the "last chance" fallback and is + the json-api adapter which is re-exported as app/adapters/-json-api. + here we override to ensure adapterFor will return `undefined`. + */ + const lookup = owner.lookup; + owner.lookup = (registrationName) => { + if (registrationName === 'adapter:application') { + return undefined; + } + if (registrationName === 'adapter:-json-api') { + return undefined; + } + return lookup.call(owner, registrationName); + }; store.adapterFor('person'); }, /Assertion Failed: No adapter was found for 'person' and no 'application' adapter was found as a fallback/); }); @@ -143,41 +160,58 @@ module('integration/store - adapterFor', function (hooks) { assert.strictEqual(appAdapter, adapter, 'We fell back to the application adapter instance'); }); - test('When the per-type, application and specified fallback adapters do not exist, we fallback to the -json-api adapter', async function (assert) { - let { owner } = this; - - let didInstantiateAdapter = false; - - class JsonApiAdapter extends TestAdapter { - didInit() { - didInstantiateAdapter = true; + deprecatedTest( + 'When the per-type, application and specified fallback adapters do not exist, we fallback to the -json-api adapter', + { + id: 'ember-data:deprecate-secret-adapter-fallback', + until: '5.0', + count: 2, + }, + async function (assert) { + let { owner } = this; + + let didInstantiateAdapter = false; + + class JsonApiAdapter extends TestAdapter { + didInit() { + didInstantiateAdapter = true; + } } - } - owner.unregister('adapter:-json-api'); - owner.register('adapter:-json-api', JsonApiAdapter); - - let adapter = store.adapterFor('person'); - - assert.ok(adapter instanceof JsonApiAdapter, 'We found the adapter'); - assert.ok(didInstantiateAdapter, 'We instantiated the adapter'); - didInstantiateAdapter = false; - - let appAdapter = store.adapterFor('application'); - assert.ok(appAdapter instanceof JsonApiAdapter, 'We found the fallback -json-api adapter for application'); - assert.notOk(didInstantiateAdapter, 'We did not instantiate the adapter again'); - didInstantiateAdapter = false; - - let jsonApiAdapter = store.adapterFor('-json-api'); - assert.ok(jsonApiAdapter instanceof JsonApiAdapter, 'We found the correct adapter'); - assert.notOk(didInstantiateAdapter, 'We did not instantiate the adapter again'); - assert.strictEqual(jsonApiAdapter, appAdapter, 'We fell back to the -json-api adapter instance for application'); - assert.strictEqual( - jsonApiAdapter, - adapter, - 'We fell back to the -json-api adapter instance for the per-type adapter' - ); - }); + const lookup = owner.lookup; + owner.lookup = (registrationName) => { + if (registrationName === 'adapter:application') { + return undefined; + } + return lookup.call(owner, registrationName); + }; + + owner.unregister('adapter:-json-api'); + owner.register('adapter:-json-api', JsonApiAdapter); + + let adapter = store.adapterFor('person'); + + assert.ok(adapter instanceof JsonApiAdapter, 'We found the adapter'); + assert.ok(didInstantiateAdapter, 'We instantiated the adapter'); + didInstantiateAdapter = false; + + let appAdapter = store.adapterFor('application'); + + assert.ok(appAdapter instanceof JsonApiAdapter, 'We found the fallback -json-api adapter for application'); + assert.notOk(didInstantiateAdapter, 'We did not instantiate the adapter again'); + didInstantiateAdapter = false; + + let jsonApiAdapter = store.adapterFor('-json-api'); + assert.ok(jsonApiAdapter instanceof JsonApiAdapter, 'We found the correct adapter'); + assert.notOk(didInstantiateAdapter, 'We did not instantiate the adapter again'); + assert.strictEqual(jsonApiAdapter, appAdapter, 'We fell back to the -json-api adapter instance for application'); + assert.strictEqual( + jsonApiAdapter, + adapter, + 'We fell back to the -json-api adapter instance for the per-type adapter' + ); + } + ); test('adapters are destroyed', async function (assert) { let { owner } = this; diff --git a/packages/-ember-data/tests/unit/adapters/rest-adapter/group-records-for-find-many-test.js b/packages/-ember-data/tests/unit/adapters/rest-adapter/group-records-for-find-many-test.js index 2614909f95f..4cd53ce2314 100644 --- a/packages/-ember-data/tests/unit/adapters/rest-adapter/group-records-for-find-many-test.js +++ b/packages/-ember-data/tests/unit/adapters/rest-adapter/group-records-for-find-many-test.js @@ -8,6 +8,7 @@ import { setupTest } from 'ember-qunit'; import RESTAdapter from '@ember-data/adapter/rest'; import Model from '@ember-data/model'; import RESTSerializer from '@ember-data/serializer/rest'; +import { recordIdentifierFor } from '@ember-data/store'; let store, requests; let maxLength; @@ -88,7 +89,7 @@ module( test('_stripIDFromURL works with id being encoded - #4190', function (assert) { let record = store.createRecord('testRecord', { id: 'id:123' }); let adapter = store.adapterFor('testRecord'); - let snapshot = record._internalModel.createSnapshot(); + let snapshot = store._instanceCache.createSnapshot(recordIdentifierFor(record)); let strippedUrl = adapter._stripIDFromURL(store, snapshot); assert.strictEqual(strippedUrl, '/testRecords/'); diff --git a/packages/-ember-data/tests/unit/model-test.js b/packages/-ember-data/tests/unit/model-test.js index 641edefd923..80cb5a1ff41 100644 --- a/packages/-ember-data/tests/unit/model-test.js +++ b/packages/-ember-data/tests/unit/model-test.js @@ -147,7 +147,7 @@ module('unit/model - Model', function (hooks) { () => { record.isArchived = true; }, - /Attempted to set 'isArchived' on the deleted record /, + /Attempted to set 'isArchived' on the deleted record /, "Assertion does not leak the 'value'" ); diff --git a/packages/model/addon/-private/attr.js b/packages/model/addon/-private/attr.js index d33c680a035..acfeb3ce57f 100644 --- a/packages/model/addon/-private/attr.js +++ b/packages/model/addon/-private/attr.js @@ -2,6 +2,7 @@ import { assert } from '@ember/debug'; import { computed } from '@ember/object'; import { DEBUG } from '@glimmer/env'; +import { recordIdentifierFor, storeFor } from '@ember-data/store'; import { recordDataFor } from '@ember-data/store/-private'; import { computedMacroWithOptionalParams } from './util'; @@ -150,18 +151,25 @@ function attr(type, options) { ); } } - if (!this.isValid) { - let oldValue = this._internalModel._recordData.getAttr(key); - if (oldValue !== value) { + assert( + `Attempted to set '${key}' on the deleted record ${recordIdentifierFor(this)}`, + !this.currentState.isDeleted + ); + const recordData = storeFor(this)._instanceCache.getRecordData(recordIdentifierFor(this)); + let currentValue = recordData.getAttr(key); + if (currentValue !== value) { + recordData.setDirtyAttribute(key, value); + + if (!this.isValid) { const { errors } = this; if (errors.get(key)) { errors.remove(key); - this.___recordState.cleanErrorRequests(); + this.currentState.cleanErrorRequests(); } } } - // TODO update state here? - return this._internalModel.setDirtyAttribute(key, value); + + return value; }, }).meta(meta); } diff --git a/packages/model/addon/-private/model.js b/packages/model/addon/-private/model.js index 1ecb316d4ef..e1e292a3c25 100644 --- a/packages/model/addon/-private/model.js +++ b/packages/model/addon/-private/model.js @@ -27,6 +27,7 @@ import { } from '@ember-data/store/-private'; import Errors from './errors'; +import notifyChanges from './notify-changes'; import RecordState, { peekTag, tagged } from './record-state'; import { relationshipFromMeta } from './system/relationships/relationship-meta'; @@ -110,20 +111,31 @@ class Model extends EmberObject { @service store; init(options = {}) { + if (DEBUG && !options._secretInit && !options._internalModel && !options._createProps) { + throw new EmberError( + 'You should not call `create` on a model. Instead, call `store.createRecord` with the attributes you would like to set.' + ); + } const createProps = options._createProps; + const _secretInit = options._secretInit; delete options._createProps; + delete options._secretInit; super.init(options); - 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.' - ); - } - } + _secretInit(this); + this.___recordState = DEBUG ? new RecordState(this) : null; - this.___recordState = new RecordState(this); this.setProperties(createProps); + + // TODO pass something in such that we don't need internalModel + // to get this info + let store = storeFor(this); + let notifications = store._notificationManager; + let identity = recordIdentifierFor(this); + + notifications.subscribe(identity, (identifier, type, key) => { + notifyChanges(identifier, type, key, this, store); + }); } /** @@ -451,8 +463,17 @@ class Model extends EmberObject { @private @type {Object} */ + // TODO we can probably make this a computeOnce + // we likely do not need to notify the currentState root anymore @tagged get currentState() { + // descriptors are called with the wrong `this` context during mergeMixins + // when using legacy/classic ember classes. Basically: lazy in prod and eager in dev. + // so we do this to try to steer folks to the nicer "dont user currentState" + // error. + if (!DEBUG && !this.___recordState) { + this.___recordState = new RecordState(this); + } return this.___recordState; } set currentState(_v) { @@ -578,7 +599,7 @@ class Model extends EmberObject { @return {Object} an object whose values are primitive JSON values only */ serialize(options) { - return this._internalModel.createSnapshot().serialize(options); + return storeFor(this)._instanceCache.createSnapshot(recordIdentifierFor(this)).serialize(options); } /* @@ -631,7 +652,10 @@ class Model extends EmberObject { @public */ deleteRecord() { - storeFor(this).deleteRecord(this); + // ensure we've populated currentState prior to deleting a new record + if (this.currentState) { + storeFor(this).deleteRecord(this); + } } /** @@ -701,7 +725,7 @@ class Model extends EmberObject { @public */ unloadRecord() { - if (this.isNew && (this.isDestroyed || this.isDestroying)) { + if (this.currentState.isNew && (this.isDestroyed || this.isDestroying)) { return; } storeFor(this).unloadRecord(this); @@ -793,16 +817,18 @@ class Model extends EmberObject { @public */ rollbackAttributes() { + const { currentState } = this; this._internalModel.rollbackAttributes(); - this.currentState.cleanErrorRequests(); + currentState.cleanErrorRequests(); } /** @method _createSnapshot @private */ + // TODO @deprecate in favor of a public API or examples of how to test successfully _createSnapshot() { - return this._internalModel.createSnapshot(); + return storeFor(this)._instanceCache.createSnapshot(recordIdentifierFor(this)); } toStringExtension() { @@ -1950,6 +1976,7 @@ class Model extends EmberObject { // the values initialized during create to `setUnknownProperty` Model.prototype._internalModel = null; Model.prototype._createProps = null; +Model.prototype._secretInit = null; if (HAS_DEBUG_PACKAGE) { /** @@ -2050,7 +2077,7 @@ if (DEBUG) { let ourDescriptor = lookupDescriptor(Model.prototype, 'currentState'); let theirDescriptor = lookupDescriptor(this, 'currentState'); - let realState = this.___recordState || this._internalModel.currentState; + let realState = this.___recordState; if (ourDescriptor.get !== theirDescriptor.get || realState !== this.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()}` diff --git a/packages/model/addon/-private/notify-changes.ts b/packages/model/addon/-private/notify-changes.ts index 47961efe595..d5520eb06fb 100644 --- a/packages/model/addon/-private/notify-changes.ts +++ b/packages/model/addon/-private/notify-changes.ts @@ -57,8 +57,8 @@ function notifyRelationship(store: CoreStore, identifier: StableRecordIdentifier function notifyAttribute(store: CoreStore, identifier: StableRecordIdentifier, key: string, record: Model) { let currentValue = cacheFor(record, key); - let internalModel = store._internalModelForResource(identifier); - if (currentValue !== internalModel._recordData.getAttr(key)) { + + if (currentValue !== store._instanceCache.getRecordData(identifier).getAttr(key)) { record.notifyPropertyChange(key); } } diff --git a/packages/model/addon/-private/record-state.ts b/packages/model/addon/-private/record-state.ts index cbe3a64f0b3..1df6455b5bc 100644 --- a/packages/model/addon/-private/record-state.ts +++ b/packages/model/addon/-private/record-state.ts @@ -1,15 +1,15 @@ import { assert } from '@ember/debug'; import { dependentKeyCompat } from '@ember/object/compat'; +import { DEBUG } from '@glimmer/env'; import { cached, tracked } from '@glimmer/tracking'; -import type { RecordData } from '@ember-data/record-data/-private'; -import { errorsArrayToHash } from '@ember-data/store/-private'; +import { storeFor } from '@ember-data/store'; +import { errorsArrayToHash, recordIdentifierFor } from '@ember-data/store/-private'; import type CoreStore from '@ember-data/store/-private/system/core-store'; import type { NotificationType } from '@ember-data/store/-private/system/record-notification-manager'; import type RequestCache from '@ember-data/store/-private/system/request-cache'; import type { StableRecordIdentifier } from '@ember-data/store/-private/ts-interfaces/identifier'; - -import notifyChanges from './notify-changes'; +import type { RecordData } from '@ember-data/store/-private/ts-interfaces/record-data'; type Model = InstanceType; @@ -152,22 +152,23 @@ export default class RecordState { declare _lastError: any; constructor(record: Model) { - const { store, identifier: identity } = record._internalModel; + const store = storeFor(record)!; + const identity = recordIdentifierFor(record); + this.identifier = identity; this.record = record; - this.recordData = record._internalModel._recordData; + this.recordData = store._instanceCache.getRecordData(identity); this.pendingCount = 0; this.fulfilledCount = 0; this.rejectedCount = 0; - this._errorRequests = []; this._lastError = null; let requests = store.getRequestStateService(); let notifications = store._notificationManager; - requests.subscribeForRecord(identity, (req) => { + const handleRequest = (req) => { if (req.type === 'mutation') { switch (req.state) { case 'pending': @@ -216,10 +217,20 @@ export default class RecordState { break; } } - }); + }; + + requests.subscribeForRecord(identity, handleRequest); + + // we instantiate lazily + // so we grab anything we don't have yet + if (!DEBUG) { + const lastRequest = requests.getLastRequestForRecord(identity); + if (lastRequest) { + handleRequest(lastRequest); + } + } notifications.subscribe(identity, (identifier: StableRecordIdentifier, type: NotificationType, key?: string) => { - notifyChanges(identifier, type, key, record, store); switch (type) { case 'state': this.notify('isNew'); @@ -251,7 +262,7 @@ export default class RecordState { } updateInvalidErrors() { - let jsonApiErrors = this.recordData.getErrors!(); + let jsonApiErrors = this.recordData.getErrors!(this.identifier); const { errors } = this.record; errors.clear(); diff --git a/packages/private-build-infra/addon/current-deprecations.ts b/packages/private-build-infra/addon/current-deprecations.ts index 9e8517b5446..3386164f9ee 100644 --- a/packages/private-build-infra/addon/current-deprecations.ts +++ b/packages/private-build-infra/addon/current-deprecations.ts @@ -46,4 +46,5 @@ export default { DEPRECATE_HAS_RECORD: '4.5', DEPRECATE_RECORD_WAS_INVALID: '4.5', DEPRECATE_STRING_ARG_SCHEMAS: '4.5', + DEPRECATE_JSON_API_FALLBACK: '4.5', }; diff --git a/packages/private-build-infra/addon/deprecations.ts b/packages/private-build-infra/addon/deprecations.ts index f97e974c84a..357e2af2375 100644 --- a/packages/private-build-infra/addon/deprecations.ts +++ b/packages/private-build-infra/addon/deprecations.ts @@ -15,3 +15,4 @@ export const DEPRECATE_STORE_FIND = deprecationState('DEPRECATE_STORE_FIND'); export const DEPRECATE_HAS_RECORD = deprecationState('DEPRECATE_HAS_RECORD'); export const DEPRECATE_RECORD_WAS_INVALID = deprecationState('DEPRECATE_RECORD_WAS_INVALID'); export const DEPRECATE_STRING_ARG_SCHEMAS = deprecationState('DEPRECATE_STRING_ARG_SCHEMAS'); +export const DEPRECATE_JSON_API_FALLBACK = deprecationState('DEPRECATE_JSON_API_FALLBACK'); diff --git a/packages/record-data/addon/-private/record-data.ts b/packages/record-data/addon/-private/record-data.ts index b014e39cd8b..acc942a8d74 100644 --- a/packages/record-data/addon/-private/record-data.ts +++ b/packages/record-data/addon/-private/record-data.ts @@ -509,8 +509,6 @@ export default class RecordDataDefault implements RelationshipRecordData { while (k < members.length) { let member = members[k++]; if (member !== null) { - // TODO this can cause materialization - // do something to avoid that return recordDataFor(member); } } diff --git a/packages/store/addon/-private/instance-cache.ts b/packages/store/addon/-private/instance-cache.ts index a97312f9aca..c3c3060ed2b 100644 --- a/packages/store/addon/-private/instance-cache.ts +++ b/packages/store/addon/-private/instance-cache.ts @@ -2,25 +2,36 @@ import { assert } from '@ember/debug'; import type { CreateRecordProperties } from './system/core-store'; import CoreStore from './system/core-store'; +import Snapshot from './system/snapshot'; import type { StableRecordIdentifier } from './ts-interfaces/identifier'; +import { RecordData } from './ts-interfaces/record-data'; import type { RecordInstance } from './ts-interfaces/record-instance'; import { FindOptions } from './ts-interfaces/store'; type Caches = { record: WeakMap; + recordData: WeakMap; }; export class InstanceCache { declare store: CoreStore; #instances: Caches = { record: new WeakMap(), + recordData: new WeakMap(), }; constructor(store: CoreStore) { this.store = store; } - - peek({ identifier, bucket }: { identifier: StableRecordIdentifier; bucket: 'record' }): RecordInstance | undefined { + peek({ identifier, bucket }: { identifier: StableRecordIdentifier; bucket: 'record' }): RecordInstance | undefined; + peek({ identifier, bucket }: { identifier: StableRecordIdentifier; bucket: 'recordData' }): RecordData | undefined; + peek({ + identifier, + bucket, + }: { + identifier: StableRecordIdentifier; + bucket: 'record' | 'recordData'; + }): RecordData | RecordInstance | undefined { return this.#instances[bucket]?.get(identifier); } set({ @@ -56,7 +67,7 @@ export class InstanceCache { if (properties && 'id' in properties) { assert(`expected id to be a string or null`, properties.id !== undefined); - this.getInternalModel(identifier).setId(properties.id); + internalModel.setId(properties.id); } record = this.store._instantiateRecord(this.getRecordData(identifier), identifier, properties); @@ -79,7 +90,15 @@ export class InstanceCache { // TODO move RecordData Cache into InstanceCache getRecordData(identifier: StableRecordIdentifier) { - return this.getInternalModel(identifier)._recordData; + let recordData = this.peek({ identifier, bucket: 'recordData' }); + + if (!recordData) { + recordData = this.store._createRecordData(identifier); + this.#instances.recordData.set(identifier, recordData); + this.getInternalModel(identifier).hasRecordData = true; + } + + return recordData; } // TODO move InternalModel cache into InstanceCache @@ -87,8 +106,7 @@ export class InstanceCache { return this.store._internalModelForResource(identifier); } - // TODO move InternalModel cache into InstanceCache - createSnapshot(identifier: StableRecordIdentifier, options?: FindOptions) { - return this.getInternalModel(identifier).createSnapshot(options); + createSnapshot(identifier: StableRecordIdentifier, options: FindOptions = {}): Snapshot { + return new Snapshot(options, identifier, this.store); } } diff --git a/packages/store/addon/-private/system/core-store.ts b/packages/store/addon/-private/system/core-store.ts index 446a8f2d7e4..ab392b41e28 100644 --- a/packages/store/addon/-private/system/core-store.ts +++ b/packages/store/addon/-private/system/core-store.ts @@ -17,6 +17,7 @@ import type DSModelClass from '@ember-data/model'; import { HAS_RECORD_DATA_PACKAGE } from '@ember-data/private-build-infra'; import { DEPRECATE_HAS_RECORD, + DEPRECATE_JSON_API_FALLBACK, DEPRECATE_RECORD_WAS_INVALID, DEPRECATE_STORE_FIND, } from '@ember-data/private-build-infra/deprecations'; @@ -93,6 +94,7 @@ const StoreMap = new WeakCache(DEBUG ? 'store' : ''); export function storeFor(record: RecordInstance): CoreStore | undefined { const store = StoreMap.get(record); + assert( `A record in a disconnected state cannot utilize the store. This typically means the record has been destroyed, most commonly by unloading it.`, store @@ -357,6 +359,7 @@ class CoreStore extends Service { //TODO Igor pass a wrapper instead of RD let record = this.instantiateRecord(identifier, createOptions, this.__recordDataFor, this._notificationManager); setRecordIdentifier(record, identifier); + setRecordDataFor(record, recordData); StoreMap.set(record, this); return record; } @@ -368,12 +371,19 @@ class CoreStore extends Service { notificationManager: NotificationManager ): DSModel | RecordInstance { let modelName = identifier.type; + let store = this; let internalModel = this._internalModelForResource(identifier); let createOptions: any = { _internalModel: internalModel, // TODO deprecate allowing unknown args setting _createProps: createRecordArgs, + // TODO @deprecate consider deprecating accessing record properties during init which the below is necessary for + _secretInit: (record: RecordInstance): void => { + setRecordIdentifier(record, identifier); + StoreMap.set(record, store); + setRecordDataFor(record, internalModel._recordData); + }, container: null, // necessary hack for setOwner? }; @@ -382,6 +392,7 @@ class CoreStore extends Service { delete createOptions.container; let record = this._modelFactoryFor(modelName).create(createOptions); + return record; } @@ -1460,6 +1471,7 @@ class CoreStore extends Service { } const internalModel = resource.data ? this._internalModelForResource(resource.data) : null; + let { isStale, hasDematerializedInverse, hasReceivedData, isEmpty, shouldForceReload } = resource._relationship .state as RelationshipState; const allInverseRecordsAreLoaded = areAllInverseRecordsLoaded(this, resource); @@ -2208,7 +2220,7 @@ class CoreStore extends Service { this.recordArrayManager.recordDidChange(identifier); } - return internalModel.identifier as StableExistingRecordIdentifier; + return identifier as StableExistingRecordIdentifier; } /** @@ -2527,10 +2539,8 @@ class CoreStore extends Service { // TODO @runspired @deprecate records should implement their own serialization if desired serializeRecord(record: RecordInstance, options?: Dict): unknown { - let identifier = recordIdentifierFor(record); - let internalModel = internalModelFactoryFor(this).peek(identifier); // TODO we used to check if the record was destroyed here - return internalModel!.createSnapshot(options).serialize(options); + return this._instanceCache.createSnapshot(recordIdentifierFor(record)).serialize(options); } // todo @runspired this should likely be publicly documented for custom records @@ -2549,7 +2559,7 @@ class CoreStore extends Service { // because a `Record` is provided there will always be a matching internalModel assert( - `Cannot initiate a save request for an unloaded record: ${internalModel.identifier}`, + `Cannot initiate a save request for an unloaded record: ${identifier}`, !internalModel.isEmpty && !internalModel.isDestroyed ); if (internalModel._isRecordFullyDeleted()) { @@ -2561,7 +2571,7 @@ class CoreStore extends Service { if (!options) { options = {}; } - let recordData = internalModel._recordData; + let recordData = this._instanceCache.getRecordData(identifier); let operation: 'createRecord' | 'deleteRecord' | 'updateRecord' = 'updateRecord'; // TODO handle missing isNew @@ -2572,7 +2582,7 @@ class CoreStore extends Service { } const saveOptions = Object.assign({ [SaveOp]: operation }, options); - let fetchManagerPromise = this._fetchManager.scheduleSave(internalModel.identifier, saveOptions); + let fetchManagerPromise = this._fetchManager.scheduleSave(identifier, saveOptions); return fetchManagerPromise.then( (payload) => { /* @@ -2601,8 +2611,6 @@ class CoreStore extends Service { } const cache = this.identifierCache; - const identifier = internalModel.identifier; - if (operation !== 'deleteRecord' && data) { cache.updateRecordIdentifier(identifier, data); } @@ -2629,6 +2637,7 @@ class CoreStore extends Service { } // TODO move this to InstanceCache + // TODO this is probably a public API from custom model classes? If not move to InstanceCache relationshipReferenceFor(identifier: RecordIdentifier, key: string): BelongsToReference | HasManyReference { let stableIdentifier = this.identifierCache.getOrCreateRecordIdentifier(identifier); let internalModel = internalModelFactoryFor(this).peek(stableIdentifier); @@ -2708,16 +2717,22 @@ class CoreStore extends Service { */ // TODO move this to InstanceCache recordDataFor(identifier: StableRecordIdentifier | { type: string }, isCreate: boolean): RecordData { - let internalModel: InternalModel; + let recordData: RecordData; if (isCreate === true) { - internalModel = internalModelFactoryFor(this).build({ type: identifier.type, id: null }); - internalModel._recordData.clientDidCreate(); - this.recordArrayManager.recordDidChange(internalModel.identifier); + // TODO remove once InternalModel is no longer essential to internal state + // and just build a new identifier directly + let internalModel = internalModelFactoryFor(this).build({ type: identifier.type, id: null }); + let stableIdentifier = internalModel.identifier; + recordData = this._instanceCache.getRecordData(stableIdentifier); + recordData.clientDidCreate(); + this.recordArrayManager.recordDidChange(stableIdentifier); } else { - internalModel = internalModelFactoryFor(this).lookup(identifier as StableRecordIdentifier); + // TODO remove once InternalModel is no longer essential to internal state + internalModelFactoryFor(this).lookup(identifier as StableRecordIdentifier); + recordData = this._instanceCache.getRecordData(identifier as StableRecordIdentifier); } - return internalModel._recordData; + return recordData; } /** @@ -2827,17 +2842,29 @@ class CoreStore extends Service { return adapter; } - // final fallback, no model specific adapter, no application adapter, no - // `adapter` property on store: use json-api adapter - // TODO we should likely deprecate this? - adapter = _adapterCache['-json-api'] || owner.lookup('adapter:-json-api'); - assert( - `No adapter was found for '${modelName}' and no 'application' adapter was found as a fallback.`, - adapter !== undefined - ); - _adapterCache[normalizedModelName] = adapter; - _adapterCache['-json-api'] = adapter; - return adapter; + if (DEPRECATE_JSON_API_FALLBACK) { + // final fallback, no model specific adapter, no application adapter, no + // `adapter` property on store: use json-api adapter + adapter = _adapterCache['-json-api'] || owner.lookup('adapter:-json-api'); + if (adapter !== undefined) { + deprecate( + `Your application is utilizing a deprecated hidden fallback adapter (-json-api). Please implement an application adapter to function as your fallback.`, + false, + { + id: 'ember-data:deprecate-secret-adapter-fallback', + for: 'ember-data', + until: '5.0', + since: { available: '4.5', enabled: '4.5' }, + } + ); + _adapterCache[normalizedModelName] = adapter; + _adapterCache['-json-api'] = adapter; + + return adapter; + } + } + + assert(`No adapter was found for '${modelName}' and no 'application' adapter was found as a fallback.`); } // .............................. diff --git a/packages/store/addon/-private/system/model/internal-model.ts b/packages/store/addon/-private/system/model/internal-model.ts index 5024587c7d9..0f116151921 100644 --- a/packages/store/addon/-private/system/model/internal-model.ts +++ b/packages/store/addon/-private/system/model/internal-model.ts @@ -31,13 +31,11 @@ import type { ChangedAttributesHash, RecordData } from '../../ts-interfaces/reco import type { JsonApiResource, JsonApiValidationError } from '../../ts-interfaces/record-data-json-api'; import type { RelationshipSchema } from '../../ts-interfaces/record-data-schemas'; import type { RecordInstance } from '../../ts-interfaces/record-instance'; -import type { FindOptions } from '../../ts-interfaces/store'; import type { Dict } from '../../ts-interfaces/utils'; import type CoreStore from '../core-store'; import { errorsHashToArray } from '../errors-utils'; import recordDataFor from '../record-data-for'; import { BelongsToReference, HasManyReference, RecordReference } from '../references'; -import Snapshot from '../snapshot'; import { internalModelFactoryFor } from '../store/internal-model-factory'; type PrivateModelModule = { @@ -87,7 +85,7 @@ export default class InternalModel { declare _id: string | null; declare modelName: string; declare clientId: string; - declare __recordData: RecordData | null; + declare hasRecordData: boolean; declare _isDestroyed: boolean; declare isError: boolean; declare _pendingRecordArrayManagerFlush: boolean; @@ -124,7 +122,7 @@ export default class InternalModel { this.clientId = identifier.lid; this.hasRecord = false; - this.__recordData = null; + this.hasRecordData = false; this._isDestroyed = false; this._doNotDestroy = false; @@ -145,7 +143,6 @@ export default class InternalModel { this._modelClass = null; this.__recordArrays = null; this._recordReference = null; - this.__recordData = null; this.error = null; @@ -163,6 +160,7 @@ export default class InternalModel { set id(value: string | null) { if (value !== this._id) { let newIdentifier = { type: this.identifier.type, lid: this.identifier.lid, id: value }; + // TODO potentially this needs to handle merged result this.store.identifierCache.updateRecordIdentifier(this.identifier, newIdentifier); this.notifyPropertyChange('id'); } @@ -182,16 +180,7 @@ export default class InternalModel { } get _recordData(): RecordData { - if (this.__recordData === null) { - let recordData = this.store._createRecordData(this.identifier); - this.__recordData = recordData; - return recordData; - } - return this.__recordData; - } - - set _recordData(newValue) { - this.__recordData = newValue; + return this.store._instanceCache.getRecordData(this.identifier); } isHiddenFromRecordArrays() { @@ -239,7 +228,7 @@ export default class InternalModel { } isNew(): boolean { - if (this.__recordData && this._recordData.isNew) { + if (this.hasRecordData && this._recordData.isNew) { return this._recordData.isNew(); } else { return false; @@ -247,7 +236,7 @@ export default class InternalModel { } get isEmpty(): boolean { - return !this.__recordData || ((!this.isNew() || this.isDeleted()) && this._recordData.isEmpty?.()) || false; + return !this.hasRecordData || ((!this.isNew() || this.isDeleted()) && this._recordData.isEmpty?.()) || false; } get isLoading() { @@ -307,7 +296,7 @@ export default class InternalModel { }); } - this.hasRecord = false; // this must occur after reltionship removal + this.hasRecord = false; // this must occur after relationship removal this.error = null; this.store.recordArrayManager.recordDidChange(this.identifier); } @@ -709,31 +698,12 @@ export default class InternalModel { return this._recordData.setDirtyBelongsTo(key, extractRecordDataFromRecord(value)); } - setDirtyAttribute(key: string, value: T): T { - assert(`Attempted to set '${key}' on the deleted record ${this}`, !this.isDeleted()); - - let currentValue = this._recordData.getAttr(key); - if (currentValue !== value) { - this._recordData.setDirtyAttribute(key, value); - let record = this.store._instanceCache.peek({ identifier: this.identifier, bucket: 'record' }); - if (record && isDSModel(record)) { - record.errors.remove(key); - } - } - - return value; - } - get isDestroyed(): boolean { return this._isDestroyed; } - createSnapshot(options: FindOptions = {}): Snapshot { - return new Snapshot(options, this.identifier, this.store); - } - hasChangedAttributes(): boolean { - if (!this.__recordData) { + if (!this.hasRecordData) { // no need to calculate changed attributes when calling `findRecord` return false; } @@ -741,7 +711,7 @@ export default class InternalModel { } changedAttributes(): ChangedAttributesHash { - if (!this.__recordData) { + if (!this.hasRecordData) { // no need to calculate changed attributes when calling `findRecord` return {}; } @@ -812,7 +782,7 @@ export default class InternalModel { } removeFromInverseRelationships() { - if (this.__recordData) { + if (this.hasRecordData) { this.store._backburner.join(() => { this._recordData.removeFromInverseRelationships(); }); @@ -907,7 +877,7 @@ export default class InternalModel { } // 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) { + if (this.hasRecordData && this._recordData.__setId) { this._recordData.__setId(id); } } diff --git a/packages/store/addon/-private/system/record-data-for.ts b/packages/store/addon/-private/system/record-data-for.ts index eeca0097f6e..32a05facff4 100644 --- a/packages/store/addon/-private/system/record-data-for.ts +++ b/packages/store/addon/-private/system/record-data-for.ts @@ -28,10 +28,15 @@ type Reference = { internalModel: InternalModel }; type Instance = StableRecordIdentifier | InternalModel | RecordData | DSModelOrSnapshot | Reference; -const RecordDataForIdentifierCache = new WeakCache(DEBUG ? 'recordData' : ''); - -export function setRecordDataFor(identifier: StableRecordIdentifier, recordData: RecordData): void { - assert(`Illegal set of identifier`, !RecordDataForIdentifierCache.has(identifier)); +const RecordDataForIdentifierCache = new WeakCache( + DEBUG ? 'recordData' : '' +); + +export function setRecordDataFor(identifier: StableRecordIdentifier | RecordInstance, recordData: RecordData): void { + assert( + `Illegal set of identifier`, + !RecordDataForIdentifierCache.has(identifier) || RecordDataForIdentifierCache.get(identifier) === recordData + ); RecordDataForIdentifierCache.set(identifier, recordData); } @@ -47,8 +52,11 @@ export default function recordDataFor(instance: Instance | object): RecordData | if (RecordDataForIdentifierCache.has(instance as StableRecordIdentifier)) { return RecordDataForIdentifierCache.get(instance as StableRecordIdentifier) as RecordData; } + let internalModel = (instance as DSModelOrSnapshot)._internalModel || (instance as Reference).internalModel || instance; - return internalModel._recordData || null; + assert(`Expected to no longer need this`, !internalModel._recordData); + + return null; } diff --git a/packages/store/addon/-private/system/snapshot.ts b/packages/store/addon/-private/system/snapshot.ts index ae9657f5968..5ebbfb3adac 100644 --- a/packages/store/addon/-private/system/snapshot.ts +++ b/packages/store/addon/-private/system/snapshot.ts @@ -25,7 +25,6 @@ import type { FindOptions } from '../ts-interfaces/store'; import type { Dict } from '../ts-interfaces/utils'; import type Store from './core-store'; import type InternalModel from './model/internal-model'; -import recordDataFor from './record-data-for'; type RecordId = string | null; @@ -137,7 +136,7 @@ export default class Snapshot implements Snapshot { */ this.modelName = identifier.type; if (internalModel.hasRecord) { - this._changedAttributes = recordDataFor(internalModel).changedAttributes(); + this._changedAttributes = this._store._instanceCache.getRecordData(identifier).changedAttributes(); } } @@ -166,12 +165,13 @@ export default class Snapshot implements Snapshot { let record = this.record; let attributes = (this.__attributes = Object.create(null)); let attrs = Object.keys(this._store._attributesDefinitionFor(this.identifier)); + let recordData = this._store._instanceCache.getRecordData(this.identifier); attrs.forEach((keyName) => { if (schemaIsDSModel(this._internalModel.modelClass)) { // if the schema is for a DSModel then the instance is too attributes[keyName] = get(record as DSModel, keyName); } else { - attributes[keyName] = recordDataFor(this._internalModel).getAttr(keyName); + attributes[keyName] = recordData.getAttr(keyName); } }); @@ -355,7 +355,7 @@ export default class Snapshot implements Snapshot { if (returnModeIsId) { result = inverseInternalModel.id; } else { - result = inverseInternalModel.createSnapshot(); + result = store._instanceCache.createSnapshot(inverseInternalModel.identifier); } } else { result = null; @@ -457,7 +457,7 @@ export default class Snapshot implements Snapshot { (member as ExistingResourceIdentifierObject | NewResourceIdentifierObject).id || null ); } else { - (results as Snapshot[]).push(internalModel.createSnapshot()); + (results as Snapshot[]).push(store._instanceCache.createSnapshot(internalModel.identifier)); } } }); 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 4e75c1ec3b0..6840f29e3d7 100644 --- a/packages/store/addon/-private/system/store/internal-model-factory.ts +++ b/packages/store/addon/-private/system/store/internal-model-factory.ts @@ -65,7 +65,7 @@ export function recordIdentifierFor(record: RecordInstance | RecordData): Stable } export function setRecordIdentifier(record: RecordInstance | RecordData, identifier: StableRecordIdentifier): void { - if (DEBUG && RecordCache.has(record)) { + if (DEBUG && RecordCache.has(record) && RecordCache.get(record) !== identifier) { throw new Error(`${record} was already assigned an identifier`); } @@ -118,6 +118,10 @@ export default class InternalModelFactory { // we cannot merge internalModels when both have records // (this may not be strictly true, we could probably swap the internalModel the record points at) if (im && otherIm && im.hasRecord && otherIm.hasRecord) { + // TODO we probably don't need to throw these errors anymore + // once InternalModel is fully removed, as we can just "swap" + // what data source the abandoned record points at so long as + // it itself is not retained by the store in any way. if ('id' in resourceData) { throw new Error( `Failed to update the 'id' for the RecordIdentifier '${identifier.type}:${identifier.id} (${identifier.lid})' to '${resourceData.id}', because that id is already in use by '${matchedIdentifier.type}:${matchedIdentifier.id} (${matchedIdentifier.lid})'` @@ -149,6 +153,7 @@ export default class InternalModelFactory { im = otherIm; // TODO do we need to notify the id change? im._id = intendedIdentifier.id; + im.identifier = intendedIdentifier; map.add(im, intendedIdentifier.lid); // just use im @@ -271,6 +276,7 @@ export default class InternalModelFactory { ); if (identifier.id === null) { + // TODO potentially this needs to handle merged result this.identifierCache.updateRecordIdentifier(identifier, { type, id }); } @@ -335,6 +341,7 @@ export default class InternalModelFactory { recordMap.remove(internalModel, clientId); const { identifier } = internalModel; + debugger; this.identifierCache.forgetRecordIdentifier(identifier); } diff --git a/packages/store/addon/-private/system/weak-cache.ts b/packages/store/addon/-private/system/weak-cache.ts index c916fa50f6e..9463f30572d 100644 --- a/packages/store/addon/-private/system/weak-cache.ts +++ b/packages/store/addon/-private/system/weak-cache.ts @@ -98,7 +98,7 @@ class WeakCache extends WeakMap { class DebugWeakCache extends WeakCache { set(obj: K, value: V): this { - if (DEBUG && super.has(obj)) { + if (DEBUG && super.has(obj) && this.get(obj) !== value) { throw new Error(`${Object.prototype.toString.call(obj)} was already assigned a value for ${this._fieldName}`); } if (DEBUG) { From 088d1d602e0e56cd06f593905ec0b80f49906d49 Mon Sep 17 00:00:00 2001 From: Chris Thoburn Date: Mon, 25 Jul 2022 13:57:20 -0700 Subject: [PATCH 14/16] remove debugger --- .../store/addon/-private/system/store/internal-model-factory.ts | 1 - 1 file changed, 1 deletion(-) 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 6840f29e3d7..ebf2398c1e9 100644 --- a/packages/store/addon/-private/system/store/internal-model-factory.ts +++ b/packages/store/addon/-private/system/store/internal-model-factory.ts @@ -341,7 +341,6 @@ export default class InternalModelFactory { recordMap.remove(internalModel, clientId); const { identifier } = internalModel; - debugger; this.identifierCache.forgetRecordIdentifier(identifier); } From 4a664811c99dd9435a4f604cb21eb1be302c4587 Mon Sep 17 00:00:00 2001 From: Chris Thoburn Date: Mon, 25 Jul 2022 16:08:40 -0700 Subject: [PATCH 15/16] fix ember 4.6 --- .../tests/acceptance/relationships/belongs-to-test.js | 1 - packages/model/addon/-private/system/promise-many-array.ts | 4 ++-- packages/store/types/@ember/array/index.d.ts | 2 ++ 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/-ember-data/tests/acceptance/relationships/belongs-to-test.js b/packages/-ember-data/tests/acceptance/relationships/belongs-to-test.js index 8d677cd2e82..e03272e2a6c 100644 --- a/packages/-ember-data/tests/acceptance/relationships/belongs-to-test.js +++ b/packages/-ember-data/tests/acceptance/relationships/belongs-to-test.js @@ -444,7 +444,6 @@ module('async belongs-to rendering tests', function (hooks) { let parent = await sedona.parent; await parent.destroyRecord(); - // TODO for some reason parent isn't notified via the destroy above :thinking_face: let newParent = await sedona.parent; assert.strictEqual(newParent, null, 'We no longer have a parent'); diff --git a/packages/model/addon/-private/system/promise-many-array.ts b/packages/model/addon/-private/system/promise-many-array.ts index 54452d6e20c..3e2cdb045cd 100644 --- a/packages/model/addon/-private/system/promise-many-array.ts +++ b/packages/model/addon/-private/system/promise-many-array.ts @@ -1,4 +1,4 @@ -import ArrayMixin from '@ember/array'; +import ArrayMixin, { NativeArray } from '@ember/array'; import type ArrayProxy from '@ember/array/proxy'; import { assert } from '@ember/debug'; import { dependentKeyCompat } from '@ember/object/compat'; @@ -55,7 +55,7 @@ export default class PromiseManyArray { const meta = Ember.meta(this); meta.hasMixin = (mixin: Object) => { - if (mixin === ArrayMixin) { + if (mixin === NativeArray || mixin === ArrayMixin) { return true; } return false; diff --git a/packages/store/types/@ember/array/index.d.ts b/packages/store/types/@ember/array/index.d.ts index 489da26f171..b9314843e77 100644 --- a/packages/store/types/@ember/array/index.d.ts +++ b/packages/store/types/@ember/array/index.d.ts @@ -100,6 +100,8 @@ interface Array extends Enumerable { declare const Array: Mixin>; export default Array; +export const NativeArray; + /** * Creates an `Ember.NativeArray` from an Array like object. * Does not modify the original object's contents. Ember.A is not needed if From 2b29b1c1ca98809342ec9205feed33e3f8553bc1 Mon Sep 17 00:00:00 2001 From: Chris Thoburn Date: Mon, 25 Jul 2022 16:25:33 -0700 Subject: [PATCH 16/16] fix test assertion --- .../tests/integration/model-for-test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/unpublished-model-encapsulation-test-app/tests/integration/model-for-test.js b/packages/unpublished-model-encapsulation-test-app/tests/integration/model-for-test.js index 7ce63f9cffc..a9a38f9f9ac 100644 --- a/packages/unpublished-model-encapsulation-test-app/tests/integration/model-for-test.js +++ b/packages/unpublished-model-encapsulation-test-app/tests/integration/model-for-test.js @@ -53,7 +53,7 @@ module('modelFor without @ember-data/model', function (hooks) { } catch (e) { assert.strictEqual( e.message, - "No model was found for 'person' and no schema handles the type", + "Assertion Failed: No model was found for 'person' and no schema handles the type", 'We throw an error when no schema is available' ); }