Mirror: The highly customizable and versatile GraphQL client with which you add on features like normalized caching as you grow.
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

at 3e4bee3e9d344f738eb9c179921f75ccd8fb79c5 325 lines 9.9 kB view raw
1import { stringifyVariables } from '@urql/core'; 2import type { Cache, Resolver, Variables, NullArray } from '../types'; 3 4export type MergeMode = 'outwards' | 'inwards'; 5 6/** Input parameters for the {@link relayPagination} factory. */ 7export interface PaginationParams { 8 /** Flip between inwards and outwards pagination. 9 * 10 * @remarks 11 * This is only relevant if you’re querying pages using forwards and 12 * backwards pagination at the same time. 13 * When set to `'inwards'`, its default, pages that have been queried 14 * forward are placed in front of all pages that were queried backwards. 15 * When set to `'outwards'`, the two sets are merged in reverse. 16 */ 17 mergeMode?: MergeMode; 18} 19 20interface PageInfo { 21 __typename: string; 22 endCursor: null | string; 23 startCursor: null | string; 24 hasNextPage: boolean; 25 hasPreviousPage: boolean; 26} 27 28interface Page { 29 __typename: string; 30 edges: NullArray<string>; 31 nodes: NullArray<string>; 32 pageInfo: PageInfo; 33} 34 35const defaultPageInfo: PageInfo = { 36 __typename: 'PageInfo', 37 endCursor: null, 38 startCursor: null, 39 hasNextPage: false, 40 hasPreviousPage: false, 41}; 42 43const ensureKey = (x: any): string | null => (typeof x === 'string' ? x : null); 44 45const concatEdges = ( 46 cache: Cache, 47 leftEdges: NullArray<string>, 48 rightEdges: NullArray<string> 49) => { 50 const ids = new Set<string>(); 51 for (let i = 0, l = leftEdges.length; i < l; i++) { 52 const edge = leftEdges[i] as string | null; 53 const node = cache.resolve(edge, 'node'); 54 if (typeof node === 'string') ids.add(node); 55 } 56 57 const newEdges = leftEdges.slice(); 58 for (let i = 0, l = rightEdges.length; i < l; i++) { 59 const edge = rightEdges[i] as string | null; 60 const node = cache.resolve(edge, 'node'); 61 if (typeof node === 'string' && !ids.has(node)) { 62 ids.add(node); 63 newEdges.push(edge); 64 } 65 } 66 67 return newEdges; 68}; 69 70const concatNodes = ( 71 leftNodes: NullArray<string>, 72 rightNodes: NullArray<string> 73) => { 74 const ids = new Set<string>(); 75 for (let i = 0, l = leftNodes.length; i < l; i++) { 76 const node = leftNodes[i]; 77 if (typeof node === 'string') ids.add(node); 78 } 79 80 const newNodes = leftNodes.slice(); 81 for (let i = 0, l = rightNodes.length; i < l; i++) { 82 const node = rightNodes[i]; 83 if (typeof node === 'string' && !ids.has(node)) { 84 ids.add(node); 85 newNodes.push(node); 86 } 87 } 88 89 return newNodes; 90}; 91 92const compareArgs = ( 93 fieldArgs: Variables, 94 connectionArgs: Variables 95): boolean => { 96 for (const key in connectionArgs) { 97 if ( 98 key === 'first' || 99 key === 'last' || 100 key === 'after' || 101 key === 'before' 102 ) { 103 continue; 104 } else if (!(key in fieldArgs)) { 105 return false; 106 } 107 108 const argA = fieldArgs[key]; 109 const argB = connectionArgs[key]; 110 111 if ( 112 typeof argA !== typeof argB || typeof argA !== 'object' 113 ? argA !== argB 114 : stringifyVariables(argA) !== stringifyVariables(argB) 115 ) { 116 return false; 117 } 118 } 119 120 for (const key in fieldArgs) { 121 if ( 122 key === 'first' || 123 key === 'last' || 124 key === 'after' || 125 key === 'before' 126 ) { 127 continue; 128 } 129 130 if (!(key in connectionArgs)) return false; 131 } 132 133 return true; 134}; 135 136const getPage = ( 137 cache: Cache, 138 entityKey: string, 139 fieldKey: string 140): Page | null => { 141 const link = ensureKey(cache.resolve(entityKey, fieldKey)); 142 if (!link) return null; 143 144 const typename = cache.resolve(link, '__typename') as string; 145 const edges = (cache.resolve(link, 'edges') || []) as NullArray<string>; 146 const nodes = (cache.resolve(link, 'nodes') || []) as NullArray<string>; 147 if (typeof typename !== 'string') { 148 return null; 149 } 150 151 const page: Page = { 152 __typename: typename, 153 edges, 154 nodes, 155 pageInfo: defaultPageInfo, 156 }; 157 158 const pageInfoKey = cache.resolve(link, 'pageInfo'); 159 if (typeof pageInfoKey === 'string') { 160 const pageInfoType = ensureKey(cache.resolve(pageInfoKey, '__typename')); 161 const endCursor = ensureKey(cache.resolve(pageInfoKey, 'endCursor')); 162 const startCursor = ensureKey(cache.resolve(pageInfoKey, 'startCursor')); 163 const hasNextPage = cache.resolve(pageInfoKey, 'hasNextPage'); 164 const hasPreviousPage = cache.resolve(pageInfoKey, 'hasPreviousPage'); 165 166 const pageInfo: PageInfo = (page.pageInfo = { 167 __typename: typeof pageInfoType === 'string' ? pageInfoType : 'PageInfo', 168 hasNextPage: typeof hasNextPage === 'boolean' ? hasNextPage : !!endCursor, 169 hasPreviousPage: 170 typeof hasPreviousPage === 'boolean' ? hasPreviousPage : !!startCursor, 171 endCursor, 172 startCursor, 173 }); 174 175 if (pageInfo.endCursor === null) { 176 const edge = edges[edges.length - 1] as string | null; 177 if (edge) { 178 const endCursor = cache.resolve(edge, 'cursor'); 179 pageInfo.endCursor = ensureKey(endCursor); 180 } 181 } 182 183 if (pageInfo.startCursor === null) { 184 const edge = edges[0] as string | null; 185 if (edge) { 186 const startCursor = cache.resolve(edge, 'cursor'); 187 pageInfo.startCursor = ensureKey(startCursor); 188 } 189 } 190 } 191 192 return page; 193}; 194 195/** Creates a {@link Resolver} that combines pages that comply to the Relay pagination spec. 196 * 197 * @param params - A {@link PaginationParams} configuration object. 198 * @returns the created Relay pagination {@link Resolver}. 199 * 200 * @remarks 201 * `relayPagination` is a factory that creates a {@link Resolver} that can combine 202 * multiple pages on a field that complies to the Relay pagination spec into a single, 203 * combined list for infinite scrolling. 204 * 205 * This resolver will only work on fields that return a `Connection` GraphQL object 206 * type, according to the Relay pagination spec. 207 * 208 * Hint: It's not recommended to use this when you can handle infinite scrolling 209 * in your UI code instead. 210 * 211 * @see {@link https://urql.dev/goto/docs/graphcache/local-resolvers#relay-pagination} for more information. 212 * @see {@link https://urql.dev/goto/docs/basics/ui-patterns/#infinite-scrolling} for an alternate approach. 213 */ 214export const relayPagination = ( 215 params: PaginationParams = {} 216): Resolver<any, any, any> => { 217 const mergeMode = params.mergeMode || 'inwards'; 218 219 return (_parent, fieldArgs, cache, info) => { 220 const { parentKey: entityKey, fieldName } = info; 221 222 const allFields = cache.inspectFields(entityKey); 223 const fieldInfos = allFields.filter(info => info.fieldName === fieldName); 224 const size = fieldInfos.length; 225 if (size === 0) { 226 return undefined; 227 } 228 229 let typename: string | null = null; 230 let startEdges: NullArray<string> = []; 231 let endEdges: NullArray<string> = []; 232 let startNodes: NullArray<string> = []; 233 let endNodes: NullArray<string> = []; 234 let pageInfo: PageInfo = { ...defaultPageInfo }; 235 236 for (let i = 0; i < size; i++) { 237 const { fieldKey, arguments: args } = fieldInfos[i]; 238 if (args === null || !compareArgs(fieldArgs, args)) { 239 continue; 240 } 241 242 const page = getPage(cache, entityKey, fieldKey); 243 if (page === null) { 244 continue; 245 } 246 if (page.nodes.length === 0 && page.edges.length === 0 && typename) { 247 continue; 248 } 249 250 if ( 251 mergeMode === 'inwards' && 252 typeof args.last === 'number' && 253 typeof args.first === 'number' 254 ) { 255 const firstEdges = page.edges.slice(0, args.first + 1); 256 const lastEdges = page.edges.slice(-args.last); 257 const firstNodes = page.nodes.slice(0, args.first + 1); 258 const lastNodes = page.nodes.slice(-args.last); 259 260 startEdges = concatEdges(cache, startEdges, firstEdges); 261 endEdges = concatEdges(cache, lastEdges, endEdges); 262 startNodes = concatNodes(startNodes, firstNodes); 263 endNodes = concatNodes(lastNodes, endNodes); 264 265 pageInfo = page.pageInfo; 266 } else if (args.after) { 267 startEdges = concatEdges(cache, startEdges, page.edges); 268 startNodes = concatNodes(startNodes, page.nodes); 269 pageInfo.endCursor = page.pageInfo.endCursor; 270 pageInfo.hasNextPage = page.pageInfo.hasNextPage; 271 } else if (args.before) { 272 endEdges = concatEdges(cache, page.edges, endEdges); 273 endNodes = concatNodes(page.nodes, endNodes); 274 pageInfo.startCursor = page.pageInfo.startCursor; 275 pageInfo.hasPreviousPage = page.pageInfo.hasPreviousPage; 276 } else if (typeof args.last === 'number') { 277 endEdges = concatEdges(cache, page.edges, endEdges); 278 endNodes = concatNodes(page.nodes, endNodes); 279 pageInfo = page.pageInfo; 280 } else { 281 startEdges = concatEdges(cache, startEdges, page.edges); 282 startNodes = concatNodes(startNodes, page.nodes); 283 pageInfo = page.pageInfo; 284 } 285 286 if (page.pageInfo.__typename !== pageInfo.__typename) 287 pageInfo.__typename = page.pageInfo.__typename; 288 if (typename !== page.__typename) typename = page.__typename; 289 } 290 291 if (typeof typename !== 'string') { 292 return undefined; 293 } 294 295 const hasCurrentPage = !!ensureKey( 296 cache.resolve(entityKey, fieldName, fieldArgs) 297 ); 298 if (!hasCurrentPage) { 299 if (!(info as any).store.schema) { 300 return undefined; 301 } else { 302 info.partial = true; 303 } 304 } 305 306 return { 307 __typename: typename, 308 edges: 309 mergeMode === 'inwards' 310 ? concatEdges(cache, startEdges, endEdges) 311 : concatEdges(cache, endEdges, startEdges), 312 nodes: 313 mergeMode === 'inwards' 314 ? concatNodes(startNodes, endNodes) 315 : concatNodes(endNodes, startNodes), 316 pageInfo: { 317 __typename: pageInfo.__typename, 318 endCursor: pageInfo.endCursor, 319 startCursor: pageInfo.startCursor, 320 hasNextPage: pageInfo.hasNextPage, 321 hasPreviousPage: pageInfo.hasPreviousPage, 322 }, 323 }; 324 }; 325};