API Docs for: v3.16.0-alpha.2
Show:

File: ../store/addon/-private/system/ds-model-store.ts

/**
  @module @ember-data/store
*/
import CoreStore from './core-store';
import { assert } from '@ember/debug';
import normalizeModelName from './normalize-model-name';
import { assign } from '@ember/polyfills';
import { DEBUG } from '@glimmer/env';
import { isPresent } from '@ember/utils';
import { deprecate } from '@ember/application/deprecations';
import EmberError from '@ember/error';
import { get } from '@ember/object';
import ShimModelClass, { getShimClass } from './model/shim-model-class';
import { setOwner, getOwner } from '@ember/application';
import { DSModel } from '../ts-interfaces/ds-model';
import NotificationManager from './record-notification-manager';
import { StableRecordIdentifier } from '../ts-interfaces/identifier';
import { DSModelSchemaDefinitionService, getModelFactory } from './schema-definition-service';
import { CUSTOM_MODEL_CLASS } from '@ember-data/canary-features';
import RecordDataRecordWrapper from '../ts-interfaces/record-data-record-wrapper';
import { SchemaDefinitionService } from '../ts-interfaces/schema-definition-service';
import { RelationshipsSchema } from '../ts-interfaces/record-data-schemas';
import notifyChanges from './model/notify-changes';
type DSModelClass = import('@ember-data/model').default;

