From 42f211360f2d1df237db5bb4e96d1fed217fed1b Mon Sep 17 00:00:00 2001 From: Chris Thoburn Date: Sun, 12 Mar 2023 06:01:14 -0700 Subject: [PATCH] feat: cache-put --- ember-data-types/cache/document.ts | 43 +++++++++ ember-data-types/q/cache.ts | 26 +++++- packages/json-api/src/-private/cache.ts | 49 +++++++++- .../src/-private/caches/identifier-cache.ts | 5 ++ .../src/-private/managers/cache-manager.ts | 90 ++++++++++++++++++- packages/store/src/-private/store-service.ts | 42 ++------- tests/docs/fixtures/expected.js | 2 + .../record-data/record-data-errors-test.ts | 4 + .../record-data/record-data-state-test.ts | 4 + .../record-data/record-data-test.ts | 4 + 10 files changed, 229 insertions(+), 40 deletions(-) create mode 100644 ember-data-types/cache/document.ts 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/q/cache.ts b/ember-data-types/q/cache.ts index e3c58ad33f8..f69e6702a70 100644 --- a/ember-data-types/q/cache.ts +++ b/ember-data-types/q/cache.ts @@ -1,5 +1,6 @@ import { LocalRelationshipOperation } from '@ember-data/graph/-private/graph/-operations'; +import { ResourceDocument, StructuredDocument } from '../cache/document'; import type { CollectionResourceRelationship, SingleResourceRelationship } from './ember-data-json-api'; import type { RecordIdentifier, StableRecordIdentifier } from './identifier'; import type { JsonApiResource, JsonApiValidationError } from './record-data-json-api'; @@ -85,8 +86,29 @@ export interface Cache { */ version: '2'; - // Cache - // ===== + /** + * 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 diff --git a/packages/json-api/src/-private/cache.ts b/packages/json-api/src/-private/cache.ts index 7b7e4350f19..a27bac03147 100644 --- a/packages/json-api/src/-private/cache.ts +++ b/packages/json-api/src/-private/cache.ts @@ -11,13 +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 { IdentifierCache } from '@ember-data/store/-private/caches/identifier-cache'; +import { ResourceDocument, StructuredDocument } from '@ember-data/types/cache/document'; import type { Cache, ChangedAttributesHash, MergeOperation } from '@ember-data/types/q/cache'; import type { CacheStoreWrapper, V2CacheStoreWrapper } from '@ember-data/types/q/cache-store-wrapper'; import type { CollectionResourceRelationship, + ExistingResourceObject, + JsonApiDocument, SingleResourceRelationship, } from '@ember-data/types/q/ember-data-json-api'; -import type { StableRecordIdentifier } from '@ember-data/types/q/identifier'; +import type { StableExistingRecordIdentifier, StableRecordIdentifier } from '@ember-data/types/q/identifier'; 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 { Dict } from '@ember-data/types/q/utils'; @@ -86,6 +90,43 @@ export default class SingletonCache implements Cache { this.__storeWrapper = storeWrapper; } + put(doc: StructuredDocument): ResourceDocument { + assert(`Cannot currently cache an ErrorDocument`, !('error' in doc)); + const jsonApiDoc = doc.data; + let included = jsonApiDoc.included; + let i: number, length: number; + const { identifierCache } = this.__storeWrapper; + + if (included) { + for (i = 0, length = included.length; i < length; i++) { + putOne(this, identifierCache, included[i]); + } + } + + if (Array.isArray(jsonApiDoc.data)) { + length = jsonApiDoc.data.length; + let identifiers: StableExistingRecordIdentifier[] = []; + + for (i = 0; i < length; i++) { + identifiers.push(putOne(this, identifierCache, 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' + ); + + let identifier: StableExistingRecordIdentifier = identifierCache.getOrCreateRecordIdentifier(jsonApiDoc.data); + this.upsert(identifier, jsonApiDoc.data, false); + return { data: identifier }; + } + /** * Private method used to populate an entry for the identifier * @@ -746,6 +787,12 @@ function patchLocalAttributes(cached: CachedResource): boolean { return hasAppliedPatch; } +function putOne(cache: SingletonCache, identifiers: IdentifierCache, resource: ExistingResourceObject) { + let identifier: StableExistingRecordIdentifier = identifiers.getOrCreateRecordIdentifier(resource); + cache.upsert(identifier, resource, false); + return identifier; +} + /* Iterates over the set of internal models reachable from `this` across exactly one relationship. diff --git a/packages/store/src/-private/caches/identifier-cache.ts b/packages/store/src/-private/caches/identifier-cache.ts index 0f3d8a3c1b7..8272da41677 100644 --- a/packages/store/src/-private/caches/identifier-cache.ts +++ b/packages/store/src/-private/caches/identifier-cache.ts @@ -16,6 +16,7 @@ import type { RecordIdentifier, ResetMethod, ResourceData, + StableExistingRecordIdentifier, StableRecordIdentifier, UpdateMethod, } from '@ember-data/types/q/identifier'; @@ -342,6 +343,10 @@ export class IdentifierCache { @returns {StableRecordIdentifier} @public */ + getOrCreateRecordIdentifier(resource: ExistingResourceObject): StableExistingRecordIdentifier; + getOrCreateRecordIdentifier( + resource: ResourceIdentifierObject | Identifier | StableRecordIdentifier + ): StableRecordIdentifier; getOrCreateRecordIdentifier(resource: ResourceData | Identifier): StableRecordIdentifier { return this._getRecordIdentifier(resource, true); } diff --git a/packages/store/src/-private/managers/cache-manager.ts b/packages/store/src/-private/managers/cache-manager.ts index 672576ad56e..d0ae267c516 100644 --- a/packages/store/src/-private/managers/cache-manager.ts +++ b/packages/store/src/-private/managers/cache-manager.ts @@ -1,18 +1,65 @@ -import { deprecate } from '@ember/debug'; +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 { ResourceDocument, StructuredDocument } from '@ember-data/types/cache/document'; import type { Cache, CacheV1, ChangedAttributesHash, MergeOperation } from '@ember-data/types/q/cache'; 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 { StableExistingRecordIdentifier, StableRecordIdentifier } from '@ember-data/types/q/identifier'; 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'; +export function legacyCachePut( + store: Store, + doc: StructuredDataDocument | { data: JsonApiDocument } +): 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 CacheManager wraps a Cache * enforcing that only the public API surface area @@ -83,6 +130,41 @@ export class NonSingletonCacheManager implements Cache { } } + /** + * 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 cacheabl + * e 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 | { data: T }): 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); + } + #isDeprecated(recordData: Cache | CacheV1): recordData is CacheV1 { let version = recordData.version || '1'; return version !== this.version; @@ -759,6 +841,10 @@ export class SingletonCacheManager implements Cache { this.#cache = cache; } + put(doc: StructuredDocument): ResourceDocument { + return this.#cache.put(doc); + } + // Cache // ===== diff --git a/packages/store/src/-private/store-service.ts b/packages/store/src/-private/store-service.ts index 4a3ec4c728e..a2969dd235a 100644 --- a/packages/store/src/-private/store-service.ts +++ b/packages/store/src/-private/store-service.ts @@ -19,6 +19,7 @@ import { DEPRECATE_JSON_API_FALLBACK, DEPRECATE_PROMISE_PROXIES, DEPRECATE_STORE_FIND, + DEPRECATE_V1_RECORD_DATA, } from '@ember-data/private-build-infra/deprecations'; import type { Cache, CacheV1 } from '@ember-data/types/q/cache'; import type { CacheStoreWrapper } from '@ember-data/types/q/cache-store-wrapper'; @@ -55,7 +56,7 @@ 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 { NonSingletonCacheManager, SingletonCacheManager } from './managers/cache-manager'; +import { legacyCachePut, NonSingletonCacheManager, SingletonCacheManager } from './managers/cache-manager'; import NotificationManager from './managers/notification-manager'; import RecordArrayManager from './managers/record-array-manager'; import FetchManager, { SaveOp } from './network/fetch-manager'; @@ -2153,43 +2154,14 @@ class Store { } 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; + if (DEPRECATE_V1_RECORD_DATA) { + ret = legacyCachePut(this, { data: jsonApiDoc }); + } else { + ret = this._instanceCache.__cacheManager.put(jsonApiDoc); } - - 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; + return ret.data; } /** diff --git a/tests/docs/fixtures/expected.js b/tests/docs/fixtures/expected.js index ea4561ada8b..01de7e752df 100644 --- a/tests/docs/fixtures/expected.js +++ b/tests/docs/fixtures/expected.js @@ -319,9 +319,11 @@ module.exports = { '(public) @ember-data/store RecordArray#save', '(public) @ember-data/store RecordArray#type', '(public) @ember-data/store RecordArray#update', + '(public) @ember-data/store Cache#put', '(public) @ember-data/store Cache#patch', '(public) @ember-data/store Cache#upsert', '(public) @ember-data/store Cache#version', + '(public) @ember-data/store CacheManager#put', '(public) @ember-data/store CacheManager#patch', '(public) @ember-data/store CacheManager#addToHasMany', '(public) @ember-data/store CacheManager#changedAttributes', 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 eae0996ba5c..9ff64bb6bc6 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 @@ -11,6 +11,7 @@ import Model, { attr } from '@ember-data/model'; import { DEPRECATE_V1_RECORD_DATA } from '@ember-data/private-build-infra/deprecations'; import JSONAPISerializer from '@ember-data/serializer/json-api'; import Store, { recordIdentifierFor } from '@ember-data/store'; +import { ResourceDocument, StructuredDocument } from '@ember-data/types/cache/document'; import type { Cache, CacheV1, ChangedAttributesHash, MergeOperation } from '@ember-data/types/q/cache'; import type { CacheStoreWrapper } from '@ember-data/types/q/cache-store-wrapper'; import { DSModel } from '@ember-data/types/q/ds-model'; @@ -29,6 +30,9 @@ if (!DEPRECATE_V1_RECORD_DATA) { patch(op: MergeOperation): void { throw new Error('Method not implemented.'); } + put(doc: StructuredDocument): ResourceDocument { + throw new Error('Method not implemented.'); + } update(operation: LocalRelationshipOperation): void { throw new Error('Method not implemented.'); } 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 5439f4eae77..5aae306f6d4 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 @@ -11,6 +11,7 @@ import Model, { attr } from '@ember-data/model'; import { DEPRECATE_V1_RECORD_DATA } from '@ember-data/private-build-infra/deprecations'; import JSONAPISerializer from '@ember-data/serializer/json-api'; import Store, { recordIdentifierFor } from '@ember-data/store'; +import { ResourceDocument, StructuredDocument } from '@ember-data/types/cache/document'; import type { Cache, CacheV1, ChangedAttributesHash, MergeOperation } from '@ember-data/types/q/cache'; import { CacheStoreWrapper } from '@ember-data/types/q/cache-store-wrapper'; import { CollectionResourceRelationship, SingleResourceRelationship } from '@ember-data/types/q/ember-data-json-api'; @@ -135,6 +136,9 @@ class V2TestRecordData implements Cache { patch(op: MergeOperation): void { throw new Error('Method not implemented.'); } + put(doc: StructuredDocument): ResourceDocument { + throw new Error('Method not implemented.'); + } update(operation: LocalRelationshipOperation): void { throw new Error('Method not implemented.'); } 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 f82d4e9cfdd..5a084841b39 100644 --- a/tests/main/tests/integration/record-data/record-data-test.ts +++ b/tests/main/tests/integration/record-data/record-data-test.ts @@ -11,6 +11,7 @@ import Model, { attr, belongsTo, hasMany } from '@ember-data/model'; import { DEPRECATE_V1_RECORD_DATA } from '@ember-data/private-build-infra/deprecations'; import JSONAPISerializer from '@ember-data/serializer/json-api'; import Store from '@ember-data/store'; +import { ResourceDocument, StructuredDocument } from '@ember-data/types/cache/document'; import type { Cache, ChangedAttributesHash, MergeOperation } from '@ember-data/types/q/cache'; import type { CacheStoreWrapper } from '@ember-data/types/q/cache-store-wrapper'; import { DSModel } from '@ember-data/types/q/ds-model'; @@ -125,6 +126,9 @@ class V2TestRecordData implements Cache { patch(op: MergeOperation): void { throw new Error('Method not implemented.'); } + put(doc: StructuredDocument): ResourceDocument { + throw new Error('Method not implemented.'); + } upsert( identifier: StableRecordIdentifier, data: JsonApiResource,