From 0474cb6534f2893249f71b474be93b8e76242085 Mon Sep 17 00:00:00 2001 From: Chris Thoburn Date: Thu, 8 Dec 2022 15:30:13 -0800 Subject: [PATCH] stash --- README.md | 64 ++- ember-data-types/cache/aliases.ts | 12 + ember-data-types/cache/cache.ts | 440 ++++++++++++++++++ ember-data-types/cache/change.ts | 9 + ember-data-types/cache/document.ts | 43 ++ ember-data-types/cache/identifier.ts | 3 + ember-data-types/cache/mutations.ts | 72 +++ ember-data-types/cache/operations.ts | 32 ++ ember-data-types/cache/relationship.ts | 21 + ember-data-types/cache/validation-error.ts | 21 + .../q/record-data-store-wrapper.ts | 34 +- ember-data-types/q/record-data.ts | 58 +-- packages/-ember-data/README.md | 48 +- packages/-ember-data/addon/-private/index.ts | 2 +- packages/-ember-data/addon/index.js | 2 +- packages/graph/package.json | 4 +- packages/graph/src/-private/graph/-utils.ts | 6 +- packages/graph/src/-private/graph/graph.ts | 12 +- packages/graph/src/-private/graph/index.ts | 10 +- packages/graph/src/index.ts | 1 + packages/json-api/src/-private.ts | 2 +- packages/json-api/src/-private/record-data.ts | 197 +++++--- packages/model/src/-private/attr.js | 15 +- .../-private/legacy-relationships-support.ts | 27 +- packages/model/src/-private/many-array.ts | 8 +- packages/model/src/-private/model.js | 8 +- packages/model/src/-private/notify-changes.ts | 11 +- packages/model/src/-private/record-state.ts | 21 +- .../src/-private/references/belongs-to.ts | 14 +- .../model/src/-private/references/has-many.ts | 14 +- .../addon/current-deprecations.ts | 2 + .../private-build-infra/addon/deprecations.ts | 2 + packages/store/README.md | 181 ++++++- .../src/-private/caches/instance-cache.ts | 139 ++++-- .../src/-private/caches/record-data-for.ts | 14 +- packages/store/src/-private/index.ts | 2 +- .../-private/managers/record-data-manager.ts | 201 ++++++-- .../managers/record-data-store-wrapper.ts | 50 +- .../store/src/-private/network/snapshot.ts | 25 +- packages/store/src/-private/store-service.ts | 203 ++++---- packages/store/src/index.ts | 145 +++++- pnpm-lock.yaml | 2 +- .../app/services/store.js | 8 +- tests/docs/fixtures/expected.js | 2 + .../integration/graph/edge-removal/helpers.ts | 2 +- .../tests/integration/graph/edge-test.ts | 42 +- tests/main/tests/helpers/accessors.ts | 4 +- .../record-data/record-data-errors-test.ts | 31 +- .../record-data/record-data-state-test.ts | 23 +- .../record-data/record-data-test.ts | 39 +- .../record-data/store-wrapper-test.ts | 30 +- .../record-data/unloading-record-data-test.js | 3 +- .../integration/records/delete-record-test.js | 13 +- .../tests/integration/records/unload-test.js | 9 +- .../custom-class-model-test.ts | 114 ++--- .../app/services/store.js | 8 +- 56 files changed, 1866 insertions(+), 639 deletions(-) create mode 100644 ember-data-types/cache/aliases.ts create mode 100644 ember-data-types/cache/cache.ts create mode 100644 ember-data-types/cache/change.ts create mode 100644 ember-data-types/cache/document.ts create mode 100644 ember-data-types/cache/identifier.ts create mode 100644 ember-data-types/cache/mutations.ts create mode 100644 ember-data-types/cache/operations.ts create mode 100644 ember-data-types/cache/relationship.ts create mode 100644 ember-data-types/cache/validation-error.ts create mode 100644 packages/graph/src/index.ts diff --git a/README.md b/README.md index 04c5f4611f6..842e9ba01dc 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,6 @@ />

-

The lightweight reactive data library for JavaScript applications

