Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ export type MultiActorEnvironmentConfig = $ReadOnly<{
scheduler?: ?TaskScheduler,
shouldProcessClientComponents?: ?boolean,
treatMissingFieldsAsNull?: boolean,
deferDeduplicatedFields?: boolean,
}>;

class MultiActorEnvironment implements IMultiActorEnvironment {
Expand All @@ -91,6 +92,7 @@ class MultiActorEnvironment implements IMultiActorEnvironment {
+_scheduler: ?TaskScheduler;
+_shouldProcessClientComponents: ?boolean;
+_treatMissingFieldsAsNull: boolean;
+_deferDeduplicatedFields: boolean;

constructor(config: MultiActorEnvironmentConfig) {
this._actorEnvironments = new Map();
Expand All @@ -106,6 +108,7 @@ class MultiActorEnvironment implements IMultiActorEnvironment {
this._relayFieldLogger = config.relayFieldLogger ?? defaultRelayFieldLogger;
this._shouldProcessClientComponents = config.shouldProcessClientComponents;
this._treatMissingFieldsAsNull = config.treatMissingFieldsAsNull ?? false;
this._deferDeduplicatedFields = config.deferDeduplicatedFields ?? false;
this._isServer = config.isServer ?? false;
this._missingFieldHandlers = config.missingFieldHandlers ?? [];
this._createStoreForActor = config.createStoreForActor;
Expand Down Expand Up @@ -471,6 +474,7 @@ class MultiActorEnvironment implements IMultiActorEnvironment {
return this.forActor(actorIdentifier).getStore();
},
treatMissingFieldsAsNull: this._treatMissingFieldsAsNull,
deferDeduplicatedFields: this._deferDeduplicatedFields,
updater,
log: this._logFn,
normalizeResponse: this._normalizeResponse,
Expand Down
9 changes: 9 additions & 0 deletions packages/relay-runtime/store/OperationExecutor.js
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ export type ExecuteConfig<TMutation: MutationParameters> = {
+sink: Sink<GraphQLResponse>,
+source: RelayObservable<GraphQLResponse>,
+treatMissingFieldsAsNull: boolean,
+deferDeduplicatedFields: boolean,
+updater?: ?SelectorStoreUpdater<TMutation['response']>,
+log: LogFunction,
};
Expand Down Expand Up @@ -127,6 +128,7 @@ class Executor<TMutation: MutationParameters> {
_actorIdentifier: ActorIdentifier;
_getDataID: GetDataID;
_treatMissingFieldsAsNull: boolean;
_deferDeduplicatedFields: boolean;
_incrementalPayloadsPending: boolean;
_incrementalResults: Map<Label, Map<PathKey, IncrementalResults>>;
_log: LogFunction;
Expand Down Expand Up @@ -177,13 +179,15 @@ class Executor<TMutation: MutationParameters> {
sink,
source,
treatMissingFieldsAsNull,
deferDeduplicatedFields,
updater,
log,
normalizeResponse,
}: ExecuteConfig<TMutation>): void {
this._actorIdentifier = actorIdentifier;
this._getDataID = getDataID;
this._treatMissingFieldsAsNull = treatMissingFieldsAsNull;
this._deferDeduplicatedFields = deferDeduplicatedFields;
this._incrementalPayloadsPending = false;
this._incrementalResults = new Map();
this._log = log;
Expand Down Expand Up @@ -660,6 +664,7 @@ class Executor<TMutation: MutationParameters> {
path: [],
shouldProcessClientComponents: this._shouldProcessClientComponents,
treatMissingFieldsAsNull,
deferDeduplicatedFields: false,
},
this._useExecTimeResolvers,
);
Expand Down Expand Up @@ -778,6 +783,7 @@ class Executor<TMutation: MutationParameters> {
path: followupPayload.path,
treatMissingFieldsAsNull: this._treatMissingFieldsAsNull,
shouldProcessClientComponents: this._shouldProcessClientComponents,
deferDeduplicatedFields: false,
},
this._useExecTimeResolvers,
);
Expand Down Expand Up @@ -862,6 +868,7 @@ class Executor<TMutation: MutationParameters> {
path: [],
treatMissingFieldsAsNull: this._treatMissingFieldsAsNull,
shouldProcessClientComponents: this._shouldProcessClientComponents,
deferDeduplicatedFields: false,
},
this._useExecTimeResolvers,
);
Expand Down Expand Up @@ -1327,6 +1334,7 @@ class Executor<TMutation: MutationParameters> {
log: this._log,
path: placeholder.path,
treatMissingFieldsAsNull: this._treatMissingFieldsAsNull,
deferDeduplicatedFields: this._deferDeduplicatedFields,
shouldProcessClientComponents: this._shouldProcessClientComponents,
},
this._useExecTimeResolvers,
Expand Down Expand Up @@ -1560,6 +1568,7 @@ class Executor<TMutation: MutationParameters> {
log: this._log,
path: [...normalizationPath, responseKey, String(itemIndex)],
treatMissingFieldsAsNull: this._treatMissingFieldsAsNull,
deferDeduplicatedFields: false,
shouldProcessClientComponents: this._shouldProcessClientComponents,
},
this._useExecTimeResolvers,
Expand Down
4 changes: 4 additions & 0 deletions packages/relay-runtime/store/RelayModernEnvironment.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ export type EnvironmentConfig = {
+configName?: string,
+handlerProvider?: ?HandlerProvider,
+treatMissingFieldsAsNull?: boolean,
+deferDeduplicatedFields?: boolean,
+log?: ?LogFunction,
+operationLoader?: ?OperationLoader,
+network: INetwork,
Expand Down Expand Up @@ -97,6 +98,7 @@ class RelayModernEnvironment implements IEnvironment {
_operationTracker: OperationTracker;
_getDataID: GetDataID;
_treatMissingFieldsAsNull: boolean;
_deferDeduplicatedFields: boolean;
_operationExecutions: Map<string, ActiveState>;
+options: mixed;
+_isServer: boolean;
Expand All @@ -106,6 +108,7 @@ class RelayModernEnvironment implements IEnvironment {
constructor(config: EnvironmentConfig) {
this.configName = config.configName;
this._treatMissingFieldsAsNull = config.treatMissingFieldsAsNull === true;
this._deferDeduplicatedFields = config.deferDeduplicatedFields === true;
const operationLoader = config.operationLoader;
if (__DEV__) {
if (operationLoader != null) {
Expand Down Expand Up @@ -500,6 +503,7 @@ class RelayModernEnvironment implements IEnvironment {
return store;
},
treatMissingFieldsAsNull: this._treatMissingFieldsAsNull,
deferDeduplicatedFields: this._deferDeduplicatedFields,
updater,
normalizeResponse: this._normalizeResponse,
});
Expand Down
4 changes: 4 additions & 0 deletions packages/relay-runtime/store/RelayModernStore.js
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ class RelayModernStore implements Store {
_resolverContext: ?ResolverContext;
_actorIdentifier: ?ActorIdentifier;
_treatMissingFieldsAsNull: boolean;
_deferDeduplicatedFields: boolean;

constructor(
source: MutableRecordSource,
Expand All @@ -134,6 +135,7 @@ class RelayModernStore implements Store {
// These additional config options are only used if the experimental
// @outputType resolver feature is used
treatMissingFieldsAsNull?: ?boolean,
deferDeduplicatedFields?: ?boolean,
actorIdentifier?: ?ActorIdentifier,
},
) {
Expand Down Expand Up @@ -182,6 +184,7 @@ class RelayModernStore implements Store {
options?.shouldProcessClientComponents ?? false;

this._treatMissingFieldsAsNull = options?.treatMissingFieldsAsNull ?? false;
this._deferDeduplicatedFields = options?.deferDeduplicatedFields ?? false;
this._actorIdentifier = options?.actorIdentifier;

initializeRecordSource(this._recordSource);
Expand Down Expand Up @@ -827,6 +830,7 @@ class RelayModernStore implements Store {
getDataID: this._getDataID,
log: this.__log,
treatMissingFieldsAsNull: this._treatMissingFieldsAsNull,
deferDeduplicatedFields: this._deferDeduplicatedFields,
shouldProcessClientComponents: this._shouldProcessClientComponents,
actorIdentifier: this._actorIdentifier,
};
Expand Down
7 changes: 6 additions & 1 deletion packages/relay-runtime/store/RelayResponseNormalizer.js
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ export type GetDataID = (
export type NormalizationOptions = {
+getDataID: GetDataID,
+treatMissingFieldsAsNull: boolean,
+deferDeduplicatedFields: boolean,
+log: ?LogFunction,
+path?: $ReadOnlyArray<string>,
+shouldProcessClientComponents?: ?boolean,
Expand Down Expand Up @@ -114,6 +115,7 @@ class RelayResponseNormalizer {
_getDataId: GetDataID;
_handleFieldPayloads: Array<HandleFieldPayload>;
_treatMissingFieldsAsNull: boolean;
_deferDeduplicatedFields: boolean;
_incrementalPlaceholders: Array<IncrementalDataPlaceholder>;
_isClientExtension: boolean;
_isUnmatchedAbstractType: boolean;
Expand All @@ -136,6 +138,7 @@ class RelayResponseNormalizer {
this._getDataId = options.getDataID;
this._handleFieldPayloads = [];
this._treatMissingFieldsAsNull = options.treatMissingFieldsAsNull;
this._deferDeduplicatedFields = options.deferDeduplicatedFields;
this._incrementalPlaceholders = [];
this._isClientExtension = false;
this._isUnmatchedAbstractType = false;
Expand Down Expand Up @@ -525,7 +528,9 @@ class RelayResponseNormalizer {
// client is assumed to be correctly configured with
// treatMissingFieldsAsNull=true.
const isOptionalField =
this._isClientExtension || this._isUnmatchedAbstractType;
this._isClientExtension ||
this._isUnmatchedAbstractType ||
this._deferDeduplicatedFields;

if (isOptionalField) {
// Field not expected to exist regardless of whether the server is pruning null
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ const {getRequest} = require('relay-runtime/query/GraphQLTag');
const defaultOptions = {
getDataID: defaultGetDataID,
treatMissingFieldsAsNull: false,
deferDeduplicatedFields: false,
log: null,
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ const {ROOT_ID} = require('relay-runtime/store/RelayStoreUtils');
const defaultOptions = {
getDataID: defaultGetDataID,
treatMissingFieldsAsNull: false,
deferDeduplicatedFields: false,
log: null,
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,90 @@ describe.each(['RelayModernEnvironment', 'MultiActorEnvironment'])(
});
});

it('processes deferred payloads with deduplicated fields', () => {
// This functionality prevents Relay from throwing an error when missing
// fields are received in a deferred response. This is required for the
// latest `@defer` spec proposal, which does not send duplicate fields
// in deferred responses. While Relay does not yet attempt to support the latest
// spec proposal (https://github.com/graphql/defer-stream-wg/discussions/69),
// this option allows users to transform responses into a format that Relay
// can accept.
environment = new RelayModernEnvironment({
network: RelayNetwork.create(fetch),
store,
handlerProvider,
deferDeduplicatedFields: true,
});

query = graphql`
query RelayModernEnvironmentExecuteWithDeferTestUserOverlappingFieldsQuery(
$id: ID!
) {
node(id: $id) {
... on User {
id
name
...RelayModernEnvironmentExecuteWithDeferTestUserOverlappingFieldsFragment
@dangerously_unaliased_fixme
@defer(label: "UserFragment")
}
}
}
`;
fragment = graphql`
fragment RelayModernEnvironmentExecuteWithDeferTestUserOverlappingFieldsFragment on User {
name
alternate_name
}
`;

variables = {id: '1'};
operation = createOperationDescriptor(query, variables);
selector = createReaderSelector(fragment, '1', {}, operation.request);

const initialSnapshot = environment.lookup(selector);
const callback = jest.fn<[Snapshot], void>();
environment.subscribe(initialSnapshot, callback);

environment.execute({operation}).subscribe(callbacks);
dataSource.next({
data: {
node: {
__typename: 'User',
id: '1',
name: 'joe',
},
},
});

jest.runAllTimers();
next.mockClear();
callback.mockClear();

dataSource.next({
data: {
alternate_name: 'joe2',
},
label:
'RelayModernEnvironmentExecuteWithDeferTestUserOverlappingFieldsQuery$defer$UserFragment',
path: ['node'],
extensions: {
is_final: true,
},
});

expect(complete).toBeCalledTimes(0);
expect(error).toBeCalledTimes(0);
expect(next).toBeCalledTimes(1);
expect(callback).toBeCalledTimes(1);
const snapshot = callback.mock.calls[0][0];
expect(snapshot.isMissingData).toBe(false);
expect(snapshot.data).toEqual({
name: 'joe',
alternate_name: 'joe2',
});
});

describe('Query with exec time resolvers', () => {
let resolverOperation;
beforeEach(() => {
Expand Down
Loading
Loading