Skip to content

Commit

Permalink
Add support for copying field errors into the store
Browse files Browse the repository at this point in the history
Reviewed By: tyao1

Differential Revision: D47607969

fbshipit-source-id: b568e792e632e253371f8478dd20519e23017fd3
  • Loading branch information
Ryan Holdren authored and facebook-github-bot committed Oct 16, 2023
1 parent 2c5cac4 commit c4618c6
Show file tree
Hide file tree
Showing 17 changed files with 2,300 additions and 38 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -1459,12 +1459,13 @@ describe.each([
const warningCalls = warning.mock.calls.filter(
call => call[0] === false,
);
expect(warningCalls.length).toEqual(2); // the other warnings are from FragmentResource.js
expect(
warningCalls[1][1].includes(
'Relay: Call to `refetch` returned data with a different __typename:',
warningCalls.some(([_condition, format, ..._args]) =>
format.includes(
'Relay: Call to `refetch` returned data with a different __typename:',
),
),
).toEqual(true);
).toBe(true);
});

it('warns if a different id is returned', () => {
Expand Down
1 change: 1 addition & 0 deletions packages/relay-runtime/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ export type {
OptimisticUpdateFunction,
PluralReaderSelector,
Props,
RecordSourceJSON,
PublishQueue,
ReaderSelector,
ReadOnlyRecordProxy,
Expand Down
1 change: 1 addition & 0 deletions packages/relay-runtime/network/RelayNetworkTypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ export type PayloadError = interface {
column: number,
...
}>,
path?: Array<string | number>,
// Not officially part of the spec, but used at Facebook
severity?: 'CRITICAL' | 'ERROR' | 'WARNING',
};
Expand Down
176 changes: 176 additions & 0 deletions packages/relay-runtime/store/RelayErrorTrie.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
/**
* 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
* @format
* @oncall relay
*/

'use strict';

import type {PayloadError} from '../network/RelayNetworkTypes';

const RelayFeatureFlags = require('../util/RelayFeatureFlags');

const SELF: Self = Symbol('$SELF');

export opaque type Self = typeof SELF;

export type RelayFieldError = $ReadOnly<{
message: string,
path?: $ReadOnlyArray<string | number>,
severity?: 'CRITICAL' | 'ERROR' | 'WARNING',
}>;

/**
* This is a highly-specialized data structure that is designed
* to store the field errors of a GraphQL response in such a way
* that they can be performantly retrieved during normalization.
*
* In particular, the trie can be constructed in O(N) time, where
* N is the number of errors, so long as the depth of the GraphQL
* response data, and therefore the expected length of any error
* paths, is relatively small and constant.
*
* As we recursively traverse the data in the GraphQL response
* during normalization, we can get the sub trie for any field
* in O(1) time.
*/
export opaque type RelayErrorTrie = Map<
string | number | Self,
RelayErrorTrie | Array<Omit<RelayFieldError, 'path'>>,
>;

function buildErrorTrie(
errors: ?$ReadOnlyArray<PayloadError>,
): RelayErrorTrie | null {
if (errors == null) {
return null;
}
if (!RelayFeatureFlags.ENABLE_FIELD_ERROR_HANDLING) {
return null;
}
const trie: $NonMaybeType<RelayErrorTrie> = new Map();
// eslint-disable-next-line no-unused-vars
ERRORS: for (const {path, locations: _, ...error} of errors) {
if (path == null) {
continue;
}
const {length} = path;
if (length === 0) {
continue;
}
const lastIndex = length - 1;
let currentTrie = trie;
for (let index = 0; index < lastIndex; index++) {
const key = path[index];
const existingValue = currentTrie.get(key);
if (existingValue instanceof Map) {
currentTrie = existingValue;
continue;
}
const newValue: RelayErrorTrie = new Map();
if (Array.isArray(existingValue)) {
newValue.set(SELF, existingValue);
}
currentTrie.set(key, newValue);
currentTrie = newValue;
}
let lastKey: string | number | symbol = path[lastIndex];
let container = currentTrie.get(lastKey);
if (container instanceof Map) {
currentTrie = container;
container = currentTrie.get(lastKey);
lastKey = SELF;
}
if (Array.isArray(container)) {
container.push(error);
} else {
currentTrie.set(lastKey, [error]);
}
}
return trie;
}

function getErrorsByKey(
trie: RelayErrorTrie,
key: string | number,
): $ReadOnlyArray<RelayFieldError> | null {
const value = trie.get(key);
if (value == null) {
return null;
}
if (Array.isArray(value)) {
return value;
}
const errors: Array<
$ReadOnly<{
message: string,
path?: Array<string | number>,
severity?: 'CRITICAL' | 'ERROR' | 'WARNING',
}>,
> = [];
recursivelyCopyErrorsIntoArray(value, errors);
return errors;
}

function recursivelyCopyErrorsIntoArray(
trieOrSet: RelayErrorTrie,
errors: Array<
$ReadOnly<{
message: string,
path?: Array<string | number>,
severity?: 'CRITICAL' | 'ERROR' | 'WARNING',
}>,
>,
): void {
for (const [childKey, value] of trieOrSet) {
const oldLength = errors.length;
if (Array.isArray(value)) {
errors.push(...value);
} else {
recursivelyCopyErrorsIntoArray(value, errors);
}
if (childKey === SELF) {
continue;
}
const newLength = errors.length;
for (let index = oldLength; index < newLength; index++) {
const error = errors[index];
if (error.path == null) {
errors[index] = {
...error,
path: [childKey],
};
} else {
error.path.unshift(childKey);
}
}
}
}

function getNestedErrorTrieByKey(
trie: RelayErrorTrie,
key: string | number,
): RelayErrorTrie | null {
const value = trie.get(key);
if (value instanceof Map) {
return value;
}
return null;
}

module.exports = ({
SELF,
buildErrorTrie,
getNestedErrorTrieByKey,
getErrorsByKey,
}: {
SELF: typeof SELF,
buildErrorTrie: typeof buildErrorTrie,
getNestedErrorTrieByKey: typeof getNestedErrorTrieByKey,
getErrorsByKey: typeof getErrorsByKey,
});
Loading

0 comments on commit c4618c6

Please sign in to comment.