[![Build Status](https://github.com/emberjs/data/workflows/CI/badge.svg)](https://github.com/emberjs/data/actions?workflow=CI) @@ -25,14 +24,14 @@ Wrangle your application's data management with scalable patterns for developer productivity. -- ⚡️ Committed to Best-In-Class Performance +- ⚡️ Committed to Best-In-Class Performance - 🌲 Focused on being as svelte as possible - 🚀 SSR Ready - 🔜 Typescript Support - 🐹 Built with ♥️ by [Ember](https://emberjs.com) - ⚛️ Supports any API: `GraphQL` `JSON:API` `REST` `tRPC` ...bespoke or a mix -### 📖 On This Page +### 📖 On This Page - [Overview](#overview) - [Architecture](#-architecture) @@ -47,7 +46,7 @@ Wrangle your application's data management with scalable patterns for developer # Overview -*Ember***Data** is a lightweight reactive data library for JavaScript applications that provides composable primitives for ordering query/mutation/peek flows, managing network and cache, and reducing data for presentation. +\*Ember**\*Data** is a lightweight reactive data library for JavaScript applications that provides composable primitives for ordering query/mutation/peek flows, managing network and cache, and reducing data for presentation. - [API Documentation](https://api.emberjs.com/ember-data/release) - [Community & Help](https://emberjs.com/community) @@ -57,21 +56,17 @@ Wrangle your application's data management with scalable patterns for developer - [Team](https://emberjs.com/team) - [Blog](https://emberjs.com/blog) - - ## 🪜 Architecture -*Ember***Data** is both *resource* centric and *document* centric in it's approach to caching, requesting and presenting data. Your application's configuration and usage drives which is important and when. +\*Ember**\*Data** is both _resource_ centric and _document_ centric in it's approach to caching, requesting and presenting data. Your application's configuration and usage drives which is important and when. The `Store` is a **coordinator**. When using a `Store` you configure what cache to use, how cache data should be presented to the UI, and where it should look for requested data when it is not available in the cache. -This coordination is handled opaquely to the nature of the requests issued and the format of the data being handled. This approach gives applications broad flexibility to configure *Ember***Data** to best suite their needs. This makes *Ember***Data** a powerful solution for applications regardless of their size and complexity. - -*Ember***Data** is designed to scale, with a religious focus on performance and asset-size to keep its footprint small but speedy while still being able to handle large complex APIs in huge data-driven applications with no additional code and no added application complexity. It's goal is to prevent applications from writing code to manage data that is difficult to maintain or reason about. - -*Ember***Data**'s power comes not from specific features, data formats, or adherence to specific API specs such as `JSON:API` `trpc` or `GraphQL`, but from solid conventions around requesting and mutating data developed over decades of experience scaling developer productivity. +This coordination is handled opaquely to the nature of the requests issued and the format of the data being handled. This approach gives applications broad flexibility to configure \*Ember**\*Data** to best suite their needs. This makes \*Ember**\*Data** a powerful solution for applications regardless of their size and complexity. +\*Ember**\*Data** is designed to scale, with a religious focus on performance and asset-size to keep its footprint small but speedy while still being able to handle large complex APIs in huge data-driven applications with no additional code and no added application complexity. It's goal is to prevent applications from writing code to manage data that is difficult to maintain or reason about. +\*Ember**\*Data**'s power comes not from specific features, data formats, or adherence to specific API specs such as `JSON:API` `trpc` or `GraphQL`, but from solid conventions around requesting and mutating data developed over decades of experience scaling developer productivity. ## Basic Installation @@ -83,12 +78,12 @@ pnpm add ember-data `ember-data` is installed by default for new applications generated with `ember-cli`. You can check what version is installed by looking in the `devDependencies` hash of your project's [package.json](https://docs.npmjs.com/cli/v8/configuring-npm/package-json) file. -If you have generated a new `Ember` application using `ember-cli` but do +If you have generated a new `Ember` application using `ember-cli` but do not wish to use `ember-data`, remove `ember-data` from your project's `package.json` file and run your package manager's install command to update your lockfile. ## Advanced Installation -*Ember***Data** is organized into primitives that compose together via public APIs. +\*Ember**\*Data** is organized into primitives that compose together via public APIs. - [@ember-data/store](./packages/store) is the core and handles coordination - [@ember-data/tracking](./packages/tracking) is required when using the core and provides tracking primitives for change notification of Tracked properties @@ -107,7 +102,7 @@ public APIs, other libraries or applications may provide their own implementatio ### Deprecation Stripping -*Ember***Data** allows users to opt-in and remove code that exists to support deprecated behaviors. +\*Ember**\*Data** allows users to opt-in and remove code that exists to support deprecated behaviors. If your app has resolved all deprecations present in a given version, you may specify that version as your "compatibility" version to remove the code that supported the deprecated behavior from your app. @@ -123,7 +118,7 @@ let app = new EmberApp(defaults, { ### randomUUID polyfill -*Ember***Data** uses `UUID V4` by default to generate identifiers for new data created on the client. Identifier generation is configurable, but we also for convenience will polyfill +\*Ember**\*Data** uses `UUID V4` by default to generate identifiers for new data created on the client. Identifier generation is configurable, but we also for convenience will polyfill the necessary feature if your browser support or deployment environment demands it. To activate this polyfill: @@ -132,7 +127,7 @@ let app = new EmberApp(defaults, { '@embroider/macros': { setConfig: { '@ember-data/store': { - polyfillUUID: true + polyfillUUID: true, }, }, }, @@ -147,8 +142,8 @@ that all support for it should be stripped from the build. ```ts let app = new EmberApp(defaults, { emberData: { - includeDataAdapterInProduction: false - } + includeDataAdapterInProduction: false, + }, }); ``` @@ -161,27 +156,26 @@ at build time. This instrumentation is always removed from production builds or that has not explicitly activated it. To activate it set the appropriate flag to `true`. ```ts - let app = new EmberApp(defaults, { - emberData: { - debug: { - LOG_PAYLOADS: false, // data store received to update cache with - LOG_OPERATIONS: false, // updates to cache remote state - LOG_MUTATIONS: false, // updates to cache local state - LOG_NOTIFICATIONS: false, - LOG_REQUEST_STATUS: false, - LOG_IDENTIFIERS: false, - LOG_GRAPH: false, // relationship storage - LOG_INSTANCE_CACHE: false, // instance creation/deletion - } - } - }); - ``` +let app = new EmberApp(defaults, { + emberData: { + debug: { + LOG_PAYLOADS: false, // data store received to update cache with + LOG_OPERATIONS: false, // updates to cache remote state + LOG_MUTATIONS: false, // updates to cache local state + LOG_NOTIFICATIONS: false, + LOG_REQUEST_STATUS: false, + LOG_IDENTIFIERS: false, + LOG_GRAPH: false, // relationship storage + LOG_INSTANCE_CACHE: false, // instance creation/deletion + }, + }, +}); +``` ## Contributing See the [Contributing](CONTRIBUTING.md) guide for details. - ### License This project is licensed under the [MIT License](LICENSE.md). diff --git a/ember-data-types/cache/aliases.ts b/ember-data-types/cache/aliases.ts new file mode 100644 index 00000000000..70929e710b7 --- /dev/null +++ b/ember-data-types/cache/aliases.ts @@ -0,0 +1,12 @@ +// The ResourceBlob is an opaque type that must +// satisfy two constraints. +// (1) it should be possible for the IdentifierCache +// to be able to generate a RecordIdentifier for it +// whether by default or due to configuration. +// (2) it should be in a format expected by the Cache. +// This format is Cache declared. +// +// this Opaqueness allows arbitrary storage of any +// serializable / transferable state including such things +// as Buffers and Strings. +export type ResourceBlob = unknown; diff --git a/ember-data-types/cache/cache.ts b/ember-data-types/cache/cache.ts new file mode 100644 index 00000000000..680073700d7 --- /dev/null +++ b/ember-data-types/cache/cache.ts @@ -0,0 +1,440 @@ +/** + * @module @ember-data/experimental-preview-types + */ +import { StableRecordIdentifier } from '@ember-data/types/q/identifier'; + +import { CollectionResourceRelationship, SingleResourceRelationship } from '../q/ember-data-json-api'; +import { ResourceBlob } from './aliases'; +import { Change } from './change'; +import { ResourceDocument, StructuredDocument } from './document'; +import { StableDocumentIdentifier } from './identifier'; +import { Mutation } from './mutations'; +import { Operation } from './operations'; +import { ValidationError } from './validation-error'; + +/** + * The interface for EmberData Caches. + * + * A Cache handles in-memory storage of Document and Resource + * data. + * + * @class Cache + */ +export interface Cache { + /** + * The Cache Version that this implementation implements. + * + * @type {'2'} + * @property version + */ + version: '2'; + + // Cache Management + // ================ + + /** + * Cache the response to a request + * + * Unlike `store.push` which has UPSERT + * semantics, `put` has `replace` semantics similar to + * the `http` method `PUT` + * + * the individually cacheable resource data it may contain + * should upsert, but the document data surrounding it should + * fully replace any existing information + * + * Note that in order to support inserting arbitrary data + * to the cache that did not originate from a request `put` + * should expect to sometimes encounter a document with only + * a `data` member and therefor must not assume the existence + * of `request` and `response` on the document. + * + * @method put + * @param {StructuredDocument} doc + * @returns {ResourceDocument} + * @public + */ + put(doc: StructuredDocument): ResourceDocument; + + /** + * Update the "remote" or "canonical" (persisted) state of the Cache + * by merging new information into the existing state. + * + * Note: currently the only valid resource operation is a MergeOperation + * which occurs when a collision of identifiers is detected. + * + * @method patch + * @public + * @param {Operation} op the operation to perform + * @returns {void} + */ + patch(op: Operation): void; + + /** + * Update the "local" or "current" (unpersisted) state of the Cache + * + * @method mutate + * @param {Mutation} mutation + * @returns {void} + * @public + */ + mutate(mutation: Mutation): void; + + /** + * Peek resource data from the Cache. + * + * In development, if the return value + * is JSON the return value + * will be deep-cloned and deep-frozen + * to prevent mutation thereby enforcing cache + * Immutability. + * + * This form of peek is useful for implementations + * that want to feed raw-data from cache to the UI + * or which want to interact with a blob of data + * directly from the presentation cache. + * + * An implementation might want to do this because + * de-referencing records which read from their own + * blob is generally safer because the record does + * not require retainining connections to the Store + * and Cache to present data on a per-field basis. + * + * This generally takes the place of `getAttr` as + * an API and may even take the place of `getRelationship` + * depending on implementation specifics, though this + * latter usage is less recommended due to the advantages + * of the Graph handling necessary entanglements and + * notifications for relational data. + * + * @method peek + * @public + * @param {StableRecordIdentifier | StableDocumentIdentifier} identifier + * @returns {ResourceDocument | ResourceBlob | null} the known resource data + */ + peek(identifier: StableRecordIdentifier): ResourceBlob | null; + peek(identifier: StableDocumentIdentifier): ResourceDocument | null; + + /** + * Peek the Cache for the existing request data associated with + * a cacheable request + * + * @method peekRequest + * @param {StableDocumentIdentifier} + * @returns {StableDocumentIdentifier | null} + * @public + */ + peekRequest(identifier: StableDocumentIdentifier): StructuredDocument | null; + + /** + * Push resource data from a remote source into the cache for this identifier + * + * @method upsert + * @public + * @param identifier + * @param data + * @param hasRecord + * @returns {void | string[]} if `hasRecord` is true then calculated key changes should be returned + */ + upsert(identifier: StableRecordIdentifier, data: ResourceBlob, hasRecord: boolean): void | string[]; + + // Cache Forking Support + // ===================== + + /** + * Create a fork of the cache from the current state. + * + * Applications should typically not call this method themselves, + * preferring instead to fork at the Store level, which will + * utilize this method to fork the cache. + * + * @method fork + * @public + * @returns Promise + */ + fork(): Promise; + + /** + * Merge a fork back into a parent Cache. + * + * Applications should typically not call this method themselves, + * preferring instead to merge at the Store level, which will + * utilize this method to merge the caches. + * + * @method merge + * @param {Cache} cache + * @public + * @returns Promise + */ + merge(cache: Cache): Promise; + + /** + * Generate the list of changes applied to all + * record in the store. + * + * Each individual resource or document that has + * been mutated should be described as an individual + * `Change` entry in the returned array. + * + * A `Change` is described by an object containing up to + * three properties: (1) the `identifier` of the entity that + * changed; (2) the `op` code of that change being one of + * `upsert` or `remove`, and if the op is `upsert` a `patch` + * containing the data to merge into the cache for the given + * entity. + * + * This `patch` is opaque to the Store but should be understood + * by the Cache and may expect to be utilized by an Adapter + * when generating data during a `save` operation. + * + * It is generally recommended that the `patch` contain only + * the updated state, ignoring fields that are unchanged + * + * ```ts + * interface Change { + * identifier: StableRecordIdentifier | StableDocumentIdentifier; + * op: 'upsert' | 'remove'; + * patch?: unknown; + * } + * ``` + * + */ + diff(): Promise; + + // SSR Support + // =========== + + /** + * Serialize the entire contents of the Cache into a Stream + * which may be fed back into a new instance of the same Cache + * via `cache.hydrate`. + * + * @method dump + * @returns {Promise} + * @public + */ + dump(): Promise>; + + /** + * hydrate a Cache from a Stream with content previously serialized + * from another instance of the same Cache, resolving when hydration + * is complete. + * + * This method should expect to be called both in the context of restoring + * the Cache during application rehydration after SSR **AND** at unknown + * times during the lifetime of an already booted application when it is + * desired to bulk-load additional information into the cache. This latter + * behavior supports optimizing pre/fetching of data for route transitions + * via data-only SSR modes. + * + * @method hydrate + * @param {ReadableStream} stream + * @returns {Promise} + * @public + */ + hydrate(stream: ReadableStream): Promise; + + // Resource Support + // ================ + + /** + * [LIFECYCLE] Signal to the cache that a new record has been instantiated on the client + * + * It returns properties from options that should be set on the record during the create + * process. This return value behavior is deprecated. + * + * @method clientDidCreate + * @public + * @param identifier + * @param createArgs + */ + clientDidCreate(identifier: StableRecordIdentifier, createArgs?: Record): Record; + + /** + * [LIFECYCLE] Signals to the cache that a resource + * will be part of a save transaction. + * + * @method willCommit + * @public + * @param identifier + */ + willCommit(identifier: StableRecordIdentifier): void; + + /** + * [LIFECYCLE] Signals to the cache that a resource + * was successfully updated as part of a save transaction. + * + * @method didCommit + * @public + * @param identifier + * @param data + */ + didCommit(identifier: StableRecordIdentifier, data: ResourceBlob | null): void; + + /** + * [LIFECYCLE] Signals to the cache that a resource + * was update via a save transaction failed. + * + * @method commitWasRejected + * @public + * @param identifier + * @param errors + */ + commitWasRejected(identifier: StableRecordIdentifier, errors?: ValidationError[]): void; + + /** + * [LIFECYCLE] Signals to the cache that all data for a resource + * should be cleared. + * + * This method is a candidate to become a mutation + * + * @method unloadRecord + * @public + * @param identifier + */ + unloadRecord(identifier: StableRecordIdentifier): void; + + // Granular Resource Data APIs + // =========================== + + /** + * Retrieve the data for an attribute from the cache + * + * @method getAttr + * @public + * @param identifier + * @param field + * @returns {unknown} + */ + getAttr(identifier: StableRecordIdentifier, field: string): unknown; + + /** + * Mutate the data for an attribute in the cache + * + * This method is a candidate to become a mutation + * + * @method setAttr + * @public + * @param identifier + * @param field + * @param value + */ + setAttr(identifier: StableRecordIdentifier, field: string, value: unknown): void; + + /** + * Query the cache for the changed attributes of a resource. + * + * @method changedAttrs + * @public + * @deprecated + * @param identifier + * @returns { : [, ] } + */ + changedAttrs(identifier: StableRecordIdentifier): Record; + + /** + * Query the cache for whether any mutated attributes exist + * + * @method hasChangedAttrs + * @public + * @param identifier + * @returns {boolean} + */ + hasChangedAttrs(identifier: StableRecordIdentifier): boolean; + + /** + * Tell the cache to discard any uncommitted mutations to attributes + * + * This method is a candidate to become a mutation + * + * @method rollbackAttrs + * @public + * @param identifier + * @returns {string[]} the names of fields that were restored + */ + rollbackAttrs(identifier: StableRecordIdentifier): string[]; + + /** + * Query the cache for the current state of a relationship property + * + * @method getRelationship + * @public + * @param identifier + * @param field + * @returns resource relationship object + */ + getRelationship( + identifier: StableRecordIdentifier, + field: string, + isCollection?: boolean + ): SingleResourceRelationship | CollectionResourceRelationship; + + // Resource State + // =============== + + /** + * Update the cache state for the given resource to be marked + * as locally deleted, or remove such a mark. + * + * This method is a candidate to become a mutation + * + * @method setIsDeleted + * @public + * @param identifier + * @param isDeleted {boolean} + */ + setIsDeleted(identifier: StableRecordIdentifier, isDeleted: boolean): void; + + /** + * Query the cache for any validation errors applicable to the given resource. + * + * @method getErrors + * @public + * @param identifier + * @returns {ValidationError[]} + */ + getErrors(identifier: StableRecordIdentifier): ValidationError[]; + + /** + * Query the cache for whether a given resource has any available data + * + * @method isEmpty + * @public + * @param identifier + * @returns {boolean} + */ + isEmpty(identifier: StableRecordIdentifier): boolean; + + /** + * Query the cache for whether a given resource was created locally and not + * yet persisted. + * + * @method isNew + * @public + * @param identifier + * @returns {boolean} + */ + isNew(identifier: StableRecordIdentifier): boolean; + + /** + * Query the cache for whether a given resource is marked as deleted (but not + * necessarily persisted yet). + * + * @method isDeleted + * @public + * @param identifier + * @returns {boolean} + */ + isDeleted(identifier: StableRecordIdentifier): boolean; + + /** + * Query the cache for whether a given resource has been deleted and that deletion + * has also been persisted. + * + * @method isDeletionCommitted + * @public + * @param identifier + * @returns {boolean} + */ + isDeletionCommitted(identifier: StableRecordIdentifier): boolean; +} diff --git a/ember-data-types/cache/change.ts b/ember-data-types/cache/change.ts new file mode 100644 index 00000000000..3f51c4f2d63 --- /dev/null +++ b/ember-data-types/cache/change.ts @@ -0,0 +1,9 @@ +import { StableRecordIdentifier } from '@ember-data/types/q/identifier'; + +import { StableDocumentIdentifier } from './identifier'; + +export interface Change { + identifier: StableRecordIdentifier | StableDocumentIdentifier; + op: 'upsert' | 'remove'; + patch?: unknown; +} diff --git a/ember-data-types/cache/document.ts b/ember-data-types/cache/document.ts new file mode 100644 index 00000000000..4cb62d58751 --- /dev/null +++ b/ember-data-types/cache/document.ts @@ -0,0 +1,43 @@ +import type { ImmutableRequestInfo, ResponseInfo as ImmutableResponseInfo } from '@ember-data/request/-private/types'; +import { Links, Meta, PaginationLinks } from '@ember-data/types/q/ember-data-json-api'; +import { StableExistingRecordIdentifier } from '@ember-data/types/q/identifier'; + +export type RequestInfo = ImmutableRequestInfo; +export type ResponseInfo = ImmutableResponseInfo; + +export interface ResourceMetaDocument { + // the url or cache-key associated with the structured document + lid?: string; + meta: Meta; + links?: Links | PaginationLinks; +} + +export interface ResourceDataDocument { + // the url or cache-key associated with the structured document + lid?: string; + links?: Links | PaginationLinks; + meta?: Meta; + data: StableExistingRecordIdentifier | StableExistingRecordIdentifier[] | null; +} + +export interface ResourceErrorDocument { + // the url or cache-key associated with the structured document + lid?: string; + links?: Links | PaginationLinks; + meta?: Meta; + error: string | object; +} + +export type ResourceDocument = ResourceMetaDocument | ResourceDataDocument | ResourceErrorDocument; + +export interface StructuredDataDocument { + request?: RequestInfo; + response?: ResponseInfo; + data: T; +} +export interface StructuredErrorDocument extends Error { + request?: RequestInfo; + response?: ResponseInfo; + error: string | object; +} +export type StructuredDocument = StructuredDataDocument | StructuredErrorDocument; diff --git a/ember-data-types/cache/identifier.ts b/ember-data-types/cache/identifier.ts new file mode 100644 index 00000000000..4a3fe7836b3 --- /dev/null +++ b/ember-data-types/cache/identifier.ts @@ -0,0 +1,3 @@ +export type StableDocumentIdentifier = { + lid: string; +}; diff --git a/ember-data-types/cache/mutations.ts b/ember-data-types/cache/mutations.ts new file mode 100644 index 00000000000..684a0e868ab --- /dev/null +++ b/ember-data-types/cache/mutations.ts @@ -0,0 +1,72 @@ +import { StableRecordIdentifier } from '@ember-data/types/q/identifier'; + +export interface AddToRelatedRecordsMutation { + op: 'addToRelatedRecords'; + record: StableRecordIdentifier; + field: string; + value: StableRecordIdentifier | StableRecordIdentifier[]; + index?: number; +} + +export interface RemoveFromRelatedRecordsMutation { + op: 'removeFromRelatedRecords'; + record: StableRecordIdentifier; + field: string; + value: StableRecordIdentifier | StableRecordIdentifier[]; + index?: number; +} + +export interface ReplaceRelatedRecordMutation { + op: 'replaceRelatedRecord'; + record: StableRecordIdentifier; + field: string; + // never null if field is a collection + value: StableRecordIdentifier | null; + // if field is a collection, + // the value we are swapping with + prior?: StableRecordIdentifier; + index?: number; +} + +export interface ReplaceRelatedRecordsMutation { + op: 'replaceRelatedRecords'; + record: StableRecordIdentifier; + field: string; + // the records to add. If no prior/index + // specified all existing should be removed + value: StableRecordIdentifier[]; + // if this is a "splice" the + // records we expect to be removed + prior?: StableRecordIdentifier[]; + // if this is a "splice" the + // index to start from + index?: number; +} + +export interface SortRelatedRecordsMutation { + op: 'sortRelatedRecords'; + record: StableRecordIdentifier; + field: string; + value: StableRecordIdentifier[]; +} +// A Mutation is an action that updates +// the local state of the Cache in some +// manner. +// Most Mutations are in theory also +// Operations; with the difference being +// that the change should be applied as +// "local" or "dirty" state instead of +// as "remote" or "clean" state. +// +// Note: this RFC does not publicly surface +// any of the mutations listed here as +// "operations", though the (private) Graph +// already expects and utilizes these. +// and we look forward to an RFC that makes +// the Graph a fully public API. +export type Mutation = + | ReplaceRelatedRecordsMutation + | ReplaceRelatedRecordMutation + | RemoveFromRelatedRecordsMutation + | AddToRelatedRecordsMutation + | SortRelatedRecordsMutation; diff --git a/ember-data-types/cache/operations.ts b/ember-data-types/cache/operations.ts new file mode 100644 index 00000000000..82c6c012e45 --- /dev/null +++ b/ember-data-types/cache/operations.ts @@ -0,0 +1,32 @@ +import { StableRecordIdentifier } from '@ember-data/types/q/identifier'; + +export interface Op { + op: string; +} + +// Occasionally the IdentifierCache +// discovers that two previously thought +// to be distinct Identifiers refer to +// the same ResourceBlob. This Operation +// will be performed giving the Cache the +// change to cleanup and merge internal +// state as desired when this discovery +// is made. +export interface MergeOperation extends Op { + op: 'mergeIdentifiers'; + // existing + record: StableRecordIdentifier; + // new + value: StableRecordIdentifier; +} + +export interface RemoveOperation extends Op { + op: 'removeIdentifier'; + record: StableRecordIdentifier; +} + +// An Operation is an action that updates +// the remote state of the Cache in some +// manner. Additional Operations will be +// added in the future. +export type Operation = MergeOperation | RemoveOperation; diff --git a/ember-data-types/cache/relationship.ts b/ember-data-types/cache/relationship.ts new file mode 100644 index 00000000000..d6a4581fdcc --- /dev/null +++ b/ember-data-types/cache/relationship.ts @@ -0,0 +1,21 @@ +import type { Value as JSONValue } from 'json-typescript'; + +import { Links, PaginationLinks } from '@ember-data/types/q/ember-data-json-api'; +import { StableRecordIdentifier } from '@ember-data/types/q/identifier'; + +// we request that it be in the stable form already. +export interface ResourceRelationship { + data?: StableRecordIdentifier | null; + meta?: Record; + links?: Links; +} + +// Note: in v1 data could be a ResourceIdentifier, now +// we request that it be in the stable form already. +export interface CollectionRelationship { + data?: StableRecordIdentifier[]; + meta?: Record; + links?: PaginationLinks; +} + +export type Relationship = ResourceRelationship | CollectionRelationship; diff --git a/ember-data-types/cache/validation-error.ts b/ember-data-types/cache/validation-error.ts new file mode 100644 index 00000000000..2fcaed5a98a --- /dev/null +++ b/ember-data-types/cache/validation-error.ts @@ -0,0 +1,21 @@ +// An error relating to a Resource +// Received when attempting to persist +// changes to that resource. +// +// considered "opaque" to the Store itself. +// +// Currently we restrict Errors to being +// shaped in JSON:API format; however, +// this is a restriction we will willingly +// recede if desired. So long as the +// presentation layer and the cache and the +// network layer are in agreement about the +// schema of these Errors, then EmberData +// has no reason to enforce this shape. +export interface ValidationError { + title: string; + detail: string; + source: { + pointer: string; + }; +} diff --git a/ember-data-types/q/record-data-store-wrapper.ts b/ember-data-types/q/record-data-store-wrapper.ts index 90899f90e74..65389c3be73 100644 --- a/ember-data-types/q/record-data-store-wrapper.ts +++ b/ember-data-types/q/record-data-store-wrapper.ts @@ -2,7 +2,7 @@ import { IdentifierCache } from '@ember-data/store/-private/caches/identifier-ca import { NotificationType } from '@ember-data/store/-private/managers/record-notification-manager'; import { StableRecordIdentifier } from './identifier'; -import type { RecordData } from './record-data'; +import type { Cache } from './record-data'; import type { AttributesSchema, RelationshipsSchema } from './record-data-schemas'; import { SchemaDefinitionService } from './schema-definition-service'; @@ -11,16 +11,16 @@ import { SchemaDefinitionService } from './schema-definition-service'; */ /** - * RecordDataStoreWrapper provides encapsulated API access to the minimal - * subset of the Store's functionality that cache (RecordData) implementations + * CacheStoreWrapper provides encapsulated API access to the minimal + * subset of the Store's functionality that cache implementations * should interact with. It is provided to the Store's `createRecordDataFor` - * hook. + * and `createCache` hooks. * * Cache implementations should not need more than this API provides. * * This class cannot be directly instantiated. * - * @class RecordDataStoreWrapper + * @class CacheStoreWrapper * @public */ export interface LegacyRecordDataStoreWrapper { @@ -159,6 +159,13 @@ export interface LegacyRecordDataStoreWrapper { notifyHasManyChange(modelName: string, id: string | null, clientId: string | null | undefined, key: string): void; /** + * [DEPRECATED] RecordData has become Cache and Cache is now always + * a singleton. + * + * You may access the Cache via Store.cache. If you are interacting + * with this wrapped from the Cache you are the Cache instance and + * thus do not need to call this anymore. + * * Used to retrieve the associated RecordData for a given identifier. * * To generate a RecordData for a new client-side resource that does not @@ -170,15 +177,16 @@ export interface LegacyRecordDataStoreWrapper { * correct "newly created" state. * * @method recordDataFor + * @deprecated * @param {StableRecordIdentifier} identifier - * @return {RecordData} the RecordData cache instance associated with the identifier + * @return {Cache} the RecordData cache instance associated with the identifier * @public */ - recordDataFor(type: string, id: string, lid?: string | null): RecordData; - recordDataFor(type: string, id: string | null, lid: string): RecordData; - recordDataFor(type: string): RecordData; - recordDataFor(type: string, id?: string | null, lid?: string | null): RecordData; - recordDataFor(identifier: StableRecordIdentifier): RecordData; + recordDataFor(type: string, id: string, lid?: string | null): Cache; + recordDataFor(type: string, id: string | null, lid: string): Cache; + recordDataFor(type: string): Cache; + recordDataFor(type: string, id?: string | null, lid?: string | null): Cache; + recordDataFor(identifier: StableRecordIdentifier): Cache; /** * Use notifyChange @@ -247,9 +255,9 @@ export interface V2RecordDataStoreWrapper { hasRecord(identifier: StableRecordIdentifier): boolean; - recordDataFor(identifier: StableRecordIdentifier): RecordData; + recordDataFor(identifier: StableRecordIdentifier): Cache; notifyChange(identifier: StableRecordIdentifier, namespace: NotificationType, key?: string): void; } -export type RecordDataStoreWrapper = LegacyRecordDataStoreWrapper | V2RecordDataStoreWrapper; +export type CacheStoreWrapper = LegacyRecordDataStoreWrapper | V2RecordDataStoreWrapper; diff --git a/ember-data-types/q/record-data.ts b/ember-data-types/q/record-data.ts index 0d2a11855be..a33d51c0fd3 100644 --- a/ember-data-types/q/record-data.ts +++ b/ember-data-types/q/record-data.ts @@ -1,7 +1,6 @@ -import { LocalRelationshipOperation } from '@ember-data/graph/-private/graph/-operations'; - import type { CollectionResourceRelationship, SingleResourceRelationship } from './ember-data-json-api'; import type { RecordIdentifier, StableRecordIdentifier } from './identifier'; +import type { Cache } from './record-data'; import type { JsonApiResource, JsonApiValidationError } from './record-data-json-api'; import { Dict } from './utils'; @@ -9,9 +8,7 @@ import { Dict } from './utils'; @module @ember-data/store */ -export interface ChangedAttributesHash { - [key: string]: [string, string]; -} +export type ChangedAttributesHash = Record; export interface MergeOperation { op: 'mergeIdentifiers'; @@ -51,10 +48,10 @@ export interface RecordDataV1 { getBelongsTo(key: string): SingleResourceRelationship; getHasMany(key: string): CollectionResourceRelationship; - setDirtyBelongsTo(name: string, recordData: RecordData | null): void; - setDirtyHasMany(key: string, recordDatas: RecordData[]): void; - addToHasMany(key: string, recordDatas: RecordData[], idx?: number): void; - removeFromHasMany(key: string, recordDatas: RecordData[]): void; + setDirtyBelongsTo(name: string, recordData: Cache | null): void; + setDirtyHasMany(key: string, recordDatas: Cache[]): void; + addToHasMany(key: string, recordDatas: Cache[], idx?: number): void; + removeFromHasMany(key: string, recordDatas: Cache[]): void; // State // ============= @@ -66,45 +63,4 @@ export interface RecordDataV1 { isDeletionCommitted(identifier: StableRecordIdentifier): boolean; } -export interface RecordData { - version: '2'; - - // Cache - // ===== - - pushData(identifier: StableRecordIdentifier, data: JsonApiResource, calculateChanges?: boolean): void | string[]; - clientDidCreate(identifier: StableRecordIdentifier, options?: Dict): Dict; - - willCommit(identifier: StableRecordIdentifier): void; - didCommit(identifier: StableRecordIdentifier, data: JsonApiResource | null): void; - commitWasRejected(identifier: StableRecordIdentifier, errors?: JsonApiValidationError[]): void; - - unloadRecord(identifier: StableRecordIdentifier): void; - sync(op: MergeOperation): void; - - // Attrs - // ===== - - getAttr(identifier: StableRecordIdentifier, propertyName: string): unknown; - setAttr(identifier: StableRecordIdentifier, propertyName: string, value: unknown): void; - changedAttrs(identifier: StableRecordIdentifier): ChangedAttributesHash; - hasChangedAttrs(identifier: StableRecordIdentifier): boolean; - rollbackAttrs(identifier: StableRecordIdentifier): string[]; - - // Relationships - // ============= - getRelationship( - identifier: StableRecordIdentifier, - propertyName: string - ): SingleResourceRelationship | CollectionResourceRelationship; - update(operation: LocalRelationshipOperation): void; - - // State - // ============= - setIsDeleted(identifier: StableRecordIdentifier, isDeleted: boolean): void; - getErrors(identifier: StableRecordIdentifier): JsonApiValidationError[]; - isEmpty(identifier: StableRecordIdentifier): boolean; - isNew(identifier: StableRecordIdentifier): boolean; - isDeleted(identifier: StableRecordIdentifier): boolean; - isDeletionCommitted(identifier: StableRecordIdentifier): boolean; -} +export { Cache } from '../cache/cache'; diff --git a/packages/-ember-data/README.md b/packages/-ember-data/README.md index 4d778f742a5..7423a156f8a 100644 --- a/packages/-ember-data/README.md +++ b/packages/-ember-data/README.md @@ -1,14 +1,12 @@ -EmberData -============================================================================== +# EmberData [![Build Status](https://github.com/emberjs/data/workflows/CI/badge.svg)](https://github.com/emberjs/data/actions?workflow=CI) [![Code Climate](https://codeclimate.com/github/emberjs/data/badges/gpa.svg)](https://codeclimate.com/github/emberjs/data) [![Discord Community Server](https://img.shields.io/discord/480462759797063690.svg?logo=discord)](https://discord.gg/zT3asNS) - # Overview -`EmberData` is a lightweight reactive data library for JavaScript applications that provides composable primitives for ordering query/mutation/peek flows, managing network and cache, and reducing data for presentation. You can plug-and-play as desired for any api structure and format. +`EmberData` is a lightweight reactive data library for JavaScript applications that provides composable primitives for ordering query/mutation/peek flows, managing network and cache, and reducing data for presentation. You can plug-and-play as desired for any api structure and format. It was designed for robustly managing data in applications built with [Ember](https://github.com/emberjs/ember.js/) and is agnostic to the underlying persistence mechanism, so it works just as well with [JSON:API](https://jsonapi.org/) or [GraphQL](https://graphql.org/) over `HTTPS` as it does with streaming `WebSockets` or local `IndexedDB` storage. @@ -22,7 +20,6 @@ It provides many of the features you'd find in server-side `ORM`s like `ActiveRe - [Team](https://emberjs.com/team) - [Blog](https://emberjs.com/blog) - ## Basic Installation Install using your javascript package manager of choice. For instance with [pnpm](https://pnpm.io/) @@ -33,7 +30,7 @@ pnpm add -D ember-data `ember-data` is installed by default for new applications generated with `ember-cli`. You can check what version is installed by looking in the `devDependencies` hash of your project's [package.json](https://docs.npmjs.com/cli/v8/configuring-npm/package-json) file. -If you have generated a new `Ember` application using `ember-cli` but do +If you have generated a new `Ember` application using `ember-cli` but do not wish to use `ember-data`, remove `ember-data` from your project's `package.json` file and run your package manager's install command to update your lockfile. ## Advanced Installation @@ -41,7 +38,7 @@ not wish to use `ember-data`, remove `ember-data` from your project's `package.j EmberData is organized into primitives that compose together via public APIs. - [@ember-data/store](https://github.com/emberjs/data/tree/master/packages/store) is the core and handles coordination -- [@ember-data/json-api](https://github.com/emberjs/data/tree/master/packages/record-data) is a resource cache for JSON:API structured data. It integrates with the store via the hook `createRecordDataFor` +- [@ember-data/json-api](https://github.com/emberjs/data/tree/master/packages/json-api) is a resource cache for JSON:API structured data. It integrates with the store via the hook `createCache` - [@ember-data/model](https://github.com/emberjs/data/tree/master/packages/model) is a presentation layer, it integrates with the store via the hooks `instantiateRecord` and `teardownRecord`. - [@ember-data/adapter](https://github.com/emberjs/data/tree/master/packages/adapter) provides various network API integrations for APIS built over specific REST or JSON:API conventions. - [@ember-data/serializer](https://github.com/emberjs/data/tree/master/packages/serializer) pairs with `@ember-data/adapter` to normalize and serialize data to and from an API format into the `JSON:API` format understood by `@ember-data/json-api`. @@ -81,7 +78,7 @@ let app = new EmberApp(defaults, { '@embroider/macros': { setConfig: { '@ember-data/store': { - polyfillUUID: true + polyfillUUID: true, }, }, }, @@ -96,8 +93,8 @@ that all support for it should be stripped from the build. ```ts let app = new EmberApp(defaults, { emberData: { - includeDataAdapterInProduction: false - } + includeDataAdapterInProduction: false, + }, }); ``` @@ -110,27 +107,26 @@ at build time. This instrumentation is always removed from production builds or that has not explicitly activated it. To activate it set the appropriate flag to `true`. ```ts - let app = new EmberApp(defaults, { - emberData: { - debug: { - LOG_PAYLOADS: false, // data store received to update cache with - LOG_OPERATIONS: false, // updates to cache remote state - LOG_MUTATIONS: false, // updates to cache local state - LOG_NOTIFICATIONS: false, - LOG_REQUEST_STATUS: false, - LOG_IDENTIFIERS: false, - LOG_GRAPH: false, // relationship storage - LOG_INSTANCE_CACHE: false, // instance creation/deletion - } - } - }); - ``` +let app = new EmberApp(defaults, { + emberData: { + debug: { + LOG_PAYLOADS: false, // data store received to update cache with + LOG_OPERATIONS: false, // updates to cache remote state + LOG_MUTATIONS: false, // updates to cache local state + LOG_NOTIFICATIONS: false, + LOG_REQUEST_STATUS: false, + LOG_IDENTIFIERS: false, + LOG_GRAPH: false, // relationship storage + LOG_INSTANCE_CACHE: false, // instance creation/deletion + }, + }, +}); +``` ## Contributing See the [Contributing](CONTRIBUTING.md) guide for details. - ### License This project is licensed under the [MIT License](LICENSE.md). diff --git a/packages/-ember-data/addon/-private/index.ts b/packages/-ember-data/addon/-private/index.ts index c9e1e3dab91..521efe3613f 100644 --- a/packages/-ember-data/addon/-private/index.ts +++ b/packages/-ember-data/addon/-private/index.ts @@ -11,7 +11,7 @@ export { Snapshot } from '@ember-data/store/-private'; // `ember-data-model-fragments' and `ember-data-change-tracker` rely on `normalizeModelName` export { RecordArrayManager, SnapshotRecordArray, normalizeModelName, coerceId } from '@ember-data/store/-private'; export { ManyArray, PromiseManyArray } from '@ember-data/model/-private'; -export { RecordData } from '@ember-data/json-api/-private'; +export { Cache as RecordData } from '@ember-data/json-api/-private'; export const PromiseArray = ArrayProxy.extend(PromiseProxyMixin); export const PromiseObject = ObjectProxy.extend(PromiseProxyMixin); diff --git a/packages/-ember-data/addon/index.js b/packages/-ember-data/addon/index.js index 9cbcb3cc672..9ed13daf31a 100644 --- a/packages/-ember-data/addon/index.js +++ b/packages/-ember-data/addon/index.js @@ -72,7 +72,7 @@ not wish to use `ember-data`, remove `ember-data` from your project's `package.j *Ember*‍**Data** is organized into primitives that compose together via public APIs. - [@ember-data/store](/ember-data/release/modules/@ember-data%2Fstore) is the core and handles coordination -- [@ember-data/json-api](/ember-data/release/modules/@ember-data%2Fjson-api) is a resource cache for JSON:API structured data. It integrates with the store via the hook `createRecordDataFor` +- [@ember-data/json-api](/ember-data/release/modules/@ember-data%2Fjson-api) provides a resource cache for JSON:API structured data. It integrates with the store via the hook `createCache` - [@ember-data/model](/ember-data/release/modules/@ember-data%2Fmodel) is a presentation layer, it integrates with the store via the hooks `instantiateRecord` and `teardownRecord`. - [@ember-data/adapter](/ember-data/release/modules/@ember-data%2Fadapter) provides various network API integrations for APIS built over specific REST or JSON:API conventions. - [@ember-data/serializer](/ember-data/release/modules/@ember-data%2Fserializer) pairs with `@ember-data/adapter` to normalize and serialize data to and from an API format into the `JSON:API` format understood by `@ember-data/json-api`. diff --git a/packages/graph/package.json b/packages/graph/package.json index 36379c11546..ded9bce9a74 100644 --- a/packages/graph/package.json +++ b/packages/graph/package.json @@ -1,14 +1,14 @@ { "name": "@ember-data/graph", "version": "4.10.0-alpha.4", - "description": "Provides the default resource cache (RecordData) implementation for ember-data", + "description": "Provides a JSON:API resource cache implementation for EmberData", "keywords": [ "ember-addon" ], "repository": { "type": "git", "url": "git+ssh://git@github.com:emberjs/data.git", - "directory": "packages/record-data" + "directory": "packages/json-api" }, "license": "MIT", "author": "", diff --git a/packages/graph/src/-private/graph/-utils.ts b/packages/graph/src/-private/graph/-utils.ts index 447d3a54925..52bd4aed223 100644 --- a/packages/graph/src/-private/graph/-utils.ts +++ b/packages/graph/src/-private/graph/-utils.ts @@ -2,9 +2,9 @@ import { assert, inspect, warn } from '@ember/debug'; import { LOG_GRAPH } from '@ember-data/private-build-infra/debugging'; import type { Store } from '@ember-data/store/-private'; -import { recordDataFor as peekRecordData } from '@ember-data/store/-private'; +import { peekCache as peekRecordData } from '@ember-data/store/-private'; import type { StableRecordIdentifier } from '@ember-data/types/q/identifier'; -import type { RecordDataStoreWrapper } from '@ember-data/types/q/record-data-store-wrapper'; +import type { CacheStoreWrapper } from '@ember-data/types/q/record-data-store-wrapper'; import type { Dict } from '@ember-data/types/q/utils'; import { coerceId } from '../coerce-id'; @@ -13,7 +13,7 @@ import type ManyRelationship from '../relationships/state/has-many'; import type { UpdateRelationshipOperation } from './-operations'; import type { Graph, ImplicitRelationship } from './graph'; -export function getStore(wrapper: RecordDataStoreWrapper | { _store: Store }): Store { +export function getStore(wrapper: CacheStoreWrapper | { _store: Store }): Store { assert(`expected a private _store property`, '_store' in wrapper); return wrapper._store; } diff --git a/packages/graph/src/-private/graph/graph.ts b/packages/graph/src/-private/graph/graph.ts index 6c8793273fd..1e0d0157c88 100644 --- a/packages/graph/src/-private/graph/graph.ts +++ b/packages/graph/src/-private/graph/graph.ts @@ -4,7 +4,7 @@ import { DEBUG } from '@glimmer/env'; import { LOG_GRAPH } from '@ember-data/private-build-infra/debugging'; import type { StableRecordIdentifier } from '@ember-data/types/q/identifier'; import { MergeOperation } from '@ember-data/types/q/record-data'; -import type { RecordDataStoreWrapper } from '@ember-data/types/q/record-data-store-wrapper'; +import type { CacheStoreWrapper } from '@ember-data/types/q/record-data-store-wrapper'; import type { Dict } from '@ember-data/types/q/utils'; import BelongsToRelationship from '../relationships/state/belongs-to'; @@ -44,7 +44,7 @@ export interface ImplicitRelationship { export type RelationshipEdge = ImplicitRelationship | ManyRelationship | BelongsToRelationship; -export const Graphs = new Map(); +export const Graphs = new Map(); /* * Graph acts as the cache for relationship data. It allows for @@ -69,7 +69,7 @@ export class Graph { declare _definitionCache: EdgeCache; declare _potentialPolymorphicTypes: Dict>; declare identifiers: Map>; - declare store: RecordDataStoreWrapper; + declare store: CacheStoreWrapper; declare isDestroyed: boolean; declare _willSyncRemote: boolean; declare _willSyncLocal: boolean; @@ -82,7 +82,7 @@ export class Graph { declare _transaction: Set | null; declare _removing: StableRecordIdentifier | null; - constructor(store: RecordDataStoreWrapper) { + constructor(store: CacheStoreWrapper) { this._definitionCache = Object.create(null) as EdgeCache; this._potentialPolymorphicTypes = Object.create(null) as Dict>; this.identifiers = new Map(); @@ -409,11 +409,11 @@ export class Graph { Graphs.delete(this.store); if (DEBUG) { - Graphs.delete(getStore(this.store) as unknown as RecordDataStoreWrapper); + Graphs.delete(getStore(this.store) as unknown as CacheStoreWrapper); } this.identifiers.clear(); - this.store = null as unknown as RecordDataStoreWrapper; + this.store = null as unknown as CacheStoreWrapper; this.isDestroyed = true; } } diff --git a/packages/graph/src/-private/graph/index.ts b/packages/graph/src/-private/graph/index.ts index 4f9968e0db7..c6894d08cbe 100644 --- a/packages/graph/src/-private/graph/index.ts +++ b/packages/graph/src/-private/graph/index.ts @@ -2,7 +2,7 @@ import { DEBUG } from '@glimmer/env'; import type Store from '@ember-data/store'; import type { StableRecordIdentifier } from '@ember-data/types/q/identifier'; -import type { RecordDataStoreWrapper } from '@ember-data/types/q/record-data-store-wrapper'; +import type { CacheStoreWrapper } from '@ember-data/types/q/record-data-store-wrapper'; import type { UpgradedMeta } from './-edge-definition'; import { getStore } from './-utils'; @@ -19,16 +19,16 @@ function isStore(maybeStore: unknown): maybeStore is Store { return (maybeStore as Store)._instanceCache !== undefined; } -function getWrapper(store: RecordDataStoreWrapper | Store): RecordDataStoreWrapper { +function getWrapper(store: CacheStoreWrapper | Store): CacheStoreWrapper { return isStore(store) ? store._instanceCache._storeWrapper : store; } -export function peekGraph(store: RecordDataStoreWrapper | Store): Graph | undefined { +export function peekGraph(store: CacheStoreWrapper | Store): Graph | undefined { return Graphs.get(getWrapper(store)); } export type peekGraph = typeof peekGraph; -export function graphFor(store: RecordDataStoreWrapper | Store): Graph { +export function graphFor(store: CacheStoreWrapper | Store): Graph { const wrapper = getWrapper(store); let graph = Graphs.get(wrapper); @@ -38,7 +38,7 @@ export function graphFor(store: RecordDataStoreWrapper | Store): Graph { // in DEBUG we attach the graph to the main store for improved debuggability if (DEBUG) { - Graphs.set(getStore(wrapper) as unknown as RecordDataStoreWrapper, graph); + Graphs.set(getStore(wrapper) as unknown as CacheStoreWrapper, graph); } } return graph; diff --git a/packages/graph/src/index.ts b/packages/graph/src/index.ts new file mode 100644 index 00000000000..4454ca93221 --- /dev/null +++ b/packages/graph/src/index.ts @@ -0,0 +1 @@ +export { Cache } from './-private'; diff --git a/packages/json-api/src/-private.ts b/packages/json-api/src/-private.ts index 3c07fbc8801..3cc327d756a 100644 --- a/packages/json-api/src/-private.ts +++ b/packages/json-api/src/-private.ts @@ -1,4 +1,4 @@ -export { default as RecordData } from './-private/record-data'; +export { default as Cache } from './-private/record-data'; /** This package provides the default cache implementation used diff --git a/packages/json-api/src/-private/record-data.ts b/packages/json-api/src/-private/record-data.ts index 890baa4fa83..2409d4526dc 100644 --- a/packages/json-api/src/-private/record-data.ts +++ b/packages/json-api/src/-private/record-data.ts @@ -11,12 +11,17 @@ import type { ImplicitRelationship } from '@ember-data/graph/-private/graph/inde import type BelongsToRelationship from '@ember-data/graph/-private/relationships/state/belongs-to'; import type ManyRelationship from '@ember-data/graph/-private/relationships/state/has-many'; import { LOG_MUTATIONS, LOG_OPERATIONS } from '@ember-data/private-build-infra/debugging'; +import { Change } from '@ember-data/types/cache/change'; +import { ResourceDocument, StructuredDocument } from '@ember-data/types/cache/document'; +import { StableDocumentIdentifier } from '@ember-data/types/cache/identifier'; +import { Mutation } from '@ember-data/types/cache/mutations'; +import { Operation } from '@ember-data/types/cache/operations'; import type { CollectionResourceRelationship, SingleResourceRelationship, } from '@ember-data/types/q/ember-data-json-api'; import type { StableRecordIdentifier } from '@ember-data/types/q/identifier'; -import type { ChangedAttributesHash, MergeOperation, RecordData } from '@ember-data/types/q/record-data'; +import type { Cache as CacheInterface, ChangedAttributesHash } from '@ember-data/types/q/record-data'; import type { AttributesHash, JsonApiResource, JsonApiValidationError } from '@ember-data/types/q/record-data-json-api'; import type { AttributeSchema, RelationshipSchema } from '@ember-data/types/q/record-data-schemas'; import type { RecordDataStoreWrapper, V2RecordDataStoreWrapper } from '@ember-data/types/q/record-data-store-wrapper'; @@ -27,6 +32,11 @@ function isImplicit( ): relationship is ImplicitRelationship { return relationship.definition.isImplicit; } +function isBelongsTo( + relationship: ManyRelationship | ImplicitRelationship | BelongsToRelationship +): relationship is BelongsToRelationship { + return relationship.definition.kind === 'belongsTo'; +} const EMPTY_ITERATOR = { iterator() { @@ -41,7 +51,7 @@ const EMPTY_ITERATOR = { /** The default cache implementation used by ember-data. The cache is configurable and using a different implementation can be - achieved by implementing the store's createRecordDataFor hook. + achieved by implementing the store's createCache hook. @class RecordDataDefault @public @@ -71,7 +81,7 @@ function makeCache(): CachedResource { }; } -export default class SingletonRecordData implements RecordData { +export default class Cache implements CacheInterface { version: '2' = '2'; __storeWrapper: V2RecordDataStoreWrapper; @@ -82,23 +92,88 @@ export default class SingletonRecordData implements RecordData { this.__storeWrapper = storeWrapper; } + peek(identifier: StableRecordIdentifier): unknown; + peek(identifier: StableDocumentIdentifier): ResourceDocument | null; + peek(identifier: unknown): unknown { + throw new Error('Method not implemented.'); + } + peekRequest(identifier: StableDocumentIdentifier): StructuredDocument | null { + throw new Error('Method not implemented.'); + } + fork(): Promise { + throw new Error('Method not implemented.'); + } + merge(cache: CacheInterface): Promise { + throw new Error('Method not implemented.'); + } + diff(): Promise { + throw new Error('Method not implemented.'); + } + dump(): Promise> { + throw new Error('Method not implemented.'); + } + hydrate(stream: ReadableStream): Promise { + throw new Error('Method not implemented.'); + } + + put(doc: StructuredDocument): ResourceDocument { + const jsonApiDoc = doc.data; + let included = jsonApiDoc.included; + let i: number, length: number; + + if (included) { + for (i = 0, length = included.length; i < length; i++) { + const resource = included[i]; + let identifier = this.__storeWrapper.identifierCache.getOrCreateRecordIdentifier(resource); + this.upsert(identifier, resource, false); + } + } + + if (Array.isArray(jsonApiDoc.data)) { + length = jsonApiDoc.data.length; + let identifiers: StableExistingRecordIdentifier[] = []; + + for (i = 0; i < length; i++) { + identifiers.push(store._instanceCache.loadData(jsonApiDoc.data[i])); + } + return { data: identifiers }; + } + + if (jsonApiDoc.data === null) { + return { data: null }; + } + + assert( + `Expected an object in the 'data' property in a call to 'push', but was ${typeof jsonApiDoc.data}`, + typeof jsonApiDoc.data === 'object' + ); + + return { data: store._instanceCache.loadData(jsonApiDoc.data) }; + } + /** - * Private method used when the store's `createRecordDataFor` hook is called - * to populate an entry for the identifier into the singleton. + * Private method used to populate an entry for the identifier + * into the singleton. * * @method createCache - * @private + * @internal * @param identifier */ - createCache(identifier: StableRecordIdentifier): void { + _createCache(identifier: StableRecordIdentifier): void { + assert(`Expected no resource data to yet exist in the cache`, !this.__cache.has(identifier)); this.__cache.set(identifier, makeCache()); } - __peek(identifier: StableRecordIdentifier, allowDestroyed = false): CachedResource { + __safePeek(identifier: StableRecordIdentifier, allowDestroyed: boolean): CachedResource | undefined { let resource = this.__cache.get(identifier); if (!resource && allowDestroyed) { resource = this.__destroyedCache.get(identifier); } + return resource; + } + + __peek(identifier: StableRecordIdentifier, allowDestroyed: boolean): CachedResource { + let resource = this.__safePeek(identifier, allowDestroyed); assert( `Expected RecordData Cache to have a resource cache for the identifier ${String(identifier)} but none was found`, resource @@ -106,17 +181,16 @@ export default class SingletonRecordData implements RecordData { return resource; } - pushData( - identifier: StableRecordIdentifier, - data: JsonApiResource, - calculateChanges?: boolean | undefined - ): void | string[] { + upsert(identifier: StableRecordIdentifier, data: JsonApiResource, calculateChanges: boolean): void | string[] { let changedKeys: string[] | undefined; - const cached = this.__peek(identifier); + if (!this.__cache.has(identifier)) { + this._createCache(identifier); + } + const cached = this.__peek(identifier, false); if (LOG_OPERATIONS) { try { - let _data = JSON.parse(JSON.stringify(data)); + let _data = JSON.parse(JSON.stringify(data)) as object; // eslint-disable-next-line no-console console.log('EmberData | Operation - pushData (upsert)', _data); } catch (e) { @@ -134,7 +208,7 @@ export default class SingletonRecordData implements RecordData { changedKeys = calculateChangedKeys(cached, data.attributes); } - cached.remoteAttrs = Object.assign(cached.remoteAttrs || Object.create(null), data.attributes); + cached.remoteAttrs = Object.assign(cached.remoteAttrs || Object.create(null), data.attributes) as Dict; if (cached.localAttrs) { if (patchLocalAttributes(cached)) { this.__storeWrapper.notifyChange(identifier, 'state'); @@ -152,10 +226,10 @@ export default class SingletonRecordData implements RecordData { return changedKeys; } - sync(op: MergeOperation): void { + patch(op: Operation): void { if (LOG_OPERATIONS) { try { - let _data = JSON.parse(JSON.stringify(op)); + let _data = JSON.parse(JSON.stringify(op)) as object; // eslint-disable-next-line no-console console.log(`EmberData | Operation - sync ${op.op}`, _data); } catch (e) { @@ -173,10 +247,10 @@ export default class SingletonRecordData implements RecordData { } } - update(op: LocalRelationshipOperation): void { + mutate(op: Mutation): void { if (LOG_MUTATIONS) { try { - let _data = JSON.parse(JSON.stringify(op)); + let _data = JSON.parse(JSON.stringify(op)) as object; // eslint-disable-next-line no-console console.log(`EmberData | Mutation - update ${op.op}`, _data); } catch (e) { @@ -190,7 +264,7 @@ export default class SingletonRecordData implements RecordData { clientDidCreate(identifier: StableRecordIdentifier, options?: Dict | undefined): Dict { if (LOG_MUTATIONS) { try { - let _data = options ? JSON.parse(JSON.stringify(options)) : options; + let _data = (options ? JSON.parse(JSON.stringify(options)) : options) as object; // eslint-disable-next-line no-console console.log(`EmberData | Mutation - clientDidCreate ${identifier.lid}`, _data); } catch (e) { @@ -198,7 +272,8 @@ export default class SingletonRecordData implements RecordData { console.log(`EmberData | Mutation - clientDidCreate ${identifier.lid}`, options); } } - const cached = this.__peek(identifier); + this._createCache(identifier); + const cached = this.__peek(identifier, false); cached.isNew = true; let createOptions = {}; @@ -220,31 +295,31 @@ export default class SingletonRecordData implements RecordData { const fieldType: AttributeSchema | RelationshipSchema | undefined = relationshipDefs[name] || attributeDefs[name]; let kind = fieldType !== undefined ? ('kind' in fieldType ? fieldType.kind : 'attribute') : null; - let relationship; + let relationship: BelongsToRelationship | ManyRelationship; switch (kind) { case 'attribute': this.setAttr(identifier, name, propertyValue); break; case 'belongsTo': - this.update({ + this.mutate({ op: 'replaceRelatedRecord', field: name, record: identifier, value: propertyValue as StableRecordIdentifier | null, }); - relationship = graph.get(identifier, name); + relationship = graph.get(identifier, name) as BelongsToRelationship | ManyRelationship; relationship.state.hasReceivedData = true; relationship.state.isEmpty = false; break; case 'hasMany': - this.update({ + this.mutate({ op: 'replaceRelatedRecords', field: name, record: identifier, value: propertyValue as StableRecordIdentifier[], }); - relationship = graph.get(identifier, name); + relationship = graph.get(identifier, name) as BelongsToRelationship | ManyRelationship; relationship.state.hasReceivedData = true; relationship.state.isEmpty = false; break; @@ -258,12 +333,12 @@ export default class SingletonRecordData implements RecordData { return createOptions; } willCommit(identifier: StableRecordIdentifier): void { - const cached = this.__peek(identifier); + const cached = this.__peek(identifier, false); cached.inflightAttrs = cached.localAttrs; cached.localAttrs = null; } didCommit(identifier: StableRecordIdentifier, data: JsonApiResource | null): void { - const cached = this.__peek(identifier); + const cached = this.__peek(identifier, false); if (cached.isDeleted) { graphFor(this.__storeWrapper).push({ op: 'deleteRecord', @@ -306,7 +381,7 @@ export default class SingletonRecordData implements RecordData { cached.remoteAttrs || Object.create(null), cached.inflightAttrs, newCanonicalAttributes - ); + ) as Dict; cached.inflightAttrs = null; patchLocalAttributes(cached); @@ -320,11 +395,11 @@ export default class SingletonRecordData implements RecordData { } commitWasRejected(identifier: StableRecordIdentifier, errors?: JsonApiValidationError[] | undefined): void { - const cached = this.__peek(identifier); + const cached = this.__peek(identifier, false); if (cached.inflightAttrs) { let keys = Object.keys(cached.inflightAttrs); if (keys.length > 0) { - let attrs = (cached.localAttrs = cached.localAttrs || Object.create(null)); + let attrs = (cached.localAttrs = (cached.localAttrs || Object.create(null)) as Dict); for (let i = 0; i < keys.length; i++) { if (attrs[keys[i]] === undefined) { attrs[keys[i]] = cached.inflightAttrs[keys[i]]; @@ -340,7 +415,14 @@ export default class SingletonRecordData implements RecordData { } unloadRecord(identifier: StableRecordIdentifier): void { - const cached = this.__peek(identifier); + // TODO this is necessary because + // we maintain memebership inside InstanceCache + // for peekAll, so even though we haven't created + // any data we think this exists. + if (!this.__cache.has(identifier)) { + return; + } + const cached = this.__peek(identifier, false); const storeWrapper = this.__storeWrapper; graphFor(storeWrapper).unload(identifier); @@ -397,7 +479,7 @@ export default class SingletonRecordData implements RecordData { } } setAttr(identifier: StableRecordIdentifier, attr: string, value: unknown): void { - const cached = this.__peek(identifier); + const cached = this.__peek(identifier, false); const existing = cached.inflightAttrs && attr in cached.inflightAttrs ? cached.inflightAttrs[attr] @@ -405,10 +487,10 @@ export default class SingletonRecordData implements RecordData { ? cached.remoteAttrs[attr] : undefined; if (existing !== value) { - cached.localAttrs = cached.localAttrs || Object.create(null); - cached.localAttrs![attr] = value; - cached.changes = cached.changes || Object.create(null); - cached.changes![attr] = [existing, value]; + cached.localAttrs = (cached.localAttrs || Object.create(null)) as Dict; + cached.localAttrs[attr] = value; + cached.changes = (cached.changes || Object.create(null)) as Dict; + cached.changes[attr] = [existing, value]; } else if (cached.localAttrs) { delete cached.localAttrs[attr]; delete cached.changes![attr]; @@ -418,7 +500,7 @@ export default class SingletonRecordData implements RecordData { } changedAttrs(identifier: StableRecordIdentifier): ChangedAttributesHash { // TODO freeze in dev - return this.__peek(identifier).changes || Object.create(null); + return (this.__peek(identifier, false).changes || Object.create(null)) as ChangedAttributesHash; } hasChangedAttrs(identifier: StableRecordIdentifier): boolean { const cached = this.__peek(identifier, true); @@ -429,7 +511,7 @@ export default class SingletonRecordData implements RecordData { ); } rollbackAttrs(identifier: StableRecordIdentifier): string[] { - const cached = this.__peek(identifier); + const cached = this.__peek(identifier, false); let dirtyKeys: string[] | undefined; cached.isDeleted = false; @@ -473,7 +555,7 @@ export default class SingletonRecordData implements RecordData { } setIsDeleted(identifier: StableRecordIdentifier, isDeleted: boolean): void { - const cached = this.__peek(identifier); + const cached = this.__peek(identifier, false); cached.isDeleted = isDeleted; if (cached.isNew) { // TODO can we delete this since we will do this in unload? @@ -489,17 +571,17 @@ export default class SingletonRecordData implements RecordData { return this.__peek(identifier, true).errors || []; } isEmpty(identifier: StableRecordIdentifier): boolean { - const cached = this.__peek(identifier, true); - return cached.remoteAttrs === null && cached.inflightAttrs === null && cached.localAttrs === null; + const cached = this.__safePeek(identifier, true); + return cached ? cached.remoteAttrs === null && cached.inflightAttrs === null && cached.localAttrs === null : true; } isNew(identifier: StableRecordIdentifier): boolean { - return this.__peek(identifier, true).isNew; + return this.__safePeek(identifier, true)?.isNew || false; } isDeleted(identifier: StableRecordIdentifier): boolean { - return this.__peek(identifier, true).isDeleted; + return this.__safePeek(identifier, true)?.isDeleted || false; } isDeletionCommitted(identifier: StableRecordIdentifier): boolean { - return this.__peek(identifier, true).isDeletionCommitted; + return this.__safePeek(identifier, true)?.isDeletionCommitted || false; } } @@ -513,20 +595,20 @@ function areAllModelsUnloaded(wrapper: V2RecordDataStoreWrapper, identifiers: St return true; } -function getLocalState(rel) { - if (rel.definition.kind === 'belongsTo') { +function getLocalState(rel: BelongsToRelationship | ManyRelationship): StableRecordIdentifier[] { + if (isBelongsTo(rel)) { return rel.localState ? [rel.localState] : []; } return rel.localState; } -function getRemoteState(rel) { - if (rel.definition.kind === 'belongsTo') { +function getRemoteState(rel: BelongsToRelationship | ManyRelationship): StableRecordIdentifier[] { + if (isBelongsTo(rel)) { return rel.remoteState ? [rel.remoteState] : []; } return rel.remoteState; } -function getDefaultValue(options: { defaultValue?: unknown } | undefined) { +function getDefaultValue(options: { defaultValue?: unknown } | undefined): unknown { if (!options) { return; } @@ -545,7 +627,7 @@ function getDefaultValue(options: { defaultValue?: unknown } | undefined) { } } -function notifyAttributes(storeWrapper: RecordDataStoreWrapper, identifier: StableRecordIdentifier, keys?: string[]) { +function notifyAttributes(storeWrapper: CacheStoreWrapper, identifier: StableRecordIdentifier, keys?: string[]) { if (!keys) { storeWrapper.notifyChange(identifier, 'attributes'); return; @@ -569,7 +651,7 @@ function calculateChangedKeys(cached: CachedResource, updates?: AttributesHash) const length = keys.length; const localAttrs = cached.localAttrs; - const original = Object.assign(Object.create(null), cached.remoteAttrs, cached.inflightAttrs); + const original = Object.assign(Object.create(null), cached.remoteAttrs, cached.inflightAttrs) as Dict; for (let i = 0; i < length; i++) { let key = keys[i]; @@ -593,7 +675,7 @@ function calculateChangedKeys(cached: CachedResource, updates?: AttributesHash) } function setupRelationships( - storeWrapper: RecordDataStoreWrapper, + storeWrapper: CacheStoreWrapper, identifier: StableRecordIdentifier, data: JsonApiResource ) { @@ -651,10 +733,7 @@ function patchLocalAttributes(cached: CachedResource): boolean { Iterates over the set of internal models reachable from `this` across exactly one relationship. */ -function _directlyRelatedRecordDatasIterable( - storeWrapper: RecordDataStoreWrapper, - originating: StableRecordIdentifier -) { +function _directlyRelatedRecordDatasIterable(storeWrapper: CacheStoreWrapper, originating: StableRecordIdentifier) { const graph = graphFor(storeWrapper); const initializedRelationships = graph.identifiers.get(originating); @@ -717,7 +796,7 @@ function _directlyRelatedRecordDatasIterable( from `this.identifier`. */ function _allRelatedRecordDatas( - storeWrapper: RecordDataStoreWrapper, + storeWrapper: CacheStoreWrapper, originating: StableRecordIdentifier ): StableRecordIdentifier[] { let array: StableRecordIdentifier[] = []; diff --git a/packages/model/src/-private/attr.js b/packages/model/src/-private/attr.js index 802ab62318b..ce93f91ba3b 100644 --- a/packages/model/src/-private/attr.js +++ b/packages/model/src/-private/attr.js @@ -2,11 +2,11 @@ import { assert } from '@ember/debug'; import { computed } from '@ember/object'; import { DEBUG } from '@glimmer/env'; +import { DEPRECATE_CREATE_RECORD_DATA_FOR_HOOK } from '@ember-data/private-build-infra/deprecations'; import { recordIdentifierFor, storeFor } from '@ember-data/store'; -import { recordDataFor } from '@ember-data/store/-private'; +import { peekCache } from '@ember-data/store/-private'; import { computedMacroWithOptionalParams } from './util'; - /** @module @ember-data/model */ @@ -125,7 +125,7 @@ function attr(type, options) { if (this.isDestroyed || this.isDestroying) { return; } - return recordDataFor(this).getAttr(recordIdentifierFor(this), key); + return peekCache(this).getAttr(recordIdentifierFor(this), key); }, set(key, value) { if (DEBUG) { @@ -140,10 +140,13 @@ function attr(type, options) { !this.currentState.isDeleted ); const identifier = recordIdentifierFor(this); - const recordData = storeFor(this)._instanceCache.getRecordData(identifier); - let currentValue = recordData.getAttr(identifier, key); + + const cache = DEPRECATE_CREATE_RECORD_DATA_FOR_HOOK + ? storeFor(this)._instanceCache.getRecordData(identifier) + : storeFor(this).cache; + let currentValue = cache.getAttr(identifier, key); if (currentValue !== value) { - recordData.setAttr(identifier, key, value); + cache.setAttr(identifier, key, value); if (!this.isValid) { const { errors } = this; diff --git a/packages/model/src/-private/legacy-relationships-support.ts b/packages/model/src/-private/legacy-relationships-support.ts index b06665bba7a..1544e9b79bd 100644 --- a/packages/model/src/-private/legacy-relationships-support.ts +++ b/packages/model/src/-private/legacy-relationships-support.ts @@ -10,14 +10,17 @@ import type { ImplicitRelationship } from '@ember-data/graph/-private/graph/inde import type BelongsToRelationship from '@ember-data/graph/-private/relationships/state/belongs-to'; import type ManyRelationship from '@ember-data/graph/-private/relationships/state/has-many'; import { HAS_JSON_API_PACKAGE } from '@ember-data/private-build-infra'; -import { DEPRECATE_PROMISE_PROXIES } from '@ember-data/private-build-infra/deprecations'; +import { + DEPRECATE_CREATE_RECORD_DATA_FOR_HOOK, + DEPRECATE_PROMISE_PROXIES, +} from '@ember-data/private-build-infra/deprecations'; import type Store from '@ember-data/store'; import { fastPush, isStableIdentifier, recordIdentifierFor, SOURCE, storeFor } from '@ember-data/store/-private'; import type { NonSingletonRecordDataManager } from '@ember-data/store/-private/managers/record-data-manager'; import type { DSModel } from '@ember-data/types/q/ds-model'; import { CollectionResourceRelationship, SingleResourceRelationship } from '@ember-data/types/q/ember-data-json-api'; import type { StableRecordIdentifier } from '@ember-data/types/q/identifier'; -import type { RecordData } from '@ember-data/types/q/record-data'; +import type { Cache } from '@ember-data/types/q/record-data'; import type { JsonApiRelationship } from '@ember-data/types/q/record-data-json-api'; import type { RecordInstance } from '@ember-data/types/q/record-instance'; import type { FindOptions } from '@ember-data/types/q/store'; @@ -38,7 +41,7 @@ type PromiseBelongsToFactory = { create(args: BelongsToProxyCreateArgs): Promise export class LegacySupport { declare record: DSModel; declare store: Store; - declare recordData: RecordData; + declare cache: Cache; declare references: Dict; declare identifier: StableRecordIdentifier; declare _manyArrayCache: Dict; @@ -52,7 +55,9 @@ export class LegacySupport { this.record = record; this.store = storeFor(record)!; this.identifier = recordIdentifierFor(record); - this.recordData = this.store._instanceCache.getRecordData(this.identifier); + this.cache = DEPRECATE_CREATE_RECORD_DATA_FOR_HOOK + ? this.store._instanceCache.getRecordData(this.identifier) + : this.store.cache; this._manyArrayCache = Object.create(null) as Dict; this._relationshipPromisesCache = Object.create(null) as Dict>; @@ -83,7 +88,7 @@ export class LegacySupport { } updateCache(operation: LocalRelationshipOperation): void { - this.recordData.update(operation); + this.cache.update(operation); } _findBelongsTo( @@ -111,7 +116,7 @@ export class LegacySupport { const relationship = graphFor(this.store).get(this.identifier, key); assert(`Expected ${key} to be a belongs-to relationship`, isBelongsTo(relationship)); - let resource = this.recordData.getRelationship(this.identifier, key) as SingleResourceRelationship; + let resource = this.cache.getRelationship(this.identifier, key) as SingleResourceRelationship; relationship.state.hasFailedLoadAttempt = false; relationship.state.shouldForceReload = true; let promise = this._findBelongsTo(key, resource, relationship, options); @@ -122,7 +127,7 @@ export class LegacySupport { } getBelongsTo(key: string, options?: FindOptions): PromiseBelongsTo | RecordInstance | null { - const { identifier, recordData } = this; + const { identifier, cache: recordData } = this; let resource = recordData.getRelationship(this.identifier, key) as SingleResourceRelationship; let relatedIdentifier = resource && resource.data ? resource.data : null; assert(`Expected a stable identifier`, !relatedIdentifier || isStableIdentifier(relatedIdentifier)); @@ -170,7 +175,7 @@ export class LegacySupport { } setDirtyBelongsTo(key: string, value: RecordInstance | null) { - return this.recordData.update( + return this.cache.update( { op: 'replaceRelatedRecord', record: this.identifier, @@ -186,7 +191,7 @@ export class LegacySupport { identifier: StableRecordIdentifier, field: string ): [StableRecordIdentifier[], CollectionResourceRelationship] { - let jsonApi = (this.recordData as NonSingletonRecordDataManager).getRelationship( + let jsonApi = (this.cache as NonSingletonRecordDataManager).getRelationship( identifier, field, true @@ -222,7 +227,7 @@ export class LegacySupport { store: this.store, type: definition.type, identifier: this.identifier, - recordData: this.recordData, + recordData: this.cache, identifiers, key, meta: doc.meta || null, @@ -254,7 +259,7 @@ export class LegacySupport { return loadingPromise; } - const jsonApi = this.recordData.getRelationship(this.identifier, key) as CollectionResourceRelationship; + const jsonApi = this.cache.getRelationship(this.identifier, key) as CollectionResourceRelationship; const promise = this._findHasManyByJsonApiResource(jsonApi, this.identifier, relationship, options); if (!promise) { diff --git a/packages/model/src/-private/many-array.ts b/packages/model/src/-private/many-array.ts index c44ff94e6ad..d80f45b07ed 100644 --- a/packages/model/src/-private/many-array.ts +++ b/packages/model/src/-private/many-array.ts @@ -11,7 +11,7 @@ import { IdentifierArrayCreateOptions } from '@ember-data/store/-private/record- import type { CreateRecordProperties } from '@ember-data/store/-private/store-service'; import type { Links, PaginationLinks } from '@ember-data/types/q/ember-data-json-api'; import type { StableRecordIdentifier } from '@ember-data/types/q/identifier'; -import type { RecordData } from '@ember-data/types/q/record-data'; +import type { Cache } from '@ember-data/types/q/record-data'; import type { RecordInstance } from '@ember-data/types/q/record-instance'; import type { FindOptions } from '@ember-data/types/q/store'; import type { Dict } from '@ember-data/types/q/utils'; @@ -26,7 +26,7 @@ export interface ManyArrayCreateArgs { manager: LegacySupport; identifier: StableRecordIdentifier; - recordData: RecordData; + recordData: Cache; meta: Dict | null; links: Links | PaginationLinks | null; key: string; @@ -142,7 +142,7 @@ export default class RelatedCollection extends RecordArray { */ declare links: Links | PaginationLinks | null; declare identifier: StableRecordIdentifier; - declare recordData: RecordData; + declare recordData: Cache; // @ts-expect-error declare _manager: LegacySupport; declare store: Store; @@ -341,7 +341,7 @@ export default class RelatedCollection extends RecordArray { RelatedCollection.prototype.isAsync = false; RelatedCollection.prototype.isPolymorphic = false; RelatedCollection.prototype.identifier = null as unknown as StableRecordIdentifier; -RelatedCollection.prototype.recordData = null as unknown as RecordData; +RelatedCollection.prototype.recordData = null as unknown as Cache; RelatedCollection.prototype._inverseIsAsync = false; RelatedCollection.prototype.key = ''; RelatedCollection.prototype.DEPRECATED_CLASS_NAME = 'ManyArray'; diff --git a/packages/model/src/-private/model.js b/packages/model/src/-private/model.js index 3db8f81313b..da938a2b0e6 100644 --- a/packages/model/src/-private/model.js +++ b/packages/model/src/-private/model.js @@ -21,7 +21,7 @@ import { DEPRECATE_SAVE_PROMISE_ACCESS, } from '@ember-data/private-build-infra/deprecations'; import { recordIdentifierFor, storeFor } from '@ember-data/store'; -import { coerceId, recordDataFor } from '@ember-data/store/-private'; +import { coerceId, peekCache } from '@ember-data/store/-private'; import { deprecatedPromiseObject } from './deprecated-promise-proxy'; import Errors from './errors'; @@ -140,7 +140,7 @@ class Model extends EmberObject { super.init(options); let identity = _secretInit.identifier; - _secretInit.cb(this, _secretInit.recordData, identity, _secretInit.store); + _secretInit.cb(this, _secretInit.cache, identity, _secretInit.store); this.___recordState = DEBUG ? new RecordState(this) : null; @@ -827,7 +827,7 @@ class Model extends EmberObject { and value is an [oldProp, newProp] array. */ changedAttributes() { - return recordDataFor(this).changedAttrs(recordIdentifierFor(this)); + return peekCache(this).changedAttrs(recordIdentifierFor(this)); } /** @@ -853,7 +853,7 @@ class Model extends EmberObject { const { isNew } = currentState; storeFor(this)._join(() => { - recordDataFor(this).rollbackAttrs(recordIdentifierFor(this)); + peekCache(this).rollbackAttrs(recordIdentifierFor(this)); this.errors.clear(); currentState.cleanErrorRequests(); if (isNew) { diff --git a/packages/model/src/-private/notify-changes.ts b/packages/model/src/-private/notify-changes.ts index 5a1e5cdca13..7152144f778 100644 --- a/packages/model/src/-private/notify-changes.ts +++ b/packages/model/src/-private/notify-changes.ts @@ -1,5 +1,6 @@ import { cacheFor } from '@ember/object/internals'; +import { DEPRECATE_CREATE_RECORD_DATA_FOR_HOOK } from '@ember-data/private-build-infra/deprecations'; import type Store from '@ember-data/store'; import type { NotificationType } from '@ember-data/store/-private/managers/record-notification-manager'; import type { StableRecordIdentifier } from '@ember-data/types/q/identifier'; @@ -66,7 +67,13 @@ function notifyRelationship(identifier: StableRecordIdentifier, key: string, rec function notifyAttribute(store: Store, identifier: StableRecordIdentifier, key: string, record: Model) { let currentValue = cacheFor(record, key); - if (currentValue !== store._instanceCache.getRecordData(identifier).getAttr(identifier, key)) { - record.notifyPropertyChange(key); + if (DEPRECATE_CREATE_RECORD_DATA_FOR_HOOK) { + if (currentValue !== store._instanceCache.getRecordData(identifier).getAttr(identifier, key)) { + record.notifyPropertyChange(key); + } + } else { + if (currentValue !== store.cache.getAttr(identifier, key)) { + record.notifyPropertyChange(key); + } } } diff --git a/packages/model/src/-private/record-state.ts b/packages/model/src/-private/record-state.ts index 03e6ab18a56..b3bf24103ba 100644 --- a/packages/model/src/-private/record-state.ts +++ b/packages/model/src/-private/record-state.ts @@ -3,6 +3,7 @@ import { dependentKeyCompat } from '@ember/object/compat'; import { DEBUG } from '@glimmer/env'; import { cached, tracked } from '@glimmer/tracking'; +import { DEPRECATE_CREATE_RECORD_DATA_FOR_HOOK } from '@ember-data/private-build-infra/deprecations'; import type Store from '@ember-data/store'; import { storeFor } from '@ember-data/store'; import { recordIdentifierFor } from '@ember-data/store/-private'; @@ -10,7 +11,7 @@ import type { NotificationType } from '@ember-data/store/-private/managers/recor import type RequestCache from '@ember-data/store/-private/network/request-cache'; import { addToTransaction, subscribe } from '@ember-data/tracking/-private'; import type { StableRecordIdentifier } from '@ember-data/types/q/identifier'; -import type { RecordData } from '@ember-data/types/q/record-data'; +import type { Cache } from '@ember-data/types/q/record-data'; type Model = InstanceType; @@ -150,7 +151,7 @@ export default class RecordState { declare pendingCount: number; declare fulfilledCount: number; declare rejectedCount: number; - declare recordData: RecordData; + declare cache: Cache; declare _errorRequests: any[]; declare _lastError: any; declare handler: object; @@ -161,7 +162,7 @@ export default class RecordState { this.identifier = identity; this.record = record; - this.recordData = store._instanceCache.getRecordData(identity); + this.cache = DEPRECATE_CREATE_RECORD_DATA_FOR_HOOK ? store._instanceCache.getRecordData(identity) : store.cache; this.pendingCount = 0; this.fulfilledCount = 0; @@ -266,9 +267,9 @@ export default class RecordState { updateInvalidErrors(errors) { assert( `Expected the RecordData instance for ${this.identifier} to implement getErrors(identifier)`, - typeof this.recordData.getErrors === 'function' + typeof this.cache.getErrors === 'function' ); - let jsonApiErrors = this.recordData.getErrors(this.identifier); + let jsonApiErrors = this.cache.getErrors(this.identifier); errors.clear(); @@ -318,7 +319,7 @@ export default class RecordState { @tagged get isSaved() { - let rd = this.recordData; + let rd = this.cache; if (this.isDeleted) { assert(`Expected RecordData to implement isDeletionCommitted()`, rd.isDeletionCommitted); return rd.isDeletionCommitted(this.identifier); @@ -331,7 +332,7 @@ export default class RecordState { @tagged get isEmpty() { - let rd = this.recordData; + let rd = this.cache; // TODO this is not actually an RFC'd concept. Determine the // correct heuristic to replace this with. assert(`Expected RecordData to implement isEmpty()`, rd.isEmpty); @@ -340,14 +341,14 @@ export default class RecordState { @tagged get isNew() { - let rd = this.recordData; + let rd = this.cache; assert(`Expected RecordData to implement isNew()`, rd.isNew); return rd.isNew(this.identifier); } @tagged get isDeleted() { - let rd = this.recordData; + let rd = this.cache; assert(`Expected RecordData to implement isDeleted()`, rd.isDeleted); return rd.isDeleted(this.identifier); } @@ -359,7 +360,7 @@ export default class RecordState { @tagged get isDirty() { - let rd = this.recordData; + let rd = this.cache; if (rd.isDeletionCommitted(this.identifier) || (this.isDeleted && this.isNew)) { return false; } diff --git a/packages/model/src/-private/references/belongs-to.ts b/packages/model/src/-private/references/belongs-to.ts index e5b278d14f7..1cf099d1cc9 100644 --- a/packages/model/src/-private/references/belongs-to.ts +++ b/packages/model/src/-private/references/belongs-to.ts @@ -8,7 +8,10 @@ import { resolve } from 'rsvp'; import type { Graph } from '@ember-data/graph/-private/graph/graph'; import type BelongsToRelationship from '@ember-data/graph/-private/relationships/state/belongs-to'; -import { DEPRECATE_PROMISE_PROXIES } from '@ember-data/private-build-infra/deprecations'; +import { + DEPRECATE_CREATE_RECORD_DATA_FOR_HOOK, + DEPRECATE_PROMISE_PROXIES, +} from '@ember-data/private-build-infra/deprecations'; import type Store from '@ember-data/store'; import { recordIdentifierFor } from '@ember-data/store/-private'; import type { NotificationType } from '@ember-data/store/-private/managers/record-notification-manager'; @@ -293,9 +296,12 @@ export default class BelongsToReference { _resource() { this._ref; // subscribe - return this.store._instanceCache - .getRecordData(this.___identifier) - .getRelationship(this.___identifier, this.key) as SingleResourceRelationship; + if (DEPRECATE_CREATE_RECORD_DATA_FOR_HOOK) { + return this.store._instanceCache + .getRecordData(this.___identifier) + .getRelationship(this.___identifier, this.key) as SingleResourceRelationship; + } + return this.store.cache.getRelationship(this.___identifier, this.key) as SingleResourceRelationship; } /** diff --git a/packages/model/src/-private/references/has-many.ts b/packages/model/src/-private/references/has-many.ts index dc80fcbda28..7787d417052 100644 --- a/packages/model/src/-private/references/has-many.ts +++ b/packages/model/src/-private/references/has-many.ts @@ -10,7 +10,10 @@ import { ManyArray } from 'ember-data/-private'; import type { Graph } from '@ember-data/graph/-private/graph/graph'; import type ManyRelationship from '@ember-data/graph/-private/relationships/state/has-many'; -import { DEPRECATE_PROMISE_PROXIES } from '@ember-data/private-build-infra/deprecations'; +import { + DEPRECATE_CREATE_RECORD_DATA_FOR_HOOK, + DEPRECATE_PROMISE_PROXIES, +} from '@ember-data/private-build-infra/deprecations'; import type Store from '@ember-data/store'; import { recordIdentifierFor } from '@ember-data/store'; import type { NotificationType } from '@ember-data/store/-private/managers/record-notification-manager'; @@ -150,9 +153,12 @@ export default class HasManyReference { } _resource() { - return this.store._instanceCache - .getRecordData(this.___identifier) - .getRelationship(this.___identifier, this.key) as CollectionResourceRelationship; + if (DEPRECATE_CREATE_RECORD_DATA_FOR_HOOK) { + return this.store._instanceCache + .getRecordData(this.___identifier) + .getRelationship(this.___identifier, this.key) as CollectionResourceRelationship; + } + return this.store.cache.getRelationship(this.___identifier, this.key) as CollectionResourceRelationship; } /** diff --git a/packages/private-build-infra/addon/current-deprecations.ts b/packages/private-build-infra/addon/current-deprecations.ts index b30a36df0af..b1b816b1f34 100644 --- a/packages/private-build-infra/addon/current-deprecations.ts +++ b/packages/private-build-infra/addon/current-deprecations.ts @@ -61,4 +61,6 @@ export default { DEPRECATE_ARRAY_LIKE: '4.7', DEPRECATE_COMPUTED_CHAINS: '4.7', DEPRECATE_NON_EXPLICIT_POLYMORPHISM: '4.7', + DEPRECATE_INSTANTIATE_RECORD_ARGS: '4.10', + DEPRECATE_CREATE_RECORD_DATA_FOR_HOOK: '4.10', }; diff --git a/packages/private-build-infra/addon/deprecations.ts b/packages/private-build-infra/addon/deprecations.ts index 8e528641fef..9a5f1978d71 100644 --- a/packages/private-build-infra/addon/deprecations.ts +++ b/packages/private-build-infra/addon/deprecations.ts @@ -30,3 +30,5 @@ export const DEPRECATE_PROMISE_PROXIES = deprecationState('DEPRECATE_PROMISE_PRO export const DEPRECATE_ARRAY_LIKE = deprecationState('DEPRECATE_ARRAY_LIKE'); export const DEPRECATE_COMPUTED_CHAINS = deprecationState('DEPRECATE_COMPUTED_CHAINS'); export const DEPRECATE_NON_EXPLICIT_POLYMORPHISM = deprecationState('DEPRECATE_NON_EXPLICIT_POLYMORPHISM'); +export const DEPRECATE_INSTANTIATE_RECORD_ARGS = deprecationState('DEPRECATE_INSTANTIATE_RECORD_ARGS'); +export const DEPRECATE_CREATE_RECORD_DATA_FOR_HOOK = deprecationState('DEPRECATE_CREATE_RECORD_DATA_FOR_HOOK'); diff --git a/packages/store/README.md b/packages/store/README.md index 4d137ca8507..670a71cc94d 100644 --- a/packages/store/README.md +++ b/packages/store/README.md @@ -1,31 +1,180 @@ -@ember-data/store -============================================================================== +

+ + +

-[Short description of the addon.] +

⚡️ The lightweight reactive data library for JavaScript applications

+This package provides [*Ember***Data**](https://github.com/emberjs/data/)'s `Store` class. -Compatibility ------------------------------------------------------------------------------- +The `Store` coordinates interaction between your application, the `Cache`, and sources of data (such as your `API` or a local persistence layer). -* Ember.js v3.4 or above -* Ember CLI v2.13 or above +```mermaid +flowchart LR + A[fa:fa-terminal App] ===> D{fa:fa-code-fork Store} + B{{fa:fa-sitemap RequestManager}} <--> C[(fa:fa-database Source)] + D <--> E[(fa:fa-archive Cache)] + D <--> B + click B href "https://github.com/emberjs/data/tree/master/packages/request" "Go to @ember-data/request" _blank + click E href "https://github.com/emberjs/data/tree/master/packages/json-api" "Go to @ember-data/json-api" _blank + style B color:#58a6ff; + style E color:#58a6ff; +``` + +Optionally, the Store can be configured to hydrate the response data into rich presentation classes. + +```mermaid +flowchart LR + A[fa:fa-terminal App] --- B(Model) + A === C{fa:fa-code-fork Store} + B --- C + click B href "https://github.com/emberjs/data/tree/master/packages/model" "Go to @ember-data/model" _blank + style B color:#58a6ff; +``` + +## Installation + +Install using your javascript package manager of choice. For instance with [pnpm](https://pnpm.io/) + +``` +pnpm add @ember-data/store +``` + +After installing you will want to configure your first `Store`. Read more below for how to create and configure stores for your application. + + +## 🔨 Creating A Store + +To use a `Store` we will need to do few things: add a `Cache` to store data **in-memory**, add an `Adapter` to fetch data from a source, and implement `instantiateRecord` to tell the store how to display the data for individual resources. + +> **Note** If you are using the package `ember-data` then a `JSON:API` cache and `instantiateRecord` are configured for you by default. + +### Configuring A Cache + +To start, let's install a `JSON:API` cache. If your app uses `GraphQL` or `REST` other caches may better fit your data. You can author your own cache by creating one that conforms to the [spec](). + +The package `@ember-data/json-api` provides a `JSON:API` cache we can use. After installing it, we can configure the store to use this cache. + +```js +import Store from '@ember-data/store'; +import { Cache } from '@ember-data/json-api'; + +class extends Store { + createCache(storeWrapper) { + return new Cache(storeWrapper); + } +} +``` + +Now that we have a `cache` let's setup something to handle fetching and saving data via our API. +> Note: [1] the cache from `@ember-data/json-api` is a special cache: if the package is present the `createCache` hook will automatically do the above wiring if the hook is not implemented. We still recommend implementing the hook. +> +> Note: [2] The `ember-data` package automatically includes the `@ember-data/json-api` cache for you. -Installation ------------------------------------------------------------------------------- +### Adding An Adapter +When *Ember***Data** needs to fetch or save data it will pass that request to your application's `Adapter` for fulfillment. How this fulfillment occurs (in-memory, device storage, via single or multiple API requests, etc.) is up to that Adapter. + +To start, let's install a `JSON:API` adapter. If your app uses `GraphQL` or `REST` other adapters may better fit your data. You can author your own adapter by creating one that conforms to the [spec](). + +The package `@ember-data/adapter` provides a `JSON:API` adapter we can use. After installing it, we can configure the store to use this adapter. + +```js +import Store from '@ember-data/store'; +import Adapter from '@ember-data/adapter/json-api'; + +class extends Store { + #adapter = new Adapter(); + + adapterFor() { + return this.#adapter; + } +} ``` -ember install @ember-data/store + +If you want to know more about using Adapters with Ember read the next section, else lets skip to [Presenting Data from the Cache](#presenting-data-from-the-cache) to configure how our application will interact with our data. + +#### Using with Ember + +Note: If you are using Ember and would like to make use of `service` injections in your adapter, you will want to additionally `setOwner` for the Adapter. + +```js +import Store from '@ember-data/store'; +import Adapter from '@ember-data/adapter/json-api'; +import { getOwner, setOwner } from '@ember/application'; + +class extends Store { + #adapter = null; + + adapterFor() { + let adapter = this.#adapter; + if (!adapter) { + const owner = getOwner(this); + adapter = new Adapter(); + setOwner(adapter, owner); + this.#adapter = adapter; + } + + return adapter; + } +} ``` +By default when using with Ember you only need to implement this hook if you want your adapter usage to be statically analyzeable. *Ember***Data** will attempt to resolve adapters using Ember's resolver. To provide a single Adapter for your application like the above you would provide it as the default export of the file `app/adapters/application.{js/ts}` + +### Presenting Data from the Cache + +Now that we have a source and a cach for our data, we need to configure how the Store delivers that data back to our application. We do this via the hook `instantiateRecord`, which allows us to transform the data for a resource before handing it to the application. + +A naive way to present the data would be to return it as JSON. Typically instead this hook will be used to add reactivity and make each uniue resource a singleton, ensuring that if the cache updates our presented data will reflect the new state. + +Below is an example of using the hooks `instantiateRecord` and a `teardownRecord` to provide minimal read-only reactive state for simple resources. + +```ts +import Store, { recordIdentifierFor } from '@ember-data/store'; +import { TrackedObject } from 'tracked-built-ins'; + +class extends Store { + instantiateRecord(identifier) { + const { cache, notifications } = this; + + // create a TrackedObject with our attributes, id and type + const record = new TrackedObject(Object.assign({}, cache.peek(identifier))); + record.type = identifier.type; + record.id = identifier.id; + + notifications.subscribe(identifier, (_, change) => { + if (change === 'attributes') { + Object.assign(record, cache.peek(identifier)); + } + }); + + return record; + } +} +``` -Usage ------------------------------------------------------------------------------- +Because `instantiateRecord` is opaque to the nature of the record, an implementation can be anything from a fairly simple object to a robust proxy that intelligently links together associated records through relationships. -[Longer description of how to use the addon in apps.] +This also enables creating a record that separates `edit` flows from `create` flows entirely. A record class might choose to implement a `checkout`method that gives access to an editable instance while the primary record continues to be read-only and reflect only persisted (non-mutated) state. +Typically you will choose an existing record implementation such as `@ember-data/model` for your application. -License ------------------------------------------------------------------------------- +Because of the boundaries around instantiation and the cache, record implementations should be capable of interop both with each other and with any `Cache`. Due to this, if needed an application can utilize multiple record implementations and multiple cache implementations either to support enhanced features for only a subset of records or to be able to incrementally migrate from one record/cache to another record or cache. -This project is licensed under the [MIT License](LICENSE.md). +> Note: [1] `@ember-data/model` is a special record implementation: if the package is present the `instantiateRecord` hook will automatically do the above wiring if the hook is not implemented. Due to the complexity of this legacy package's use of Ember's resolver, we do not recommend wiring this package manually. +> +> Note: [2] The `ember-data` package automatically includes the `@ember-data/model` implementation for you. diff --git a/packages/store/src/-private/caches/instance-cache.ts b/packages/store/src/-private/caches/instance-cache.ts index 3cf4cde2374..6ea96aa6880 100644 --- a/packages/store/src/-private/caches/instance-cache.ts +++ b/packages/store/src/-private/caches/instance-cache.ts @@ -8,7 +8,12 @@ import type { Graph } from '@ember-data/graph/-private/graph/graph'; import type { peekGraph } from '@ember-data/graph/-private/graph/index'; import { HAS_GRAPH_PACKAGE, HAS_JSON_API_PACKAGE } from '@ember-data/private-build-infra'; import { LOG_INSTANCE_CACHE } from '@ember-data/private-build-infra/debugging'; -import { DEPRECATE_V1_RECORD_DATA, DEPRECATE_V1CACHE_STORE_APIS } from '@ember-data/private-build-infra/deprecations'; +import { + DEPRECATE_CREATE_RECORD_DATA_FOR_HOOK, + DEPRECATE_INSTANTIATE_RECORD_ARGS, + DEPRECATE_V1_RECORD_DATA, + DEPRECATE_V1CACHE_STORE_APIS, +} from '@ember-data/private-build-infra/deprecations'; import type { ExistingResourceIdentifierObject, ExistingResourceObject, @@ -19,10 +24,10 @@ import type { StableExistingRecordIdentifier, StableRecordIdentifier, } from '@ember-data/types/q/identifier'; -import type { RecordData } from '@ember-data/types/q/record-data'; +import type { Cache } from '@ember-data/types/q/record-data'; import type { JsonApiRelationship, JsonApiResource } from '@ember-data/types/q/record-data-json-api'; import type { RelationshipSchema } from '@ember-data/types/q/record-data-schemas'; -import type { RecordDataStoreWrapper as StoreWrapper } from '@ember-data/types/q/record-data-store-wrapper'; +import type { CacheStoreWrapper as StoreWrapper } from '@ember-data/types/q/record-data-store-wrapper'; import type { RecordInstance } from '@ember-data/types/q/record-instance'; import type { FindOptions } from '@ember-data/types/q/store'; import type { Dict } from '@ember-data/types/q/utils'; @@ -115,19 +120,20 @@ export function storeFor(record: RecordInstance): Store | undefined { type Caches = { record: Map; - recordData: Map; + recordData: Map; reference: WeakMap; }; export class InstanceCache { declare store: Store; + declare cache: Cache; declare _storeWrapper: RecordDataStoreWrapper; - declare __recordDataFor: (resource: RecordIdentifier) => RecordData; + declare __recordDataFor: (resource: RecordIdentifier) => Cache; declare __cacheManager: NonSingletonRecordDataManager; __instances: Caches = { record: new Map(), - recordData: new Map(), + recordData: new Map(), reference: new WeakMap(), }; @@ -135,11 +141,13 @@ export class InstanceCache { this.store = store; this._storeWrapper = new RecordDataStoreWrapper(this.store); - this.__recordDataFor = (resource: RecordIdentifier) => { - // TODO enforce strict - const identifier = this.store.identifierCache.getOrCreateRecordIdentifier(resource); - return this.getRecordData(identifier); - }; + if (DEPRECATE_CREATE_RECORD_DATA_FOR_HOOK) { + this.__recordDataFor = (resource: RecordIdentifier) => { + // TODO enforce strict + const identifier = this.store.identifierCache.getOrCreateRecordIdentifier(resource); + return this.getRecordData(identifier); + }; + } store.identifierCache.__configureMerge( (identifier: StableRecordIdentifier, matchedIdentifier: StableRecordIdentifier, resourceData) => { @@ -184,18 +192,46 @@ export class InstanceCache { ); } + if (DEPRECATE_CREATE_RECORD_DATA_FOR_HOOK) { + let recordData = keptRecordData || staleRecordData; + + if (recordData) { + recordData.patch({ + op: 'mergeIdentifiers', + record: staleIdentifier, + value: keptIdentifier, + }); + } else if (HAS_JSON_API_PACKAGE) { + this.store.cache.patch({ + op: 'mergeIdentifiers', + record: staleIdentifier, + value: keptIdentifier, + }); + } + } else { + this.store.cache.patch({ + op: 'mergeIdentifiers', + record: staleIdentifier, + value: keptIdentifier, + }); + } + let recordData = keptRecordData || staleRecordData; if (recordData) { - recordData.sync({ + recordData.patch({ + op: 'mergeIdentifiers', + record: staleIdentifier, + value: keptIdentifier, + }); + } else if (!DEPRECATE_CREATE_RECORD_DATA_FOR_HOOK) { + this.store.cache.patch({ op: 'mergeIdentifiers', record: staleIdentifier, value: keptIdentifier, }); } else if (HAS_JSON_API_PACKAGE) { - // TODO notify cache always, this requires it always being a singleton - // and not ever specific to one record-data - this.store.__private_singleton_recordData?.sync({ + this.store.cache.patch({ op: 'mergeIdentifiers', record: staleIdentifier, value: keptIdentifier, @@ -222,14 +258,14 @@ export class InstanceCache { ); } peek({ identifier, bucket }: { identifier: StableRecordIdentifier; bucket: 'record' }): RecordInstance | undefined; - peek({ identifier, bucket }: { identifier: StableRecordIdentifier; bucket: 'recordData' }): RecordData | undefined; + peek({ identifier, bucket }: { identifier: StableRecordIdentifier; bucket: 'recordData' }): Cache | undefined; peek({ identifier, bucket, }: { identifier: StableRecordIdentifier; bucket: 'record' | 'recordData'; - }): RecordData | RecordInstance | undefined { + }): Cache | RecordInstance | undefined { return this.__instances[bucket]?.get(identifier); } @@ -237,14 +273,32 @@ export class InstanceCache { let record = this.__instances.record.get(identifier); if (!record) { - const recordData = this.getRecordData(identifier); + const recordData = DEPRECATE_CREATE_RECORD_DATA_FOR_HOOK ? this.getRecordData(identifier) : this.store.cache; + + if (DEPRECATE_INSTANTIATE_RECORD_ARGS) { + if (this.store.instantiateRecord.length > 2) { + deprecate( + `Expected store.instantiateRecord to have an arity of 2. recordDataFor and notificationManager args have been deprecated.`, + false, + { + for: '@ember-data/store', + id: 'ember-data:deprecate-instantiate-record-args', + since: { available: '4.10', enabled: '4.10' }, + until: '5.0', + } + ); + } + record = this.store.instantiateRecord( + identifier, + properties || {}, + // @ts-expect-error + this.__recordDataFor, + this.store.notifications + ); + } else { + record = this.store.instantiateRecord(identifier, properties || {}); + } - record = this.store.instantiateRecord( - identifier, - properties || {}, - this.__recordDataFor, - this.store.notifications - ); setRecordIdentifier(record, identifier); setRecordDataFor(record, recordData); StoreMap.set(record, this.store); @@ -259,11 +313,11 @@ export class InstanceCache { return record; } - getRecordData(identifier: StableRecordIdentifier): RecordData { + getRecordData(identifier: StableRecordIdentifier): Cache { let recordData = this.__instances.recordData.get(identifier); if (DEPRECATE_V1CACHE_STORE_APIS) { - if (!recordData && this.store.createRecordDataFor.length > 2) { + if (!recordData && this.store.createRecordDataFor && this.store.createRecordDataFor.length > 2) { deprecate( `Store.createRecordDataFor(, , , ) has been deprecated in favor of Store.createRecordDataFor(, )`, false, @@ -291,16 +345,25 @@ export class InstanceCache { } if (!recordData) { - let recordDataInstance = this.store.createRecordDataFor(identifier, this._storeWrapper); + let recordDataInstance: Cache; + if (DEPRECATE_CREATE_RECORD_DATA_FOR_HOOK) { + recordDataInstance = this.store.createRecordDataFor + ? this.store.createRecordDataFor(identifier, this._storeWrapper) + : this.store.cache; + } else { + recordDataInstance = this.store.cache; + } if (DEPRECATE_V1_RECORD_DATA) { - recordData = new NonSingletonRecordDataManager(this.store, recordDataInstance, identifier); + if (recordDataInstance.version !== '2') { + recordData = new NonSingletonRecordDataManager(this.store, recordDataInstance, identifier); + } else { + recordData = recordDataInstance; + } } else { if (DEBUG) { - recordData = this.__cacheManager = this.__cacheManager || new SingletonRecordDataManager(); - (recordData as SingletonRecordDataManager)._addRecordData(identifier, recordDataInstance as RecordData); - } else { - recordData = recordDataInstance as RecordData; + (recordDataInstance as SingletonRecordDataManager)._addRecordData(identifier, recordDataInstance); } + recordData = recordDataInstance; } setRecordDataFor(identifier, recordData); @@ -572,13 +635,13 @@ export class InstanceCache { identifier = this.store.identifierCache.getOrCreateRecordIdentifier(data); } - const recordData = this.getRecordData(identifier); - if (recordData.isNew(identifier)) { + const cache = DEPRECATE_CREATE_RECORD_DATA_FOR_HOOK ? this.getRecordData(identifier) : this.store.cache; + if (cache.isNew(identifier)) { this.store.notifications.notify(identifier, 'identity'); } const hasRecord = this.__instances.record.has(identifier); - recordData.pushData(identifier, data, hasRecord); + cache.upsert(identifier, data, hasRecord); if (!isUpdate) { this.store.recordArrayManager.identifierAdded(identifier); @@ -588,7 +651,7 @@ export class InstanceCache { } } -function _recordDataIsFullDeleted(identifier: StableRecordIdentifier, recordData: RecordData): boolean { +function _recordDataIsFullDeleted(identifier: StableRecordIdentifier, recordData: Cache): boolean { return ( recordData.isDeletionCommitted(identifier) || (recordData.isNew(identifier) && recordData.isDeleted(identifier)) ); @@ -635,7 +698,9 @@ export function preloadData(store: Store, identifier: StableRecordIdentifier, pr jsonPayload.attributes[key] = preloadValue; } }); - store._instanceCache.getRecordData(identifier).pushData(identifier, jsonPayload); + DEPRECATE_CREATE_RECORD_DATA_FOR_HOOK + ? store._instanceCache.getRecordData(identifier).upsert(identifier, jsonPayload, false) + : store.cache.upsert(identifier, jsonPayload, false); } function preloadRelationship( diff --git a/packages/store/src/-private/caches/record-data-for.ts b/packages/store/src/-private/caches/record-data-for.ts index 8f83e7e4e1b..f3812791efc 100644 --- a/packages/store/src/-private/caches/record-data-for.ts +++ b/packages/store/src/-private/caches/record-data-for.ts @@ -1,7 +1,7 @@ import { assert } from '@ember/debug'; import type { StableRecordIdentifier } from '@ember-data/types/q/identifier'; -import type { RecordData } from '@ember-data/types/q/record-data'; +import type { Cache } from '@ember-data/types/q/record-data'; import type { RecordInstance } from '@ember-data/types/q/record-instance'; /* @@ -9,9 +9,9 @@ import type { RecordInstance } from '@ember-data/types/q/record-instance'; * Model or Identifier */ -const RecordDataForIdentifierCache = new Map(); +const RecordDataForIdentifierCache = new Map(); -export function setRecordDataFor(identifier: StableRecordIdentifier | RecordInstance, recordData: RecordData): void { +export function setRecordDataFor(identifier: StableRecordIdentifier | RecordInstance, recordData: Cache): void { assert( `Illegal set of identifier`, !RecordDataForIdentifierCache.has(identifier) || RecordDataForIdentifierCache.get(identifier) === recordData @@ -23,11 +23,11 @@ export function removeRecordDataFor(identifier: StableRecordIdentifier | RecordI RecordDataForIdentifierCache.delete(identifier); } -export default function recordDataFor(instance: StableRecordIdentifier): RecordData | null; -export default function recordDataFor(instance: RecordInstance): RecordData; -export default function recordDataFor(instance: StableRecordIdentifier | RecordInstance): RecordData | null { +export default function peekCache(instance: StableRecordIdentifier): Cache | null; +export default function peekCache(instance: RecordInstance): Cache; +export default function peekCache(instance: StableRecordIdentifier | RecordInstance): Cache | null { if (RecordDataForIdentifierCache.has(instance as StableRecordIdentifier)) { - return RecordDataForIdentifierCache.get(instance as StableRecordIdentifier) as RecordData; + return RecordDataForIdentifierCache.get(instance as StableRecordIdentifier) as Cache; } return null; diff --git a/packages/store/src/-private/index.ts b/packages/store/src/-private/index.ts index 1c730096eac..bbc57442449 100644 --- a/packages/store/src/-private/index.ts +++ b/packages/store/src/-private/index.ts @@ -56,4 +56,4 @@ export { default as RecordArrayManager, fastPush } from './managers/record-array export { default as SnapshotRecordArray } from './network/snapshot-record-array'; // leaked for private use / test use, should investigate removing -export { default as recordDataFor, removeRecordDataFor } from './caches/record-data-for'; +export { default as peekCache, removeRecordDataFor } from './caches/record-data-for'; diff --git a/packages/store/src/-private/managers/record-data-manager.ts b/packages/store/src/-private/managers/record-data-manager.ts index 6175dea2d83..58ba6e3e6ae 100644 --- a/packages/store/src/-private/managers/record-data-manager.ts +++ b/packages/store/src/-private/managers/record-data-manager.ts @@ -1,18 +1,64 @@ import { assert, deprecate } from '@ember/debug'; import type { LocalRelationshipOperation } from '@ember-data/graph/-private/graph/-operations'; +import { StructuredDataDocument } from '@ember-data/request/-private/types'; +import { Change } from '@ember-data/types/cache/change'; +import { ResourceDocument, StructuredDocument } from '@ember-data/types/cache/document'; +import { StableDocumentIdentifier } from '@ember-data/types/cache/identifier'; import type { CollectionResourceRelationship, + JsonApiDocument, SingleResourceRelationship, } from '@ember-data/types/q/ember-data-json-api'; -import type { StableRecordIdentifier } from '@ember-data/types/q/identifier'; -import type { ChangedAttributesHash, MergeOperation, RecordData, RecordDataV1 } from '@ember-data/types/q/record-data'; +import type { StableExistingRecordIdentifier, StableRecordIdentifier } from '@ember-data/types/q/identifier'; +import type { Cache, ChangedAttributesHash, MergeOperation, RecordDataV1 } from '@ember-data/types/q/record-data'; import type { JsonApiResource, JsonApiValidationError } from '@ember-data/types/q/record-data-json-api'; import type { Dict } from '@ember-data/types/q/utils'; import { isStableIdentifier } from '../caches/identifier-cache'; import type Store from '../store-service'; +function legacyCachePut(store: Store, doc: StructuredDataDocument): ResourceDocument { + const jsonApiDoc = doc.data; + let ret: ResourceDocument; + store._join(() => { + let included = jsonApiDoc.included; + let i: number, length: number; + + if (included) { + for (i = 0, length = included.length; i < length; i++) { + store._instanceCache.loadData(included[i]); + } + } + + if (Array.isArray(jsonApiDoc.data)) { + length = jsonApiDoc.data.length; + let identifiers: StableExistingRecordIdentifier[] = []; + + for (i = 0; i < length; i++) { + identifiers.push(store._instanceCache.loadData(jsonApiDoc.data[i])); + } + ret = { data: identifiers }; + return; + } + + if (jsonApiDoc.data === null) { + ret = { data: null }; + return; + } + + assert( + `Expected an object in the 'data' property in a call to 'push', but was ${typeof jsonApiDoc.data}`, + typeof jsonApiDoc.data === 'object' + ); + + ret = { data: store._instanceCache.loadData(jsonApiDoc.data) }; + return; + }); + + return ret!; +} + /** * The RecordDataManager wraps a RecordData cache * enforcing that only the public API surface area @@ -53,18 +99,18 @@ import type Store from '../store-service'; * @class RecordDataManager * @public */ -export class NonSingletonRecordDataManager implements RecordData { +export class NonSingletonRecordDataManager implements Cache { version: '2' = '2'; #store: Store; - #recordData: RecordData | RecordDataV1; + #recordData: Cache | RecordDataV1; #identifier: StableRecordIdentifier; get managedVersion() { return this.#recordData.version || '1'; } - constructor(store: Store, recordData: RecordData | RecordDataV1, identifier: StableRecordIdentifier) { + constructor(store: Store, recordData: Cache | RecordDataV1, identifier: StableRecordIdentifier) { this.#store = store; this.#recordData = recordData; this.#identifier = identifier; @@ -83,7 +129,7 @@ export class NonSingletonRecordDataManager implements RecordData { } } - #isDeprecated(recordData: RecordData | RecordDataV1): recordData is RecordDataV1 { + #isDeprecated(recordData: Cache | RecordDataV1): recordData is RecordDataV1 { let version = recordData.version || '1'; return version !== this.version; } @@ -104,17 +150,29 @@ export class NonSingletonRecordDataManager implements RecordData { return this.#identifier; } + put(doc: StructuredDocument): ResourceDocument { + const recordData = this.#recordData; + if (this.#isDeprecated(recordData)) { + if (doc instanceof Error) { + // in legacy we don't know how to handle this + throw doc; + } + return legacyCachePut(this.#store, doc as StructuredDataDocument); + } + return recordData.put(doc); + } + /** * Push resource data from a remote source into the cache for this identifier * - * @method pushData + * @method upsert * @public * @param identifier * @param data * @param hasRecord * @returns {void | string[]} if `hasRecord` is true then calculated key changes should be returned */ - pushData(identifier: StableRecordIdentifier, data: JsonApiResource, hasRecord?: boolean): void | string[] { + upsert(identifier: StableRecordIdentifier, data: JsonApiResource, hasRecord: boolean): void | string[] { const recordData = this.#recordData; // called by something V1 if (!isStableIdentifier(identifier)) { @@ -125,7 +183,7 @@ export class NonSingletonRecordDataManager implements RecordData { if (this.#isDeprecated(recordData)) { return recordData.pushData(data, hasRecord); } - return recordData.pushData(identifier, data, hasRecord); + return recordData.upsert(identifier, data, hasRecord); } /** @@ -139,24 +197,24 @@ export class NonSingletonRecordDataManager implements RecordData { * @param op the operation to perform * @returns {void} */ - sync(op: MergeOperation): void { + patch(op: MergeOperation): void { const recordData = this.#recordData; if (this.#isDeprecated(recordData)) { return; } - recordData.sync(op); + recordData.patch(op); } /** * Update resource data with a local mutation. Currently supports operations * on relationships only. * - * @method update + * @method mutate * @public * @param operation */ // isCollection is only needed for interop with v1 cache - update(operation: LocalRelationshipOperation, isResource?: boolean): void { + mutate(operation: LocalRelationshipOperation, isResource?: boolean): void { if (this.#isDeprecated(this.#recordData)) { const cache = this.#store._instanceCache; switch (operation.op) { @@ -196,8 +254,60 @@ export class NonSingletonRecordDataManager implements RecordData { return; } } else { - this.#recordData.update(operation); + this.#recordData.mutate(operation); + } + } + + peek(identifier: StableRecordIdentifier): unknown; + peek(identifier: StableDocumentIdentifier): ResourceDocument | null; + peek(identifier: StableRecordIdentifier | StableDocumentIdentifier): unknown { + const recordData = this.#recordData; + if (this.#isDeprecated(recordData)) { + throw new Error(`Expected cache to implement peek`); + } + return recordData.peek(identifier); + } + peekRequest(identifier: StableDocumentIdentifier): StructuredDocument | null { + const recordData = this.#recordData; + if (this.#isDeprecated(recordData)) { + throw new Error(`Expected cache to implement peekRequest`); } + return recordData.peekRequest(identifier); + } + fork(): Promise { + const recordData = this.#recordData; + if (this.#isDeprecated(recordData)) { + throw new Error(`Expected cache to implement fork`); + } + return recordData.fork(); + } + merge(cache: Cache): Promise { + const recordData = this.#recordData; + if (this.#isDeprecated(recordData)) { + throw new Error(`Expected cache to implement merge`); + } + return recordData.merge(cache); + } + diff(): Promise { + const recordData = this.#recordData; + if (this.#isDeprecated(recordData)) { + throw new Error(`Expected cache to implement diff`); + } + return recordData.diff(); + } + dump(): Promise> { + const recordData = this.#recordData; + if (this.#isDeprecated(recordData)) { + throw new Error(`Expected cache to implement dump`); + } + return recordData.dump(); + } + hydrate(stream: ReadableStream): Promise { + const recordData = this.#recordData; + if (this.#isDeprecated(recordData)) { + throw new Error(`Expected cache to implement hydrate`); + } + return recordData.hydrate(stream); } /** @@ -559,7 +669,7 @@ export class NonSingletonRecordDataManager implements RecordData { this.#isDeprecated(recordData) ? recordData.setDirtyBelongsTo(propertyName, value) - : recordData.update({ + : recordData.mutate({ op: 'replaceRelatedRecord', record: this.#identifier, field: propertyName, @@ -587,7 +697,7 @@ export class NonSingletonRecordDataManager implements RecordData { this.#isDeprecated(recordData) ? recordData.addToHasMany(propertyName, value, idx) - : recordData.update({ + : recordData.mutate({ op: 'addToRelatedRecords', field: propertyName, record: identifier, @@ -606,13 +716,13 @@ export class NonSingletonRecordDataManager implements RecordData { * @param propertyName * @param value */ - removeFromHasMany(propertyName: string, value: RecordData[]): void { + removeFromHasMany(propertyName: string, value: Cache[]): void { const identifier = this.#identifier; const recordData = this.#recordData; this.#isDeprecated(recordData) ? recordData.removeFromHasMany(propertyName, value) - : recordData.update({ + : recordData.mutate({ op: 'removeFromRelatedRecords', record: identifier, field: propertyName, @@ -636,7 +746,7 @@ export class NonSingletonRecordDataManager implements RecordData { this.#isDeprecated(recordData) ? recordData.setDirtyHasMany(propertyName, value) - : recordData.update({ + : recordData.mutate({ op: 'replaceRelatedRecords', record: this.#identifier, field: propertyName, @@ -734,20 +844,25 @@ export class NonSingletonRecordDataManager implements RecordData { } } -export class SingletonRecordDataManager implements RecordData { +export class SingletonRecordDataManager implements Cache { version: '2' = '2'; - #recordDatas: Map; + #recordDatas: Map; + #cache: Cache; - constructor() { + constructor(cache: Cache) { + this.#cache = cache; this.#recordDatas = new Map(); } + put(doc: StructuredDocument): ResourceDocument { + throw new Error('Method not implemented.'); + } - _addRecordData(identifier: StableRecordIdentifier, recordData: RecordData) { + _addRecordData(identifier: StableRecordIdentifier, recordData: Cache) { this.#recordDatas.set(identifier, recordData); } - #recordData(identifier: StableRecordIdentifier): RecordData { + #recordData(identifier: StableRecordIdentifier): Cache { assert(`No RecordData Yet Exists!`, this.#recordDatas.has(identifier)); return this.#recordDatas.get(identifier)!; } @@ -755,12 +870,37 @@ export class SingletonRecordDataManager implements RecordData { // Cache // ===== - pushData(identifier: StableRecordIdentifier, data: JsonApiResource, hasRecord?: boolean): void | string[] { - return this.#recordData(identifier).pushData(identifier, data, hasRecord); + upsert(identifier: StableRecordIdentifier, data: JsonApiResource, hasRecord: boolean): void | string[] { + return this.#recordData(identifier).upsert(identifier, data, hasRecord); + } + + patch(op: MergeOperation): void { + this.#recordData(op.record).patch(op); } - sync(op: MergeOperation): void { - this.#recordData(op.record).sync(op); + peek(identifier: StableRecordIdentifier): unknown; + peek(identifier: StableDocumentIdentifier): ResourceDocument | null; + peek(identifier: StableRecordIdentifier | StableDocumentIdentifier): unknown { + return this.#cache.peek(identifier); + } + peekRequest(identifier: StableDocumentIdentifier): StructuredDocument | null { + return this.#cache.peekRequest(identifier); + } + + fork(): Promise { + return this.#cache.fork(); + } + merge(cache: Cache): Promise { + return this.#cache.merge(cache); + } + diff(): Promise { + return this.#cache.diff(); + } + dump(): Promise> { + return this.#cache.dump(); + } + hydrate(stream: ReadableStream): Promise { + return this.#cache.hydrate(stream); } clientDidCreate(identifier: StableRecordIdentifier, options?: Dict): Dict { @@ -812,8 +952,9 @@ export class SingletonRecordDataManager implements RecordData { ): SingleResourceRelationship | CollectionResourceRelationship { return this.#recordData(identifier).getRelationship(identifier, propertyName); } - update(operation: LocalRelationshipOperation): void { - this.#recordData(operation.record).update(operation); + + mutate(operation: LocalRelationshipOperation): void { + this.#recordData(operation.record).mutate(operation); } // State diff --git a/packages/store/src/-private/managers/record-data-store-wrapper.ts b/packages/store/src/-private/managers/record-data-store-wrapper.ts index 2b431e3a8bd..301d213caf5 100644 --- a/packages/store/src/-private/managers/record-data-store-wrapper.ts +++ b/packages/store/src/-private/managers/record-data-store-wrapper.ts @@ -1,8 +1,11 @@ import { assert, deprecate } from '@ember/debug'; -import { DEPRECATE_V1CACHE_STORE_APIS } from '@ember-data/private-build-infra/deprecations'; +import { + DEPRECATE_CREATE_RECORD_DATA_FOR_HOOK, + DEPRECATE_V1CACHE_STORE_APIS, +} from '@ember-data/private-build-infra/deprecations'; import type { RecordIdentifier, StableRecordIdentifier } from '@ember-data/types/q/identifier'; -import type { RecordData } from '@ember-data/types/q/record-data'; +import type { Cache } from '@ember-data/types/q/record-data'; import type { AttributesSchema, RelationshipsSchema } from '@ember-data/types/q/record-data-schemas'; import type { LegacyRecordDataStoreWrapper, @@ -212,11 +215,11 @@ class LegacyWrapper implements LegacyRecordDataStoreWrapper { this._store.recordArrayManager.identifierChanged(identifier); } - recordDataFor(type: string, id: string, lid?: string | null): RecordData; - recordDataFor(type: string, id: string | null, lid: string): RecordData; - recordDataFor(type: string): RecordData; - recordDataFor(type: StableRecordIdentifier): RecordData; - recordDataFor(type: string | StableRecordIdentifier, id?: string | null, lid?: string | null): RecordData { + recordDataFor(type: string, id: string, lid?: string | null): Cache; + recordDataFor(type: string, id: string | null, lid: string): Cache; + recordDataFor(type: string): Cache; + recordDataFor(type: StableRecordIdentifier): Cache; + recordDataFor(type: string | StableRecordIdentifier, id?: string | null, lid?: string | null): Cache { let identifier: StableRecordIdentifier; if (DEPRECATE_V1CACHE_STORE_APIS) { if (!isStableIdentifier(type)) { @@ -239,14 +242,18 @@ class LegacyWrapper implements LegacyRecordDataStoreWrapper { identifier = type; } - const recordData = this._store._instanceCache.getRecordData(identifier); + const cache = DEPRECATE_CREATE_RECORD_DATA_FOR_HOOK + ? this._store._instanceCache.getRecordData(identifier) + : this._store.cache; - if (!id && !lid) { - recordData.clientDidCreate(identifier); - this._store.recordArrayManager.identifierAdded(identifier); + if (DEPRECATE_V1CACHE_STORE_APIS) { + if (!id && !lid && typeof type === 'string') { + cache.clientDidCreate(identifier); + this._store.recordArrayManager.identifierAdded(identifier); + } } - return recordData; + return cache; } setRecordId(type: string | StableRecordIdentifier, id: string, lid?: string) { @@ -399,10 +406,25 @@ class V2RecordDataStoreWrapper implements StoreWrapper { return this._store.getSchemaDefinitionService(); } - recordDataFor(identifier: StableRecordIdentifier): RecordData { + recordDataFor(identifier: StableRecordIdentifier): Cache { assert(`Expected a stable identifier`, isStableIdentifier(identifier)); - return this._store._instanceCache.getRecordData(identifier); + if (DEPRECATE_CREATE_RECORD_DATA_FOR_HOOK) { + deprecate( + `StoreWrapper.recordDataFor is deprecated. With Singleton Cache, this method is no longer needed as the caller is its own cache reference.`, + false, + { + for: '@ember-data/store', + id: 'ember-data:deprecate-record-data-for', + since: { available: '4.10', enabled: '4.10' }, + until: '5.0', + } + ); + } + + return DEPRECATE_CREATE_RECORD_DATA_FOR_HOOK + ? this._store._instanceCache.getRecordData(identifier) + : (void 0 as unknown as Cache); } setRecordId(identifier: StableRecordIdentifier, id: string) { diff --git a/packages/store/src/-private/network/snapshot.ts b/packages/store/src/-private/network/snapshot.ts index 44c2c32df77..d524c4643b3 100644 --- a/packages/store/src/-private/network/snapshot.ts +++ b/packages/store/src/-private/network/snapshot.ts @@ -8,7 +8,10 @@ import { importSync } from '@embroider/macros'; import type BelongsToRelationship from '@ember-data/graph/-private/relationships/state/belongs-to'; import type ManyRelationship from '@ember-data/graph/-private/relationships/state/has-many'; import { HAS_JSON_API_PACKAGE } from '@ember-data/private-build-infra'; -import { DEPRECATE_SNAPSHOT_MODEL_CLASS_ACCESS } from '@ember-data/private-build-infra/deprecations'; +import { + DEPRECATE_CREATE_RECORD_DATA_FOR_HOOK, + DEPRECATE_SNAPSHOT_MODEL_CLASS_ACCESS, +} from '@ember-data/private-build-infra/deprecations'; import type { StableRecordIdentifier } from '@ember-data/types/q/identifier'; import type { OptionsHash } from '@ember-data/types/q/minimum-serializer-interface'; import type { ChangedAttributesHash } from '@ember-data/types/q/record-data'; @@ -127,7 +130,9 @@ export default class Snapshot implements Snapshot { */ this.modelName = identifier.type; if (hasRecord) { - this._changedAttributes = this._store._instanceCache.getRecordData(identifier).changedAttrs(identifier); + this._changedAttributes = DEPRECATE_CREATE_RECORD_DATA_FOR_HOOK + ? this._store._instanceCache.getRecordData(identifier).changedAttrs(identifier) + : this._store.cache.changedAttrs(identifier); } } @@ -156,10 +161,12 @@ export default class Snapshot implements Snapshot { let attributes = (this.__attributes = Object.create(null)); const { identifier } = this; let attrs = Object.keys(this._store.getSchemaDefinitionService().attributesDefinitionFor(identifier)); - let recordData = this._store._instanceCache.getRecordData(identifier); + let cache = DEPRECATE_CREATE_RECORD_DATA_FOR_HOOK + ? this._store._instanceCache.getRecordData(identifier) + : this._store.cache; attrs.forEach((keyName) => { - attributes[keyName] = recordData.getAttr(identifier, keyName); + attributes[keyName] = cache.getAttr(identifier, keyName); }); return attributes; @@ -338,7 +345,10 @@ export default class Snapshot implements Snapshot { let inverseIdentifier = data ? store.identifierCache.getOrCreateRecordIdentifier(data) : null; if (value && value.data !== undefined) { - if (inverseIdentifier && !store._instanceCache.getRecordData(inverseIdentifier).isDeleted(inverseIdentifier)) { + const cache = DEPRECATE_CREATE_RECORD_DATA_FOR_HOOK + ? inverseIdentifier && store._instanceCache.getRecordData(inverseIdentifier) + : store.cache; + if (inverseIdentifier && !cache!.isDeleted(inverseIdentifier)) { if (returnModeIsId) { result = inverseIdentifier.id; } else { @@ -438,7 +448,10 @@ export default class Snapshot implements Snapshot { results = []; value.data.forEach((member) => { let inverseIdentifier = store.identifierCache.getOrCreateRecordIdentifier(member); - if (!store._instanceCache.getRecordData(inverseIdentifier).isDeleted(inverseIdentifier)) { + let cache = DEPRECATE_CREATE_RECORD_DATA_FOR_HOOK + ? store._instanceCache.getRecordData(inverseIdentifier) + : store.cache; + if (!cache.isDeleted(inverseIdentifier)) { if (returnModeIsIds) { (results as RecordId[]).push(inverseIdentifier.id); } else { diff --git a/packages/store/src/-private/store-service.ts b/packages/store/src/-private/store-service.ts index ba8ffa28732..cf018000a7d 100644 --- a/packages/store/src/-private/store-service.ts +++ b/packages/store/src/-private/store-service.ts @@ -10,17 +10,19 @@ import { DEBUG } from '@glimmer/env'; import { importSync } from '@embroider/macros'; import { reject, resolve } from 'rsvp'; -import type { RecordData as RecordDataClass } from '@ember-data/json-api/-private'; +import type { Cache as CacheClass } from '@ember-data/json-api/-private'; import type DSModelClass from '@ember-data/model'; import { HAS_GRAPH_PACKAGE, HAS_JSON_API_PACKAGE, HAS_MODEL_PACKAGE } from '@ember-data/private-build-infra'; import { LOG_PAYLOADS } from '@ember-data/private-build-infra/debugging'; import { + DEPRECATE_CREATE_RECORD_DATA_FOR_HOOK, DEPRECATE_HAS_RECORD, DEPRECATE_JSON_API_FALLBACK, DEPRECATE_PROMISE_PROXIES, DEPRECATE_STORE_FIND, - DEPRECATE_V1CACHE_STORE_APIS, } from '@ember-data/private-build-infra/deprecations'; +import type { RequestManager } from '@ember-data/request'; +// import { Future, RequestInfo } from '@ember-data/request/-private/types'; import type { DSModel } from '@ember-data/types/q/ds-model'; import type { CollectionResourceDocument, @@ -32,9 +34,9 @@ import type { import type { StableExistingRecordIdentifier, StableRecordIdentifier } from '@ember-data/types/q/identifier'; import type { MinimumAdapterInterface } from '@ember-data/types/q/minimum-adapter-interface'; import type { MinimumSerializerInterface } from '@ember-data/types/q/minimum-serializer-interface'; -import type { RecordData, RecordDataV1 } from '@ember-data/types/q/record-data'; +import type { Cache } from '@ember-data/types/q/record-data'; import { JsonApiValidationError } from '@ember-data/types/q/record-data-json-api'; -import type { RecordDataStoreWrapper } from '@ember-data/types/q/record-data-store-wrapper'; +import type { CacheStoreWrapper } from '@ember-data/types/q/record-data-store-wrapper'; import type { RecordInstance } from '@ember-data/types/q/record-instance'; import type { SchemaDefinitionService } from '@ember-data/types/q/schema-definition-service'; import type { FindOptions } from '@ember-data/types/q/store'; @@ -51,13 +53,13 @@ import { storeFor, StoreMap, } from './caches/instance-cache'; -import recordDataFor, { setRecordDataFor } from './caches/record-data-for'; +import peekCache, { setRecordDataFor } from './caches/record-data-for'; import RecordReference from './legacy-model-support/record-reference'; import { DSModelSchemaDefinitionService, getModelFactory } from './legacy-model-support/schema-definition-service'; import type ShimModelClass from './legacy-model-support/shim-model-class'; import { getShimClass } from './legacy-model-support/shim-model-class'; import RecordArrayManager from './managers/record-array-manager'; -import type { NonSingletonRecordDataManager } from './managers/record-data-manager'; +import { NonSingletonRecordDataManager, SingletonRecordDataManager } from './managers/record-data-manager'; import NotificationManager from './managers/record-notification-manager'; import FetchManager, { SaveOp } from './network/fetch-manager'; import { _findAll, _query, _queryRecord } from './network/finders'; @@ -75,8 +77,8 @@ import promiseRecord from './utils/promise-record'; export { storeFor }; // hello world -type RecordDataConstruct = typeof RecordDataClass; -let _RecordData: RecordDataConstruct | undefined; +type CacheConstruct = typeof CacheClass; +let _Cache: CacheConstruct | undefined; type AsyncTrackingToken = Readonly<{ label: string; trace: Error | string }>; @@ -167,9 +169,11 @@ export interface CreateRecordProperties { @extends Ember.Service */ -class Store { - __private_singleton_recordData!: RecordData; +interface Store { + createRecordDataFor?(identifier: StableRecordIdentifier, wrapper: CacheStoreWrapper): Cache; +} +class Store { declare recordArrayManager: RecordArrayManager; /** @@ -198,6 +202,7 @@ class Store { declare _trackAsyncRequestEnd: (token: AsyncTrackingToken) => void; declare __asyncWaiter: () => boolean; declare DISABLE_WAITER?: boolean; + declare requestManager: RequestManager; isDestroying: boolean = false; isDestroyed: boolean = false; @@ -326,6 +331,22 @@ class Store { return this._fetchManager.requestCache; } + /** + * Issue a request via the configured RequestManager, + * inserting the response into the cache and handing + * back a Future which resolves to a ResponseDocument + * + * @method request + * @returns {Future} + * @public + */ + // request(req: RequestInfo): Future> { + // return this.requestManager.request(req).then( + // (doc) => {}, + // (error) => {} + // ); + // } + /** * A hook which an app or addon may implement. Called when * the Store is attempting to create a Record Instance for @@ -338,28 +359,26 @@ class Store { * @method instantiateRecord (hook) * @param identifier * @param createRecordArgs - * @param recordDataFor - * @param notificationManager + * @param recordDataFor deprecated use this.cache + * @param notificationManager deprecated use this.notifications * @returns A record instance * @public */ instantiateRecord( identifier: StableRecordIdentifier, - createRecordArgs: { [key: string]: unknown }, - recordDataFor: (identifier: StableRecordIdentifier) => RecordData, - notificationManager: NotificationManager + createRecordArgs: { [key: string]: unknown } ): DSModel | RecordInstance { if (HAS_MODEL_PACKAGE) { let modelName = identifier.type; - let recordData = this._instanceCache.getRecordData(identifier); + let cache = DEPRECATE_CREATE_RECORD_DATA_FOR_HOOK ? this._instanceCache.getRecordData(identifier) : this.cache; // TODO deprecate allowing unknown args setting let createOptions: any = { _createProps: createRecordArgs, // TODO @deprecate consider deprecating accessing record properties during init which the below is necessary for _secretInit: { identifier, - recordData, + cache, store: this, cb: secretInit, }, @@ -610,15 +629,17 @@ class Store { } const identifier = this.identifierCache.createIdentifierForNewRecord(resource); - const recordData = this._instanceCache.getRecordData(identifier); + const cache = DEPRECATE_CREATE_RECORD_DATA_FOR_HOOK + ? this._instanceCache.getRecordData(identifier) + : this.cache; const createOptions = normalizeProperties( this, identifier, properties, - (recordData as NonSingletonRecordDataManager).managedVersion === '1' + (cache as NonSingletonRecordDataManager).managedVersion === '1' ); - const resultProps = recordData.clientDidCreate(identifier, createOptions); + const resultProps = cache.clientDidCreate(identifier, createOptions); this.recordArrayManager.identifierAdded(identifier); record = this._instanceCache.getRecord(identifier, resultProps); @@ -2137,7 +2158,7 @@ class Store { @param {Object} jsonApiDoc @return {StableRecordIdentifier|Array} identifiers for the primary records that had data loaded */ - _push(jsonApiDoc): StableExistingRecordIdentifier | StableExistingRecordIdentifier[] | null { + _push(jsonApiDoc: JsonApiDocument): StableExistingRecordIdentifier | StableExistingRecordIdentifier[] | null { if (DEBUG) { assertDestroyingStore(this, '_push'); } @@ -2151,45 +2172,8 @@ class Store { console.log('EmberData | Payload - push', jsonApiDoc); } } - let ret; - this._join(() => { - let included = jsonApiDoc.included; - let i, length; - - if (included) { - for (i = 0, length = included.length; i < length; i++) { - this._instanceCache.loadData(included[i]); - } - } - - if (Array.isArray(jsonApiDoc.data)) { - length = jsonApiDoc.data.length; - let identifiers = new Array(length); - - for (i = 0; i < length; i++) { - identifiers[i] = this._instanceCache.loadData(jsonApiDoc.data[i]); - } - ret = identifiers; - return; - } - - if (jsonApiDoc.data === null) { - ret = null; - return; - } - - 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' - ); - - ret = this._instanceCache.loadData(jsonApiDoc.data); - return; - }); - - return ret; + const result = this.cache.put({ data: jsonApiDoc }); + return 'data' in result ? result.data : null; } /** @@ -2367,15 +2351,17 @@ class Store { ); } - const cache = this.identifierCache; + const identifierCache = this.identifierCache; let actualIdentifier = identifier; if (operation !== 'deleteRecord' && data) { - actualIdentifier = cache.updateRecordIdentifier(identifier, data); + actualIdentifier = identifierCache.updateRecordIdentifier(identifier, data); } //We first make sure the primary data has been updated - const recordData = this._instanceCache.getRecordData(actualIdentifier); - recordData.didCommit(identifier, data); + const cache = DEPRECATE_CREATE_RECORD_DATA_FOR_HOOK + ? this._instanceCache.getRecordData(actualIdentifier) + : this.cache; + cache.didCommit(identifier, data); if (operation === 'deleteRecord') { this.recordArrayManager.identifierRemoved(actualIdentifier); @@ -2401,59 +2387,55 @@ class Store { /** * Instantiation hook allowing applications or addons to configure the store - * to utilize a custom RecordData implementation. + * to utilize a custom Cache implementation. * - * @method createRecordDataFor (hook) + * @method createCache (hook) * @public - * @param identifier * @param storeWrapper + * @returns {Cache} */ - createRecordDataFor( - identifier: StableRecordIdentifier, - storeWrapper: RecordDataStoreWrapper - ): RecordData | RecordDataV1 { + createCache(storeWrapper: CacheStoreWrapper): Cache { if (HAS_JSON_API_PACKAGE) { // we can't greedily use require as this causes // a cycle we can't easily fix (or clearly pin point) at present. // // it can be reproduced in partner tests by running // node ./scripts/packages-for-commit.js && pnpm test-external:ember-observer - if (_RecordData === undefined) { - _RecordData = (importSync('@ember-data/json-api/-private') as typeof import('@ember-data/json-api/-private')) - .RecordData; + if (_Cache === undefined) { + _Cache = (importSync('@ember-data/json-api') as typeof import('@ember-data/json-api')).Cache; } - if (DEPRECATE_V1CACHE_STORE_APIS) { - if (arguments.length === 4) { - deprecate( - `Store.createRecordDataFor(, , , ) has been deprecated in favor of Store.createRecordDataFor(, )`, - false, - { - id: 'ember-data:deprecate-v1cache-store-apis', - for: 'ember-data', - until: '5.0', - since: { enabled: '4.7', available: '4.7' }, - } - ); - identifier = this.identifierCache.getOrCreateRecordIdentifier({ - type: arguments[0], - id: arguments[1], - lid: arguments[2], - }); - storeWrapper = arguments[3]; - } - } - - this.__private_singleton_recordData = this.__private_singleton_recordData || new _RecordData(storeWrapper); - ( - this.__private_singleton_recordData as RecordData & { createCache(identifier: StableRecordIdentifier): void } - ).createCache(identifier); - return this.__private_singleton_recordData; + return new _Cache(storeWrapper); } - assert(`Expected store.createRecordDataFor to be implemented but it wasn't`); + assert(`Expected store.createCache to be implemented but it wasn't`); + } + + get cache(): Cache { + let { cache } = this._instanceCache; + if (!cache) { + cache = this._instanceCache.cache = this.createCache(this._instanceCache._storeWrapper); + if (DEBUG) { + cache = new SingletonRecordDataManager(cache); + } + } + return cache; } + /** + * [DEPRECATED] use Store.createCache + * + * Instantiation hook allowing applications or addons to configure the store + * to utilize a custom RecordData implementation. + * + * @method createRecordDataFor (hook) + * @deprecated + * @public + * @param identifier + * @param storeWrapper + * @returns {Cache} + */ + /** `normalize` converts a json payload into the normalized form that [push](../methods/push?anchor=push) expects. @@ -2738,21 +2720,21 @@ function adapterDidInvalidate( error.errors = errorsHashToArray(errorsHash); } } - const recordData = store._instanceCache.getRecordData(identifier); + const cache = DEPRECATE_CREATE_RECORD_DATA_FOR_HOOK ? store._instanceCache.getRecordData(identifier) : store.cache; if (error.errors) { assert( `Expected the RecordData implementation for ${identifier} to have a getErrors(identifier) method for retreiving errors.`, - typeof recordData.getErrors === 'function' + typeof cache.getErrors === 'function' ); let jsonApiErrors: JsonApiValidationError[] = error.errors; if (jsonApiErrors.length === 0) { jsonApiErrors = [{ title: 'Invalid Error', detail: '', source: { pointer: '/data' } }]; } - recordData.commitWasRejected(identifier, jsonApiErrors); + cache.commitWasRejected(identifier, jsonApiErrors); } else { - recordData.commitWasRejected(identifier); + cache.commitWasRejected(identifier); } } @@ -2868,7 +2850,7 @@ function extractIdentifierFromRecord( if (!recordOrPromiseRecord) { return null; } - const extract = isForV1 ? recordDataFor : recordIdentifierFor; + const extract = isForV1 ? peekCache : recordIdentifierFor; if (DEPRECATE_PROMISE_PROXIES) { if (isPromiseRecord(recordOrPromiseRecord)) { @@ -2901,13 +2883,8 @@ function isPromiseRecord(record: PromiseProxyRecord | RecordInstance): record is return !!record.then; } -function secretInit( - record: RecordInstance, - recordData: RecordData, - identifier: StableRecordIdentifier, - store: Store -): void { +function secretInit(record: RecordInstance, cache: Cache, identifier: StableRecordIdentifier, store: Store): void { setRecordIdentifier(record, identifier); StoreMap.set(record, store); - setRecordDataFor(record, recordData); + setRecordDataFor(record, cache); } diff --git a/packages/store/src/index.ts b/packages/store/src/index.ts index 74e75e7eac2..f1bc39de89c 100644 --- a/packages/store/src/index.ts +++ b/packages/store/src/index.ts @@ -1,6 +1,147 @@ /** - @module @ember-data/store -*/ + * This package provides [*Ember***Data**](https://github.com/emberjs/data/)'s `Store` class. + * + * The `Store` coordinates interaction between your application, the `Cache`, and sources of data (such as your `API` or a local persistence layer). + * Optionally, the Store can be configured to hydrate the response data into rich presentation classes. + * + * ## Installation + * + * Install using your javascript package manager of choice. For instance with [pnpm](https://pnpm.io/) + * + * ``` + * pnpm add @ember-data/store + * ``` + * + * After installing you will want to configure your first `Store`. Read more below for how to create and configure stores for your application. + * + * + * ## 🔨 Creating A Store + * + * To use a `Store` we will need to do few things: add a `Cache` to store data **in-memory**, add an `Adapter` to fetch data from a source, and implement `instantiateRecord` to tell the store how to display the data for individual resources. + * + * > **Note** If you are using the package `ember-data` then a `JSON:API` cache and `instantiateRecord` are configured for you by default. + * + * ### Configuring A Cache + * + * To start, let's install a `JSON:API` cache. If your app uses `GraphQL` or `REST` other caches may better fit your data. You can author your own cache by creating one that conforms to the [spec](). + * + * The package `@ember-data/json-api` provides a `JSON:API` cache we can use. After installing it, we can configure the store to use this cache. + * + * ```js + * import Store from '@ember-data/store'; + * import { Cache } from '@ember-data/json-api'; + * + * class extends Store { + * createCache(storeWrapper) { + * return new Cache(storeWrapper); + * } + * } + * ``` + * + * Now that we have a `cache` let's setup something to handle fetching and saving data via our API. + * + * > Note: [1] the cache from `@ember-data/json-api` is a special cache: if the package is present the `createCache` hook will automatically do the above wiring if the hook is not implemented. We still recommend implementing the hook. + * > + * > Note: [2] The `ember-data` package automatically includes the `@ember-data/json-api` cache for you. + * + * ### Adding An Adapter + * + * When *Ember***Data** needs to fetch or save data it will pass that request to your application's `Adapter` for fulfillment. How this fulfillment occurs (in-memory, device storage, via single or multiple API requests, etc.) is up to that Adapter. + * + * To start, let's install a `JSON:API` adapter. If your app uses `GraphQL` or `REST` other adapters may better fit your data. You can author your own adapter by creating one that conforms to the [spec](). + * + * The package `@ember-data/adapter` provides a `JSON:API` adapter we can use. After installing it, we can configure the store to use this adapter. + * + * ```js + * import Store from '@ember-data/store'; + * import Adapter from '@ember-data/adapter/json-api'; + * + * class extends Store { + * #adapter = new Adapter(); + * + * adapterFor() { + * return this.#adapter; + * } + * } + * ``` + * + * If you want to know more about using Adapters with Ember read the next section, else lets skip to [Presenting Data from the Cache](#presenting-data-from-the-cache) to configure how our application will interact with our data. + * + * #### Using with Ember + * + * Note: If you are using Ember and would like to make use of `service` injections in your adapter, you will want to additionally `setOwner` for the Adapter. + * + * ```js + * import Store from '@ember-data/store'; + * import Adapter from '@ember-data/adapter/json-api'; + * import { getOwner, setOwner } from '@ember/application'; + * + * class extends Store { + * #adapter = null; + * + * adapterFor() { + * let adapter = this.#adapter; + * if (!adapter) { + * const owner = getOwner(this); + * adapter = new Adapter(); + * setOwner(adapter, owner); + * this.#adapter = adapter; + * } + * + * return adapter; + * } + * } + * ``` + * + * By default when using with Ember you only need to implement this hook if you want your adapter usage to be statically analyzeable. *Ember***Data** will attempt to resolve adapters using Ember's resolver. To provide a single Adapter for your application like the above you would provide it as the default export of the file `app/adapters/application.{js/ts}` + * + * ### Presenting Data from the Cache + * + * Now that we have a source and a cach for our data, we need to configure how the Store delivers that data back to our application. We do this via the hook `instantiateRecord`, which allows us to transform the data for a resource before handing it to the application. + * + * A naive way to present the data would be to return it as JSON. Typically instead this hook will be used to add reactivity and make each uniue resource a singleton, ensuring that if the cache updates our presented data will reflect the new state. + * + * Below is an example of using the hooks `instantiateRecord` and a `teardownRecord` to provide minimal read-only reactive state for simple resources. + * + * ```ts + * import Store, { recordIdentifierFor } from '@ember-data/store'; + * import { TrackedObject } from 'tracked-built-ins'; + * + * class extends Store { + * instantiateRecord(identifier) { + * const { cache, notifications } = this; + * + * // create a TrackedObject with our attributes, id and type + * const record = new TrackedObject(Object.assign({}, cache.peek(identifier))); + * record.type = identifier.type; + * record.id = identifier.id; + * + * notifications.subscribe(identifier, (_, change) => { + * if (change === 'attributes') { + * Object.assign(record, cache.peek(identifier)); + * } + * }); + * + * return record; + * } + * } + * ``` + * + * Because `instantiateRecord` is opaque to the nature of the record, an implementation can be anything from a fairly simple object to a robust proxy that intelligently links together associated records through relationships. + * + * This also enables creating a record that separates `edit` flows from `create` flows entirely. A record class might choose to implement a `checkout`method that gives access to an editable instance while the primary record continues to be read-only and reflect only persisted (non-mutated) state. + * + * Typically you will choose an existing record implementation such as `@ember-data/model` for your application. + * + * Because of the boundaries around instantiation and the cache, record implementations should be capable of interop both with each other and with any `Cache`. Due to this, if needed an application can utilize multiple record implementations and multiple cache implementations either to support enhanced features for only a subset of records or to be able to incrementally migrate from one record/cache to another record or cache. + * + * > Note: [1] `@ember-data/model` is a special record implementation: if the package is present the `instantiateRecord` hook will automatically do the above wiring if the hook is not implemented. Due to the complexity of this legacy package's use of Ember's resolver, we do not recommend wiring this package manually. + * > + * > Note: [2] The `ember-data` package automatically includes the `@ember-data/model` implementation for you. + * + * @module @ember-data/store + * @main @ember-data/store + */ export { Store as default, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e85b383e661..07bf1e3891a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4643,7 +4643,7 @@ packages: /@types/ember__controller/4.0.4: resolution: {integrity: sha512-+f0knTIJJkRX5xijeSI/n4FvLfhMFFxIxODyFFFFB483EryYuts3QzpTwU5D66WQ5rAbZvpPRXRMPTTCNJoUhg==} dependencies: - '@types/ember__object': 4.0.5_@babel+core@7.19.6 + '@types/ember__object': 4.0.5_@babel+core@7.20.2 dev: true /@types/ember__controller/4.0.4_@babel+core@7.19.6: diff --git a/tests/adapter-encapsulation/app/services/store.js b/tests/adapter-encapsulation/app/services/store.js index 7f524ecfa7a..d079fcebea3 100644 --- a/tests/adapter-encapsulation/app/services/store.js +++ b/tests/adapter-encapsulation/app/services/store.js @@ -1,10 +1,8 @@ -import { RecordData } from '@ember-data/json-api/-private'; +import { Cache } from '@ember-data/json-api'; import Store from '@ember-data/store'; export default class DefaultStore extends Store { - createRecordDataFor(identifier, storeWrapper) { - this.__private_singleton_recordData = this.__private_singleton_recordData || new RecordData(storeWrapper); - this.__private_singleton_recordData.createCache(identifier); - return this.__private_singleton_recordData; + createCache(storeWrapper) { + return new Cache(storeWrapper); } } diff --git a/tests/docs/fixtures/expected.js b/tests/docs/fixtures/expected.js index e9fc04126da..2cc6cccb249 100644 --- a/tests/docs/fixtures/expected.js +++ b/tests/docs/fixtures/expected.js @@ -320,6 +320,7 @@ module.exports = { '(public) @ember-data/store RecordArray#type', '(public) @ember-data/store RecordArray#update', '(public) @ember-data/store RecordDataManager#sync', + '(public) @ember-data/store RecordDataManager#peek', '(public) @ember-data/store RecordDataManager#addToHasMany', '(public) @ember-data/store RecordDataManager#changedAttributes', '(public) @ember-data/store RecordDataManager#changedAttrs', @@ -403,6 +404,7 @@ module.exports = { '(public) @ember-data/store Store#unloadAll', '(public) @ember-data/store Store#unloadRecord', '(public) @ember-data/store Store#saveRecord', + '(public) @ember-data/store Store#createCache (hook)', '(public) @ember-data/store Store#createRecordDataFor (hook)', '(public) @ember-data/store Store#instantiateRecord (hook)', '(public) @ember-data/store Store#teardownRecord (hook)', diff --git a/tests/graph/tests/integration/graph/edge-removal/helpers.ts b/tests/graph/tests/integration/graph/edge-removal/helpers.ts index fcdee19e0ac..eae3b8b9156 100644 --- a/tests/graph/tests/integration/graph/edge-removal/helpers.ts +++ b/tests/graph/tests/integration/graph/edge-removal/helpers.ts @@ -1,6 +1,6 @@ import { settled } from '@ember/test-helpers'; -import { ImplicitRelationship } from '@ember-data/graph/-private/graph'; +import type { ImplicitRelationship } from '@ember-data/graph/-private/graph'; import Model, { attr, belongsTo, hasMany } from '@ember-data/model'; import { recordIdentifierFor } from '@ember-data/store'; import type { StableRecordIdentifier } from '@ember-data/types/q/identifier'; diff --git a/tests/graph/tests/integration/graph/edge-test.ts b/tests/graph/tests/integration/graph/edge-test.ts index 333ee09f6cf..2b89746dec6 100644 --- a/tests/graph/tests/integration/graph/edge-test.ts +++ b/tests/graph/tests/integration/graph/edge-test.ts @@ -5,7 +5,7 @@ import { setupTest } from 'ember-qunit'; import { graphFor } from '@ember-data/graph/-private'; import Model, { attr, belongsTo, hasMany } from '@ember-data/model'; import Store from '@ember-data/store'; -import { recordDataFor } from '@ember-data/store/-private'; +import { peekCache } from '@ember-data/store/-private'; import { stateOf } from './edge-removal/setup'; @@ -43,7 +43,7 @@ module('Integration | Graph | Edges', function (hooks) { const bestFriend = graph.get(identifier, 'bestFriend'); assert.strictEqual( - recordDataFor(identifier), + peekCache(identifier), null, 'We have no record data instance afer accessing the relationships for this identifier' ); @@ -51,7 +51,7 @@ module('Integration | Graph | Edges', function (hooks) { assert.ok(bestFriend, 'We can access a specific relationship'); assert.strictEqual( - recordDataFor(identifier), + peekCache(identifier), null, 'We still have no record data instance after accessing a named relationship' ); @@ -66,7 +66,7 @@ module('Integration | Graph | Edges', function (hooks) { }); assert.strictEqual( - recordDataFor(identifier), + peekCache(identifier), null, 'We still have no record data instance after push of only an identifier within a relationship' ); @@ -84,7 +84,7 @@ module('Integration | Graph | Edges', function (hooks) { }); assert.strictEqual( - recordDataFor(identifier)?.getAttr(identifier, 'name'), + peekCache(identifier)?.getAttr(identifier, 'name'), 'Chris', 'We lazily associate the correct record data instance' ); @@ -113,7 +113,7 @@ module('Integration | Graph | Edges', function (hooks) { }); assert.strictEqual( - recordDataFor(identifier), + peekCache(identifier), null, 'We have no record data instance after push of only an identifier within a relationship' ); @@ -137,7 +137,7 @@ module('Integration | Graph | Edges', function (hooks) { assert.deepEqual(state.local, [identifier3], 'Our current state is correct after canonical update'); assert.strictEqual( - recordDataFor(identifier), + peekCache(identifier), null, 'We still have no record data instance after updating the canonical state' ); @@ -156,7 +156,7 @@ module('Integration | Graph | Edges', function (hooks) { assert.deepEqual(state.local, [identifier2], 'Our current state is correct after local update'); assert.strictEqual( - recordDataFor(identifier), + peekCache(identifier), null, 'We still have no record data instance after updating the local state' ); @@ -170,7 +170,7 @@ module('Integration | Graph | Edges', function (hooks) { }); assert.strictEqual( - recordDataFor(identifier)?.getAttr(identifier, 'name'), + peekCache(identifier)?.getAttr(identifier, 'name'), 'Chris', 'We lazily associate the correct record data instance' ); @@ -199,7 +199,7 @@ module('Integration | Graph | Edges', function (hooks) { }); assert.strictEqual( - recordDataFor(identifier), + peekCache(identifier), null, 'We have no record data instance after push of only an identifier within a relationship' ); @@ -223,7 +223,7 @@ module('Integration | Graph | Edges', function (hooks) { assert.deepEqual(state.local, [identifier3], 'Our current state is correct after canonical update'); assert.strictEqual( - recordDataFor(identifier), + peekCache(identifier), null, 'We still have no record data instance after updating the canonical state' ); @@ -242,7 +242,7 @@ module('Integration | Graph | Edges', function (hooks) { assert.deepEqual(state.local, [identifier2], 'Our current state is correct after local update'); assert.strictEqual( - recordDataFor(identifier), + peekCache(identifier), null, 'We still have no record data instance after updating the local state' ); @@ -256,7 +256,7 @@ module('Integration | Graph | Edges', function (hooks) { }); assert.strictEqual( - recordDataFor(identifier)?.getAttr(identifier, 'name'), + peekCache(identifier)?.getAttr(identifier, 'name'), 'Chris', 'We lazily associate the correct record data instance' ); @@ -290,7 +290,7 @@ module('Integration | Graph | Edges', function (hooks) { }); assert.strictEqual( - recordDataFor(identifier), + peekCache(identifier), null, 'We have no record data instance after push of only an identifier within a relationship' ); @@ -317,7 +317,7 @@ module('Integration | Graph | Edges', function (hooks) { assert.deepEqual(state.local, [identifier2, identifier3], 'Our current state is correct after canonical update'); assert.strictEqual( - recordDataFor(identifier), + peekCache(identifier), null, 'We still have no record data instance after updating the canonical state' ); @@ -341,7 +341,7 @@ module('Integration | Graph | Edges', function (hooks) { ); assert.strictEqual( - recordDataFor(identifier), + peekCache(identifier), null, 'We still have no record data instance after updating the local state' ); @@ -355,7 +355,7 @@ module('Integration | Graph | Edges', function (hooks) { }); assert.strictEqual( - recordDataFor(identifier)?.getAttr(identifier, 'name'), + peekCache(identifier)?.getAttr(identifier, 'name'), 'Chris', 'We lazily associate the correct record data instance' ); @@ -389,7 +389,7 @@ module('Integration | Graph | Edges', function (hooks) { }); assert.strictEqual( - recordDataFor(identifier), + peekCache(identifier), null, 'We have no record data instance after push of only an identifier within a relationship' ); @@ -416,7 +416,7 @@ module('Integration | Graph | Edges', function (hooks) { assert.deepEqual(state.local, [identifier2, identifier3], 'Our current state is correct after canonical update'); assert.strictEqual( - recordDataFor(identifier), + peekCache(identifier), null, 'We still have no record data instance after updating the canonical state' ); @@ -440,7 +440,7 @@ module('Integration | Graph | Edges', function (hooks) { ); assert.strictEqual( - recordDataFor(identifier), + peekCache(identifier), null, 'We still have no record data instance after updating the local state' ); @@ -454,7 +454,7 @@ module('Integration | Graph | Edges', function (hooks) { }); assert.strictEqual( - recordDataFor(identifier)?.getAttr(identifier, 'name'), + peekCache(identifier)?.getAttr(identifier, 'name'), 'Chris', 'We lazily associate the correct record data instance' ); diff --git a/tests/main/tests/helpers/accessors.ts b/tests/main/tests/helpers/accessors.ts index 87837fe9a89..3b4ed2e5807 100644 --- a/tests/main/tests/helpers/accessors.ts +++ b/tests/main/tests/helpers/accessors.ts @@ -5,7 +5,7 @@ import type ManyRelationship from '@ember-data/graph/-private/relationships/stat import type Store from '@ember-data/store'; import { recordIdentifierFor } from '@ember-data/store'; import type { StableRecordIdentifier } from '@ember-data/types/q/identifier'; -import type { RecordDataStoreWrapper } from '@ember-data/types/q/record-data-store-wrapper'; +import type { CacheStoreWrapper } from '@ember-data/types/q/record-data-store-wrapper'; import type { ConfidentDict as RelationshipDict } from '@ember-data/types/q/utils'; export function getRelationshipStateForRecord( @@ -28,7 +28,7 @@ export function hasRelationshipForRecord( } export function implicitRelationshipsFor( - storeWrapper: RecordDataStoreWrapper, + storeWrapper: CacheStoreWrapper, identifier: StableRecordIdentifier ): RelationshipDict { const rels = graphFor(storeWrapper).identifiers.get(identifier); diff --git a/tests/main/tests/integration/record-data/record-data-errors-test.ts b/tests/main/tests/integration/record-data/record-data-errors-test.ts index 4977b1fd61f..47e96af709c 100644 --- a/tests/main/tests/integration/record-data/record-data-errors-test.ts +++ b/tests/main/tests/integration/record-data/record-data-errors-test.ts @@ -14,9 +14,9 @@ import Store, { recordIdentifierFor } from '@ember-data/store'; import { DSModel } from '@ember-data/types/q/ds-model'; import { CollectionResourceRelationship, SingleResourceRelationship } from '@ember-data/types/q/ember-data-json-api'; import type { NewRecordIdentifier, RecordIdentifier, StableRecordIdentifier } from '@ember-data/types/q/identifier'; -import type { ChangedAttributesHash, MergeOperation, RecordData, RecordDataV1 } from '@ember-data/types/q/record-data'; +import type { Cache, ChangedAttributesHash, MergeOperation, RecordDataV1 } from '@ember-data/types/q/record-data'; import type { JsonApiResource, JsonApiValidationError } from '@ember-data/types/q/record-data-json-api'; -import type { RecordDataStoreWrapper } from '@ember-data/types/q/record-data-store-wrapper'; +import type { CacheStoreWrapper } from '@ember-data/types/q/record-data-store-wrapper'; import { Dict } from '@ember-data/types/q/utils'; if (!DEPRECATE_V1_RECORD_DATA) { @@ -25,7 +25,10 @@ if (!DEPRECATE_V1_RECORD_DATA) { @attr declare lastName: string; } - class TestRecordData implements RecordData { + class TestRecordData implements Cache { + peek(identifier: StableRecordIdentifier): Record { + throw new Error('Method not implemented.'); + } sync(op: MergeOperation): void { throw new Error('Method not implemented.'); } @@ -121,7 +124,7 @@ if (!DEPRECATE_V1_RECORD_DATA) { } } class TestStore extends Store { - createRecordDataFor(identifier: StableRecordIdentifier, storeWrapper: RecordDataStoreWrapper) { + createRecordDataFor(identifier: StableRecordIdentifier, storeWrapper: CacheStoreWrapper) { return new LifecycleRecordData(); } } @@ -181,7 +184,7 @@ if (!DEPRECATE_V1_RECORD_DATA) { } } class TestStore extends Store { - createRecordDataFor(identifier: StableRecordIdentifier, storeWrapper: RecordDataStoreWrapper) { + createRecordDataFor(identifier: StableRecordIdentifier, storeWrapper: CacheStoreWrapper) { return new LifecycleRecordData(); } } @@ -231,7 +234,7 @@ if (!DEPRECATE_V1_RECORD_DATA) { } class TestStore extends Store { - createRecordDataFor(identifier: StableRecordIdentifier, sw: RecordDataStoreWrapper): RecordData { + createRecordDataFor(identifier: StableRecordIdentifier, sw: CacheStoreWrapper): Cache { storeWrapper = sw; return new LifecycleRecordData(); } @@ -384,15 +387,15 @@ if (!DEPRECATE_V1_RECORD_DATA) { return false; } - addToHasMany(key: string, recordDatas: RecordData[], idx?: number) {} - removeFromHasMany(key: string, recordDatas: RecordData[]) {} - setDirtyHasMany(key: string, recordDatas: RecordData[]) {} + addToHasMany(key: string, recordDatas: Cache[], idx?: number) {} + removeFromHasMany(key: string, recordDatas: Cache[]) {} + setDirtyHasMany(key: string, recordDatas: Cache[]) {} getBelongsTo(key: string) { return {}; } - setDirtyBelongsTo(name: string, recordData: RecordData | null) {} + setDirtyBelongsTo(name: string, recordData: Cache | null) {} didCommit(data) {} @@ -402,7 +405,7 @@ if (!DEPRECATE_V1_RECORD_DATA) { } class CustomStore extends Store { - createRecordDataFor(identifier: StableRecordIdentifier, wrapper: RecordDataStoreWrapper) { + createRecordDataFor(identifier: StableRecordIdentifier, wrapper: CacheStoreWrapper) { return new TestRecordData(); } } @@ -437,7 +440,7 @@ if (!DEPRECATE_V1_RECORD_DATA) { } class TestStore extends Store { - createRecordDataFor(identifier: StableRecordIdentifier, storeWrapper: RecordDataStoreWrapper) { + createRecordDataFor(identifier: StableRecordIdentifier, storeWrapper: CacheStoreWrapper) { return new LifecycleRecordData(); } } @@ -497,7 +500,7 @@ if (!DEPRECATE_V1_RECORD_DATA) { } class TestStore extends Store { - createRecordDataFor(identifier: StableRecordIdentifier, wrapper: RecordDataStoreWrapper) { + createRecordDataFor(identifier: StableRecordIdentifier, wrapper: CacheStoreWrapper) { return new LifecycleRecordData(); } } @@ -558,7 +561,7 @@ if (!DEPRECATE_V1_RECORD_DATA) { } class TestStore extends Store { - createRecordDataFor(identifier: StableRecordIdentifier, wrapper: RecordDataStoreWrapper) { + createRecordDataFor(identifier: StableRecordIdentifier, wrapper: CacheStoreWrapper) { return new LifecycleRecordData(wrapper); } } diff --git a/tests/main/tests/integration/record-data/record-data-state-test.ts b/tests/main/tests/integration/record-data/record-data-state-test.ts index 542b5053115..96418476905 100644 --- a/tests/main/tests/integration/record-data/record-data-state-test.ts +++ b/tests/main/tests/integration/record-data/record-data-state-test.ts @@ -13,9 +13,9 @@ import JSONAPISerializer from '@ember-data/serializer/json-api'; import Store, { recordIdentifierFor } from '@ember-data/store'; import { CollectionResourceRelationship, SingleResourceRelationship } from '@ember-data/types/q/ember-data-json-api'; import type { NewRecordIdentifier, RecordIdentifier, StableRecordIdentifier } from '@ember-data/types/q/identifier'; -import type { ChangedAttributesHash, MergeOperation, RecordData, RecordDataV1 } from '@ember-data/types/q/record-data'; +import type { Cache, ChangedAttributesHash, MergeOperation, RecordDataV1 } from '@ember-data/types/q/record-data'; import type { JsonApiResource, JsonApiValidationError } from '@ember-data/types/q/record-data-json-api'; -import { RecordDataStoreWrapper } from '@ember-data/types/q/record-data-store-wrapper'; +import { CacheStoreWrapper } from '@ember-data/types/q/record-data-store-wrapper'; import { Dict } from '@ember-data/types/q/utils'; class Person extends Model { @@ -98,15 +98,15 @@ class V1TestRecordData implements RecordDataV1 { return false; } - addToHasMany(key: string, recordDatas: RecordData[], idx?: number) {} - removeFromHasMany(key: string, recordDatas: RecordData[]) {} - setDirtyHasMany(key: string, recordDatas: RecordData[]) {} + addToHasMany(key: string, recordDatas: Cache[], idx?: number) {} + removeFromHasMany(key: string, recordDatas: Cache[]) {} + setDirtyHasMany(key: string, recordDatas: Cache[]) {} getBelongsTo(key: string) { return {}; } - setDirtyBelongsTo(name: string, recordData: RecordData | null) {} + setDirtyBelongsTo(name: string, recordData: Cache | null) {} didCommit(data) {} @@ -114,7 +114,10 @@ class V1TestRecordData implements RecordDataV1 { return {}; } } -class V2TestRecordData implements RecordData { +class V2TestRecordData implements Cache { + peek(identifier: StableRecordIdentifier): Record { + throw new Error('Method not implemented.'); + } sync(op: MergeOperation): void { throw new Error('Method not implemented.'); } @@ -196,7 +199,7 @@ class V2TestRecordData implements RecordData { const TestRecordData = DEPRECATE_V1_RECORD_DATA ? V1TestRecordData : V2TestRecordData; class CustomStore extends Store { - createRecordDataFor(identifier: StableRecordIdentifier, wrapper: RecordDataStoreWrapper) { + createRecordDataFor(identifier: StableRecordIdentifier, wrapper: CacheStoreWrapper) { return new TestRecordData(); } } @@ -252,7 +255,7 @@ module('integration/record-data - Record Data State', function (hooks) { class TestStore extends Store { // @ts-expect-error - createRecordDataFor(identifier: StableRecordIdentifier, wrapper: RecordDataStoreWrapper) { + createRecordDataFor(identifier: StableRecordIdentifier, wrapper: CacheStoreWrapper) { return new LifecycleRecordData(); } } @@ -348,7 +351,7 @@ module('integration/record-data - Record Data State', function (hooks) { class TestStore extends Store { // @ts-expect-error - createRecordDataFor(identifier: StableRecordIdentifier, wrapper: RecordDataStoreWrapper) { + createRecordDataFor(identifier: StableRecordIdentifier, wrapper: CacheStoreWrapper) { return new LifecycleRecordData(wrapper); } } diff --git a/tests/main/tests/integration/record-data/record-data-test.ts b/tests/main/tests/integration/record-data/record-data-test.ts index 2b2be4f3954..14027b0fc76 100644 --- a/tests/main/tests/integration/record-data/record-data-test.ts +++ b/tests/main/tests/integration/record-data/record-data-test.ts @@ -16,9 +16,9 @@ import type { SingleResourceRelationship, } from '@ember-data/types/q/ember-data-json-api'; import type { StableRecordIdentifier } from '@ember-data/types/q/identifier'; -import type { ChangedAttributesHash, MergeOperation, RecordData } from '@ember-data/types/q/record-data'; +import type { Cache, ChangedAttributesHash, MergeOperation } from '@ember-data/types/q/record-data'; import type { JsonApiResource, JsonApiValidationError } from '@ember-data/types/q/record-data-json-api'; -import type { RecordDataStoreWrapper } from '@ember-data/types/q/record-data-store-wrapper'; +import type { CacheStoreWrapper } from '@ember-data/types/q/record-data-store-wrapper'; import type { Dict } from '@ember-data/types/q/utils'; class Person extends Model { @@ -42,10 +42,10 @@ class House extends Model { // TODO: this should work // class TestRecordData implements RecordDatav1 class V1TestRecordData { - _storeWrapper: RecordDataStoreWrapper; + _storeWrapper: CacheStoreWrapper; _identifier: StableRecordIdentifier; - constructor(wrapper: RecordDataStoreWrapper, identifier: StableRecordIdentifier) { + constructor(wrapper: CacheStoreWrapper, identifier: StableRecordIdentifier) { this._storeWrapper = wrapper; this._identifier = identifier; } @@ -107,18 +107,21 @@ class V1TestRecordData { } } -class V2TestRecordData implements RecordData { +class V2TestRecordData implements Cache { version: '2' = '2'; _errors?: JsonApiValidationError[]; _isNew: boolean = false; - _storeWrapper: RecordDataStoreWrapper; + _storeWrapper: CacheStoreWrapper; _identifier: StableRecordIdentifier; - constructor(wrapper: RecordDataStoreWrapper, identifier: StableRecordIdentifier) { + constructor(wrapper: CacheStoreWrapper, identifier: StableRecordIdentifier) { this._storeWrapper = wrapper; this._identifier = identifier; } + peek(identifier: StableRecordIdentifier): Record { + throw new Error('Method not implemented.'); + } sync(op: MergeOperation): void { throw new Error('Method not implemented.'); } @@ -188,7 +191,7 @@ const TestRecordData: typeof V2TestRecordData | typeof V1TestRecordData = !DEPRE class CustomStore extends Store { // @ts-expect-error - createRecordDataFor(identifier: StableRecordIdentifier, storeWrapper: RecordDataStoreWrapper) { + createRecordDataFor(identifier: StableRecordIdentifier, storeWrapper: CacheStoreWrapper) { return new TestRecordData(storeWrapper, identifier); } } @@ -353,7 +356,7 @@ module('integration/record-data - Custom RecordData Implementations', function ( class TestStore extends Store { // @ts-expect-error - createRecordDataFor(identifier: StableRecordIdentifier, storeWrapper: RecordDataStoreWrapper) { + createRecordDataFor(identifier: StableRecordIdentifier, storeWrapper: CacheStoreWrapper) { return new LifecycleRecordData(storeWrapper, identifier); } } @@ -495,7 +498,7 @@ module('integration/record-data - Custom RecordData Implementations', function ( class TestStore extends Store { // @ts-expect-error - createRecordDataFor(identifier: StableRecordIdentifier, storeWrapper: RecordDataStoreWrapper) { + createRecordDataFor(identifier: StableRecordIdentifier, storeWrapper: CacheStoreWrapper) { return new AttributeRecordData(storeWrapper, identifier); } } @@ -564,11 +567,11 @@ module('integration/record-data - Custom RecordData Implementations', function ( class TestStore extends Store { // @ts-expect-error - createRecordDataFor(identifier: StableRecordIdentifier, storeWrapper: RecordDataStoreWrapper) { + createRecordDataFor(identifier: StableRecordIdentifier, storeWrapper: CacheStoreWrapper) { if (identifier.type === 'house') { return new RelationshipRecordData(storeWrapper, identifier); } else { - return super.createRecordDataFor(identifier, storeWrapper); + return this.cache; } } } @@ -634,11 +637,11 @@ module('integration/record-data - Custom RecordData Implementations', function ( }; } class TestStore extends Store { - createRecordDataFor(identifier: StableRecordIdentifier, storeWrapper: RecordDataStoreWrapper) { + createRecordDataFor(identifier: StableRecordIdentifier, storeWrapper: CacheStoreWrapper) { if (identifier.type === 'house') { return new RelationshipRecordData(storeWrapper, identifier); } else { - return super.createRecordDataFor(identifier, storeWrapper); + return this.cache; } } } @@ -727,13 +730,13 @@ module('integration/record-data - Custom RecordData Implementations', function ( class TestStore extends Store { // @ts-expect-error - createRecordDataFor(identifier: StableRecordIdentifier, storeWrapper: RecordDataStoreWrapper) { + createRecordDataFor(identifier: StableRecordIdentifier, storeWrapper: CacheStoreWrapper) { if (identifier.type === 'house') { notifier = storeWrapper; houseIdentifier = identifier; return new RelationshipRecordData(storeWrapper, identifier); } else { - return super.createRecordDataFor(identifier, storeWrapper); + return this.cache; } } } @@ -872,11 +875,11 @@ module('integration/record-data - Custom RecordData Implementations', function ( class TestStore extends Store { // @ts-expect-error - createRecordDataFor(identifier: StableRecordIdentifier, storeWrapper: RecordDataStoreWrapper) { + createRecordDataFor(identifier: StableRecordIdentifier, storeWrapper: CacheStoreWrapper) { if (identifier.type === 'house') { return new RelationshipRecordData(storeWrapper, identifier); } else { - return super.createRecordDataFor(identifier, storeWrapper); + return this.cache; } } } diff --git a/tests/main/tests/integration/record-data/store-wrapper-test.ts b/tests/main/tests/integration/record-data/store-wrapper-test.ts index fedcb37f78d..520941b5248 100644 --- a/tests/main/tests/integration/record-data/store-wrapper-test.ts +++ b/tests/main/tests/integration/record-data/store-wrapper-test.ts @@ -6,7 +6,7 @@ import Model, { attr, belongsTo, hasMany } from '@ember-data/model'; import { DEPRECATE_V1_RECORD_DATA } from '@ember-data/private-build-infra/deprecations'; import Store from '@ember-data/store'; import { StableRecordIdentifier } from '@ember-data/types/q/identifier'; -import { RecordDataStoreWrapper } from '@ember-data/types/q/record-data-store-wrapper'; +import { CacheStoreWrapper } from '@ember-data/types/q/record-data-store-wrapper'; import publicProps from '@ember-data/unpublished-test-infra/test-support/public-props'; class Person extends Model { @@ -97,7 +97,7 @@ class TestRecordData { class CustomStore extends Store { // @ts-expect-error - createRecordDataFor(identifier: StableRecordIdentifier, wrapper: RecordDataStoreWrapper) { + createRecordDataFor(identifier: StableRecordIdentifier, wrapper: CacheStoreWrapper) { return new TestRecordData(); } } @@ -139,7 +139,7 @@ module('integration/store-wrapper - RecordData StoreWrapper tests', function (ho let { owner } = this; class RelationshipRD extends TestRecordData { - constructor(identifier: StableRecordIdentifier, storeWrapper: RecordDataStoreWrapper) { + constructor(identifier: StableRecordIdentifier, storeWrapper: CacheStoreWrapper) { super(); let houseAttrs = { name: { @@ -205,7 +205,7 @@ module('integration/store-wrapper - RecordData StoreWrapper tests', function (ho class TestStore extends Store { // @ts-expect-error - createRecordDataFor(identifier: StableRecordIdentifier, wrapper: RecordDataStoreWrapper) { + createRecordDataFor(identifier: StableRecordIdentifier, wrapper: CacheStoreWrapper) { if (identifier.type === 'house') { return new RelationshipRD(identifier, wrapper); } else { @@ -234,7 +234,7 @@ module('integration/store-wrapper - RecordData StoreWrapper tests', function (ho class RecordDataForTest extends TestRecordData { id: string; - constructor(identifier: StableRecordIdentifier, storeWrapper: RecordDataStoreWrapper) { + constructor(identifier: StableRecordIdentifier, storeWrapper: CacheStoreWrapper) { super(); count++; this.id = identifier.id!; @@ -260,11 +260,11 @@ module('integration/store-wrapper - RecordData StoreWrapper tests', function (ho class TestStore extends Store { // @ts-expect-error - createRecordDataFor(identifier: StableRecordIdentifier, wrapper: RecordDataStoreWrapper) { + createRecordDataFor(identifier: StableRecordIdentifier, wrapper: CacheStoreWrapper) { if (identifier.type === 'house') { return new RecordDataForTest(identifier, wrapper); } else { - return super.createRecordDataFor(identifier, wrapper); + return this.cache; } } } @@ -294,7 +294,7 @@ module('integration/store-wrapper - RecordData StoreWrapper tests', function (ho id: string; _isNew: boolean = false; - constructor(identifier: StableRecordIdentifier, wrapper: RecordDataStoreWrapper) { + constructor(identifier: StableRecordIdentifier, wrapper: CacheStoreWrapper) { super(); count++; this.id = identifier.id!; @@ -321,7 +321,7 @@ module('integration/store-wrapper - RecordData StoreWrapper tests', function (ho class TestStore extends Store { // @ts-expect-error - createRecordDataFor(identifier: StableRecordIdentifier, wrapper: RecordDataStoreWrapper) { + createRecordDataFor(identifier: StableRecordIdentifier, wrapper: CacheStoreWrapper) { if (identifier.type === 'house') { return new RecordDataForTest(identifier, wrapper); } else { @@ -362,7 +362,7 @@ module('integration/store-wrapper - RecordData StoreWrapper tests', function (ho class RecordDataForTest extends TestRecordData { id: string; - constructor(identifier: StableRecordIdentifier, wrapper: RecordDataStoreWrapper) { + constructor(identifier: StableRecordIdentifier, wrapper: CacheStoreWrapper) { super(); wrapper.setRecordId(identifier, '17'); this.id = '17'; @@ -371,7 +371,7 @@ module('integration/store-wrapper - RecordData StoreWrapper tests', function (ho class TestStore extends Store { // @ts-expect-error - createRecordDataFor(identifier: StableRecordIdentifier, wrapper: RecordDataStoreWrapper) { + createRecordDataFor(identifier: StableRecordIdentifier, wrapper: CacheStoreWrapper) { if (identifier.type === 'house') { return new RecordDataForTest(identifier, wrapper); } else { @@ -400,7 +400,7 @@ module('integration/store-wrapper - RecordData StoreWrapper tests', function (ho let { owner } = this; class RecordDataForTest extends TestRecordData { - constructor(identifier: StableRecordIdentifier, wrapper: RecordDataStoreWrapper) { + constructor(identifier: StableRecordIdentifier, wrapper: CacheStoreWrapper) { super(); if (!identifier.id) { const id1 = wrapper.identifierCache.getOrCreateRecordIdentifier({ type: 'house', id: '1' }); @@ -415,7 +415,7 @@ module('integration/store-wrapper - RecordData StoreWrapper tests', function (ho class TestStore extends Store { // @ts-expect-error - createRecordDataFor(identifier: StableRecordIdentifier, wrapper: RecordDataStoreWrapper) { + createRecordDataFor(identifier: StableRecordIdentifier, wrapper: CacheStoreWrapper) { if (identifier.type === 'house') { return new RecordDataForTest(identifier, wrapper); } else { @@ -449,7 +449,7 @@ module('integration/store-wrapper - RecordData StoreWrapper tests', function (ho let identifier; class RecordDataForTest extends TestRecordData { - constructor(stableIdentifier: StableRecordIdentifier, storeWrapper: RecordDataStoreWrapper) { + constructor(stableIdentifier: StableRecordIdentifier, storeWrapper: CacheStoreWrapper) { super(); wrapper = storeWrapper; identifier = stableIdentifier; @@ -458,7 +458,7 @@ module('integration/store-wrapper - RecordData StoreWrapper tests', function (ho class TestStore extends Store { // @ts-expect-error - createRecordDataFor(identifier: StableRecordIdentifier, wrapper: RecordDataStoreWrapper) { + createRecordDataFor(identifier: StableRecordIdentifier, wrapper: CacheStoreWrapper) { if (identifier.type === 'house') { return new RecordDataForTest(identifier, wrapper); } else { diff --git a/tests/main/tests/integration/record-data/unloading-record-data-test.js b/tests/main/tests/integration/record-data/unloading-record-data-test.js index 9fa679788aa..c4108f0de1a 100644 --- a/tests/main/tests/integration/record-data/unloading-record-data-test.js +++ b/tests/main/tests/integration/record-data/unloading-record-data-test.js @@ -173,7 +173,6 @@ module('RecordData Compatibility', function (hooks) { const CustomRecordData = DEPRECATE_V1_RECORD_DATA ? V1CustomRecordData : V2CustomRecordData; test(`store.unloadRecord on a record with default RecordData with relationship to a record with custom RecordData does not error`, async function (assert) { - const originalCreateRecordDataFor = store.createRecordDataFor; let customCalled = 0, customCalledFor = [], originalCalled = 0, @@ -186,7 +185,7 @@ module('RecordData Compatibility', function (hooks) { } else { originalCalled++; originalCalledFor.push(identifier); - return originalCreateRecordDataFor.call(this, identifier, storeWrapper); + return this.cache; } }; diff --git a/tests/main/tests/integration/records/delete-record-test.js b/tests/main/tests/integration/records/delete-record-test.js index ca125137e12..78fa776fae5 100644 --- a/tests/main/tests/integration/records/delete-record-test.js +++ b/tests/main/tests/integration/records/delete-record-test.js @@ -230,11 +230,11 @@ module('integration/deletedRecord - Deleting Records', function (hooks) { assert.strictEqual(get(store.peekAll('person'), 'length'), 1, 'The new person should be in the store'); let identifier = recordIdentifierFor(record); - let recordData = store._instanceCache.getRecordData(identifier); + let cache = store.cache; record.deleteRecord(); - assert.true(recordData.isEmpty(identifier), 'new person state is empty'); + assert.true(cache.isEmpty(identifier), 'new person state is empty'); assert.strictEqual(get(store.peekAll('person'), 'length'), 0, 'The new person should be removed from the store'); }); @@ -273,11 +273,10 @@ module('integration/deletedRecord - Deleting Records', function (hooks) { assert.strictEqual(get(store.peekAll('person'), 'length'), 1, 'The new person should be in the store'); let identifier = recordIdentifierFor(record); - let recordData = store._instanceCache.getRecordData(identifier); await record.destroyRecord(); - assert.true(recordData.isEmpty(identifier), 'new person state is empty'); + assert.true(store.cache.isEmpty(identifier), 'new person state is empty'); assert.strictEqual(get(store.peekAll('person'), 'length'), 0, 'The new person should be removed from the store'); }); @@ -329,11 +328,10 @@ module('integration/deletedRecord - Deleting Records', function (hooks) { assert.strictEqual(get(store.peekAll('person'), 'length'), 1, 'The new person should be in the store'); let identifier = recordIdentifierFor(record); - let recordData = store._instanceCache.getRecordData(identifier); record.deleteRecord(); - assert.true(recordData.isEmpty(identifier), 'We reached the correct persisted saved state'); + assert.true(store.cache.isEmpty(identifier), 'We reached the correct persisted saved state'); assert.strictEqual(get(store.peekAll('person'), 'length'), 0, 'The new person should be removed from the store'); assert.strictEqual( store._instanceCache.peek({ identifier, bucket: 'recordData' }), @@ -363,12 +361,11 @@ module('integration/deletedRecord - Deleting Records', function (hooks) { assert.strictEqual(get(store.peekAll('person'), 'length'), 1, 'The new person should be in the store'); let identifier = recordIdentifierFor(record); - let recordData = store._instanceCache.getRecordData(identifier); record.deleteRecord(); await settled(); - assert.true(recordData.isEmpty(identifier), 'We reached the correct persisted saved state'); + assert.true(store.cache.isEmpty(identifier), 'We reached the correct persisted saved state'); assert.strictEqual(get(store.peekAll('person'), 'length'), 0, 'The new person should be removed from the store'); assert.strictEqual( store._instanceCache.peek({ identifier, bucket: 'recordData' }), diff --git a/tests/main/tests/integration/records/unload-test.js b/tests/main/tests/integration/records/unload-test.js index 02a1ca36be1..94fd80a66ab 100644 --- a/tests/main/tests/integration/records/unload-test.js +++ b/tests/main/tests/integration/records/unload-test.js @@ -739,14 +739,13 @@ module('integration/unload - Unloading Records', function (hooks) { }); let identifier = recordIdentifierFor(record); - let recordData = store._instanceCache.getRecordData(identifier); assert.strictEqual(record.currentState.stateName, 'root.loaded.saved', 'We are loaded initially'); // we test that we can sync call unloadRecord followed by findRecord assert.strictEqual(record.cars.at(0).make, 'jeep'); store.unloadRecord(record); assert.true(record.isDestroying, 'the record is destroying'); - assert.true(recordData.isEmpty(identifier), 'Expected the previous data to be unloaded'); + assert.true(store.cache.isEmpty(identifier), 'Expected the previous data to be unloaded'); const recordAgain = await store.findRecord('person', '1'); assert.strictEqual(recordAgain.cars.length, 0, 'Expected relationship to be cleared by the new push'); @@ -798,7 +797,6 @@ module('integration/unload - Unloading Records', function (hooks) { }); let identifier = recordIdentifierFor(record); - let recordData = store._instanceCache.getRecordData(identifier); const bike = store.peekRecord('bike', '1'); assert.strictEqual(record.currentState.stateName, 'root.loaded.saved', 'We are loaded initially'); @@ -809,7 +807,7 @@ module('integration/unload - Unloading Records', function (hooks) { store.unloadRecord(record); assert.true(record.isDestroying, 'the record is destroying'); assert.false(record.isDestroyed, 'the record is NOT YET destroyed'); - assert.true(recordData.isEmpty(identifier), 'We are unloaded after unloadRecord'); + assert.true(store.cache.isEmpty(identifier), 'We are unloaded after unloadRecord'); let wait = store.findRecord('person', '1').then((newRecord) => { assert.false(record.isDestroyed, 'the record is NOT YET destroyed'); @@ -854,13 +852,12 @@ module('integration/unload - Unloading Records', function (hooks) { }); let identifier = recordIdentifierFor(record); - let recordData = store._instanceCache.getRecordData(identifier); assert.strictEqual(record.currentState.stateName, 'root.loaded.saved', 'We are loaded initially'); run(function () { store.unloadRecord(record); assert.true(record.isDestroying, 'the record is destroying'); - assert.true(recordData.isEmpty(identifier), 'We are unloaded after unloadRecord'); + assert.true(store.cache.isEmpty(identifier), 'We are unloaded after unloadRecord'); }); run(function () { diff --git a/tests/main/tests/unit/custom-class-support/custom-class-model-test.ts b/tests/main/tests/unit/custom-class-support/custom-class-model-test.ts index e80e91fa336..9fe9f54c6dd 100644 --- a/tests/main/tests/unit/custom-class-support/custom-class-model-test.ts +++ b/tests/main/tests/unit/custom-class-support/custom-class-model-test.ts @@ -1,3 +1,5 @@ +import { TestContext } from '@ember/test-helpers'; + import { module, test } from 'qunit'; import RSVP from 'rsvp'; @@ -7,13 +9,13 @@ import JSONAPIAdapter from '@ember-data/adapter/json-api'; import JSONAPISerializer from '@ember-data/serializer/json-api'; import Store from '@ember-data/store'; import type { Snapshot } from '@ember-data/store/-private'; -import type NotificationManager from '@ember-data/store/-private/managers/record-notification-manager'; import type { RecordIdentifier, StableRecordIdentifier } from '@ember-data/types/q/identifier'; -import { RecordData } from '@ember-data/types/q/record-data'; +import { Cache } from '@ember-data/types/q/record-data'; import type { AttributesSchema, RelationshipsSchema } from '@ember-data/types/q/record-data-schemas'; -import type { RecordDataStoreWrapper } from '@ember-data/types/q/record-data-store-wrapper'; +import type { CacheStoreWrapper } from '@ember-data/types/q/record-data-store-wrapper'; import type { RecordInstance } from '@ember-data/types/q/record-instance'; import type { SchemaDefinitionService } from '@ember-data/types/q/schema-definition-service'; +import { deprecatedTest } from '@ember-data/unpublished-test-infra/test-support/deprecated-test'; module('unit/model - Custom Class Model', function (hooks) { let store: Store; @@ -50,7 +52,7 @@ module('unit/model - Custom Class Model', function (hooks) { }, }); } - instantiateRecord(identifier, createOptions, recordDataFor, notificationManager) { + instantiateRecord(identifier, createOptions) { return new Person(this); } teardownRecord(record) {} @@ -77,19 +79,14 @@ module('unit/model - Custom Class Model', function (hooks) { let identifier; let storeWrapper; class CreationStore extends CustomStore { - createRecordDataFor(identifier: StableRecordIdentifier, sw: RecordDataStoreWrapper) { - let rd = super.createRecordDataFor(identifier, sw); + createCache(sw: CacheStoreWrapper) { + let rd = super.createCache(sw); storeWrapper = sw; return rd; } - instantiateRecord( - id: StableRecordIdentifier, - createRecordArgs, - recordDataFor, - notificationManager: NotificationManager - ): Object { + instantiateRecord(id: StableRecordIdentifier, createRecordArgs): Object { identifier = id; - notificationManager.subscribe(identifier, (passedId, key) => { + this.notifications.subscribe(identifier, (passedId, key) => { notificationCount++; assert.strictEqual(passedId, identifier, 'passed the identifier to the callback'); if (notificationCount === 1) { @@ -121,7 +118,7 @@ module('unit/model - Custom Class Model', function (hooks) { assert.expect(5); let returnValue; class CreationStore extends CustomStore { - instantiateRecord(identifier, createRecordArgs, recordDataFor, notificationManager) { + instantiateRecord(identifier, createRecordArgs) { assert.strictEqual(identifier.type, 'person', 'Identifier type passed in correctly'); assert.deepEqual(createRecordArgs, { otherProp: 'unk' }, 'createRecordArg passed in'); returnValue = {}; @@ -138,41 +135,46 @@ module('unit/model - Custom Class Model', function (hooks) { assert.deepEqual(returnValue, person, 'record instantiating does not modify the returned value'); }); - test('recordData lookup', function (assert) { - assert.expect(1); - let rd; - class CreationStore extends Store { - instantiateRecord(identifier, createRecordArgs, recordDataFor, notificationManager) { - rd = recordDataFor(identifier); - assert.strictEqual(rd.getAttr(identifier, 'name'), 'chris', 'Can look up record data from recordDataFor'); - return {}; + deprecatedTest( + 'recordData lookup', + { id: 'ember-data:deprecate-instantiate-record-args', count: 1, until: '5.0' }, + function (this: TestContext, assert: Assert) { + assert.expect(1); + let rd; + class CreationStore extends Store { + // @ts-expect-error + instantiateRecord(identifier: StableRecordIdentifier, createRecordArgs, recordDataFor): RecordInstance { + rd = recordDataFor(identifier); + assert.strictEqual(rd.getAttr(identifier, 'name'), 'chris', 'Can look up record data from recordDataFor'); + return {}; + } + teardownRecord(record) {} } - teardownRecord(record) {} - } - this.owner.register('service:store', CreationStore); - store = this.owner.lookup('service:store') as Store; - let schema: SchemaDefinitionService = { - attributesDefinitionFor({ type: string }): AttributesSchema { - return { - name: { - type: 'string', - options: {}, - name: 'name', - kind: 'attribute', - }, - }; - }, - relationshipsDefinitionFor({ type: string }): RelationshipsSchema { - return {}; - }, - doesTypeExist() { - return true; - }, - }; - store.registerSchemaDefinitionService(schema); + this.owner.register('service:store', CreationStore); + store = this.owner.lookup('service:store') as Store; + let schema: SchemaDefinitionService = { + attributesDefinitionFor({ type: string }): AttributesSchema { + return { + name: { + type: 'string', + options: {}, + name: 'name', + kind: 'attribute', + }, + }; + }, + relationshipsDefinitionFor({ type: string }): RelationshipsSchema { + return {}; + }, + doesTypeExist() { + return true; + }, + }; + store.registerSchemaDefinitionService(schema); - store.createRecord('person', { name: 'chris' }); - }); + store.createRecord('person', { name: 'chris' }); + } + ); test('attribute and relationship with custom schema definition', async function (assert) { assert.expect(18); @@ -232,7 +234,7 @@ module('unit/model - Custom Class Model', function (hooks) { }) ); class CustomStore extends Store { - instantiateRecord(identifier, createOptions, recordDataFor, notificationManager) { + instantiateRecord(identifier, createOptions) { return new Person(this); } teardownRecord(record) {} @@ -316,7 +318,6 @@ module('unit/model - Custom Class Model', function (hooks) { }); test('store.deleteRecord', async function (assert) { - let rd: RecordData; let ident: StableRecordIdentifier; assert.expect(9); this.owner.register( @@ -330,13 +331,12 @@ module('unit/model - Custom Class Model', function (hooks) { }) ); class CreationStore extends CustomStore { - instantiateRecord(identifier, createRecordArgs, recordDataFor, notificationManager) { + instantiateRecord(identifier, createRecordArgs) { ident = identifier; - rd = recordDataFor(identifier); - assert.false(rd.isDeleted(identifier), 'we are not deleted when we start'); - notificationManager.subscribe(identifier, (passedId, key) => { + assert.false(this.cache.isDeleted(identifier), 'we are not deleted when we start'); + this.notifications.subscribe(identifier, (passedId, key) => { assert.strictEqual(key, 'state', 'state change to deleted has been notified'); - assert.true(recordDataFor(identifier).isDeleted(identifier), 'we have been marked as deleted'); + assert.true(this.cache.isDeleted(identifier), 'we have been marked as deleted'); }); return {}; } @@ -348,9 +348,9 @@ module('unit/model - Custom Class Model', function (hooks) { store = this.owner.lookup('service:store') as Store; let person = store.push({ data: { type: 'person', id: '1', attributes: { name: 'chris' } } }); store.deleteRecord(person); - assert.true(rd!.isDeleted(ident!), 'record has been marked as deleted'); + assert.true(store.cache.isDeleted(ident!), 'record has been marked as deleted'); await store.saveRecord(person); - assert.true(rd!.isDeletionCommitted(ident!), 'deletion has been commited'); + assert.true(store.cache.isDeletionCommitted(ident!), 'deletion has been commited'); }); test('record serialize', function (assert) { @@ -365,7 +365,7 @@ module('unit/model - Custom Class Model', function (hooks) { }) ); class CustomStore extends Store { - instantiateRecord(identifier, createOptions, recordDataFor, notificationManager) { + instantiateRecord(identifier, createOptions) { return new Person(this); } teardownRecord(record) {} diff --git a/tests/serializer-encapsulation/app/services/store.js b/tests/serializer-encapsulation/app/services/store.js index 7f524ecfa7a..1161a353794 100644 --- a/tests/serializer-encapsulation/app/services/store.js +++ b/tests/serializer-encapsulation/app/services/store.js @@ -1,10 +1,8 @@ -import { RecordData } from '@ember-data/json-api/-private'; +import { Cache } from '@ember-data/json-api'; import Store from '@ember-data/store'; export default class DefaultStore extends Store { - createRecordDataFor(identifier, storeWrapper) { - this.__private_singleton_recordData = this.__private_singleton_recordData || new RecordData(storeWrapper); - this.__private_singleton_recordData.createCache(identifier); - return this.__private_singleton_recordData; + createRecordDataFor(storeWrapper) { + return new Cache(storeWrapper); } }