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.

(populate) - support (nested) interfaces & unions in the return type (#963)

* support populating interfaces

* handle nested interfaces

* add changeset

* remove ts-ignore

* add potential unwrapping

* add invariant

* remove invariant

* (populate) - refactor and remove the need for visit & typeinfo (#966)

* remove need for visit with typeinfo

* simplify logic

* simplify document logic

* add todo

* remove one visit

* remove one visit

* remove unused import

* remove another visit

* remove last visit

* cleanup implementation

* remove ts-ignore

* cleanup implementation

* Fix warning

Co-authored-by: Phil Pluckthun <phil@kitten.sh>

Co-authored-by: Phil Pluckthun <phil@kitten.sh>

authored by

Jovi De Croock
Phil Pluckthun
and committed by
GitHub
e39bcc5f 8c74924c

+451 -154
+5
.changeset/thin-cows-hug.md
··· 1 + --- 2 + '@urql/exchange-populate': minor 3 + --- 4 + 5 + Support interfaces and nested interfaces
-14
exchanges/populate/src/helpers/help.ts
··· 26 26 const helpUrl = '\nhttps://bit.ly/2XbVrpR#'; 27 27 const cache = new Set<string>(); 28 28 29 - export function invariant( 30 - condition: any, 31 - message: string, 32 - code: ErrorCode 33 - ): asserts condition { 34 - if (!condition) { 35 - const errorMessage = message || 'Minfied Error #' + code + '\n'; 36 - 37 - const error = new Error(errorMessage + helpUrl + code); 38 - error.name = 'Graphcache Error'; 39 - throw error; 40 - } 41 - } 42 - 43 29 export function warn(message: string, code: ErrorCode) { 44 30 if (!cache.has(message)) { 45 31 console.warn(message + helpUrl + code);
+8
exchanges/populate/src/helpers/node.ts
··· 5 5 GraphQLOutputType, 6 6 isWrappingType, 7 7 GraphQLWrappingType, 8 + Kind, 8 9 } from 'graphql'; 9 10 10 11 export type SelectionSet = ReadonlyArray<SelectionNode>; ··· 28 29 29 30 return type || null; 30 31 }; 32 + 33 + export function createNameNode(value: string): NameNode { 34 + return { 35 + kind: Kind.NAME, 36 + value, 37 + }; 38 + }
+96
exchanges/populate/src/helpers/traverse.ts
··· 1 + import { 2 + SelectionNode, 3 + Kind, 4 + ASTNode, 5 + DefinitionNode, 6 + GraphQLSchema, 7 + GraphQLFieldMap, 8 + isAbstractType, 9 + FragmentDefinitionNode, 10 + FragmentSpreadNode, 11 + } from 'graphql'; 12 + import { unwrapType, getName } from './node'; 13 + 14 + export function traverse( 15 + node: ASTNode, 16 + enter?: (n: ASTNode) => ASTNode | void, 17 + exit?: (n: ASTNode) => ASTNode | void 18 + ): any { 19 + if (enter) { 20 + node = enter(node) || node; 21 + } 22 + 23 + switch (node.kind) { 24 + case Kind.DOCUMENT: { 25 + node = { 26 + ...node, 27 + definitions: node.definitions.map( 28 + n => traverse(n, enter, exit) as DefinitionNode 29 + ), 30 + }; 31 + break; 32 + } 33 + case Kind.OPERATION_DEFINITION: 34 + case Kind.FIELD: 35 + case Kind.FRAGMENT_DEFINITION: { 36 + if (node.selectionSet) { 37 + node = { 38 + ...node, 39 + selectionSet: { 40 + ...node.selectionSet, 41 + selections: node.selectionSet.selections.map( 42 + n => traverse(n, enter, exit) as SelectionNode 43 + ), 44 + }, 45 + }; 46 + } 47 + break; 48 + } 49 + } 50 + 51 + if (exit) { 52 + node = exit(node) || node; 53 + } 54 + 55 + return node; 56 + } 57 + 58 + export function resolveFields( 59 + schema: GraphQLSchema, 60 + visits: string[] 61 + ): GraphQLFieldMap<any, any> { 62 + let currentFields = schema.getQueryType()!.getFields(); 63 + 64 + for (let i = 0; i < visits.length; i++) { 65 + const t = unwrapType(currentFields[visits[i]].type); 66 + 67 + if (isAbstractType(t)) { 68 + currentFields = {}; 69 + schema.getPossibleTypes(t).forEach(implementedType => { 70 + currentFields = { 71 + ...currentFields, 72 + // @ts-ignore TODO: proper casting 73 + ...schema.getType(implementedType.name)!.toConfig().fields, 74 + }; 75 + }); 76 + } else { 77 + // @ts-ignore TODO: proper casting 78 + currentFields = schema.getType(t!.name)!.toConfig().fields; 79 + } 80 + } 81 + 82 + return currentFields; 83 + } 84 + 85 + /** Get fragment names referenced by node. */ 86 + export function getUsedFragmentNames(node: FragmentDefinitionNode) { 87 + const names: string[] = []; 88 + 89 + traverse(node, n => { 90 + if (n.kind === Kind.FRAGMENT_SPREAD) { 91 + names.push(getName(n as FragmentSpreadNode)); 92 + } 93 + }); 94 + 95 + return names; 96 + }
+168
exchanges/populate/src/populateExchange.test.ts
··· 33 33 34 34 union UnionType = User | Todo 35 35 36 + interface Product { 37 + id: ID! 38 + name: String! 39 + price: Int! 40 + } 41 + 42 + interface Store { 43 + id: ID! 44 + name: String! 45 + } 46 + 47 + type PhysicalStore implements Store { 48 + id: ID! 49 + name: String! 50 + address: String 51 + } 52 + 53 + type OnlineStore implements Store { 54 + id: ID! 55 + name: String! 56 + website: String 57 + } 58 + 59 + type SimpleProduct implements Product { 60 + id: ID! 61 + name: String! 62 + price: Int! 63 + store: PhysicalStore 64 + } 65 + 66 + type ComplexProduct implements Product { 67 + id: ID! 68 + name: String! 69 + price: Int! 70 + tax: Int! 71 + store: OnlineStore 72 + } 73 + 36 74 type Query { 37 75 todos: [Todo!] 38 76 users: [User!]! 77 + products: [Product]! 39 78 } 40 79 41 80 type Mutation { 42 81 addTodo: [Todo] 43 82 removeTodo: [Node] 44 83 updateTodo: [UnionType] 84 + addProduct: Product 45 85 } 46 86 `; 47 87 ··· 507 547 }); 508 548 }); 509 549 }); 550 + 551 + describe('interface returned in mutation', () => { 552 + const queryOp = { 553 + key: 1234, 554 + operationName: 'query', 555 + query: gql` 556 + query { 557 + products { 558 + id 559 + text 560 + price 561 + tax 562 + } 563 + } 564 + `, 565 + } as Operation; 566 + 567 + const mutationOp = { 568 + key: 5678, 569 + operationName: 'mutation', 570 + query: gql` 571 + mutation MyMutation { 572 + addProduct @populate 573 + } 574 + `, 575 + } as Operation; 576 + 577 + it('should correctly make the inline-fragments', () => { 578 + const response = pipe<Operation, any, Operation[]>( 579 + fromArray([queryOp, mutationOp]), 580 + populateExchange({ schema })(exchangeArgs), 581 + toArray 582 + ); 583 + 584 + expect(print(response[1].query)).toMatchInlineSnapshot(` 585 + "mutation MyMutation { 586 + addProduct { 587 + ...SimpleProduct_PopulateFragment_0 588 + ...ComplexProduct_PopulateFragment_0 589 + } 590 + } 591 + 592 + fragment SimpleProduct_PopulateFragment_0 on SimpleProduct { 593 + id 594 + price 595 + } 596 + 597 + fragment ComplexProduct_PopulateFragment_0 on ComplexProduct { 598 + id 599 + price 600 + tax 601 + } 602 + " 603 + `); 604 + }); 605 + }); 606 + 607 + describe('nested interfaces', () => { 608 + const queryOp = { 609 + key: 1234, 610 + operationName: 'query', 611 + query: gql` 612 + query { 613 + products { 614 + id 615 + text 616 + price 617 + tax 618 + store { 619 + id 620 + name 621 + address 622 + website 623 + } 624 + } 625 + } 626 + `, 627 + } as Operation; 628 + 629 + const mutationOp = { 630 + key: 5678, 631 + operationName: 'mutation', 632 + query: gql` 633 + mutation MyMutation { 634 + addProduct @populate 635 + } 636 + `, 637 + } as Operation; 638 + 639 + it('should correctly make the inline-fragments', () => { 640 + const response = pipe<Operation, any, Operation[]>( 641 + fromArray([queryOp, mutationOp]), 642 + populateExchange({ schema })(exchangeArgs), 643 + toArray 644 + ); 645 + 646 + expect(print(response[1].query)).toMatchInlineSnapshot(` 647 + "mutation MyMutation { 648 + addProduct { 649 + ...SimpleProduct_PopulateFragment_0 650 + ...ComplexProduct_PopulateFragment_0 651 + } 652 + } 653 + 654 + fragment SimpleProduct_PopulateFragment_0 on SimpleProduct { 655 + id 656 + price 657 + store { 658 + id 659 + name 660 + address 661 + } 662 + } 663 + 664 + fragment ComplexProduct_PopulateFragment_0 on ComplexProduct { 665 + id 666 + price 667 + tax 668 + store { 669 + id 670 + name 671 + website 672 + } 673 + } 674 + " 675 + `); 676 + }); 677 + });
+174 -140
exchanges/populate/src/populateExchange.ts
··· 1 1 import { 2 2 DocumentNode, 3 3 buildClientSchema, 4 - visitWithTypeInfo, 5 - TypeInfo, 6 4 FragmentDefinitionNode, 7 5 GraphQLSchema, 8 6 IntrospectionQuery, 9 7 FragmentSpreadNode, 10 - NameNode, 11 - ASTNode, 12 8 isCompositeType, 13 9 isAbstractType, 14 10 Kind, 15 - visit, 11 + SelectionSetNode, 12 + GraphQLObjectType, 13 + SelectionNode, 16 14 } from 'graphql'; 17 - import { getName, getSelectionSet, unwrapType } from './helpers/node'; 18 - import { invariant, warn } from './helpers/help'; 19 - 20 15 import { pipe, tap, map } from 'wonka'; 21 16 import { Exchange, Operation } from '@urql/core'; 17 + 18 + import { warn } from './helpers/help'; 19 + import { 20 + getName, 21 + getSelectionSet, 22 + unwrapType, 23 + createNameNode, 24 + } from './helpers/node'; 25 + import { 26 + traverse, 27 + resolveFields, 28 + getUsedFragmentNames, 29 + } from './helpers/traverse'; 22 30 23 31 interface PopulateExchangeOpts { 24 32 schema: IntrospectionQuery; ··· 136 144 ) => { 137 145 const extractedFragments: FragmentDefinitionNode[] = []; 138 146 const newFragments: FragmentDefinitionNode[] = []; 139 - const typeInfo = new TypeInfo(schema); 147 + 148 + const sanitizeSelectionSet = ( 149 + selectionSet: SelectionSetNode, 150 + type: string 151 + ) => { 152 + const selections: SelectionNode[] = []; 153 + const validTypes = (schema.getType(type) as GraphQLObjectType).getFields(); 154 + const validTypeProperties = Object.keys(validTypes); 155 + 156 + selectionSet.selections.forEach(selection => { 157 + if (selection.kind === Kind.FIELD) { 158 + if (validTypeProperties.includes(selection.name.value)) { 159 + if (selection.selectionSet) { 160 + selections.push({ 161 + ...selection, 162 + selectionSet: sanitizeSelectionSet( 163 + selection.selectionSet, 164 + unwrapType(validTypes[selection.name.value].type)!.toString() 165 + ), 166 + }); 167 + } else { 168 + selections.push(selection); 169 + } 170 + } 171 + } else { 172 + selections.push(selection); 173 + } 174 + }); 175 + 176 + return { ...selectionSet, selections }; 177 + }; 178 + 179 + const visits: string[] = []; 140 180 141 - visit( 181 + traverse( 142 182 query, 143 - visitWithTypeInfo(typeInfo, { 144 - Field: node => { 145 - if (node.selectionSet) { 146 - const type = getTypeName(typeInfo); 183 + node => { 184 + if (node.kind === Kind.FRAGMENT_DEFINITION) { 185 + extractedFragments.push(node); 186 + } else if (node.kind === Kind.FIELD && node.selectionSet) { 187 + const type = unwrapType( 188 + resolveFields(schema, visits)[node.name.value].type 189 + ); 190 + 191 + visits.push(node.name.value); 192 + 193 + if (isAbstractType(type)) { 194 + const types = schema.getPossibleTypes(type); 195 + types.forEach(t => { 196 + newFragments.push({ 197 + kind: Kind.FRAGMENT_DEFINITION, 198 + typeCondition: { 199 + kind: Kind.NAMED_TYPE, 200 + name: createNameNode(t.toString()), 201 + }, 202 + name: createNameNode(`${t.toString()}_PopulateFragment_`), 203 + selectionSet: sanitizeSelectionSet( 204 + node.selectionSet as SelectionSetNode, 205 + t.toString() 206 + ), 207 + }); 208 + }); 209 + } else if (type) { 147 210 newFragments.push({ 148 211 kind: Kind.FRAGMENT_DEFINITION, 149 212 typeCondition: { 150 213 kind: Kind.NAMED_TYPE, 151 - name: nameNode(type), 214 + name: createNameNode(type.toString()), 152 215 }, 153 - name: nameNode(`${type}_PopulateFragment_`), 216 + name: createNameNode(`${type.toString()}_PopulateFragment_`), 154 217 selectionSet: node.selectionSet, 155 218 }); 156 219 } 157 - }, 158 - FragmentDefinition: node => { 159 - extractedFragments.push(node); 160 - }, 161 - }) 220 + } 221 + }, 222 + node => { 223 + if (node.kind === Kind.FIELD && node.selectionSet) visits.pop(); 224 + } 162 225 ); 163 226 164 227 return [extractedFragments, newFragments]; ··· 170 233 query: DocumentNode, 171 234 activeTypeFragments: TypeFragmentMap, 172 235 userFragments: UserFragmentMap 173 - ) => { 174 - const typeInfo = new TypeInfo(schema); 175 - 236 + ): DocumentNode => { 176 237 const requiredUserFragments: Record< 177 238 string, 178 239 FragmentDefinitionNode ··· 186 247 /** Fragments provided and used by the current query */ 187 248 const existingFragmentsForQuery: Set<string> = new Set(); 188 249 189 - return visit( 250 + return traverse( 190 251 query, 191 - visitWithTypeInfo(typeInfo, { 192 - Field: { 193 - enter: node => { 194 - if (!node.directives) { 195 - return; 196 - } 197 - 198 - const directives = node.directives.filter( 199 - d => getName(d) !== 'populate' 200 - ); 201 - if (directives.length === node.directives.length) { 202 - return; 252 + node => { 253 + if (node.kind === Kind.DOCUMENT) { 254 + node.definitions.reduce((set, definition) => { 255 + if (definition.kind === Kind.FRAGMENT_DEFINITION) { 256 + set.add(definition.name.value); 203 257 } 204 258 205 - const possibleTypes = getTypes(schema, typeInfo); 206 - const newSelections = possibleTypes.reduce((p, possibleType) => { 207 - const typeFrags = activeTypeFragments[possibleType.name]; 208 - if (!typeFrags) { 209 - return p; 210 - } 259 + return set; 260 + }, existingFragmentsForQuery); 261 + } else if (node.kind === Kind.FIELD) { 262 + if (!node.directives) return; 211 263 212 - for (let i = 0, l = typeFrags.length; i < l; i++) { 213 - const { fragment } = typeFrags[i]; 214 - const fragmentName = getName(fragment); 215 - const usedFragments = getUsedFragments(fragment); 264 + const directives = node.directives.filter( 265 + d => getName(d) !== 'populate' 266 + ); 267 + if (directives.length === node.directives.length) return; 216 268 217 - // Add used fragment for insertion at Document node 218 - for (let j = 0, l = usedFragments.length; j < l; j++) { 219 - const name = usedFragments[j]; 220 - if (!existingFragmentsForQuery.has(name)) { 221 - requiredUserFragments[name] = userFragments[name]; 222 - } 223 - } 224 - 225 - // Add fragment for insertion at Document node 226 - additionalFragments[fragmentName] = fragment; 269 + const type = unwrapType( 270 + schema.getMutationType()!.getFields()[node.name.value].type 271 + ); 227 272 228 - p.push({ 229 - kind: Kind.FRAGMENT_SPREAD, 230 - name: nameNode(fragmentName), 231 - }); 232 - } 273 + let possibleTypes: readonly GraphQLObjectType<any, any>[] = []; 274 + if (!isCompositeType(type)) { 275 + warn( 276 + 'Invalid type: The type `' + 277 + type + 278 + '` is used with @populate but does not exist.', 279 + 17 280 + ); 281 + } else { 282 + possibleTypes = isAbstractType(type) 283 + ? schema.getPossibleTypes(type) 284 + : [type]; 285 + } 233 286 287 + const newSelections = possibleTypes.reduce((p, possibleType) => { 288 + const typeFrags = activeTypeFragments[possibleType.name]; 289 + if (!typeFrags) { 234 290 return p; 235 - }, [] as FragmentSpreadNode[]); 291 + } 236 292 237 - const existingSelections = getSelectionSet(node); 293 + for (let i = 0, l = typeFrags.length; i < l; i++) { 294 + const { fragment } = typeFrags[i]; 295 + const fragmentName = getName(fragment); 296 + const usedFragments = getUsedFragmentNames(fragment); 238 297 239 - const selections = 240 - existingSelections.length + newSelections.length !== 0 241 - ? [...newSelections, ...existingSelections] 242 - : [ 243 - { 244 - kind: Kind.FIELD, 245 - name: nameNode('__typename'), 246 - }, 247 - ]; 248 - 249 - return { 250 - ...node, 251 - directives, 252 - selectionSet: { 253 - kind: Kind.SELECTION_SET, 254 - selections, 255 - }, 256 - }; 257 - }, 258 - }, 259 - Document: { 260 - enter: node => { 261 - node.definitions.reduce((set, definition) => { 262 - if (definition.kind === 'FragmentDefinition') { 263 - set.add(definition.name.value); 298 + // Add used fragment for insertion at Document node 299 + for (let j = 0, l = usedFragments.length; j < l; j++) { 300 + const name = usedFragments[j]; 301 + if (!existingFragmentsForQuery.has(name)) { 302 + requiredUserFragments[name] = userFragments[name]; 303 + } 264 304 } 265 - return set; 266 - }, existingFragmentsForQuery); 267 - }, 268 - leave: node => { 269 - const definitions = [...node.definitions]; 270 - for (const key in additionalFragments) 271 - definitions.push(additionalFragments[key]); 272 - for (const key in requiredUserFragments) 273 - definitions.push(requiredUserFragments[key]); 274 - return { ...node, definitions }; 275 - }, 276 - }, 277 - }) 278 - ); 279 - }; 280 305 281 - const nameNode = (value: string): NameNode => ({ 282 - kind: Kind.NAME, 283 - value, 284 - }); 306 + // Add fragment for insertion at Document node 307 + additionalFragments[fragmentName] = fragment; 285 308 286 - /** Get all possible types for node with TypeInfo. */ 287 - const getTypes = (schema: GraphQLSchema, typeInfo: TypeInfo) => { 288 - const type = unwrapType(typeInfo.getType()); 289 - if (!isCompositeType(type)) { 290 - warn( 291 - 'Invalid type: The type ` + type + ` is used with @populate but does not exist.', 292 - 17 293 - ); 294 - return []; 295 - } 309 + p.push({ 310 + kind: Kind.FRAGMENT_SPREAD, 311 + name: createNameNode(fragmentName), 312 + }); 313 + } 296 314 297 - return isAbstractType(type) ? schema.getPossibleTypes(type) : [type]; 298 - }; 315 + return p; 316 + }, [] as FragmentSpreadNode[]); 299 317 300 - /** Get name of non-abstract type for adding to 'activeTypeFragments'. */ 301 - const getTypeName = (typeInfo: TypeInfo) => { 302 - const type = unwrapType(typeInfo.getType()); 303 - invariant( 304 - type && !isAbstractType(type), 305 - 'Invalid TypeInfo state: Found no flat schema type when one was expected.', 306 - 18 307 - ); 318 + const existingSelections = getSelectionSet(node); 308 319 309 - return type.toString(); 310 - }; 311 - 312 - /** Get fragment names referenced by node. */ 313 - const getUsedFragments = (node: ASTNode) => { 314 - const names: string[] = []; 320 + const selections = 321 + existingSelections.length || newSelections.length 322 + ? [...newSelections, ...existingSelections] 323 + : [ 324 + { 325 + kind: Kind.FIELD, 326 + name: createNameNode('__typename'), 327 + }, 328 + ]; 315 329 316 - visit(node, { 317 - FragmentSpread: f => { 318 - names.push(getName(f)); 330 + return { 331 + ...node, 332 + directives, 333 + selectionSet: { 334 + kind: Kind.SELECTION_SET, 335 + selections, 336 + }, 337 + }; 338 + } 319 339 }, 320 - }); 321 - 322 - return names; 340 + node => { 341 + if (node.kind === Kind.DOCUMENT) { 342 + return { 343 + ...node, 344 + definitions: [ 345 + ...node.definitions, 346 + ...Object.keys(additionalFragments).map( 347 + key => additionalFragments[key] 348 + ), 349 + ...Object.keys(requiredUserFragments).map( 350 + key => requiredUserFragments[key] 351 + ), 352 + ], 353 + }; 354 + } 355 + } 356 + ); 323 357 };