diff --git a/packages/core-types/src/cache.ts b/packages/core-types/src/cache.ts index a219c3b4f81..08abf403db8 100644 --- a/packages/core-types/src/cache.ts +++ b/packages/core-types/src/cache.ts @@ -141,6 +141,43 @@ export interface Cache { peek(identifier: StableRecordIdentifier>): T | null; peek(identifier: StableDocumentIdentifier): ResourceDocument | null; + /** + * Peek remote resource data from the Cache. + * + * This will give the data provided from the server without any local changes. + * + * 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 + * @return {ResourceDocument | ResourceBlob | null} the known resource data + */ + peekRemoteState(identifier: StableRecordIdentifier>): T | null; + peekRemoteState(identifier: StableDocumentIdentifier): ResourceDocument | null; + /** * Peek the Cache for the existing request data associated with * a cacheable request @@ -341,6 +378,17 @@ export interface Cache { */ getAttr(identifier: StableRecordIdentifier, field: string | string[]): Value | undefined; + /** + * Retrieve remote state without any local changes for a specific attribute + * + * @method getRemoteAttr + * @public + * @param identifier + * @param field + * @return {unknown} + */ + getRemoteAttr(identifier: StableRecordIdentifier, field: string | string[]): Value | undefined; + /** * Mutate the data for an attribute in the cache * @@ -460,6 +508,21 @@ export interface Cache { isCollection?: boolean ): ResourceRelationship | CollectionRelationship; + /** + * Query the cache for the server state of a relationship property without any local changes + * + * @method getRelationship + * @public + * @param {StableRecordIdentifier} identifier + * @param {string} field + * @return resource relationship object + */ + getRemoteRelationship( + identifier: StableRecordIdentifier, + field: string, + isCollection?: boolean + ): ResourceRelationship | CollectionRelationship; + // Resource State // =============== diff --git a/packages/experiments/src/persisted-cache/cache.ts b/packages/experiments/src/persisted-cache/cache.ts index 961a3a59977..02ea4b68e70 100644 --- a/packages/experiments/src/persisted-cache/cache.ts +++ b/packages/experiments/src/persisted-cache/cache.ts @@ -152,6 +152,44 @@ export class PersistedCache implements Cache { return this._cache.peek(identifier); } + /** + * 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 + * @internal + * @param {StableRecordIdentifier | StableDocumentIdentifier} identifier + * @returns {ResourceDocument | ResourceBlob | null} the known resource data + */ + peekRemoteState(identifier: StableRecordIdentifier>): T | null; + peekRemoteState(identifier: StableDocumentIdentifier): ResourceDocument | null; + peekRemoteState(identifier: StableRecordIdentifier | StableDocumentIdentifier): unknown { + return this._cache.peekRemoteState(identifier); + } + /** * Peek the Cache for the existing request data associated with * a cacheable request @@ -374,6 +412,19 @@ export class PersistedCache implements Cache { return this._cache.getAttr(identifier, field); } + /** + * Retrieve the remote state for an attribute from the cache + * + * @method getAttr + * @internal + * @param identifier + * @param propertyName + * @return {unknown} + */ + getRemoteAttr(identifier: StableRecordIdentifier, field: string): Value | undefined { + return this._cache.getRemoteAttr(identifier, field); + } + /** * Mutate the data for an attribute in the cache * @@ -502,6 +553,23 @@ export class PersistedCache implements Cache { return this._cache.getRelationship(identifier, field, isCollection); } + /** + * Query the remote state for the current state of a relationship property + * + * @method getRelationship + * @internal + * @param identifier + * @param propertyName + * @return resource relationship object + */ + getRemoteRelationship( + identifier: StableRecordIdentifier, + field: string, + isCollection?: boolean + ): ResourceRelationship | CollectionRelationship { + return this._cache.getRemoteRelationship(identifier, field, isCollection); + } + // Resource State // =============== diff --git a/packages/graph/src/-private/edges/collection.ts b/packages/graph/src/-private/edges/collection.ts index a61e678aa04..0c64c044e0e 100644 --- a/packages/graph/src/-private/edges/collection.ts +++ b/packages/graph/src/-private/edges/collection.ts @@ -68,12 +68,15 @@ export function createCollectionEdge(definition: UpgradedMeta, identifier: Stabl }; } -export function legacyGetCollectionRelationshipData(source: CollectionEdge): CollectionRelationship { +export function legacyGetCollectionRelationshipData( + source: CollectionEdge, + getRemoteState: boolean +): CollectionRelationship { source.accessed = true; const payload: CollectionRelationship = {}; if (source.state.hasReceivedData) { - payload.data = computeLocalState(source); + payload.data = getRemoteState ? source.remoteState.slice() : computeLocalState(source); } if (source.links) { diff --git a/packages/graph/src/-private/edges/resource.ts b/packages/graph/src/-private/edges/resource.ts index 5776734009c..4cdd89f754e 100644 --- a/packages/graph/src/-private/edges/resource.ts +++ b/packages/graph/src/-private/edges/resource.ts @@ -40,16 +40,19 @@ export function createResourceEdge(definition: UpgradedMeta, identifier: StableR }; } -export function legacyGetResourceRelationshipData(source: ResourceEdge): ResourceRelationship { +export function legacyGetResourceRelationshipData(source: ResourceEdge, getRemoteState: boolean): ResourceRelationship { source.accessed = true; let data: StableRecordIdentifier | null | undefined; const payload: ResourceRelationship = {}; - if (source.localState) { + if (getRemoteState && source.remoteState) { + data = source.remoteState; + } else if (!getRemoteState && source.localState) { data = source.localState; } - if (source.localState === null && source.state.hasReceivedData) { + if (((getRemoteState && source.remoteState === null) || source.localState === null) && source.state.hasReceivedData) { data = null; } + if (source.links) { payload.links = source.links; } diff --git a/packages/graph/src/-private/graph.ts b/packages/graph/src/-private/graph.ts index 18211af7fa2..71403689d5d 100644 --- a/packages/graph/src/-private/graph.ts +++ b/packages/graph/src/-private/graph.ts @@ -162,10 +162,25 @@ export class Graph { assert(`Cannot getData() on an implicit relationship`, !isImplicit(relationship)); if (isBelongsTo(relationship)) { - return legacyGetResourceRelationshipData(relationship); + return legacyGetResourceRelationshipData(relationship, false); } - return legacyGetCollectionRelationshipData(relationship); + return legacyGetCollectionRelationshipData(relationship, false); + } + + getRemoteData( + identifier: StableRecordIdentifier, + propertyName: string + ): ResourceRelationship | CollectionRelationship { + const relationship = this.get(identifier, propertyName); + + assert(`Cannot getRemoteData() on an implicit relationship`, !isImplicit(relationship)); + + if (isBelongsTo(relationship)) { + return legacyGetResourceRelationshipData(relationship, true); + } + + return legacyGetCollectionRelationshipData(relationship, true); } /* @@ -334,7 +349,7 @@ export class Graph { additions: new Set(relationship.additions), removals: new Set(relationship.removals), remoteState: relationship.remoteState, - localState: legacyGetCollectionRelationshipData(relationship).data || [], + localState: legacyGetCollectionRelationshipData(relationship, false).data || [], reordered, }); } diff --git a/packages/json-api/src/-private/cache.ts b/packages/json-api/src/-private/cache.ts index ccefec1b8e2..8fdef4919e4 100644 --- a/packages/json-api/src/-private/cache.ts +++ b/packages/json-api/src/-private/cache.ts @@ -491,6 +491,64 @@ export default class JSONAPICache implements Cache { return null; } + peekRemoteState(identifier: StableRecordIdentifier): ResourceObject | null; + peekRemoteState(identifier: StableDocumentIdentifier): ResourceDocument | null; + peekRemoteState( + identifier: StableDocumentIdentifier | StableRecordIdentifier + ): ResourceObject | ResourceDocument | null { + if ('type' in identifier) { + const peeked = this.__safePeek(identifier, false); + + if (!peeked) { + return null; + } + + const { type, id, lid } = identifier; + const attributes = Object.assign({}, peeked.remoteAttrs) as ObjectValue; + const relationships: ResourceObject['relationships'] = {}; + + const rels = this.__graph.identifiers.get(identifier); + if (rels) { + Object.keys(rels).forEach((key) => { + const rel = rels[key]; + if (rel.definition.isImplicit) { + return; + } else { + relationships[key] = this.__graph.getData(identifier, key); + } + }); + } + + upgradeCapabilities(this._capabilities); + const store = this._capabilities._store; + const attrs = this._capabilities.schema.fields(identifier); + attrs.forEach((attr, key) => { + if (key in attributes && attributes[key] !== undefined) { + return; + } + const defaultValue = getDefaultValue(attr, identifier, store); + + if (defaultValue !== undefined) { + attributes[key] = defaultValue; + } + }); + + return { + type, + id, + lid, + attributes, + relationships, + }; + } + + const document = this.peekRequest(identifier); + + if (document) { + if ('content' in document) return document.content!; + } + return null; + } /** * Peek the Cache for the existing request data associated with * a cacheable request. @@ -1176,6 +1234,63 @@ export default class JSONAPICache implements Cache { return current; } + getRemoteAttr(identifier: StableRecordIdentifier, attr: string | string[]): Value | undefined { + const isSimplePath = !Array.isArray(attr) || attr.length === 1; + if (Array.isArray(attr) && attr.length === 1) { + attr = attr[0]; + } + + if (isSimplePath) { + const attribute = attr as string; + const cached = this.__peek(identifier, true); + assert( + `Cannot retrieve remote attributes for identifier ${String(identifier)} as it is not present in the cache`, + cached + ); + + // in Prod we try to recover when accessing something that + // doesn't exist + if (!cached) { + return undefined; + } + + if (cached.remoteAttrs && attribute in cached.remoteAttrs) { + return cached.remoteAttrs[attribute]; + } else if (cached.defaultAttrs && attribute in cached.defaultAttrs) { + return cached.defaultAttrs[attribute]; + } else { + const attrSchema = this._capabilities.schema.fields(identifier).get(attribute); + + upgradeCapabilities(this._capabilities); + const defaultValue = getDefaultValue(attrSchema, identifier, this._capabilities._store); + if (schemaHasLegacyDefaultValueFn(attrSchema)) { + cached.defaultAttrs = cached.defaultAttrs || (Object.create(null) as Record); + cached.defaultAttrs[attribute] = defaultValue; + } + return defaultValue; + } + } + + // TODO @runspired consider whether we need a defaultValue cache in SchemaRecord + // like we do for the simple case above. + const path: string[] = attr as string[]; + const cached = this.__peek(identifier, true); + const basePath = path[0]; + let current = cached.remoteAttrs && basePath in cached.remoteAttrs ? cached.remoteAttrs[basePath] : undefined; + + if (current === undefined) { + return undefined; + } + + for (let i = 1; i < path.length; i++) { + current = (current as ObjectValue)[path[i]]; + if (current === undefined) { + return undefined; + } + } + return current; + } + /** * Mutate the data for an attribute in the cache * @@ -1471,6 +1586,13 @@ export default class JSONAPICache implements Cache { return this.__graph.getData(identifier, field); } + getRemoteRelationship( + identifier: StableRecordIdentifier, + field: string + ): ResourceRelationship | CollectionRelationship { + return this.__graph.getRemoteData(identifier, field); + } + // Resource State // =============== diff --git a/packages/store/src/-private/managers/cache-manager.ts b/packages/store/src/-private/managers/cache-manager.ts index 8c335c2b8b8..3305338e902 100644 --- a/packages/store/src/-private/managers/cache-manager.ts +++ b/packages/store/src/-private/managers/cache-manager.ts @@ -133,6 +133,11 @@ export class CacheManager implements Cache { return this.#cache.peek(identifier); } + peekRemoteState(identifier: StableRecordIdentifier): unknown; + peekRemoteState(identifier: StableDocumentIdentifier): ResourceDocument | null; + peekRemoteState(identifier: StableRecordIdentifier | StableDocumentIdentifier): unknown { + return this.#cache.peekRemoteState(identifier); + } /** * Peek the Cache for the existing request data associated with * a cacheable request @@ -355,6 +360,19 @@ export class CacheManager implements Cache { return this.#cache.getAttr(identifier, propertyName); } + /** + * Retrieve the remote state for an attribute from the cache + * + * @method getRemoteAttr + * @public + * @param identifier + * @param propertyName + * @return {unknown} + */ + getRemoteAttr(identifier: StableRecordIdentifier, propertyName: string): Value | undefined { + return this.#cache.getRemoteAttr(identifier, propertyName); + } + /** * Mutate the data for an attribute in the cache * @@ -482,6 +500,22 @@ export class CacheManager implements Cache { return this.#cache.getRelationship(identifier, propertyName); } + /** + * Query the cache for the remote state of a relationship property + * + * @method getRelationship + * @public + * @param identifier + * @param propertyName + * @return resource relationship object + */ + getRemoteRelationship( + identifier: StableRecordIdentifier, + propertyName: string + ): ResourceRelationship | CollectionRelationship { + return this.#cache.getRemoteRelationship(identifier, propertyName); + } + // Resource State // =============== diff --git a/tests/docs/fixtures/expected.js b/tests/docs/fixtures/expected.js index 61097015b4d..7912aff4b4d 100644 --- a/tests/docs/fixtures/expected.js +++ b/tests/docs/fixtures/expected.js @@ -175,6 +175,7 @@ module.exports = { '(public) @ember-data/experimental-preview-types Cache#dump', '(public) @ember-data/experimental-preview-types Cache#fork', '(public) @ember-data/experimental-preview-types Cache#getAttr', + '(public) @ember-data/experimental-preview-types Cache#getRemoteAttr', '(public) @ember-data/experimental-preview-types Cache#getErrors', '(public) @ember-data/experimental-preview-types Cache#getRelationship', '(public) @ember-data/experimental-preview-types Cache#hasChangedAttrs', @@ -479,6 +480,7 @@ module.exports = { '(public) @ember-data/store CacheManager#dump', '(public) @ember-data/store CacheManager#fork', '(public) @ember-data/store CacheManager#getAttr', + `(public) @ember-data/store CacheManager#getRemoteAttr`, '(public) @ember-data/store CacheManager#getErrors', '(public) @ember-data/store CacheManager#getRelationship', '(public) @ember-data/store CacheManager#hasChangedAttrs', diff --git a/tests/ember-data__json-api/tests/integration/cache/collection-data-documents-test.ts b/tests/ember-data__json-api/tests/integration/cache/collection-data-documents-test.ts index d99aa8a37c1..0ed1be5a263 100644 --- a/tests/ember-data__json-api/tests/integration/cache/collection-data-documents-test.ts +++ b/tests/ember-data__json-api/tests/integration/cache/collection-data-documents-test.ts @@ -159,6 +159,14 @@ module('Integration | @ember-data/json-api Cache.put()', 'Resource Blob is kept updated in the cache after mutation' ); + const remoteData = store.cache.peekRemoteState(identifier); + + assert.deepEqual( + remoteData, + { type: 'user', id: '1', lid: '@lid:user-1', attributes: { name: 'Chris' }, relationships: {} }, + 'Remote State is not updated in the cache after mutation' + ); + store.cache.put( asStructuredDocument({ content: { @@ -195,6 +203,127 @@ module('Integration | @ember-data/json-api Cache.put()', ); }); + test('object fields are accessible via `peek`', function (assert) { + const store = new TestStore(); + store.schema.registerResource({ + identity: null, + type: 'user', + fields: [ + { kind: 'attribute', name: 'name', type: null }, + { + kind: 'object', + name: 'business', + }, + ], + }); + + let responseDocument: CollectionResourceDataDocument; + store._run(() => { + responseDocument = store.cache.put( + asStructuredDocument({ + content: { + data: [ + { + type: 'user', + id: '1', + attributes: { + name: 'Chris', + business: { + name: 'My Business', + address: { street: '123 Main Street', city: 'Anytown', state: 'NY', zip: '23456' }, + }, + }, + }, + ], + }, + }) + ); + }); + const identifier = store.identifierCache.getOrCreateRecordIdentifier({ type: 'user', id: '1' }); + assert.deepEqual(responseDocument!.data, [identifier], 'We were given the correct data back'); + + let resourceData = store.cache.peek(identifier); + + assert.deepEqual(resourceData, { + type: 'user', + id: '1', + lid: '@lid:user-1', + attributes: { + name: 'Chris', + business: { + name: 'My Business', + address: { + street: '123 Main Street', + city: 'Anytown', + state: 'NY', + zip: '23456', + }, + }, + }, + relationships: {}, + }); + + const record = store.peekRecord<{ + name: string | null; + business: { address: { street: string; city: string; state: string; zip: string } }; + }>(identifier); + + assert.equal(record?.business?.address?.street, '123 Main Street', 'record name is correct'); + + store.cache.setAttr(identifier, 'business', { + name: 'My Business', + address: { street: '456 Other Street', city: 'Anytown', state: 'NY', zip: '23456' }, + }); + resourceData = store.cache.peek(identifier); + + assert.deepEqual( + resourceData, + { + type: 'user', + id: '1', + lid: '@lid:user-1', + attributes: { + name: 'Chris', + business: { + name: 'My Business', + address: { + street: '456 Other Street', + city: 'Anytown', + state: 'NY', + zip: '23456', + }, + }, + }, + relationships: {}, + }, + 'Record is accessible via peek' + ); + + const remoteData = store.cache.peekRemoteState(identifier); + assert.deepEqual( + remoteData, + { + type: 'user', + id: '1', + lid: '@lid:user-1', + attributes: { + name: 'Chris', + business: { + name: 'My Business', + address: { + street: '123 Main Street', + city: 'Anytown', + state: 'NY', + zip: '23456', + }, + }, + }, + relationships: {}, + }, + 'Remote state is not updated after setAttr' + ); + }); + test('resource relationships are accessible via `peek`', function (assert) { const store = new TestStore(); store.schema.registerResource({ diff --git a/tests/main/tests/integration/cache/spec-cache-errors-test.ts b/tests/main/tests/integration/cache/spec-cache-errors-test.ts index 144446857a2..9ff28497f51 100644 --- a/tests/main/tests/integration/cache/spec-cache-errors-test.ts +++ b/tests/main/tests/integration/cache/spec-cache-errors-test.ts @@ -22,6 +22,7 @@ import type { StableExistingRecordIdentifier, StableRecordIdentifier, } from '@warp-drive/core-types/identifier'; +import type { TypeFromInstanceOrString } from '@warp-drive/core-types/record'; import type { CollectionResourceDataDocument, ResourceDocument, @@ -81,6 +82,11 @@ class TestCache implements Cache { peek(identifier: StableDocumentIdentifier | StableRecordIdentifier): ResourceBlob | ResourceDocument | null { throw new Error(`Not Implemented`); } + peekRemoteState(identifier: StableRecordIdentifier>): T | null; + peekRemoteState(identifier: StableDocumentIdentifier): ResourceDocument | null; + peekRemoteState(identifier: unknown): T | ResourceDocument | null { + throw new Error(`Not Implemented`); + } peekRequest(identifier: StableDocumentIdentifier): StructuredDocument | null { throw new Error(`Not Implemented`); } @@ -138,6 +144,9 @@ class TestCache implements Cache { getAttr(identifier: StableRecordIdentifier, propertyName: string): string { return ''; } + getRemoteAttr(identifier: StableRecordIdentifier, propertyName: string): string { + return ''; + } setAttr(identifier: StableRecordIdentifier, propertyName: string, value: unknown): void { throw new Error('Method not implemented.'); } @@ -156,6 +165,13 @@ class TestCache implements Cache { ): ResourceRelationship | CollectionRelationship { throw new Error('Method not implemented.'); } + getRemoteRelationship( + identifier: StableRecordIdentifier, + field: string, + isCollection?: boolean + ): ResourceRelationship | CollectionRelationship { + throw new Error('Method not implemented.'); + } addToHasMany( identifier: StableRecordIdentifier, propertyName: string, diff --git a/tests/main/tests/integration/cache/spec-cache-state-test.ts b/tests/main/tests/integration/cache/spec-cache-state-test.ts index d3605790ea3..4574ab40d32 100644 --- a/tests/main/tests/integration/cache/spec-cache-state-test.ts +++ b/tests/main/tests/integration/cache/spec-cache-state-test.ts @@ -22,6 +22,8 @@ import type { StableExistingRecordIdentifier, StableRecordIdentifier, } from '@warp-drive/core-types/identifier'; +import type { Value } from '@warp-drive/core-types/json/raw'; +import type { TypeFromInstanceOrString } from '@warp-drive/core-types/record'; import type { CollectionResourceDataDocument, ResourceDocument, @@ -104,6 +106,11 @@ class TestCache implements Cache { peek(identifier: StableDocumentIdentifier | StableRecordIdentifier): ResourceBlob | ResourceDocument | null { throw new Error(`Not Implemented`); } + peekRemoteState(identifier: StableRecordIdentifier>): T | null; + peekRemoteState(identifier: StableDocumentIdentifier): ResourceDocument | null; + peekRemoteState(identifier: unknown): T | ResourceDocument | null { + throw new Error(`Not Implemented`); + } peekRequest(identifier: StableDocumentIdentifier): StructuredDocument | null { throw new Error(`Not Implemented`); } @@ -159,6 +166,9 @@ class TestCache implements Cache { getAttr(identifier: StableRecordIdentifier, propertyName: string): string { return ''; } + getRemoteAttr(identifier: StableRecordIdentifier, field: string | string[]): Value | undefined { + return ''; + } setAttr(identifier: StableRecordIdentifier, propertyName: string, value: unknown): void { throw new Error('Method not implemented.'); } @@ -177,6 +187,13 @@ class TestCache implements Cache { ): ResourceRelationship | CollectionRelationship { throw new Error('Method not implemented.'); } + getRemoteRelationship( + identifier: StableRecordIdentifier, + field: string, + isCollection?: boolean + ): ResourceRelationship | CollectionRelationship { + throw new Error('Method not implemented.'); + } addToHasMany( identifier: StableRecordIdentifier, propertyName: string, diff --git a/tests/main/tests/integration/cache/spec-cache-test.ts b/tests/main/tests/integration/cache/spec-cache-test.ts index b7a7c0af44d..0dc60a2008a 100644 --- a/tests/main/tests/integration/cache/spec-cache-test.ts +++ b/tests/main/tests/integration/cache/spec-cache-test.ts @@ -23,6 +23,8 @@ import type { StableExistingRecordIdentifier, StableRecordIdentifier, } from '@warp-drive/core-types/identifier'; +import type { Value } from '@warp-drive/core-types/json/raw'; +import type { TypeFromInstanceOrString } from '@warp-drive/core-types/record'; import type { CollectionResourceDataDocument, ResourceDocument, @@ -118,6 +120,11 @@ class TestCache implements Cache { peek(identifier: StableDocumentIdentifier | StableRecordIdentifier): ResourceBlob | ResourceDocument | null { throw new Error(`Not Implemented`); } + peekRemoteState(identifier: StableRecordIdentifier>): T | null; + peekRemoteState(identifier: StableDocumentIdentifier): ResourceDocument | null; + peekRemoteState(identifier: unknown): T | ResourceDocument | null { + throw new Error(`Not Implemented`); + } peekRequest(identifier: StableDocumentIdentifier): StructuredDocument | null { throw new Error(`Not Implemented`); } @@ -165,6 +172,9 @@ class TestCache implements Cache { getAttr(identifier: StableRecordIdentifier, propertyName: string): string { return ''; } + getRemoteAttr(identifier: StableRecordIdentifier, field: string | string[]): Value | undefined { + return ''; + } setAttr(identifier: StableRecordIdentifier, propertyName: string, value: unknown): void { throw new Error('Method not implemented.'); } @@ -183,6 +193,13 @@ class TestCache implements Cache { ): ResourceRelationship | CollectionRelationship { throw new Error('Method not implemented.'); } + getRemoteRelationship( + identifier: StableRecordIdentifier, + field: string, + isCollection?: boolean + ): ResourceRelationship | CollectionRelationship { + throw new Error('Method not implemented.'); + } mutate(operation: LocalRelationshipOperation): void { throw new Error('Method not implemented.'); } diff --git a/tests/main/tests/integration/relationships/rollback-test.ts b/tests/main/tests/integration/relationships/rollback-test.ts index 4d726de0bfb..6276d7fce66 100644 --- a/tests/main/tests/integration/relationships/rollback-test.ts +++ b/tests/main/tests/integration/relationships/rollback-test.ts @@ -739,4 +739,18 @@ module('Integration | Relationships | Rollback', function (hooks) { assert.strictEqual(changed.size, 0, 'we have no diff'); }); }); + + module('.getRemoteRelationship', function () { + test('it returns the correct remote relationship for a belongsTo', function (assert) { + const store = this.owner.lookup('service:store') as Store; + const config = store.peekRecord('config', '1') as Config; + const app2 = store.peekRecord('app', '2') as App; + + const configIdentifier = store.identifierCache.getOrCreateRecordIdentifier({ type: 'config', id: '1' }); + const relationship = store.cache.getRelationship(configIdentifier, 'app'); + config.app = app2; + const remote = store.cache.getRemoteRelationship(configIdentifier, 'app'); + assert.deepEqual(remote, relationship, 'remote relationship is correct'); + }); + }); }); diff --git a/tests/warp-drive__schema-record/tests/writes/object-test.ts b/tests/warp-drive__schema-record/tests/writes/object-test.ts index 66884a0d1b1..801b6aa96f4 100644 --- a/tests/warp-drive__schema-record/tests/writes/object-test.ts +++ b/tests/warp-drive__schema-record/tests/writes/object-test.ts @@ -1006,4 +1006,97 @@ module('Writes | object fields', function (hooks) { 'we did not mutate the source object' ); }); + + module('Cache Remote Values', function () { + test('Cache remote values are retrievable after update', async function (assert) { + const store = this.owner.lookup('service:store') as Store; + const { schema } = store; + registerDerivations(schema); + + schema.registerResource( + withDefaults({ + type: 'user', + fields: [ + { + name: 'name', + kind: 'field', + }, + { + name: 'address', + kind: 'object', + }, + ], + }) + ); + + const immutableRecord = store.push({ + data: { + type: 'user', + id: '1', + attributes: { + name: 'Rey Pupatine', + address: { + street: '123 Main Street', + city: 'Anytown', + state: 'NY', + zip: '12345', + }, + }, + }, + }); + + const record = await immutableRecord[Checkout](); + assert.deepEqual( + record.address, + { + street: '123 Main Street', + city: 'Anytown', + state: 'NY', + zip: '12345', + }, + 'address is correct pre-update' + ); + + record.address = { + street: '456 Elm Street', + city: 'Sometown', + state: 'NJ', + zip: '23456', + }; + + assert.deepEqual( + record.address, + { + street: '456 Elm Street', + city: 'Sometown', + state: 'NJ', + zip: '23456', + }, + 'address is correct post-update' + ); + const identifier = recordIdentifierFor(record); + const cachedAddress = store.cache.getAttr(identifier, 'address'); + assert.deepEqual( + cachedAddress, + { + street: '456 Elm Street', + city: 'Sometown', + state: 'NJ', + zip: '23456', + }, + 'address is correct in cache' + ); + const remoteAddress = store.cache.getRemoteAttr(identifier, 'address'); + assert.deepEqual( + remoteAddress, + { + street: '123 Main Street', + city: 'Anytown', + state: 'NY', + zip: '12345', + }, + 'remote address is correct in cache' + ); + }); + }); });