relay-runtime 9.0.0 → 9.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/handlers/RelayDefaultHandlerProvider.js.flow +34 -0
- package/handlers/connection/ConnectionHandler.js.flow +549 -0
- package/handlers/connection/ConnectionInterface.js.flow +92 -0
- package/index.js +1 -1
- package/index.js.flow +314 -0
- package/lib/handlers/connection/ConnectionHandler.js +1 -3
- package/lib/index.js +1 -2
- package/lib/mutations/RelayDeclarativeMutationConfig.js +22 -45
- package/lib/mutations/RelayRecordProxy.js +1 -3
- package/lib/mutations/RelayRecordSourceMutator.js +1 -3
- package/lib/mutations/RelayRecordSourceProxy.js +1 -3
- package/lib/mutations/RelayRecordSourceSelectorProxy.js +1 -3
- package/lib/mutations/commitMutation.js +2 -0
- package/lib/mutations/validateMutation.js +13 -4
- package/lib/network/RelayObservable.js +9 -9
- package/lib/network/RelayQueryResponseCache.js +8 -6
- package/lib/query/fetchQueryInternal.js +1 -8
- package/lib/store/DataChecker.js +23 -51
- package/lib/store/RelayConcreteVariables.js +6 -2
- package/lib/store/RelayModernEnvironment.js +30 -12
- package/lib/store/RelayModernFragmentSpecResolver.js +9 -13
- package/lib/store/RelayModernQueryExecutor.js +73 -37
- package/lib/store/RelayModernRecord.js +14 -9
- package/lib/store/RelayModernStore.js +107 -70
- package/lib/store/RelayOperationTracker.js +35 -78
- package/lib/store/RelayOptimisticRecordSource.js +7 -5
- package/lib/store/RelayPublishQueue.js +1 -3
- package/lib/store/RelayReader.js +1 -3
- package/lib/store/RelayRecordSource.js +1 -3
- package/lib/store/RelayRecordSourceMapImpl.js +13 -18
- package/lib/store/RelayReferenceMarker.js +2 -6
- package/lib/store/RelayResponseNormalizer.js +9 -10
- package/lib/store/StoreInspector.js +7 -5
- package/lib/store/normalizeRelayPayload.js +6 -2
- package/lib/subscription/requestSubscription.js +4 -2
- package/lib/util/RelayFeatureFlags.js +1 -1
- package/lib/util/RelayReplaySubject.js +1 -3
- package/lib/util/createPayloadFor3DField.js +7 -2
- package/mutations/RelayDeclarativeMutationConfig.js.flow +380 -0
- package/mutations/RelayRecordProxy.js.flow +165 -0
- package/mutations/RelayRecordSourceMutator.js.flow +238 -0
- package/mutations/RelayRecordSourceProxy.js.flow +164 -0
- package/mutations/RelayRecordSourceSelectorProxy.js.flow +119 -0
- package/mutations/applyOptimisticMutation.js.flow +76 -0
- package/mutations/commitLocalUpdate.js.flow +24 -0
- package/mutations/commitMutation.js.flow +184 -0
- package/mutations/validateMutation.js.flow +211 -0
- package/network/ConvertToExecuteFunction.js.flow +49 -0
- package/network/RelayNetwork.js.flow +84 -0
- package/network/RelayNetworkTypes.js.flow +123 -0
- package/network/RelayObservable.js.flow +634 -0
- package/network/RelayQueryResponseCache.js.flow +111 -0
- package/package.json +1 -1
- package/query/GraphQLTag.js.flow +166 -0
- package/query/fetchQuery.js.flow +47 -0
- package/query/fetchQueryInternal.js.flow +349 -0
- package/relay-runtime.js +2 -2
- package/relay-runtime.min.js +2 -2
- package/store/ClientID.js.flow +43 -0
- package/store/DataChecker.js.flow +426 -0
- package/store/RelayConcreteVariables.js.flow +96 -0
- package/store/RelayModernEnvironment.js.flow +526 -0
- package/store/RelayModernFragmentSpecResolver.js.flow +426 -0
- package/store/RelayModernOperationDescriptor.js.flow +88 -0
- package/store/RelayModernQueryExecutor.js.flow +1327 -0
- package/store/RelayModernRecord.js.flow +403 -0
- package/store/RelayModernSelector.js.flow +444 -0
- package/store/RelayModernStore.js.flow +757 -0
- package/store/RelayOperationTracker.js.flow +164 -0
- package/store/RelayOptimisticRecordSource.js.flow +119 -0
- package/store/RelayPublishQueue.js.flow +401 -0
- package/store/RelayReader.js.flow +376 -0
- package/store/RelayRecordSource.js.flow +29 -0
- package/store/RelayRecordSourceMapImpl.js.flow +87 -0
- package/store/RelayRecordState.js.flow +37 -0
- package/store/RelayReferenceMarker.js.flow +236 -0
- package/store/RelayResponseNormalizer.js.flow +556 -0
- package/store/RelayStoreTypes.js.flow +873 -0
- package/store/RelayStoreUtils.js.flow +218 -0
- package/store/StoreInspector.js.flow +173 -0
- package/store/ViewerPattern.js.flow +26 -0
- package/store/cloneRelayHandleSourceField.js.flow +66 -0
- package/store/createFragmentSpecResolver.js.flow +55 -0
- package/store/createRelayContext.js.flow +44 -0
- package/store/defaultGetDataID.js.flow +27 -0
- package/store/hasOverlappingIDs.js.flow +34 -0
- package/store/isRelayModernEnvironment.js.flow +27 -0
- package/store/normalizeRelayPayload.js.flow +51 -0
- package/store/readInlineData.js.flow +75 -0
- package/subscription/requestSubscription.js.flow +100 -0
- package/util/JSResourceTypes.flow.js.flow +20 -0
- package/util/NormalizationNode.js.flow +191 -0
- package/util/ReaderNode.js.flow +208 -0
- package/util/RelayConcreteNode.js.flow +80 -0
- package/util/RelayDefaultHandleKey.js.flow +17 -0
- package/util/RelayError.js.flow +33 -0
- package/util/RelayFeatureFlags.js.flow +30 -0
- package/util/RelayProfiler.js.flow +284 -0
- package/util/RelayReplaySubject.js.flow +134 -0
- package/util/RelayRuntimeTypes.js.flow +70 -0
- package/util/createPayloadFor3DField.js.flow +43 -0
- package/util/deepFreeze.js.flow +36 -0
- package/util/generateID.js.flow +21 -0
- package/util/getFragmentIdentifier.js.flow +52 -0
- package/util/getRelayHandleKey.js.flow +41 -0
- package/util/getRequestIdentifier.js.flow +41 -0
- package/util/isPromise.js.flow +21 -0
- package/util/isScalarAndEqual.js.flow +26 -0
- package/util/recycleNodesInto.js.flow +80 -0
- package/util/resolveImmediate.js.flow +30 -0
- package/util/stableCopy.js.flow +35 -0
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) Facebook, Inc. and its affiliates.
|
|
3
|
+
*
|
|
4
|
+
* This source code is licensed under the MIT license found in the
|
|
5
|
+
* LICENSE file in the root directory of this source tree.
|
|
6
|
+
*
|
|
7
|
+
* @flow strict-local
|
|
8
|
+
* @format
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
// flowlint ambiguous-object-type:error
|
|
12
|
+
|
|
13
|
+
'use strict';
|
|
14
|
+
|
|
15
|
+
const ConnectionHandler = require('./connection/ConnectionHandler');
|
|
16
|
+
|
|
17
|
+
const invariant = require('invariant');
|
|
18
|
+
|
|
19
|
+
import type {Handler} from '../store/RelayStoreTypes';
|
|
20
|
+
export type HandlerProvider = (name: string) => ?Handler;
|
|
21
|
+
|
|
22
|
+
function RelayDefaultHandlerProvider(handle: string): Handler {
|
|
23
|
+
switch (handle) {
|
|
24
|
+
case 'connection':
|
|
25
|
+
return ConnectionHandler;
|
|
26
|
+
}
|
|
27
|
+
invariant(
|
|
28
|
+
false,
|
|
29
|
+
'RelayDefaultHandlerProvider: No handler provided for `%s`.',
|
|
30
|
+
handle,
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
module.exports = RelayDefaultHandlerProvider;
|
|
@@ -0,0 +1,549 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) Facebook, Inc. and its affiliates.
|
|
3
|
+
*
|
|
4
|
+
* This source code is licensed under the MIT license found in the
|
|
5
|
+
* LICENSE file in the root directory of this source tree.
|
|
6
|
+
*
|
|
7
|
+
* @flow
|
|
8
|
+
* @format
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
// flowlint ambiguous-object-type:error
|
|
12
|
+
|
|
13
|
+
'use strict';
|
|
14
|
+
|
|
15
|
+
const ConnectionInterface = require('./ConnectionInterface');
|
|
16
|
+
|
|
17
|
+
const getRelayHandleKey = require('../../util/getRelayHandleKey');
|
|
18
|
+
const invariant = require('invariant');
|
|
19
|
+
const warning = require('warning');
|
|
20
|
+
|
|
21
|
+
const {generateClientID} = require('../../store/ClientID');
|
|
22
|
+
|
|
23
|
+
import type {
|
|
24
|
+
HandleFieldPayload,
|
|
25
|
+
RecordProxy,
|
|
26
|
+
ReadOnlyRecordProxy,
|
|
27
|
+
RecordSourceProxy,
|
|
28
|
+
} from '../../store/RelayStoreTypes';
|
|
29
|
+
import type {DataID, Variables} from '../../util/RelayRuntimeTypes';
|
|
30
|
+
|
|
31
|
+
export type ConnectionMetadata = {
|
|
32
|
+
path: ?Array<string>,
|
|
33
|
+
direction: ?('forward' | 'backward' | 'bidirectional'),
|
|
34
|
+
cursor: ?string,
|
|
35
|
+
count: ?string,
|
|
36
|
+
stream?: boolean,
|
|
37
|
+
...
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const CONNECTION = 'connection';
|
|
41
|
+
|
|
42
|
+
// Per-instance incrementing index used to generate unique edge IDs
|
|
43
|
+
const NEXT_EDGE_INDEX = '__connection_next_edge_index';
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* @public
|
|
47
|
+
*
|
|
48
|
+
* A default runtime handler for connection fields that appends newly fetched
|
|
49
|
+
* edges onto the end of a connection, regardless of the arguments used to fetch
|
|
50
|
+
* those edges.
|
|
51
|
+
*/
|
|
52
|
+
function update(store: RecordSourceProxy, payload: HandleFieldPayload): void {
|
|
53
|
+
const record = store.get(payload.dataID);
|
|
54
|
+
if (!record) {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const {
|
|
59
|
+
EDGES,
|
|
60
|
+
END_CURSOR,
|
|
61
|
+
HAS_NEXT_PAGE,
|
|
62
|
+
HAS_PREV_PAGE,
|
|
63
|
+
PAGE_INFO,
|
|
64
|
+
PAGE_INFO_TYPE,
|
|
65
|
+
START_CURSOR,
|
|
66
|
+
} = ConnectionInterface.get();
|
|
67
|
+
|
|
68
|
+
const serverConnection = record.getLinkedRecord(payload.fieldKey);
|
|
69
|
+
const serverPageInfo =
|
|
70
|
+
serverConnection && serverConnection.getLinkedRecord(PAGE_INFO);
|
|
71
|
+
if (!serverConnection) {
|
|
72
|
+
record.setValue(null, payload.handleKey);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
// In rare cases the handleKey field may be unset even though the client
|
|
76
|
+
// connection record exists, in this case new edges should still be merged
|
|
77
|
+
// into the existing client connection record (and the field reset to point
|
|
78
|
+
// to that record).
|
|
79
|
+
const clientConnectionID = generateClientID(
|
|
80
|
+
record.getDataID(),
|
|
81
|
+
payload.handleKey,
|
|
82
|
+
);
|
|
83
|
+
const clientConnectionField = record.getLinkedRecord(payload.handleKey);
|
|
84
|
+
const clientConnection =
|
|
85
|
+
clientConnectionField ?? store.get(clientConnectionID);
|
|
86
|
+
let clientPageInfo =
|
|
87
|
+
clientConnection && clientConnection.getLinkedRecord(PAGE_INFO);
|
|
88
|
+
if (!clientConnection) {
|
|
89
|
+
// Initial fetch with data: copy fields from the server record
|
|
90
|
+
const connection = store.create(
|
|
91
|
+
clientConnectionID,
|
|
92
|
+
serverConnection.getType(),
|
|
93
|
+
);
|
|
94
|
+
connection.setValue(0, NEXT_EDGE_INDEX);
|
|
95
|
+
connection.copyFieldsFrom(serverConnection);
|
|
96
|
+
let serverEdges = serverConnection.getLinkedRecords(EDGES);
|
|
97
|
+
if (serverEdges) {
|
|
98
|
+
serverEdges = serverEdges.map(edge =>
|
|
99
|
+
buildConnectionEdge(store, connection, edge),
|
|
100
|
+
);
|
|
101
|
+
connection.setLinkedRecords(serverEdges, EDGES);
|
|
102
|
+
}
|
|
103
|
+
record.setLinkedRecord(connection, payload.handleKey);
|
|
104
|
+
|
|
105
|
+
clientPageInfo = store.create(
|
|
106
|
+
generateClientID(connection.getDataID(), PAGE_INFO),
|
|
107
|
+
PAGE_INFO_TYPE,
|
|
108
|
+
);
|
|
109
|
+
clientPageInfo.setValue(false, HAS_NEXT_PAGE);
|
|
110
|
+
clientPageInfo.setValue(false, HAS_PREV_PAGE);
|
|
111
|
+
clientPageInfo.setValue(null, END_CURSOR);
|
|
112
|
+
clientPageInfo.setValue(null, START_CURSOR);
|
|
113
|
+
if (serverPageInfo) {
|
|
114
|
+
clientPageInfo.copyFieldsFrom(serverPageInfo);
|
|
115
|
+
}
|
|
116
|
+
connection.setLinkedRecord(clientPageInfo, PAGE_INFO);
|
|
117
|
+
} else {
|
|
118
|
+
if (clientConnectionField == null) {
|
|
119
|
+
// If the handleKey field was unset but the client connection record
|
|
120
|
+
// existed, update the field to point to the record
|
|
121
|
+
record.setLinkedRecord(clientConnection, payload.handleKey);
|
|
122
|
+
}
|
|
123
|
+
const connection = clientConnection;
|
|
124
|
+
// Subsequent fetches:
|
|
125
|
+
// - updated fields on the connection
|
|
126
|
+
// - merge prev/next edges, de-duplicating by node id
|
|
127
|
+
// - synthesize page info fields
|
|
128
|
+
let serverEdges = serverConnection.getLinkedRecords(EDGES);
|
|
129
|
+
if (serverEdges) {
|
|
130
|
+
serverEdges = serverEdges.map(edge =>
|
|
131
|
+
buildConnectionEdge(store, connection, edge),
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
const prevEdges = connection.getLinkedRecords(EDGES);
|
|
135
|
+
const prevPageInfo = connection.getLinkedRecord(PAGE_INFO);
|
|
136
|
+
connection.copyFieldsFrom(serverConnection);
|
|
137
|
+
// Reset EDGES and PAGE_INFO fields
|
|
138
|
+
if (prevEdges) {
|
|
139
|
+
connection.setLinkedRecords(prevEdges, EDGES);
|
|
140
|
+
}
|
|
141
|
+
if (prevPageInfo) {
|
|
142
|
+
connection.setLinkedRecord(prevPageInfo, PAGE_INFO);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
let nextEdges = [];
|
|
146
|
+
const args = payload.args;
|
|
147
|
+
if (prevEdges && serverEdges) {
|
|
148
|
+
if (args.after != null) {
|
|
149
|
+
// Forward pagination from the end of the connection: append edges
|
|
150
|
+
if (
|
|
151
|
+
clientPageInfo &&
|
|
152
|
+
args.after === clientPageInfo.getValue(END_CURSOR)
|
|
153
|
+
) {
|
|
154
|
+
const nodeIDs = new Set();
|
|
155
|
+
mergeEdges(prevEdges, nextEdges, nodeIDs);
|
|
156
|
+
mergeEdges(serverEdges, nextEdges, nodeIDs);
|
|
157
|
+
} else {
|
|
158
|
+
warning(
|
|
159
|
+
false,
|
|
160
|
+
'Relay: Unexpected after cursor `%s`, edges must ' +
|
|
161
|
+
'be fetched from the end of the list (`%s`).',
|
|
162
|
+
args.after,
|
|
163
|
+
clientPageInfo && clientPageInfo.getValue(END_CURSOR),
|
|
164
|
+
);
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
} else if (args.before != null) {
|
|
168
|
+
// Backward pagination from the start of the connection: prepend edges
|
|
169
|
+
if (
|
|
170
|
+
clientPageInfo &&
|
|
171
|
+
args.before === clientPageInfo.getValue(START_CURSOR)
|
|
172
|
+
) {
|
|
173
|
+
const nodeIDs = new Set();
|
|
174
|
+
mergeEdges(serverEdges, nextEdges, nodeIDs);
|
|
175
|
+
mergeEdges(prevEdges, nextEdges, nodeIDs);
|
|
176
|
+
} else {
|
|
177
|
+
warning(
|
|
178
|
+
false,
|
|
179
|
+
'Relay: Unexpected before cursor `%s`, edges must ' +
|
|
180
|
+
'be fetched from the beginning of the list (`%s`).',
|
|
181
|
+
args.before,
|
|
182
|
+
clientPageInfo && clientPageInfo.getValue(START_CURSOR),
|
|
183
|
+
);
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
} else {
|
|
187
|
+
// The connection was refetched from the beginning/end: replace edges
|
|
188
|
+
nextEdges = serverEdges;
|
|
189
|
+
}
|
|
190
|
+
} else if (serverEdges) {
|
|
191
|
+
nextEdges = serverEdges;
|
|
192
|
+
} else {
|
|
193
|
+
nextEdges = prevEdges;
|
|
194
|
+
}
|
|
195
|
+
// Update edges only if they were updated, the null check is
|
|
196
|
+
// for Flow (prevEdges could be null).
|
|
197
|
+
if (nextEdges != null && nextEdges !== prevEdges) {
|
|
198
|
+
connection.setLinkedRecords(nextEdges, EDGES);
|
|
199
|
+
}
|
|
200
|
+
// Page info should be updated even if no new edge were returned.
|
|
201
|
+
if (clientPageInfo && serverPageInfo) {
|
|
202
|
+
if (args.after == null && args.before == null) {
|
|
203
|
+
// The connection was refetched from the beginning/end: replace
|
|
204
|
+
// page_info
|
|
205
|
+
clientPageInfo.copyFieldsFrom(serverPageInfo);
|
|
206
|
+
} else if (args.before != null || (args.after == null && args.last)) {
|
|
207
|
+
clientPageInfo.setValue(
|
|
208
|
+
!!serverPageInfo.getValue(HAS_PREV_PAGE),
|
|
209
|
+
HAS_PREV_PAGE,
|
|
210
|
+
);
|
|
211
|
+
const startCursor = serverPageInfo.getValue(START_CURSOR);
|
|
212
|
+
if (typeof startCursor === 'string') {
|
|
213
|
+
clientPageInfo.setValue(startCursor, START_CURSOR);
|
|
214
|
+
}
|
|
215
|
+
} else if (args.after != null || (args.before == null && args.first)) {
|
|
216
|
+
clientPageInfo.setValue(
|
|
217
|
+
!!serverPageInfo.getValue(HAS_NEXT_PAGE),
|
|
218
|
+
HAS_NEXT_PAGE,
|
|
219
|
+
);
|
|
220
|
+
const endCursor = serverPageInfo.getValue(END_CURSOR);
|
|
221
|
+
if (typeof endCursor === 'string') {
|
|
222
|
+
clientPageInfo.setValue(endCursor, END_CURSOR);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* @public
|
|
231
|
+
*
|
|
232
|
+
* Given a record and the name of the schema field for which a connection was
|
|
233
|
+
* fetched, returns the linked connection record.
|
|
234
|
+
*
|
|
235
|
+
* Example:
|
|
236
|
+
*
|
|
237
|
+
* Given that data has already been fetched on some user `<id>` on the `friends`
|
|
238
|
+
* field:
|
|
239
|
+
*
|
|
240
|
+
* ```
|
|
241
|
+
* fragment FriendsFragment on User {
|
|
242
|
+
* friends(first: 10) @connection(key: "FriendsFragment_friends") {
|
|
243
|
+
* edges {
|
|
244
|
+
* node {
|
|
245
|
+
* id
|
|
246
|
+
* }
|
|
247
|
+
* }
|
|
248
|
+
* }
|
|
249
|
+
* }
|
|
250
|
+
* ```
|
|
251
|
+
*
|
|
252
|
+
* The `friends` connection record can be accessed with:
|
|
253
|
+
*
|
|
254
|
+
* ```
|
|
255
|
+
* store => {
|
|
256
|
+
* const user = store.get('<id>');
|
|
257
|
+
* const friends = ConnectionHandler.getConnection(user, 'FriendsFragment_friends');
|
|
258
|
+
* // Access fields on the connection:
|
|
259
|
+
* const edges = friends.getLinkedRecords('edges');
|
|
260
|
+
* }
|
|
261
|
+
* ```
|
|
262
|
+
*
|
|
263
|
+
* TODO: t15733312
|
|
264
|
+
* Currently we haven't run into this case yet, but we need to add a `getConnections`
|
|
265
|
+
* that returns an array of the connections under the same `key` regardless of the variables.
|
|
266
|
+
*/
|
|
267
|
+
function getConnection(
|
|
268
|
+
record: ReadOnlyRecordProxy,
|
|
269
|
+
key: string,
|
|
270
|
+
filters?: ?Variables,
|
|
271
|
+
): ?RecordProxy {
|
|
272
|
+
const handleKey = getRelayHandleKey(CONNECTION, key, null);
|
|
273
|
+
return record.getLinkedRecord(handleKey, filters);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* @public
|
|
278
|
+
*
|
|
279
|
+
* Inserts an edge after the given cursor, or at the end of the list if no
|
|
280
|
+
* cursor is provided.
|
|
281
|
+
*
|
|
282
|
+
* Example:
|
|
283
|
+
*
|
|
284
|
+
* Given that data has already been fetched on some user `<id>` on the `friends`
|
|
285
|
+
* field:
|
|
286
|
+
*
|
|
287
|
+
* ```
|
|
288
|
+
* fragment FriendsFragment on User {
|
|
289
|
+
* friends(first: 10) @connection(key: "FriendsFragment_friends") {
|
|
290
|
+
* edges {
|
|
291
|
+
* node {
|
|
292
|
+
* id
|
|
293
|
+
* }
|
|
294
|
+
* }
|
|
295
|
+
* }
|
|
296
|
+
* }
|
|
297
|
+
* ```
|
|
298
|
+
*
|
|
299
|
+
* An edge can be appended with:
|
|
300
|
+
*
|
|
301
|
+
* ```
|
|
302
|
+
* store => {
|
|
303
|
+
* const user = store.get('<id>');
|
|
304
|
+
* const friends = ConnectionHandler.getConnection(user, 'FriendsFragment_friends');
|
|
305
|
+
* const edge = store.create('<edge-id>', 'FriendsEdge');
|
|
306
|
+
* ConnectionHandler.insertEdgeAfter(friends, edge);
|
|
307
|
+
* }
|
|
308
|
+
* ```
|
|
309
|
+
*/
|
|
310
|
+
function insertEdgeAfter(
|
|
311
|
+
record: RecordProxy,
|
|
312
|
+
newEdge: RecordProxy,
|
|
313
|
+
cursor?: ?string,
|
|
314
|
+
): void {
|
|
315
|
+
const {CURSOR, EDGES} = ConnectionInterface.get();
|
|
316
|
+
|
|
317
|
+
const edges = record.getLinkedRecords(EDGES);
|
|
318
|
+
if (!edges) {
|
|
319
|
+
record.setLinkedRecords([newEdge], EDGES);
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
let nextEdges;
|
|
323
|
+
if (cursor == null) {
|
|
324
|
+
nextEdges = edges.concat(newEdge);
|
|
325
|
+
} else {
|
|
326
|
+
nextEdges = [];
|
|
327
|
+
let foundCursor = false;
|
|
328
|
+
for (let ii = 0; ii < edges.length; ii++) {
|
|
329
|
+
const edge = edges[ii];
|
|
330
|
+
nextEdges.push(edge);
|
|
331
|
+
if (edge == null) {
|
|
332
|
+
continue;
|
|
333
|
+
}
|
|
334
|
+
const edgeCursor = edge.getValue(CURSOR);
|
|
335
|
+
if (cursor === edgeCursor) {
|
|
336
|
+
nextEdges.push(newEdge);
|
|
337
|
+
foundCursor = true;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
if (!foundCursor) {
|
|
341
|
+
nextEdges.push(newEdge);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
record.setLinkedRecords(nextEdges, EDGES);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* @public
|
|
349
|
+
*
|
|
350
|
+
* Creates an edge for a connection record, given a node and edge type.
|
|
351
|
+
*/
|
|
352
|
+
function createEdge(
|
|
353
|
+
store: RecordSourceProxy,
|
|
354
|
+
record: RecordProxy,
|
|
355
|
+
node: RecordProxy,
|
|
356
|
+
edgeType: string,
|
|
357
|
+
): RecordProxy {
|
|
358
|
+
const {NODE} = ConnectionInterface.get();
|
|
359
|
+
|
|
360
|
+
// An index-based client ID could easily conflict (unless it was
|
|
361
|
+
// auto-incrementing, but there is nowhere to the store the id)
|
|
362
|
+
// Instead, construct a client ID based on the connection ID and node ID,
|
|
363
|
+
// which will only conflict if the same node is added to the same connection
|
|
364
|
+
// twice. This is acceptable since the `insertEdge*` functions ignore
|
|
365
|
+
// duplicates.
|
|
366
|
+
const edgeID = generateClientID(record.getDataID(), node.getDataID());
|
|
367
|
+
let edge = store.get(edgeID);
|
|
368
|
+
if (!edge) {
|
|
369
|
+
edge = store.create(edgeID, edgeType);
|
|
370
|
+
}
|
|
371
|
+
edge.setLinkedRecord(node, NODE);
|
|
372
|
+
return edge;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* @public
|
|
377
|
+
*
|
|
378
|
+
* Inserts an edge before the given cursor, or at the beginning of the list if
|
|
379
|
+
* no cursor is provided.
|
|
380
|
+
*
|
|
381
|
+
* Example:
|
|
382
|
+
*
|
|
383
|
+
* Given that data has already been fetched on some user `<id>` on the `friends`
|
|
384
|
+
* field:
|
|
385
|
+
*
|
|
386
|
+
* ```
|
|
387
|
+
* fragment FriendsFragment on User {
|
|
388
|
+
* friends(first: 10) @connection(key: "FriendsFragment_friends") {
|
|
389
|
+
* edges {
|
|
390
|
+
* node {
|
|
391
|
+
* id
|
|
392
|
+
* }
|
|
393
|
+
* }
|
|
394
|
+
* }
|
|
395
|
+
* }
|
|
396
|
+
* ```
|
|
397
|
+
*
|
|
398
|
+
* An edge can be prepended with:
|
|
399
|
+
*
|
|
400
|
+
* ```
|
|
401
|
+
* store => {
|
|
402
|
+
* const user = store.get('<id>');
|
|
403
|
+
* const friends = ConnectionHandler.getConnection(user, 'FriendsFragment_friends');
|
|
404
|
+
* const edge = store.create('<edge-id>', 'FriendsEdge');
|
|
405
|
+
* ConnectionHandler.insertEdgeBefore(friends, edge);
|
|
406
|
+
* }
|
|
407
|
+
* ```
|
|
408
|
+
*/
|
|
409
|
+
function insertEdgeBefore(
|
|
410
|
+
record: RecordProxy,
|
|
411
|
+
newEdge: RecordProxy,
|
|
412
|
+
cursor?: ?string,
|
|
413
|
+
): void {
|
|
414
|
+
const {CURSOR, EDGES} = ConnectionInterface.get();
|
|
415
|
+
|
|
416
|
+
const edges = record.getLinkedRecords(EDGES);
|
|
417
|
+
if (!edges) {
|
|
418
|
+
record.setLinkedRecords([newEdge], EDGES);
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
let nextEdges;
|
|
422
|
+
if (cursor == null) {
|
|
423
|
+
nextEdges = [newEdge].concat(edges);
|
|
424
|
+
} else {
|
|
425
|
+
nextEdges = [];
|
|
426
|
+
let foundCursor = false;
|
|
427
|
+
for (let ii = 0; ii < edges.length; ii++) {
|
|
428
|
+
const edge = edges[ii];
|
|
429
|
+
if (edge != null) {
|
|
430
|
+
const edgeCursor = edge.getValue(CURSOR);
|
|
431
|
+
if (cursor === edgeCursor) {
|
|
432
|
+
nextEdges.push(newEdge);
|
|
433
|
+
foundCursor = true;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
nextEdges.push(edge);
|
|
437
|
+
}
|
|
438
|
+
if (!foundCursor) {
|
|
439
|
+
nextEdges.unshift(newEdge);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
record.setLinkedRecords(nextEdges, EDGES);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* @public
|
|
447
|
+
*
|
|
448
|
+
* Remove any edges whose `node.id` matches the given id.
|
|
449
|
+
*/
|
|
450
|
+
function deleteNode(record: RecordProxy, nodeID: DataID): void {
|
|
451
|
+
const {EDGES, NODE} = ConnectionInterface.get();
|
|
452
|
+
|
|
453
|
+
const edges = record.getLinkedRecords(EDGES);
|
|
454
|
+
if (!edges) {
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
let nextEdges;
|
|
458
|
+
for (let ii = 0; ii < edges.length; ii++) {
|
|
459
|
+
const edge = edges[ii];
|
|
460
|
+
const node = edge && edge.getLinkedRecord(NODE);
|
|
461
|
+
if (node != null && node.getDataID() === nodeID) {
|
|
462
|
+
if (nextEdges === undefined) {
|
|
463
|
+
nextEdges = edges.slice(0, ii);
|
|
464
|
+
}
|
|
465
|
+
} else if (nextEdges !== undefined) {
|
|
466
|
+
nextEdges.push(edge);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
if (nextEdges !== undefined) {
|
|
470
|
+
record.setLinkedRecords(nextEdges, EDGES);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* @internal
|
|
476
|
+
*
|
|
477
|
+
* Creates a copy of an edge with a unique ID based on per-connection-instance
|
|
478
|
+
* incrementing edge index. This is necessary to avoid collisions between edges,
|
|
479
|
+
* which can occur because (edge) client IDs are assigned deterministically
|
|
480
|
+
* based on the path from the nearest node with an id.
|
|
481
|
+
*
|
|
482
|
+
* Example: if the first N edges of the same connection are refetched, the edges
|
|
483
|
+
* from the second fetch will be assigned the same IDs as the first fetch, even
|
|
484
|
+
* though the nodes they point to may be different (or the same and in different
|
|
485
|
+
* order).
|
|
486
|
+
*/
|
|
487
|
+
function buildConnectionEdge(
|
|
488
|
+
store: RecordSourceProxy,
|
|
489
|
+
connection: RecordProxy,
|
|
490
|
+
edge: ?RecordProxy,
|
|
491
|
+
): ?RecordProxy {
|
|
492
|
+
if (edge == null) {
|
|
493
|
+
return edge;
|
|
494
|
+
}
|
|
495
|
+
const {EDGES} = ConnectionInterface.get();
|
|
496
|
+
|
|
497
|
+
const edgeIndex = connection.getValue(NEXT_EDGE_INDEX);
|
|
498
|
+
invariant(
|
|
499
|
+
typeof edgeIndex === 'number',
|
|
500
|
+
'ConnectionHandler: Expected %s to be a number, got `%s`.',
|
|
501
|
+
NEXT_EDGE_INDEX,
|
|
502
|
+
edgeIndex,
|
|
503
|
+
);
|
|
504
|
+
const edgeID = generateClientID(connection.getDataID(), EDGES, edgeIndex);
|
|
505
|
+
const connectionEdge = store.create(edgeID, edge.getType());
|
|
506
|
+
connectionEdge.copyFieldsFrom(edge);
|
|
507
|
+
connection.setValue(edgeIndex + 1, NEXT_EDGE_INDEX);
|
|
508
|
+
return connectionEdge;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
/**
|
|
512
|
+
* @internal
|
|
513
|
+
*
|
|
514
|
+
* Adds the source edges to the target edges, skipping edges with
|
|
515
|
+
* duplicate node ids.
|
|
516
|
+
*/
|
|
517
|
+
function mergeEdges(
|
|
518
|
+
sourceEdges: Array<?RecordProxy>,
|
|
519
|
+
targetEdges: Array<?RecordProxy>,
|
|
520
|
+
nodeIDs: Set<mixed>,
|
|
521
|
+
): void {
|
|
522
|
+
const {NODE} = ConnectionInterface.get();
|
|
523
|
+
|
|
524
|
+
for (let ii = 0; ii < sourceEdges.length; ii++) {
|
|
525
|
+
const edge = sourceEdges[ii];
|
|
526
|
+
if (!edge) {
|
|
527
|
+
continue;
|
|
528
|
+
}
|
|
529
|
+
const node = edge.getLinkedRecord(NODE);
|
|
530
|
+
const nodeID = node && node.getDataID();
|
|
531
|
+
if (nodeID) {
|
|
532
|
+
if (nodeIDs.has(nodeID)) {
|
|
533
|
+
continue;
|
|
534
|
+
}
|
|
535
|
+
nodeIDs.add(nodeID);
|
|
536
|
+
}
|
|
537
|
+
targetEdges.push(edge);
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
module.exports = {
|
|
542
|
+
buildConnectionEdge,
|
|
543
|
+
createEdge,
|
|
544
|
+
deleteNode,
|
|
545
|
+
getConnection,
|
|
546
|
+
insertEdgeAfter,
|
|
547
|
+
insertEdgeBefore,
|
|
548
|
+
update,
|
|
549
|
+
};
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) Facebook, Inc. and its affiliates.
|
|
3
|
+
*
|
|
4
|
+
* This source code is licensed under the MIT license found in the
|
|
5
|
+
* LICENSE file in the root directory of this source tree.
|
|
6
|
+
*
|
|
7
|
+
* @flow strict-local
|
|
8
|
+
* @format
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
// flowlint ambiguous-object-type:error
|
|
12
|
+
|
|
13
|
+
'use strict';
|
|
14
|
+
|
|
15
|
+
import type {Record} from '../../store/RelayStoreTypes';
|
|
16
|
+
|
|
17
|
+
type Call = {name: string, ...};
|
|
18
|
+
|
|
19
|
+
export type EdgeRecord = Record & {
|
|
20
|
+
cursor: mixed,
|
|
21
|
+
node: Record,
|
|
22
|
+
...
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export type PageInfo = {
|
|
26
|
+
endCursor: ?string,
|
|
27
|
+
hasNextPage: boolean,
|
|
28
|
+
hasPreviousPage: boolean,
|
|
29
|
+
startCursor: ?string,
|
|
30
|
+
...
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
type ConnectionConfig = {|
|
|
34
|
+
CLIENT_MUTATION_ID: string,
|
|
35
|
+
CURSOR: string,
|
|
36
|
+
EDGES: string,
|
|
37
|
+
END_CURSOR: string,
|
|
38
|
+
HAS_NEXT_PAGE: string,
|
|
39
|
+
HAS_PREV_PAGE: string,
|
|
40
|
+
NODE: string,
|
|
41
|
+
PAGE_INFO: string,
|
|
42
|
+
PAGE_INFO_TYPE: string,
|
|
43
|
+
START_CURSOR: string,
|
|
44
|
+
|};
|
|
45
|
+
|
|
46
|
+
const CONNECTION_CALLS = {
|
|
47
|
+
after: true,
|
|
48
|
+
before: true,
|
|
49
|
+
find: true,
|
|
50
|
+
first: true,
|
|
51
|
+
last: true,
|
|
52
|
+
surrounds: true,
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
let config: ConnectionConfig = {
|
|
56
|
+
CLIENT_MUTATION_ID: 'clientMutationId',
|
|
57
|
+
CURSOR: 'cursor',
|
|
58
|
+
EDGES: 'edges',
|
|
59
|
+
END_CURSOR: 'endCursor',
|
|
60
|
+
HAS_NEXT_PAGE: 'hasNextPage',
|
|
61
|
+
HAS_PREV_PAGE: 'hasPreviousPage',
|
|
62
|
+
NODE: 'node',
|
|
63
|
+
PAGE_INFO_TYPE: 'PageInfo',
|
|
64
|
+
PAGE_INFO: 'pageInfo',
|
|
65
|
+
START_CURSOR: 'startCursor',
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* @internal
|
|
70
|
+
*
|
|
71
|
+
* Defines logic relevant to the informal "Connection" GraphQL interface.
|
|
72
|
+
*/
|
|
73
|
+
const ConnectionInterface = {
|
|
74
|
+
inject(newConfig: ConnectionConfig) {
|
|
75
|
+
config = newConfig;
|
|
76
|
+
},
|
|
77
|
+
|
|
78
|
+
get(): ConnectionConfig {
|
|
79
|
+
return config;
|
|
80
|
+
},
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Checks whether a call exists strictly to encode which parts of a connection
|
|
84
|
+
* to fetch. Fields that only differ by connection call values should have the
|
|
85
|
+
* same identity.
|
|
86
|
+
*/
|
|
87
|
+
isConnectionCall(call: Call): boolean {
|
|
88
|
+
return CONNECTION_CALLS.hasOwnProperty(call.name);
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
module.exports = ConnectionInterface;
|
package/index.js
CHANGED