diff --git a/packages/react-relay/relay-hooks/react-cache/useRefetchableFragmentInternal_REACT_CACHE.js b/packages/react-relay/relay-hooks/react-cache/useRefetchableFragmentInternal_REACT_CACHE.js index 4d2029dc6d5bf..9da7cd87bb035 100644 --- a/packages/react-relay/relay-hooks/react-cache/useRefetchableFragmentInternal_REACT_CACHE.js +++ b/packages/react-relay/relay-hooks/react-cache/useRefetchableFragmentInternal_REACT_CACHE.js @@ -538,7 +538,7 @@ if (__DEV__) { } const recordSource = environment.getStore().getSource(); const record = recordSource.get(id); - const typename = record && Record.getType(record); + const typename = record == null ? null : Record.getType(record); if (typename == null) { return null; } diff --git a/packages/react-relay/relay-hooks/useRefetchableFragmentNode.js b/packages/react-relay/relay-hooks/useRefetchableFragmentNode.js index 7662a8ce0c0ed..2e0f928aa7d80 100644 --- a/packages/react-relay/relay-hooks/useRefetchableFragmentNode.js +++ b/packages/react-relay/relay-hooks/useRefetchableFragmentNode.js @@ -550,7 +550,7 @@ if (__DEV__) { } const recordSource = environment.getStore().getSource(); const record = recordSource.get(id); - const typename = record && Record.getType(record); + const typename = record == null ? null : Record.getType(record); if (typename == null) { return null; } diff --git a/packages/relay-runtime/multi-actor-environment/__tests__/MultiActorEnvironment-commitMultiActorUpdate-test.js b/packages/relay-runtime/multi-actor-environment/__tests__/MultiActorEnvironment-commitMultiActorUpdate-test.js index 97c29a8f9476f..1c93c271a9398 100644 --- a/packages/relay-runtime/multi-actor-environment/__tests__/MultiActorEnvironment-commitMultiActorUpdate-test.js +++ b/packages/relay-runtime/multi-actor-environment/__tests__/MultiActorEnvironment-commitMultiActorUpdate-test.js @@ -13,6 +13,7 @@ import type {MultiActorStoreUpdater} from '../MultiActorEnvironmentTypes'; +const RelayModernRecord = require('../../store/RelayModernRecord'); const {getActorIdentifier} = require('../ActorIdentifier'); const MultiActorEnvironment = require('../MultiActorEnvironment'); @@ -47,7 +48,7 @@ describe('commitMultiActorUpdate', () => { throw new Error('Test record is null.'); } expect(testRecord).toHaveProperty('test'); - expect(testRecord.test).toBe(42); + expect(RelayModernRecord.getValue(testRecord, 'test')).toBe(42); }); expect(actorsCalled.includes('actor1')).toBe(true); diff --git a/packages/relay-runtime/mutations/__tests__/RelayRecordSourceProxy-test.js b/packages/relay-runtime/mutations/__tests__/RelayRecordSourceProxy-test.js index aa7aadd0b3617..77bc183bb5d41 100644 --- a/packages/relay-runtime/mutations/__tests__/RelayRecordSourceProxy-test.js +++ b/packages/relay-runtime/mutations/__tests__/RelayRecordSourceProxy-test.js @@ -12,6 +12,7 @@ 'use strict'; const defaultGetDataID = require('../../store/defaultGetDataID'); +const RelayModernRecord = require('../../store/RelayModernRecord'); const RelayRecordSource = require('../../store/RelayRecordSource'); const RelayStoreUtils = require('../../store/RelayStoreUtils'); const RelayRecordProxy = require('../RelayRecordProxy'); @@ -132,8 +133,7 @@ describe('RelayRecordSourceProxy', () => { const root = baseSource.get(ROOT_ID); expect(root).not.toBeUndefined(); if (root != null) { - // Flow - root.__typename = 'User'; + RelayModernRecord.setValue(root, TYPENAME_KEY, 'User'); } expect(() => { store.getRoot(); diff --git a/packages/relay-runtime/store/RelayModernRecord.js b/packages/relay-runtime/store/RelayModernRecord.js index 81e4dfeac5824..5d522a4e1f494 100644 --- a/packages/relay-runtime/store/RelayModernRecord.js +++ b/packages/relay-runtime/store/RelayModernRecord.js @@ -13,7 +13,6 @@ import type {ActorIdentifier} from '../multi-actor-environment/ActorIdentifier'; import type {DataID} from '../util/RelayRuntimeTypes'; -import type {Record} from './RelayStoreTypes'; const deepFreeze = require('../util/deepFreeze'); const {generateClientObjectClientID, isClientID} = require('./ClientID'); @@ -34,6 +33,11 @@ const areEqual = require('areEqual'); const invariant = require('invariant'); const warning = require('warning'); +/* + * An individual cached graph object. + */ +export opaque type Record = {[key: string]: mixed, ...}; + /** * @public * @@ -122,6 +126,15 @@ function create(dataID: DataID, typeName: string): Record { return record; } +/** + * @public + * + * Convert an object to a record. + */ +function fromObject(object: {[key: string]: mixed, ...}): Record { + return object; +} + /** * @public * @@ -171,6 +184,15 @@ function getValue(record: Record, storageKey: string): mixed { return value; } +/** + * @public + * + * Check if a record has a value for the given field. + */ +function hasValue(record: Record, storageKey: string): boolean { + return storageKey in record; +} + /** * @public * @@ -509,6 +531,7 @@ module.exports = { copyFields, create, freeze, + fromObject, getDataID, getFields, getInvalidationEpoch, @@ -516,6 +539,7 @@ module.exports = { getLinkedRecordIDs, getType, getValue, + hasValue, merge, setValue, setLinkedRecordID, diff --git a/packages/relay-runtime/store/RelayOptimisticRecordSource.js b/packages/relay-runtime/store/RelayOptimisticRecordSource.js index 3739b95aa9dd7..79d6d01ec4f5f 100644 --- a/packages/relay-runtime/store/RelayOptimisticRecordSource.js +++ b/packages/relay-runtime/store/RelayOptimisticRecordSource.js @@ -19,12 +19,15 @@ import type { RecordSource, } from './RelayStoreTypes'; +const RelayModernRecord = require('./RelayModernRecord'); const RelayRecordSource = require('./RelayRecordSource'); const invariant = require('invariant'); -const UNPUBLISH_RECORD_SENTINEL = Object.freeze({ - __UNPUBLISH_RECORD_SENTINEL: true, -}); +const UNPUBLISH_RECORD_SENTINEL = RelayModernRecord.fromObject( + Object.freeze({ + __UNPUBLISH_RECORD_SENTINEL: true, + }), +); /** * An implementation of MutableRecordSource that represents a base RecordSource diff --git a/packages/relay-runtime/store/RelayReader.js b/packages/relay-runtime/store/RelayReader.js index 072f884c943c9..81e0135146501 100644 --- a/packages/relay-runtime/store/RelayReader.js +++ b/packages/relay-runtime/store/RelayReader.js @@ -845,9 +845,9 @@ class RelayReader { const fragmentRef = {}; this._createFragmentPointer( field.fragmentSpread, - { + RelayModernRecord.fromObject({ __id: dataID, - }, + }), fragmentRef, ); data[applicationName] = { @@ -991,7 +991,7 @@ class RelayReader { record, fieldData, ); - return fieldData; + return RelayModernRecord.fromObject(fieldData); } // Has three possible return values: diff --git a/packages/relay-runtime/store/RelayRecordSource.js b/packages/relay-runtime/store/RelayRecordSource.js index f65431265d0e8..6a046404cb9e5 100644 --- a/packages/relay-runtime/store/RelayRecordSource.js +++ b/packages/relay-runtime/store/RelayRecordSource.js @@ -13,16 +13,18 @@ import type {DataID} from '../util/RelayRuntimeTypes'; import type {RecordState} from './RelayRecordState'; -import type { - MutableRecordSource, - Record, - RecordObjectMap, -} from './RelayStoreTypes'; +import type {MutableRecordSource, Record} from './RelayStoreTypes'; +const RelayModernRecord = require('./RelayModernRecord'); const RelayRecordState = require('./RelayRecordState'); const {EXISTENT, NONEXISTENT, UNKNOWN} = RelayRecordState; +/** + * A collection of records keyed by id. + */ +type RecordObjectMap = {[DataID]: ?{[key: string]: mixed}}; + /** * An implementation of the `MutableRecordSource` interface (defined in * `RelayStoreTypes`) that holds all records in memory (JS Map). @@ -34,7 +36,10 @@ class RelayRecordSource implements MutableRecordSource { this._records = new Map(); if (records != null) { Object.keys(records).forEach(key => { - this._records.set(key, records[key]); + const object = records[key]; + const record = + object == null ? object : RelayModernRecord.fromObject(object); + this._records.set(key, record); }); } } diff --git a/packages/relay-runtime/store/RelayStoreTypes.js b/packages/relay-runtime/store/RelayStoreTypes.js index 56194d9691089..e33566299a523 100644 --- a/packages/relay-runtime/store/RelayStoreTypes.js +++ b/packages/relay-runtime/store/RelayStoreTypes.js @@ -49,6 +49,7 @@ import type { UpdatableQuery, Variables, } from '../util/RelayRuntimeTypes'; +import type {Record as RelayModernRecord} from './RelayModernRecord'; import type {InvalidationState} from './RelayModernStore'; import type RelayOperationTracker from './RelayOperationTracker'; import type {RecordState} from './RelayRecordState'; @@ -56,22 +57,14 @@ import type {RecordState} from './RelayRecordState'; export opaque type FragmentType = empty; export type OperationTracker = RelayOperationTracker; +export type Record = RelayModernRecord; + export type MutationParameters = { +response: {...}, +variables: {...}, +rawResponse?: {...}, }; -/* - * An individual cached graph object. - */ -export type Record = {[key: string]: mixed, ...}; - -/** - * A collection of records keyed by id. - */ -export type RecordObjectMap = {[DataID]: ?Record}; - export type FragmentMap = {[key: string]: ReaderFragment, ...}; /** diff --git a/packages/relay-runtime/store/ResolverCache.js b/packages/relay-runtime/store/ResolverCache.js index ca38072a5602e..7baf2019bd348 100644 --- a/packages/relay-runtime/store/ResolverCache.js +++ b/packages/relay-runtime/store/ResolverCache.js @@ -223,11 +223,22 @@ class RecordResolverCache implements ResolverCache { } // $FlowFixMe[incompatible-type] - will always be empty - const answer: T = linkedRecord[RELAY_RESOLVER_VALUE_KEY]; + const answer: T = RelayModernRecord.getValue( + linkedRecord, + RELAY_RESOLVER_VALUE_KEY, + ); + // $FlowFixMe[incompatible-type] - casting mixed - const snapshot: ?Snapshot = linkedRecord[RELAY_RESOLVER_SNAPSHOT_KEY]; + const snapshot: ?Snapshot = RelayModernRecord.getValue( + linkedRecord, + RELAY_RESOLVER_SNAPSHOT_KEY, + ); + // $FlowFixMe[incompatible-type] - casting mixed - const error: ?Error = linkedRecord[RELAY_RESOLVER_ERROR_KEY]; + const error: ?Error = RelayModernRecord.getValue( + linkedRecord, + RELAY_RESOLVER_ERROR_KEY, + ); return [answer, linkedID, error, snapshot, undefined, undefined]; } diff --git a/packages/relay-runtime/store/StoreInspector.js b/packages/relay-runtime/store/StoreInspector.js index eb7d5f051b911..6e2db9c5800df 100644 --- a/packages/relay-runtime/store/StoreInspector.js +++ b/packages/relay-runtime/store/StoreInspector.js @@ -136,6 +136,7 @@ if (__DEV__) { return record; } return new Proxy( + // $FlowFixMe: Do not assume that record is an object {...record}, { get(target, prop) { diff --git a/packages/relay-runtime/store/__tests__/RelayModernRecord-test.js b/packages/relay-runtime/store/__tests__/RelayModernRecord-test.js index 1c9c4c4bfd948..f349fa44797fd 100644 --- a/packages/relay-runtime/store/__tests__/RelayModernRecord-test.js +++ b/packages/relay-runtime/store/__tests__/RelayModernRecord-test.js @@ -67,6 +67,14 @@ describe('RelayModernRecord', () => { }); }); + describe('fromObject()', () => { + it('returns the given object', () => { + const object = {}; + const record = RelayModernRecord.fromObject(object); + expect(record).toBe(object); + }); + }); + describe('getLinkedRecordIDs()', () => { let record; @@ -244,6 +252,50 @@ describe('RelayModernRecord', () => { }); }); + describe('hasValue()', () => { + let record; + + beforeEach(() => { + record = { + [ID_KEY]: '4', + [TYPENAME_KEY]: 'User', + fieldThatIsString: 'applesauce', + fieldThatIsNull: null, + fieldThatIsUndefined: undefined, + }; + }); + + it('has no special treatment for the id field', () => { + expect(RelayModernRecord.hasValue(record, ID_KEY)).toBe(true); + }); + + it('has no special treatment for the typename field', () => { + expect(RelayModernRecord.hasValue(record, TYPENAME_KEY)).toBe(true); + }); + + it('returns true when the value is a string', () => { + expect(RelayModernRecord.hasValue(record, 'fieldThatIsString')).toBe( + true, + ); + }); + + it('returns true when the value is null', () => { + expect(RelayModernRecord.hasValue(record, 'fieldThatIsNull')).toBe(true); + }); + + it('returns true when the value is explicitly undefined', () => { + expect(RelayModernRecord.hasValue(record, 'fieldThatIsUndefined')).toBe( + true, + ); + }); + + it('returns false when the value is missing', () => { + expect(RelayModernRecord.hasValue(record, 'fieldThatIsMissing')).toBe( + false, + ); + }); + }); + describe('update()', () => { it('returns the first record if there are no changes', () => { const prev = RelayModernRecord.create('4', 'User'); diff --git a/packages/relay-runtime/store/__tests__/RelayModernStore-Subscriptions-test.js b/packages/relay-runtime/store/__tests__/RelayModernStore-Subscriptions-test.js index f65c95ba1e938..34bf64fe3ef5f 100644 --- a/packages/relay-runtime/store/__tests__/RelayModernStore-Subscriptions-test.js +++ b/packages/relay-runtime/store/__tests__/RelayModernStore-Subscriptions-test.js @@ -621,7 +621,9 @@ function cloneEventWithSets(event: LogEvent) { if (!record) { throw new Error('Expected to find record with id client:1'); } - expect(record[INVALIDATED_AT_KEY]).toEqual(1); + expect( + RelayModernRecord.getValue(record, INVALIDATED_AT_KEY), + ).toEqual(1); expect(store.check(owner)).toEqual({status: 'stale'}); }); @@ -657,7 +659,9 @@ function cloneEventWithSets(event: LogEvent) { if (!record) { throw new Error('Expected to find record with id "4"'); } - expect(record[INVALIDATED_AT_KEY]).toEqual(1); + expect( + RelayModernRecord.getValue(record, INVALIDATED_AT_KEY), + ).toEqual(1); expect(store.check(owner)).toEqual({status: 'stale'}); }); }); diff --git a/packages/relay-runtime/store/__tests__/RelayModernStore-test.js b/packages/relay-runtime/store/__tests__/RelayModernStore-test.js index 00ca84b9b6fcb..5d69c65941b17 100644 --- a/packages/relay-runtime/store/__tests__/RelayModernStore-test.js +++ b/packages/relay-runtime/store/__tests__/RelayModernStore-test.js @@ -934,7 +934,9 @@ function cloneEventWithSets(event: LogEvent) { if (!record) { throw new Error('Expected to find record with id client:1'); } - expect(record[INVALIDATED_AT_KEY]).toEqual(1); + expect( + RelayModernRecord.getValue(record, INVALIDATED_AT_KEY), + ).toEqual(1); expect(store.check(owner)).toEqual({status: 'stale'}); }); @@ -970,7 +972,9 @@ function cloneEventWithSets(event: LogEvent) { if (!record) { throw new Error('Expected to find record with id "4"'); } - expect(record[INVALIDATED_AT_KEY]).toEqual(1); + expect( + RelayModernRecord.getValue(record, INVALIDATED_AT_KEY), + ).toEqual(1); expect(store.check(owner)).toEqual({status: 'stale'}); }); }); diff --git a/packages/relay-runtime/store/__tests__/RelayReferenceMarker-test.js b/packages/relay-runtime/store/__tests__/RelayReferenceMarker-test.js index ddfc2ff85f298..21a9dc158adb3 100644 --- a/packages/relay-runtime/store/__tests__/RelayReferenceMarker-test.js +++ b/packages/relay-runtime/store/__tests__/RelayReferenceMarker-test.js @@ -11,7 +11,6 @@ 'use strict'; -import type {RecordObjectMap} from '../RelayStoreTypes'; import type {DataID} from 'relay-runtime/util/RelayRuntimeTypes'; import RelayNetwork from '../../network/RelayNetwork'; @@ -239,7 +238,7 @@ describe('RelayReferenceMarker', () => { }); it('marks "handle" nodes with key and filters for queries', () => { - const data: RecordObjectMap = { + const data = { '1': { __id: '1', __typename: 'User', diff --git a/packages/relay-runtime/store/experimental-live-resolvers/LiveResolverCache.js b/packages/relay-runtime/store/experimental-live-resolvers/LiveResolverCache.js index 801eb7444f5bc..7b260315a720c 100644 --- a/packages/relay-runtime/store/experimental-live-resolvers/LiveResolverCache.js +++ b/packages/relay-runtime/store/experimental-live-resolvers/LiveResolverCache.js @@ -295,9 +295,15 @@ class LiveResolverCache implements ResolverCache { const answer: T = this._getResolverValue(linkedRecord); // $FlowFixMe[incompatible-type] - casting mixed - const snapshot: ?Snapshot = linkedRecord[RELAY_RESOLVER_SNAPSHOT_KEY]; + const snapshot: ?Snapshot = RelayModernRecord.getValue( + linkedRecord, + RELAY_RESOLVER_SNAPSHOT_KEY, + ); // $FlowFixMe[incompatible-type] - casting mixed - const error: ?Error = linkedRecord[RELAY_RESOLVER_ERROR_KEY]; + const error: ?Error = RelayModernRecord.getValue( + linkedRecord, + RELAY_RESOLVER_ERROR_KEY, + ); let suspenseID = null; @@ -405,7 +411,12 @@ class LiveResolverCache implements ResolverCache { return; } - if (!(RELAY_RESOLVER_LIVE_STATE_VALUE in currentRecord)) { + if ( + !RelayModernRecord.hasValue( + currentRecord, + RELAY_RESOLVER_LIVE_STATE_VALUE, + ) + ) { warning( false, 'Unexpected callback for a incomplete live resolver record (__id: `%s`). The record has missing live state value. ' +