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.

(introspection) - Implement initial @urql/introspection package (#976)

* Implement initial @urql/introspection package

* Fix typos and update Graphcache test

* Update all Graphcache tests with minifyIntrospectionQuery

* Update schema awareness docs

* Fix potentially nullable field

authored by

Phil Pluckthun and committed by
GitHub
b53403f3 34181a3a

+442 -34
+105 -18
docs/graphcache/schema-awareness.md
··· 7 7 8 8 As mentioned in the docs we allow for the schema to be passed 9 9 to the `cacheExchange`. This allows for partial results and deterministic 10 - fragment matching. 11 - With deterministic fragment matching we mean that if you use an interface 12 - or a union we will be 100% sure you're allowed to do so, therefore we'll check if the 13 - type you request can actually be returned from this union/interface. 10 + fragment matching. This schema argument is of type `IntrospectionQuery`, as JSON structure that 11 + describes your entire server-side `GraphQLSchema`. 12 + 13 + With deterministic fragment matching if we use an interface or a union _Graphcache_ can be 100% sure 14 + what the expected types and shape of the data must be and whether the match is permitted. It also 15 + enables a feature called ["Partial Results"](#partial-results). 14 16 15 17 ## Getting your schema 16 18 17 - But how do you get this schema? Well let's consider some steps, first 18 - make sure `introspection` is turned on on your server. This is very crucial 19 - else your server won't allow the schema to be shown. 19 + But how do you get this introspected schema? The process of introspecting a schema is running an 20 + introspection query on the GraphQL API, which will give us our `IntrospectionQuery` result. So an 21 + introspection is just another query we can run against our GraphQL APIs or schemas. 20 22 21 - We can run a script that generates the introspection result like this: 23 + As long as `introspection` as turned on and permitted, we can download an introspectin schema by 24 + running a normal GraphQL query against the API and save the result in a JSON file. 22 25 23 26 ```js 24 - // import a fetch library for node. 25 27 import { getIntrospectionQuery } from 'graphql'; 28 + import fetch from 'node-fetch'; // or your preferred request in Node.js 29 + import * as fs from 'fs'; 26 30 27 31 fetch('http://localhost:3000/graphql', { 28 32 method: 'POST', ··· 44 48 }); 45 49 ``` 46 50 47 - ## Integrating 51 + Alternatively, if you're already using [GraphQL Code Generator](https://graphql-code-generator.com/) 52 + you can use [their `@graphql-codegen/introspection` 53 + plugin](https://graphql-code-generator.com/docs/plugins/introspection) to do the same automatically 54 + against a local schema. Furthermore it's also possible to 55 + [`execute`](https://graphql.org/graphql-js/execution/#execute) the introspection query directly 56 + against your `GraphQLSchema`. 57 + 58 + ## Optimizing a schema 59 + 60 + An `IntrospectionQuery` JSON blob from a GraphQL API can without modification become quite large. 61 + The shape of this data is `{ "__schema": ... }` and this _schema_ data will contain information on 62 + all directives, types, input objects, scalars, deprecation, enums, and more. This can quickly add up and one of the 63 + largest schemas, the GitHub GraphQL API's schema, has an introspection size of about 1.1MB, or about 64 + 50KB gzipped. 65 + 66 + However, we can use the `@urql/introspection` package's `minifyIntrospectionQuery` helper to reduce 67 + the size of this introspection data. This helper strips out information on directives, scalars, 68 + input types, deprecation, enums, and redundant fields to only leave information that _Graphcache_ 69 + actually requires. 70 + 71 + In the example of the GitHub GraphQL API this reduces the introspected data to around 20kB gzipped, 72 + which is much more acceptable. 73 + 74 + ### Installation & Setup 75 + 76 + First, install the `@urql/introspection` package: 77 + 78 + ```sh 79 + yarn add @urql/introspection 80 + # or 81 + npm install --save @urql/introspection 82 + ``` 83 + 84 + You'll then need to integrate it into your introspection script or in another place where it can 85 + optimise the introspection data. For this example, we'll just add it to the fetching script from 86 + [above](#getting-your-schema). 87 + 88 + ```js 89 + import { getIntrospectionQuery } from 'graphql'; 90 + import fetch from 'node-fetch'; // or your preferred request in Node.js 91 + import * as fs from 'fs'; 92 + 93 + import { getIntrospectedSchema, minifyIntrospectionQuery } from '@urql/introspection'; 94 + 95 + fetch('http://localhost:3000/graphql', { 96 + method: 'POST', 97 + headers: { 'Content-Type': 'application/json' }, 98 + body: JSON.stringify({ 99 + variables: {}, 100 + query: getIntrospectionQuery({ descriptions: false }), 101 + }), 102 + }) 103 + .then(result => result.json()) 104 + .then(({ data }) => { 105 + const minified = minifyIntrospectionQuery(getIntrospectedSchema(data)); 106 + fs.writeFileSync('./schema.json', JSON.stringify(minified)); 107 + }); 108 + ``` 109 + 110 + The `getIntrospectionQuery` doesn't only accept `IntrospectionQuery` JSON data as inputs, but also 111 + allows you to pass a JSON string, `GraphQLSchema`, or GraphQL Schema SDL strings. It's a convenience 112 + helper and not needed in the above example. 113 + 114 + ## Integrating a schema 48 115 49 - next up we can just import this schema and add it to the cacheExchange: 116 + Once we have a schema that's already saved to a JSON file, we can load it and pass it to the 117 + `cacheExchange`'s `schema` option: 50 118 51 119 ```js 52 120 import schema from './schema.json'; ··· 54 122 const cache = cacheExchange({ schema }); 55 123 ``` 56 124 57 - So what benefits do we have now that graphCache is aware of the shape of our schema? 125 + It may be worth checking what your bundler or framework does when you import a JSON file. Typically 126 + you can reduce the parsing time by making sure it's turned into a string and parsed using 127 + `JSON.parse` 58 128 59 - ### Partial results 129 + **What do we get from adding the schema to _Graphcache_?** 60 130 61 - Let's approach this with the example from [Computed queries](./computed-queries.md#resolve) we have 62 - our `TodosQuery` result (a list) and now we want to get a specific `Todo` we wire these up through 63 - `resolve` but we are missing an optional field for this, without a schema we don't know this is optional 64 - and we will not show you the partial result. Now that we have a schema we can check if this is allowed to 65 - be left out, we'll return you the entity and fetch the missing properties in the background. 131 + ### Partial Results 132 + 133 + Once _Schema Awareness_ is activated in _Graphcache_, it can use the schema to check which fields 134 + and lists are marked as optional fields. This is then used to delivery partial results when 135 + possible, which means that different queries may give you partial data where some uncached fields 136 + have been replaced with `null`, while loading more data in the background, instead of our apps 137 + having to wait for all data to be available. 138 + 139 + Let's approach this with the example from ["Computed Queries"](./computed-queries.md#resolve): We 140 + have our `TodosQuery` result which loads a list, and our app may want to get a specific `Todo` when 141 + the app transitions to a details page. We may have already written a resolver that tells 142 + _Graphcache_ what `Query.todo` does, but it may be missing some optional field to actuall give us 143 + the full detailed `Todo`. 144 + 145 + Without a schema _Graphcache_ would assume that because some fields are uncached and missing, it 146 + can't serve this query's data. But if it has a schema, it may see that the uncached fields are 147 + optional anyway and it can return a partial result for the `Todo` while it's fetching the full query 148 + in the background, which in the `OperationResult` also causes `stale` to be set to `true`. 149 + 150 + This means that _Schema Awareness_ can enable us to create apps that can display already cached data 151 + on page transitions, while the page's full data loads in the background, which can often feel much 152 + faster to the user.
+8 -2
exchanges/graphcache/src/cacheExchange.test.ts
··· 1 1 import gql from 'graphql-tag'; 2 + 2 3 import { 3 4 createClient, 4 5 ExchangeIO, 5 6 Operation, 6 7 OperationResult, 7 8 } from '@urql/core'; 9 + 8 10 import { 9 11 Source, 10 12 pipe, ··· 18 20 publish, 19 21 delay, 20 22 } from 'wonka'; 23 + 24 + import { minifyIntrospectionQuery } from '@urql/introspection'; 21 25 import { cacheExchange } from './cacheExchange'; 22 26 23 27 const queryOne = gql` ··· 1412 1416 1413 1417 pipe( 1414 1418 cacheExchange({ 1415 - // eslint-disable-next-line 1416 - schema: require('./test-utils/simple_schema.json'), 1419 + schema: minifyIntrospectionQuery( 1420 + // eslint-disable-next-line 1421 + require('./test-utils/simple_schema.json') 1422 + ), 1417 1423 })({ forward, client, dispatchDebug })(ops$), 1418 1424 tap(result), 1419 1425 publish
+11 -3
exchanges/graphcache/src/operations/query.test.ts
··· 1 - import { Store } from '../store'; 1 + /* eslint-disable @typescript-eslint/no-var-requires */ 2 + 2 3 import gql from 'graphql-tag'; 4 + import { minifyIntrospectionQuery } from '@urql/introspection'; 5 + 6 + import { Store } from '../store'; 3 7 import { write } from './write'; 4 8 import { query } from './query'; 5 9 ··· 24 28 let schema, store, alteredRoot; 25 29 26 30 beforeAll(() => { 27 - schema = require('../test-utils/simple_schema.json'); 28 - alteredRoot = require('../test-utils/altered_root_schema.json'); 31 + schema = minifyIntrospectionQuery( 32 + require('../test-utils/simple_schema.json') 33 + ); 34 + alteredRoot = minifyIntrospectionQuery( 35 + require('../test-utils/altered_root_schema.json') 36 + ); 29 37 }); 30 38 31 39 beforeEach(() => {
+7 -1
exchanges/graphcache/src/operations/write.test.ts
··· 1 + /* eslint-disable @typescript-eslint/no-var-requires */ 2 + 1 3 import gql from 'graphql-tag'; 4 + import { minifyIntrospectionQuery } from '@urql/introspection'; 5 + 2 6 import { write } from './write'; 3 7 import * as InMemoryData from '../store/data'; 4 8 import { Store } from '../store'; ··· 24 28 let schema, store; 25 29 26 30 beforeAll(() => { 27 - schema = require('../test-utils/simple_schema.json'); 31 + schema = minifyIntrospectionQuery( 32 + require('../test-utils/simple_schema.json') 33 + ); 28 34 }); 29 35 30 36 beforeEach(() => {
+35 -10
exchanges/graphcache/src/store/store.test.ts
··· 1 + /* eslint-disable @typescript-eslint/no-var-requires */ 2 + 1 3 import gql from 'graphql-tag'; 4 + import { minifyIntrospectionQuery } from '@urql/introspection'; 2 5 import { mocked } from 'ts-jest/utils'; 6 + 3 7 import { Data, StorageAdapter } from '../types'; 4 8 import { query } from '../operations/query'; 5 9 import { write, writeOptimistic } from '../operations/write'; ··· 115 119 116 120 it('should not warn if Mutation/Subscription operations do exist in the schema', function () { 117 121 new Store({ 118 - schema: require('../test-utils/simple_schema.json'), 122 + schema: minifyIntrospectionQuery( 123 + require('../test-utils/simple_schema.json') 124 + ), 119 125 updates: { 120 126 Mutation: { 121 127 toggleTodo: noop, ··· 131 137 132 138 it("should warn if Mutation operations don't exist in the schema", function () { 133 139 new Store({ 134 - schema: require('../test-utils/simple_schema.json'), 140 + schema: minifyIntrospectionQuery( 141 + require('../test-utils/simple_schema.json') 142 + ), 135 143 updates: { 136 144 Mutation: { 137 145 doTheChaChaSlide: noop, ··· 149 157 150 158 it("should warn if Subscription operations don't exist in the schema", function () { 151 159 new Store({ 152 - schema: require('../test-utils/simple_schema.json'), 160 + schema: minifyIntrospectionQuery( 161 + require('../test-utils/simple_schema.json') 162 + ), 153 163 updates: { 154 164 Subscription: { 155 165 someoneDidTheChaChaSlide: noop, ··· 186 196 187 197 it('should not warn if keys do exist in the schema', function () { 188 198 new Store({ 189 - schema: require('../test-utils/simple_schema.json'), 199 + schema: minifyIntrospectionQuery( 200 + require('../test-utils/simple_schema.json') 201 + ), 190 202 keys: { 191 203 Todo: () => 'Todo', 192 204 }, ··· 197 209 198 210 it("should warn if a key doesn't exist in the schema", function () { 199 211 new Store({ 200 - schema: require('../test-utils/simple_schema.json'), 212 + schema: minifyIntrospectionQuery( 213 + require('../test-utils/simple_schema.json') 214 + ), 201 215 keys: { 202 216 Todo: () => 'todo', 203 217 NotInSchema: () => 'foo', ··· 236 250 237 251 it('should not warn if resolvers do exist in the schema', function () { 238 252 new Store({ 239 - schema: require('../test-utils/simple_schema.json'), 253 + schema: minifyIntrospectionQuery( 254 + require('../test-utils/simple_schema.json') 255 + ), 240 256 resolvers: { 241 257 Query: { 242 258 latestTodo: () => 'todo', ··· 254 270 255 271 it("should warn if a Query doesn't exist in the schema", function () { 256 272 new Store({ 257 - schema: require('../test-utils/simple_schema.json'), 273 + schema: minifyIntrospectionQuery( 274 + require('../test-utils/simple_schema.json') 275 + ), 258 276 resolvers: { 259 277 Query: { 260 278 todos: () => ['todo 1', 'todo 2'], ··· 274 292 275 293 it("should warn if a type doesn't exist in the schema", function () { 276 294 new Store({ 277 - schema: require('../test-utils/simple_schema.json'), 295 + schema: minifyIntrospectionQuery( 296 + require('../test-utils/simple_schema.json') 297 + ), 278 298 resolvers: { 279 299 Todo: { 280 300 complete: () => true, ··· 296 316 297 317 it("should warn if a type's property doesn't exist in the schema", function () { 298 318 new Store({ 299 - schema: require('../test-utils/simple_schema.json'), 319 + schema: minifyIntrospectionQuery( 320 + require('../test-utils/simple_schema.json') 321 + ), 300 322 resolvers: { 301 323 Todo: { 302 324 complete: () => true, ··· 783 805 784 806 it("should warn if an optimistic field doesn't exist in the schema's mutations", function () { 785 807 new Store({ 786 - schema: require('../test-utils/simple_schema.json'), 808 + schema: minifyIntrospectionQuery( 809 + require('../test-utils/simple_schema.json') 810 + ), 787 811 updates: { 788 812 Mutation: { 789 813 toggleTodo: noop, ··· 805 829 }); 806 830 807 831 it('should not warn for an introspection result root', function () { 832 + // NOTE: Do not wrap this require in `minifyIntrospectionQuery`! 808 833 // eslint-disable-next-line 809 834 const schema = require('../test-utils/simple_schema.json'); 810 835 const store = new Store({ schema });
+5
packages/introspection/CHANGELOG.md
··· 1 + # @urql/introspection 2 + 3 + ## 0.1.0 4 + 5 + **Initial Release**
+3
packages/introspection/README.md
··· 1 + <h2 align="center">@urql/introspection</h2> 2 + 3 + <p align="center"><strong>Utilities for dealing with Introspection Queries and Client Schemas</strong></p>
+61
packages/introspection/package.json
··· 1 + { 2 + "name": "@urql/introspection", 3 + "version": "0.1.0", 4 + "description": "Utilities for dealing with Introspection Queries and Client Schemas", 5 + "sideEffects": false, 6 + "homepage": "https://formidable.com/open-source/urql/docs/", 7 + "bugs": "https://github.com/FormidableLabs/urql/issues", 8 + "license": "MIT", 9 + "repository": { 10 + "type": "git", 11 + "url": "https://github.com/FormidableLabs/urql.git", 12 + "directory": "packages/introspection" 13 + }, 14 + "keywords": [ 15 + "graphql client", 16 + "graphql schema", 17 + "schema", 18 + "formidablelabs" 19 + ], 20 + "main": "dist/urql-introspection", 21 + "module": "dist/urql-introspection.mjs", 22 + "types": "dist/types/index.d.ts", 23 + "source": "src/index.ts", 24 + "exports": { 25 + ".": { 26 + "import": "./dist/urql-introspection.mjs", 27 + "require": "./dist/urql-introspection.js", 28 + "types": "./dist/types/index.d.ts", 29 + "source": "./src/index.ts" 30 + }, 31 + "./package.json": "./package.json" 32 + }, 33 + "files": [ 34 + "LICENSE", 35 + "README.md", 36 + "dist/" 37 + ], 38 + "scripts": { 39 + "test": "jest", 40 + "clean": "rimraf dist", 41 + "check": "tsc --noEmit", 42 + "lint": "eslint --ext=js,jsx,ts,tsx .", 43 + "build": "rollup -c ../../scripts/rollup/config.js", 44 + "prepare": "node ../../scripts/prepare/index.js", 45 + "prepublishOnly": "run-s clean build" 46 + }, 47 + "jest": { 48 + "preset": "../../scripts/jest/preset" 49 + }, 50 + "devDependencies": { 51 + "graphql": "^15.1.0", 52 + "graphql-tag": "^2.10.1" 53 + }, 54 + "peerDependencies": { 55 + "graphql": "^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0" 56 + }, 57 + "dependencies": {}, 58 + "publishConfig": { 59 + "access": "public" 60 + } 61 + }
+37
packages/introspection/src/getIntrospectedSchema.ts
··· 1 + import { 2 + IntrospectionQuery, 3 + GraphQLSchema, 4 + parse, 5 + buildSchema, 6 + executeSync, 7 + getIntrospectionQuery, 8 + } from 'graphql'; 9 + 10 + export const getIntrospectedSchema = ( 11 + input: string | IntrospectionQuery | GraphQLSchema 12 + ): IntrospectionQuery => { 13 + if (typeof input === 'string') { 14 + try { 15 + input = JSON.parse(input); 16 + } catch (_error) { 17 + input = buildSchema(input as string); 18 + } 19 + } 20 + 21 + if (typeof input === 'object' && '__schema' in input) { 22 + return input; 23 + } 24 + 25 + const initialIntrospection = executeSync({ 26 + document: parse(getIntrospectionQuery({ descriptions: false })), 27 + schema: input as GraphQLSchema, 28 + }); 29 + 30 + if (!initialIntrospection.data || !initialIntrospection.data.__schema) { 31 + throw new TypeError( 32 + 'GraphQL could not generate an IntrospectionQuery from the given schema.' 33 + ); 34 + } 35 + 36 + return initialIntrospection.data as IntrospectionQuery; 37 + };
+2
packages/introspection/src/index.ts
··· 1 + export * from './getIntrospectedSchema'; 2 + export * from './minifyIntrospectionQuery';
+155
packages/introspection/src/minifyIntrospectionQuery.ts
··· 1 + import { 2 + IntrospectionQuery, 3 + IntrospectionType, 4 + IntrospectionTypeRef, 5 + } from 'graphql'; 6 + 7 + const anyType: IntrospectionTypeRef = { 8 + kind: 'SCALAR', 9 + name: 'Any', 10 + }; 11 + 12 + const mapType = ( 13 + fromType: any, 14 + toType: IntrospectionTypeRef 15 + ): IntrospectionTypeRef => { 16 + switch (fromType.kind) { 17 + case 'NON_NULL': 18 + case 'LIST': 19 + return { 20 + kind: fromType.kind, 21 + ofType: mapType(fromType.ofType, toType), 22 + }; 23 + 24 + case 'SCALAR': 25 + case 'INPUT_OBJECT': 26 + case 'ENUM': 27 + return toType; 28 + 29 + case 'OBJECT': 30 + case 'INTERFACE': 31 + case 'UNION': 32 + return { 33 + kind: fromType.kind, 34 + name: fromType.name, 35 + }; 36 + 37 + default: 38 + throw new TypeError( 39 + `Unrecognized type reference of type: ${(fromType as any).kind}.` 40 + ); 41 + } 42 + }; 43 + 44 + const minifyIntrospectionType = ( 45 + type: IntrospectionType 46 + ): IntrospectionType => { 47 + switch (type.kind) { 48 + case 'OBJECT': { 49 + return { 50 + kind: 'OBJECT', 51 + name: type.name, 52 + fields: type.fields.map( 53 + field => 54 + ({ 55 + name: field.name, 56 + type: field.type && mapType(field.type, anyType), 57 + args: 58 + field.args && 59 + field.args.map(arg => ({ 60 + ...arg, 61 + type: mapType(arg.type, anyType), 62 + defaultValue: undefined, 63 + })), 64 + } as any) 65 + ), 66 + interfaces: 67 + type.interfaces && 68 + type.interfaces.map(int => ({ 69 + kind: 'INTERFACE', 70 + name: int.name, 71 + })), 72 + }; 73 + } 74 + 75 + case 'INTERFACE': { 76 + return { 77 + kind: 'INTERFACE', 78 + name: type.name, 79 + fields: type.fields.map( 80 + field => 81 + ({ 82 + name: field.name, 83 + type: field.type && mapType(field.type, anyType), 84 + args: 85 + field.args && 86 + field.args.map(arg => ({ 87 + ...arg, 88 + type: mapType(arg.type, anyType), 89 + defaultValue: undefined, 90 + })), 91 + } as any) 92 + ), 93 + interfaces: 94 + type.interfaces && 95 + type.interfaces.map(int => ({ 96 + kind: 'INTERFACE', 97 + name: int.name, 98 + })), 99 + possibleTypes: 100 + type.possibleTypes && 101 + type.possibleTypes.map(type => ({ 102 + kind: type.kind, 103 + name: type.name, 104 + })), 105 + }; 106 + } 107 + 108 + case 'UNION': { 109 + return { 110 + kind: 'UNION', 111 + name: type.name, 112 + possibleTypes: type.possibleTypes.map(type => ({ 113 + kind: type.kind, 114 + name: type.name, 115 + })), 116 + }; 117 + } 118 + 119 + default: 120 + return type; 121 + } 122 + }; 123 + 124 + export const minifyIntrospectionQuery = ( 125 + schema: IntrospectionQuery 126 + ): IntrospectionQuery => { 127 + if (!schema || !('__schema' in schema)) { 128 + throw new TypeError('Expected to receive an IntrospectionQuery.'); 129 + } 130 + 131 + const { 132 + __schema: { queryType, mutationType, subscriptionType, types }, 133 + } = schema; 134 + 135 + const minifiedTypes = types 136 + .filter( 137 + type => 138 + type.kind === 'OBJECT' || 139 + type.kind === 'INTERFACE' || 140 + type.kind === 'UNION' 141 + ) 142 + .map(minifyIntrospectionType); 143 + 144 + minifiedTypes.push({ kind: 'SCALAR', name: anyType.name }); 145 + 146 + return { 147 + __schema: { 148 + queryType, 149 + mutationType, 150 + subscriptionType, 151 + types: minifiedTypes, 152 + directives: [], 153 + }, 154 + }; 155 + };
+13
packages/introspection/tsconfig.json
··· 1 + { 2 + "extends": "../../tsconfig.json", 3 + "include": ["src"], 4 + "compilerOptions": { 5 + "baseUrl": "./", 6 + "paths": { 7 + "urql": ["../../node_modules/urql/src"], 8 + "*-urql": ["../../node_modules/*-urql/src"], 9 + "@urql/core/*": ["../../node_modules/@urql/core/src/*"], 10 + "@urql/*": ["../../node_modules/@urql/*/src"] 11 + } 12 + } 13 + }