/**
  The store service contains all of the data for records loaded from the server.
  It is also responsible for creating instances of `Model` that wrap
  the individual data for a record, so that they can be bound to in your
  Handlebars templates.

  By default, applications will have a single `Store` service that is
  automatically created.

  The store can be customized by extending the service in the following manner:

  ```app/services/store.js
  import Store from '@ember-data/store';

  export default class MyStore extends Store {}
  ```

  You can retrieve models from the store in several ways. To retrieve a record
  for a specific id, use the `Store`'s `findRecord()` method:

  ```javascript
  store.findRecord('person', 123).then(function (person) {
  });
  ```

  By default, the store will talk to your backend using a standard
  REST mechanism. You can customize how the store talks to your
  backend by specifying a custom adapter:

  ```app/adapters/application.js
  import DS from 'ember-data';

  export default Adapter.extend({
  });
  ```

  You can learn more about writing a custom adapter by reading the `Adapter`
  documentation.

  ### Store createRecord() vs. push() vs. pushPayload()

  The store provides multiple ways to create new record objects. They have
  some subtle differences in their use which are detailed below:

  [createRecord](Store/methods/createRecord?anchor=createRecord) is used for creating new
  records on the client side. This will return a new record in the
  `created.uncommitted` state. In order to persist this record to the
  backend, you will need to call `record.save()`.

  [push](Store/methods/push?anchor=push) is used to notify Ember Data's store of new or
  updated records that exist in the backend. This will return a record
  in the `loaded.saved` state. The primary use-case for `store#push` is
  to notify Ember Data about record updates (full or partial) that happen
  outside of the normal adapter methods (for example
  [SSE](http://dev.w3.org/html5/eventsource/) or [Web
  Sockets](http://www.w3.org/TR/2009/WD-websockets-20091222/)).

  [pushPayload](Store/methods/pushPayload?anchor=pushPayload) is a convenience wrapper for
  `store#push` that will deserialize payloads if the
  Serializer implements a `pushPayload` method.

  Note: When creating a new record using any of the above methods
  Ember Data will update `RecordArray`s such as those returned by
  `store#peekAll()` or `store#findAll()`. This means any
  data bindings or computed properties that depend on the RecordArray
  will automatically be synced to include the new or updated record
  values.

  @class Store
  @main @ember-data/store
  @extends Ember.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,
      currentState: internalModel.currentState,
      container: null,
    };
    assign(createOptions, createRecordArgs);

    // ensure that `getOwner(this)` works inside a model instance
    setOwner(createOptions, getOwner(this));

    delete createOptions.container;
    let record = this._modelFactoryFor(modelName).create(createOptions);
    //todo optimize
    notificationManager.subscribe(identifier, (identifier, value) => notifyChanges(identifier, value, record, this));
    return record;
  }

  teardownRecord(record: DSModel) {
    record.destroy();
  }

  /**
  Returns the model class for the particular `modelName`.

  The class of a model might be useful if you want to get a list of all the
  relationship names of the model, see
  [`relationshipNames`](/ember-data/release/classes/Model?anchor=relationshipNames)
  for example.

  @method modelFor
  @param {String} modelName
  @return {Model}
    */
  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 (!CUSTOM_MODEL_CLASS || !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;
  }

  /*
  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.

  @private
 */
  _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'
    );

    if (CUSTOM_MODEL_CLASS) {
      return this.getSchemaDefinitionService().doesTypeExist(modelName);
    } else {
      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'
      );
      let normalizedModelName = normalizeModelName(modelName);
      let factory = getModelFactory(this, this._modelFactoryCache, normalizedModelName);

      return factory !== null;
    }
  }

  _relationshipMetaFor(modelName: string, id: string | null, key: string) {
    if (CUSTOM_MODEL_CLASS) {
      return this._relationshipsDefinitionFor(modelName)[key];
    } else {
      let modelClass = this.modelFor(modelName);
      let relationshipsByName = get(modelClass, 'relationshipsByName');
      return relationshipsByName.get(key);
    }
  }

  _attributesDefinitionFor(modelName: string, identifier?: StableRecordIdentifier) {
    if (CUSTOM_MODEL_CLASS) {
      if (identifier) {
        return this.getSchemaDefinitionService().attributesDefinitionFor(identifier);
      } else {
        return this.getSchemaDefinitionService().attributesDefinitionFor(modelName);
      }
    } else {
      let attributes = this._attributesDefCache[modelName];

      if (attributes === undefined) {
        let modelClass = this.modelFor(modelName);
        let attributeMap = get(modelClass, 'attributes');

        attributes = Object.create(null);
        attributeMap.forEach((meta, name) => (attributes[name] = meta));
        this._attributesDefCache[modelName] = attributes;
      }

      return attributes;
    }
  }

  _relationshipsDefinitionFor(modelName: string, identifier?: StableRecordIdentifier): RelationshipsSchema {
    if (CUSTOM_MODEL_CLASS) {
      if (identifier) {
        return this.getSchemaDefinitionService().relationshipsDefinitionFor(identifier);
      } else {
        return this.getSchemaDefinitionService().relationshipsDefinitionFor(modelName);
      }
    } else {
      let relationships = this._relationshipsDefCache[modelName];

      if (relationships === undefined) {
        let modelClass = this.modelFor(modelName);
        relationships = get(modelClass, 'relationshipsObject') || null;
        this._relationshipsDefCache[modelName] = relationships;
      }

      return relationships;
    }
  }

  getSchemaDefinitionService(): SchemaDefinitionService {
    if (CUSTOM_MODEL_CLASS) {
      if (!this._schemaDefinitionService) {
        this._schemaDefinitionService = new DSModelSchemaDefinitionService(this);
      }
      return this._schemaDefinitionService;
    } else {
      throw 'schema service is only available when custom model class feature flag is on';
    }
  }
}

let assertDestroyingStore: Function;
let assertDestroyedStoreOnly: Function;

if (DEBUG) {
  assertDestroyingStore = function assertDestroyedStore(store, method) {
    if (!store.shouldAssertMethodCallsOnDestroyedStore) {
      deprecate(
        `Attempted to call store.${method}(), but the store instance has already been destroyed.`,
        !(store.isDestroying || store.isDestroyed),
        {
          id: 'ember-data:method-calls-on-destroyed-store',
          until: '3.8',
        }
      );
    } else {
      assert(
        `Attempted to call store.${method}(), but the store instance has already been destroyed.`,
        !(store.isDestroying || store.isDestroyed)
      );
    }
  };
  assertDestroyedStoreOnly = function assertDestroyedStoreOnly(store, method) {
    if (!store.shouldAssertMethodCallsOnDestroyedStore) {
      deprecate(
        `Attempted to call store.${method}(), but the store instance has already been destroyed.`,
        !store.isDestroyed,
        {
          id: 'ember-data:method-calls-on-destroyed-store',
          until: '3.8',
        }
      );
    } else {
      assert(
        `Attempted to call store.${method}(), but the store instance has already been destroyed.`,
        !store.isDestroyed
      );
    }
  };
}

export default Store;