diff --git a/packages/core-types/src/cache.ts b/packages/core-types/src/cache.ts index a219c3b4f81..715425c3f15 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 diff --git a/packages/diagnostic/server/default-setup.js b/packages/diagnostic/server/default-setup.js index e0ac49ce1f6..58b4e84fcc6 100644 --- a/packages/diagnostic/server/default-setup.js +++ b/packages/diagnostic/server/default-setup.js @@ -35,7 +35,7 @@ export default async function launchDefault(overrides = {}) { Object.assign(overrides, flags); const RETRY_TESTS = - ('retry' in overrides ? overrides.retry : (process.env.CI ?? process.env.RETRY_TESTS)) && FAILURES.length; + ('retry' in overrides ? overrides.retry : process.env.CI ?? process.env.RETRY_TESTS) && FAILURES.length; const _parallel = process.env.DIAGNOSTIC_PARALLEL && !isNaN(Number(process.env.DIAGNOSTIC_PARALLEL)) ? Number(process.env.DIAGNOSTIC_PARALLEL) diff --git a/packages/experiments/src/persisted-cache/cache.ts b/packages/experiments/src/persisted-cache/cache.ts index 961a3a59977..f576804daf9 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 diff --git a/packages/json-api/src/-private/cache.ts b/packages/json-api/src/-private/cache.ts index 4f6cbeb4fea..c95686aaa60 100644 --- a/packages/json-api/src/-private/cache.ts +++ b/packages/json-api/src/-private/cache.ts @@ -483,6 +483,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. diff --git a/packages/store/src/-private/managers/cache-manager.ts b/packages/store/src/-private/managers/cache-manager.ts index 8c335c2b8b8..a7f12a37b43 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 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({