packages/relay-runtime/mutations/createUpdatableProxy.js (270 lines of code) (raw):
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @emails oncall+relay
* @format
*/
// flowlint ambiguous-object-type:error
'use strict';
import type {RecordProxy, RecordSourceProxy} from '../store/RelayStoreTypes';
import type {ReaderLinkedField, ReaderSelection} from '../util/ReaderNode';
import type {Variables} from '../util/RelayRuntimeTypes';
const {getArgumentValues} = require('../store/RelayStoreUtils');
const nonUpdatableKeys = ['id', '__id', '__typename', 'js'];
function createUpdatableProxy<TData: {...}>(
updatableProxyRootRecord: RecordProxy,
variables: Variables,
selections: $ReadOnlyArray<ReaderSelection>,
recordSourceProxy: RecordSourceProxy,
): TData {
const mutableUpdatableProxy = {};
updateProxyFromSelections(
mutableUpdatableProxy,
updatableProxyRootRecord,
variables,
selections,
recordSourceProxy,
);
if (__DEV__) {
Object.freeze(mutableUpdatableProxy);
}
// unless ReaderSelection carries more type information, we will never be able
// to flowtype mutableUpdatableProxy without a type assertion.
// $FlowFixMe[unclear-type]
return ((mutableUpdatableProxy: any): TData);
}
function updateProxyFromSelections<TData>(
mutableUpdatableProxy: TData,
updatableProxyRootRecord: RecordProxy,
variables: Variables,
selections: $ReadOnlyArray<ReaderSelection>,
recordSourceProxy: RecordSourceProxy,
) {
for (const selection of selections) {
switch (selection.kind) {
case 'LinkedField':
if (selection.plural) {
Object.defineProperty(
mutableUpdatableProxy,
selection.alias ?? selection.name,
{
// $FlowFixMe[incompatible-call] these getters and setters have different types on purpose
get: createGetterForPluralLinkedField(
selection,
variables,
updatableProxyRootRecord,
recordSourceProxy,
),
set: createSetterForPluralLinkedField(
selection,
variables,
updatableProxyRootRecord,
recordSourceProxy,
),
},
);
} else {
Object.defineProperty(
mutableUpdatableProxy,
selection.alias ?? selection.name,
{
get: createGetterForSingularLinkedField(
selection,
variables,
updatableProxyRootRecord,
recordSourceProxy,
),
set: createSetterForSingularLinkedField(
selection,
variables,
updatableProxyRootRecord,
recordSourceProxy,
),
},
);
}
break;
case 'ScalarField':
const scalarFieldName = selection.alias ?? selection.name;
Object.defineProperty(mutableUpdatableProxy, scalarFieldName, {
get: function () {
const newVariables = getArgumentValues(
selection.args ?? [],
variables,
);
// Flow incorrect assumes that the return value for the get method must match
// the set parameter.
return (updatableProxyRootRecord.getValue(
selection.name,
newVariables,
// $FlowFixMe[unclear-type] Typed by the generated updatable query flow type
): any);
},
set: nonUpdatableKeys.includes(selection.name)
? undefined
: // $FlowFixMe[unclear-type] Typed by the generated updatable query flow type
function (newValue: ?any) {
const newVariables = getArgumentValues(
selection.args ?? [],
variables,
);
updatableProxyRootRecord.setValue(
newValue,
selection.name,
newVariables,
);
},
});
break;
case 'InlineFragment':
if (updatableProxyRootRecord.getType() === selection.type) {
updateProxyFromSelections(
mutableUpdatableProxy,
updatableProxyRootRecord,
variables,
selection.selections,
recordSourceProxy,
);
}
break;
case 'ClientExtension':
updateProxyFromSelections(
mutableUpdatableProxy,
updatableProxyRootRecord,
variables,
selection.selections,
recordSourceProxy,
);
break;
case 'FragmentSpread':
// Explicitly ignore
break;
default:
throw new Error(
'Encountered an unexpected ReaderSelection variant in RelayRecordSourceProxy. This indicates a bug in Relay.',
);
}
}
}
function createSetterForPluralLinkedField(
selection: ReaderLinkedField,
variables: Variables,
updatableProxyRootRecord: RecordProxy,
recordSourceProxy: RecordSourceProxy,
) {
return function set(newValue: $ReadOnlyArray<{__id: string, ...}>) {
const newVariables = getArgumentValues(selection.args ?? [], variables);
if (newValue == null) {
throw new Error(
'Do not assign null to plural linked fields; assign an empty array instead.',
);
} else {
const recordProxies = newValue.map(item => {
if (item == null) {
throw new Error(
'When assigning an array of items, none of the items should be null or undefined.',
);
}
const {__id} = item;
if (__id == null) {
throw new Error(
'The __id field must be present on each item passed to the setter. This indicates a bug in Relay.',
);
}
const newValueRecord = recordSourceProxy.get(__id);
if (newValueRecord == null) {
throw new Error(
`Did not find item with data id ${__id} in the store.`,
);
}
return newValueRecord;
});
updatableProxyRootRecord.setLinkedRecords(
recordProxies,
selection.name,
newVariables,
);
}
};
}
function createSetterForSingularLinkedField(
selection: ReaderLinkedField,
variables: Variables,
updatableProxyRootRecord: RecordProxy,
recordSourceProxy: RecordSourceProxy,
) {
return function set(newValue: ?{__id: string, ...}) {
const newVariables = getArgumentValues(selection.args ?? [], variables);
if (newValue == null) {
updatableProxyRootRecord.setValue(newValue, selection.name, newVariables);
} else {
const {__id} = newValue;
if (__id == null) {
throw new Error(
'The __id field must be present on the argument. This indicates a bug in Relay.',
);
}
const newValueRecord = recordSourceProxy.get(__id);
if (newValueRecord == null) {
throw new Error(`Did not find item with data id ${__id} in the store.`);
}
updatableProxyRootRecord.setLinkedRecord(
newValueRecord,
selection.name,
newVariables,
);
}
};
}
function createGetterForPluralLinkedField(
selection: ReaderLinkedField,
variables: Variables,
updatableProxyRootRecord: RecordProxy,
recordSourceProxy: RecordSourceProxy,
) {
return function () {
const newVariables = getArgumentValues(selection.args ?? [], variables);
const linkedRecords = updatableProxyRootRecord.getLinkedRecords(
selection.name,
newVariables,
);
if (linkedRecords != null) {
return (linkedRecords.map(linkedRecord => {
if (linkedRecord != null) {
const updatableProxy = {};
updateProxyFromSelections(
updatableProxy,
linkedRecord,
variables,
selection.selections,
recordSourceProxy,
);
if (__DEV__) {
Object.freeze(updatableProxy);
}
// Flow incorrect assumes that the return value for the get method must match
// the set parameter.
// $FlowFixMe[unclear-type] Typed by the generated updatable query flow type
return (updatableProxy: any);
} else {
return linkedRecord;
}
// $FlowFixMe[unclear-type] Typed by the generated updatable query flow type
}): any);
} else {
return linkedRecords;
}
};
}
function createGetterForSingularLinkedField(
selection: ReaderLinkedField,
variables: Variables,
updatableProxyRootRecord: RecordProxy,
recordSourceProxy: RecordSourceProxy,
) {
return function () {
const newVariables = getArgumentValues(selection.args ?? [], variables);
const linkedRecord = updatableProxyRootRecord.getLinkedRecord(
selection.name,
newVariables,
);
if (linkedRecord != null) {
const updatableProxy = {};
updateProxyFromSelections(
updatableProxy,
linkedRecord,
variables,
selection.selections,
recordSourceProxy,
);
if (__DEV__) {
Object.freeze(updatableProxy);
}
// Flow incorrect assumes that the return value for the get method must match
// the set parameter.
// $FlowFixMe[unclear-type] Typed by the generated updatable query flow type
return (updatableProxy: any);
} else {
return linkedRecord;
}
};
}
module.exports = {createUpdatableProxy};