├── .gitignore ├── src ├── fixtures │ ├── mutation.graphql │ ├── customScalarQuery.graphql │ ├── subscription.graphql │ ├── variables │ │ ├── query.graphql │ │ └── schema.graphql │ ├── basicQuery.graphql │ ├── queryWithVariables.graphql │ ├── basicQueryWithArgs.graphql │ ├── queryWithVariablesVariables.ts │ ├── schema.graphql │ ├── customScalarHandlers.ts │ └── index.ts ├── response │ ├── index.ts │ ├── test.ts │ ├── decode.ts │ └── encode.ts ├── nestedBitmask │ ├── index.ts │ ├── calculateBitmaskCount.ts │ ├── encode.ts │ ├── decode.ts │ └── test.ts ├── index.ts ├── query │ ├── extractTargetType.ts │ ├── jsonDecoder.ts │ ├── documentDecoder.ts │ ├── types.ts │ ├── test.ts │ ├── encode.ts │ └── decode.ts ├── mergeArrays.ts ├── iterator.ts ├── varint │ ├── test.ts │ └── index.ts ├── scalarHandlers.ts └── test.ts ├── tsconfig.json ├── codegen.yml ├── package.json └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules -------------------------------------------------------------------------------- /src/fixtures/mutation.graphql: -------------------------------------------------------------------------------- 1 | mutation NoArgs { 2 | noArgs 3 | } -------------------------------------------------------------------------------- /src/fixtures/customScalarQuery.graphql: -------------------------------------------------------------------------------- 1 | query customScalarQuery { 2 | date 3 | } -------------------------------------------------------------------------------- /src/fixtures/subscription.graphql: -------------------------------------------------------------------------------- 1 | subscription NoArgsSubscription { 2 | noArgs 3 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "esModuleInterop": true 4 | } 5 | } -------------------------------------------------------------------------------- /src/response/index.ts: -------------------------------------------------------------------------------- 1 | export { default as decodeResponse } from './decode' 2 | export { default as encodeResponse } from './encode' 3 | -------------------------------------------------------------------------------- /src/nestedBitmask/index.ts: -------------------------------------------------------------------------------- 1 | export { default as decodeNestedBitmask } from './decode' 2 | export { default as encodeNestedBitmask } from './encode' -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Decoder } from './query/decode' 2 | export { default as Encoder } from './query/encode' 3 | export * as decodeResponse from './response/decode' 4 | export * as encodeResponse from './response/encode' 5 | -------------------------------------------------------------------------------- /codegen.yml: -------------------------------------------------------------------------------- 1 | overwrite: true 2 | schema: src/fixtures/schema.graphql 3 | documents: src/fixtures/* 4 | generates: 5 | src/fixtures/index.ts: 6 | plugins: 7 | - typescript 8 | - typescript-operations 9 | - typed-document-node -------------------------------------------------------------------------------- /src/fixtures/variables/query.graphql: -------------------------------------------------------------------------------- 1 | query QueryWithVariables( 2 | $A: Int 3 | $B: Int 4 | ) { 5 | withArgs(firstArg: $B secondArg: $A) 6 | field 7 | field2 8 | anotherWithArgs(firstArg: $A secondArg: 20) { 9 | subfield 10 | subWithArgs(firstArg: $A) 11 | } 12 | } -------------------------------------------------------------------------------- /src/query/extractTargetType.ts: -------------------------------------------------------------------------------- 1 | import { Kind, TypeNode } from 'graphql' 2 | 3 | function extractTargetType(type: TypeNode): string { 4 | if (type.kind === Kind.NAMED_TYPE) return type.name.value 5 | else return extractTargetType(type.type) 6 | } 7 | 8 | export default extractTargetType 9 | -------------------------------------------------------------------------------- /src/nestedBitmask/calculateBitmaskCount.ts: -------------------------------------------------------------------------------- 1 | const calculateBitmaskCount = (fieldsCount: number): number => { 2 | if (fieldsCount < 9) return 1 3 | const minBits = Math.floor(fieldsCount / 8) 4 | const remainderBits = fieldsCount % 8 5 | return (8 - minBits) < remainderBits 6 | ? minBits + 1 7 | : minBits 8 | } 9 | 10 | export default calculateBitmaskCount -------------------------------------------------------------------------------- /src/fixtures/basicQuery.graphql: -------------------------------------------------------------------------------- 1 | query Basic { 2 | int 3 | float 4 | boolean 5 | string 6 | withArgs 7 | scalarArray 8 | mapArray { 9 | id 10 | map { 11 | id 12 | map { 13 | id 14 | } 15 | } 16 | } 17 | map { 18 | id 19 | map { 20 | id 21 | map { 22 | id 23 | } 24 | } 25 | } 26 | } -------------------------------------------------------------------------------- /src/fixtures/queryWithVariables.graphql: -------------------------------------------------------------------------------- 1 | query WithVariables ( 2 | # Variable names will not be preserved 3 | # so to not break the test an uppercased 4 | # alphabetic char sequence should be used 5 | $A: Int 6 | $B: Float 7 | $C: Boolean 8 | $D: String 9 | $E: Enumerable 10 | $F: InputMap 11 | ) { 12 | withArgs( 13 | int: $A 14 | float: $B 15 | boolean: $C 16 | string: $D 17 | enum: $E 18 | inputMap: $F 19 | ) 20 | } -------------------------------------------------------------------------------- /src/fixtures/variables/schema.graphql: -------------------------------------------------------------------------------- 1 | type Query { 2 | withArgs(firstArg: Int secondArg: Int): String 3 | field: Int 4 | field2: Int 5 | anotherWithArgs(firstArg: Int secondArg: Int): MapWithArgs 6 | withInputType(input: InputMap): String 7 | } 8 | 9 | type MapWithArgs { 10 | subfield: Int 11 | subWithArgs(firstArg: Int): String 12 | } 13 | 14 | input InputMap { 15 | int: Int 16 | string: String 17 | intList: [Int] 18 | subMap: InputMap 19 | subMaps: [InputMap] 20 | } -------------------------------------------------------------------------------- /src/fixtures/basicQueryWithArgs.graphql: -------------------------------------------------------------------------------- 1 | query WithArgs { 2 | int 3 | float 4 | boolean 5 | string 6 | withArgs ( 7 | int: 1 8 | float: 1.5 9 | boolean: true 10 | string: "string" 11 | enum: FIRST 12 | ) 13 | scalarArray 14 | mapArray { 15 | id 16 | map { 17 | id 18 | maps { 19 | id 20 | } 21 | } 22 | } 23 | map { 24 | id 25 | map { 26 | id 27 | map { 28 | id 29 | } 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /src/mergeArrays.ts: -------------------------------------------------------------------------------- 1 | export function mergeArrays(...arrays: Uint8Array[]): Uint8Array { 2 | const result = new Uint8Array(arrays.reduce(lengthsReducer, 0)) 3 | let currentIndex = 0 4 | for (let i = 0; i < arrays.length; i++) { 5 | result.set(arrays[i], currentIndex) 6 | currentIndex += arrays[i].length 7 | } 8 | return result 9 | } 10 | 11 | function lengthsReducer(result: number, data: ArrayLike): number { 12 | return result + data.length 13 | } 14 | 15 | export default mergeArrays 16 | -------------------------------------------------------------------------------- /src/fixtures/queryWithVariablesVariables.ts: -------------------------------------------------------------------------------- 1 | import { Enumerable, WithVariablesQueryVariables } from '.' 2 | 3 | const preparedVariables: WithVariablesQueryVariables = { 4 | A: 1, 5 | B: 2.5, 6 | C: true, 7 | D: 'test', 8 | E: Enumerable.First, 9 | F: { 10 | inputMap: { 11 | int: 123, 12 | // FIXME [1, 2, 3, 4, 5, 255] 13 | inputListScalar: [1, 2, 3, 4, 2], 14 | inputListMap: [ 15 | { 16 | int: 123, 17 | inputListScalar: [1, 2, 3, 4] 18 | } 19 | ] 20 | } 21 | } 22 | } 23 | 24 | export default preparedVariables 25 | -------------------------------------------------------------------------------- /src/iterator.ts: -------------------------------------------------------------------------------- 1 | export interface ByteIterator { 2 | take(): number 3 | take(length: number): Uint8Array 4 | atEnd(): boolean 5 | current(): number 6 | } 7 | 8 | export function createIterator(array: Uint8Array, end: number): ByteIterator { 9 | let index = 0 10 | return { 11 | take(length?: number): any { 12 | if (length !== undefined) return array.slice(index, (index += length)) 13 | else { 14 | index += 1 15 | return array[index - 1] 16 | } 17 | }, 18 | current: () => array[index], 19 | atEnd: () => array[index] === end || array[index] === undefined 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/query/jsonDecoder.ts: -------------------------------------------------------------------------------- 1 | import { DataDecoder } from './types' 2 | 3 | const jsonDecoder: DataDecoder, any> = { 4 | list: () => { 5 | const accumulator: Array = [] 6 | return { 7 | accumulate: (value) => accumulator.push(value), 8 | commit: () => accumulator 9 | } 10 | }, 11 | vector: () => { 12 | const accumulator: { [key: string]: any } = {} 13 | return { 14 | accumulate: (key) => ({ 15 | addValue: (value) => (accumulator[key] = value) 16 | }), 17 | commit: () => accumulator 18 | } 19 | } 20 | } 21 | 22 | export default jsonDecoder 23 | -------------------------------------------------------------------------------- /src/fixtures/schema.graphql: -------------------------------------------------------------------------------- 1 | type Query { 2 | int: Int 3 | float: Float 4 | boolean: Boolean 5 | string: String 6 | map: Map 7 | scalarArray: [String] 8 | mapArray: [Map] 9 | withArgs ( 10 | int: Int 11 | float: Float 12 | boolean: Boolean 13 | string: String 14 | enum: Enumerable 15 | inputMap: InputMap 16 | ): String 17 | date: UDateS 18 | } 19 | 20 | scalar UDateS 21 | 22 | input InputMap { 23 | int: Int 24 | inputMap: InputMap 25 | inputListScalar: [Int] 26 | inputListMap: [InputMap] 27 | } 28 | 29 | type Mutation { 30 | noArgs: String 31 | } 32 | 33 | type Subscription { 34 | noArgs: String 35 | } 36 | 37 | type Map { 38 | id: ID 39 | map: Map 40 | maps: [Map] 41 | } 42 | 43 | enum Enumerable { 44 | FIRST 45 | SECOND 46 | THIRD 47 | } -------------------------------------------------------------------------------- /src/nestedBitmask/encode.ts: -------------------------------------------------------------------------------- 1 | import calculateBitmaskCount from './calculateBitmaskCount' 2 | 3 | const encodePositionsAsBitmask = ( 4 | fieldsCount: number, 5 | positions: Array 6 | ): Uint8Array => { 7 | // FIXME add checks 8 | positions.sort((a, b) => a < b ? -1 : 1) 9 | 10 | const bitmaskCount = calculateBitmaskCount(fieldsCount) 11 | 12 | let result: Array = [0] 13 | 14 | for (const position of positions) { 15 | const shiftedPosition = bitmaskCount + position 16 | if (shiftedPosition < 8) 17 | result[0] |= (1 << shiftedPosition) 18 | else { 19 | const bitmaskIndex = Math.floor(shiftedPosition / 8) 20 | result[0] |= (1 << bitmaskIndex - 1) 21 | result[bitmaskIndex] |= (1 << shiftedPosition % 8) 22 | } 23 | } 24 | // FIXME this might be avoided 25 | result = result.filter(item => item != null) 26 | return new Uint8Array(result) 27 | } 28 | 29 | export default encodePositionsAsBitmask -------------------------------------------------------------------------------- /src/fixtures/customScalarHandlers.ts: -------------------------------------------------------------------------------- 1 | import { ByteIterator } from '../iterator' 2 | import { ScalarHandlers } from '../scalarHandlers' 3 | 4 | const length = 4 5 | 6 | const customScalarHandlers: ScalarHandlers = { 7 | // Date with seconds precision spanning from Jan 01 1970 till ~Feb 07 2106 8 | // that takes up a 4 byte integer unlike JSON representation which would 9 | // take 10 bytes for seconds or 13 bytes for milliseconds timestamp. 10 | // It is also nicely decoded directly to Date() object 11 | UDateS: { 12 | decode: (data: ByteIterator) => { 13 | const subset = data.take(length) 14 | const view = new DataView(subset.buffer) 15 | return new Date(view.getInt32(0) * 1000) 16 | }, 17 | encode: (data: Date) => { 18 | const timestampSeconds = data.getTime() / 1000 19 | return new Uint8Array([ 20 | (timestampSeconds & 0xff000000) >> 24, 21 | (timestampSeconds & 0x00ff0000) >> 16, 22 | (timestampSeconds & 0x0000ff00) >> 8, 23 | timestampSeconds & 0x000000ff 24 | ]) 25 | } 26 | } 27 | } 28 | 29 | export default customScalarHandlers 30 | -------------------------------------------------------------------------------- /src/nestedBitmask/decode.ts: -------------------------------------------------------------------------------- 1 | import calculateBitmaskCount from './calculateBitmaskCount' 2 | 3 | export const MAX_FIELDS = 8 ** 2 4 | 5 | export const decodePositionsFromBitmask = ( 6 | fieldsCount: number, 7 | bytes: Uint8Array 8 | ): Array => { 9 | 10 | // FIXME add checks 11 | if (bytes.length < 1) 12 | throw new Error('Field Bitmask cannot be empty') 13 | 14 | if (fieldsCount > MAX_FIELDS) 15 | throw new Error(`Fields count cannot be more than ${MAX_FIELDS}`) 16 | 17 | const bitmaskCount = calculateBitmaskCount(fieldsCount) 18 | 19 | const result = new Array() 20 | const controlByte = bytes[0] 21 | 22 | for (let i = bitmaskCount; i < 8; i++) 23 | if (controlByte & (1 << i)) 24 | result.push(i - bitmaskCount) 25 | 26 | // FIXME SIMD can be used here 27 | let shift = 0 28 | for (let i = 0; i < bitmaskCount; i++) 29 | if (controlByte & (1 << i)) { 30 | shift += 1 31 | for (let j = 0; j < 8; j++) 32 | if (bytes[shift] & (1 << j)) 33 | result.push((8 - bitmaskCount) + (8 * i) + j) 34 | } 35 | 36 | return result 37 | } 38 | 39 | export default decodePositionsFromBitmask -------------------------------------------------------------------------------- /src/varint/test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | decodeSignedLeb128, 3 | decodeUnsignedLeb128, 4 | encodeSignedLeb128, 5 | encodeUnsignedLeb128 6 | } from '.' 7 | import { createIterator } from '../iterator' 8 | import { END } from '../query/types' 9 | 10 | const unsignedLengths = [ 11 | 2 ** 7 - 1, 12 | 2 ** 14 - 1, 13 | 2 ** 21 - 1, 14 | 2 ** 28 - 1 15 | // 2 ** 35 - 1 FIXME does not work for this 16 | ] 17 | 18 | const signedLengths = [ 19 | 2 ** 7 - 1, 20 | -(2 ** 14 - 1), 21 | 2 ** 21 - 1, 22 | -(2 ** 28 - 1) 23 | // 2 ** 35 - 1 FIXME does not work for this 24 | ] 25 | 26 | unsignedLengths.map((value, index) => 27 | test(`correct unsigned ${index + 1} byte length`, () => { 28 | const encoded = encodeUnsignedLeb128(value) 29 | const iterator = createIterator(encoded, END) 30 | const decoded = decodeUnsignedLeb128(iterator) 31 | expect(decoded).toEqual(value) 32 | }) 33 | ) 34 | 35 | signedLengths.map((value, index) => 36 | test(`correct signed ${index + 1} byte length`, () => { 37 | const encoded = encodeSignedLeb128(value) 38 | const iterator = createIterator(encoded, END) 39 | const decoded = decodeSignedLeb128(iterator) 40 | expect(decoded).toEqual(value) 41 | }) 42 | ) 43 | -------------------------------------------------------------------------------- /src/nestedBitmask/test.ts: -------------------------------------------------------------------------------- 1 | import { decodeNestedBitmask, encodeNestedBitmask } from '.' 2 | import { MAX_FIELDS } from './decode' 3 | 4 | for (let index = 0; index < MAX_FIELDS; index++) { 5 | const fieldsCount = index 6 | const positions = Array.from(Array(index).keys()) 7 | test('decoded nested bitmask matches encoded', async () => { 8 | const encoded = encodeNestedBitmask(fieldsCount, positions) 9 | const decoded = decodeNestedBitmask(fieldsCount, encoded) 10 | expect(positions).toEqual(decoded) 11 | }) 12 | } 13 | 14 | for (let index = 0; index < MAX_FIELDS; index++) { 15 | const fieldsCount = index 16 | const positions = Array.from(Array(index).keys()).sort(() => 0.5 - Math.random()) 17 | test('unsorted decoded nested bitmask matches encoded', async () => { 18 | const encoded = encodeNestedBitmask(fieldsCount, positions) 19 | const decoded = decodeNestedBitmask(fieldsCount, encoded) 20 | // FIXME positions are sorted in place anyway, use something immutable 21 | expect(positions.sort((a, b) => a < b ? -1 : 1)).toEqual(decoded) 22 | }) 23 | } 24 | 25 | for (let index = 0; index < MAX_FIELDS; index++) { 26 | const fieldsCount = index 27 | const positions: Array = [] 28 | for (let jindex = 0; jindex < index; jindex++) 29 | if (0.5 > Math.random()) 30 | positions.push(jindex) 31 | test('sparse nested bitmask matches encoded', async () => { 32 | const encoded = encodeNestedBitmask(fieldsCount, positions) 33 | const decoded = decodeNestedBitmask(fieldsCount, encoded) 34 | expect(positions).toEqual(decoded) 35 | }) 36 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "graphql-binary", 3 | "version": "0.1.0", 4 | "description": "GraphQL binary protocol for smaller network traffic and parsing performance", 5 | "main": "index.js", 6 | "repository": "https://github.com/esseswann/graphql-binary", 7 | "author": "jsus ", 8 | "license": "MIT", 9 | "private": false, 10 | "scripts": { 11 | "test": "jest --watch --detectOpenHandles", 12 | "dev": "nodemon --exec ts-node" 13 | }, 14 | "dependencies": { 15 | "graphql": "16.2.0" 16 | }, 17 | "devDependencies": { 18 | "@graphql-codegen/cli": "2.3.1", 19 | "@graphql-codegen/typed-document-node": "2.2.2", 20 | "@graphql-codegen/typescript": "2.4.2", 21 | "@graphql-codegen/typescript-operations": "2.2.2", 22 | "@graphql-tools/mock": "8.5.1", 23 | "@graphql-tools/schema": "8.3.1", 24 | "@graphql-typed-document-node/core": "3.1.1", 25 | "@types/jest": "27.4.0", 26 | "@types/node": "17.0.9", 27 | "graphql-query-compress": "1.2.4", 28 | "jest": "27.4.7", 29 | "nodemon": "2.0.15", 30 | "prettier": "2.5.1", 31 | "ts-jest": "27.1.3", 32 | "ts-node": "10.4.0", 33 | "typescript": "4.5.4" 34 | }, 35 | "nodemonConfig": { 36 | "watch": [ 37 | "src" 38 | ], 39 | "ext": "ts,graphql" 40 | }, 41 | "prettier": { 42 | "tabWidth": 2, 43 | "useTabs": false, 44 | "semi": false, 45 | "singleQuote": true, 46 | "trailingComma": "none" 47 | }, 48 | "jest": { 49 | "roots": [ 50 | "/src" 51 | ], 52 | "testMatch": [ 53 | "**/__tests__/**/*.+(ts)", 54 | "**/?(*.)+(test).+(ts)" 55 | ], 56 | "transform": { 57 | "^.+\\.(ts|js)$": "ts-jest" 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/scalarHandlers.ts: -------------------------------------------------------------------------------- 1 | import { TextDecoder, TextEncoder } from 'util' // FIXME should use some crossplatform solution 2 | import { ByteIterator } from './iterator' 3 | import mergeArrays from './mergeArrays' 4 | import { 5 | decodeSignedLeb128, 6 | decodeUnsignedLeb128, 7 | encodeSignedLeb128, 8 | encodeUnsignedLeb128 9 | } from './varint' 10 | 11 | export type ScalarHandlers = { 12 | [key: string]: ScalarHandler 13 | } 14 | 15 | interface ScalarHandler { 16 | encode: (data: T) => Uint8Array 17 | decode: (data: ByteIterator) => T 18 | } 19 | 20 | const stringHandler: ScalarHandler = { 21 | encode: (data: string) => { 22 | const textEncoder = new TextEncoder() 23 | const result = textEncoder.encode(data) 24 | return mergeArrays(encodeUnsignedLeb128(result.length), result) 25 | }, 26 | decode: (data: ByteIterator) => { 27 | const length = decodeUnsignedLeb128(data) 28 | const textDecoder = new TextDecoder() 29 | // FIXME this is probably incorrect 30 | const result = textDecoder.decode(data.take(length)) 31 | return result 32 | } 33 | } 34 | 35 | const scalarHandlers: ScalarHandlers = { 36 | Int: { 37 | encode: encodeSignedLeb128, 38 | decode: decodeSignedLeb128 39 | }, 40 | Float: { 41 | encode: (data: number) => { 42 | const array = new Float32Array(1) // FIXME we need to make precision variable 43 | array[0] = data 44 | return new Uint8Array(array.buffer) 45 | }, 46 | decode: (data: ByteIterator) => { 47 | const takenData = data.take(4) 48 | const view = new DataView(takenData.buffer) 49 | return view.getFloat32(0, true) 50 | } 51 | }, 52 | String: stringHandler, 53 | Boolean: { 54 | encode: (data: boolean) => new Uint8Array([data ? 1 : 0]), 55 | decode: (data) => !!data.take() 56 | }, 57 | ID: stringHandler 58 | } 59 | 60 | export default scalarHandlers 61 | -------------------------------------------------------------------------------- /src/response/test.ts: -------------------------------------------------------------------------------- 1 | import { addMocksToSchema } from '@graphql-tools/mock' 2 | import { makeExecutableSchema } from '@graphql-tools/schema' 3 | import fs from 'fs' 4 | import { buildSchema, graphql, print } from 'graphql' 5 | import { BasicDocument, CustomScalarQueryDocument } from '../fixtures' 6 | import customScalarHandlers from '../fixtures/customScalarHandlers' 7 | import Decoder from '../query/decode' 8 | import Encoder from '../query/encode' 9 | import decode from './decode' 10 | import encode from './encode' 11 | 12 | const schemaString = fs.readFileSync('src/fixtures/schema.graphql', 'utf8') 13 | const schema = buildSchema(schemaString) 14 | 15 | const decoder = new Decoder(schema, customScalarHandlers) 16 | const encoder = new Encoder(schema, customScalarHandlers) 17 | 18 | const executableSchema = makeExecutableSchema({ typeDefs: schema }) 19 | const schemaWithMocks = addMocksToSchema({ 20 | schema: executableSchema, 21 | mocks: { 22 | Float: () => 3.5 // FIXME should do something with precision being lost during decoding 23 | } 24 | }) 25 | 26 | test('decoded response matches encoded', async () => { 27 | const response = await graphql({ 28 | schema: schemaWithMocks, 29 | source: print(BasicDocument) 30 | }) 31 | const encodedResponse = encode(decoder, BasicDocument, response.data) 32 | const decodedResponse = decode(encoder, BasicDocument, encodedResponse) 33 | expect(decodedResponse).toEqual(response.data) 34 | }) 35 | 36 | test('extendable types are applied', () => { 37 | const data = { 38 | // Note that we set seconds to zero or tests will fail because we loose precision 39 | date: new Date('December 17, 1995 03:24:00') 40 | } 41 | const encodedResponse = encode(decoder, CustomScalarQueryDocument, data) 42 | const decodedResponse = decode( 43 | encoder, 44 | CustomScalarQueryDocument, 45 | encodedResponse 46 | ) 47 | expect(decodedResponse).toEqual(data) 48 | }) 49 | -------------------------------------------------------------------------------- /src/query/documentDecoder.ts: -------------------------------------------------------------------------------- 1 | import { Kind } from 'graphql' 2 | import { 3 | ArgumentNode, 4 | FieldNode, 5 | SelectionSetNode, 6 | VariableDefinitionNode 7 | } from 'graphql/language/ast' 8 | import { QueryDecoder } from './types' 9 | 10 | export const documentDecoder: QueryDecoder< 11 | SelectionSetNode, 12 | Array 13 | > = { 14 | vector: () => { 15 | const accumulator: Array = [] 16 | return { 17 | accumulate: (key) => { 18 | const args: Array = [] 19 | let selectionSet: SelectionSetNode 20 | return { 21 | addValue: (value) => (selectionSet = value), 22 | addArg: (key, variableName) => 23 | args.push({ 24 | kind: Kind.ARGUMENT, 25 | value: { 26 | kind: Kind.VARIABLE, 27 | name: { 28 | kind: Kind.NAME, 29 | value: variableName 30 | } 31 | }, 32 | name: { 33 | kind: Kind.NAME, 34 | value: key 35 | } 36 | }), 37 | commit: () => 38 | accumulator.push({ 39 | kind: Kind.FIELD, 40 | name: { 41 | kind: Kind.NAME, 42 | value: key 43 | }, 44 | ...(args.length && { arguments: args }), 45 | ...(selectionSet && { selectionSet }) 46 | }) 47 | } 48 | }, 49 | commit: (): SelectionSetNode => ({ 50 | kind: Kind.SELECTION_SET, 51 | selections: accumulator 52 | }) 53 | } 54 | }, 55 | variables: () => { 56 | const accumulator: Array = [] 57 | return { 58 | accumulate: (key, type) => 59 | accumulator.push({ 60 | kind: Kind.VARIABLE_DEFINITION, 61 | type: type, 62 | variable: { 63 | kind: Kind.VARIABLE, 64 | name: { 65 | kind: Kind.NAME, 66 | value: key 67 | } 68 | } 69 | }), 70 | commit: () => accumulator 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/varint/index.ts: -------------------------------------------------------------------------------- 1 | // https://github.com/stoklund/varint#prefixvarint 2 | // WebAssembly/design#601 3 | import { ByteIterator } from '../iterator' 4 | 5 | const MAX_BITS = Number.MAX_SAFE_INTEGER.toString(2).length 6 | const BYTE_SIZE = 8 7 | const MAX_SHIFTS = Math.floor(MAX_BITS / BYTE_SIZE) 8 | 9 | export function decodeUnsignedLeb128(data: ByteIterator): number { 10 | let number: number = 0 11 | let index: number = 0 12 | let byte: number 13 | 14 | while (index < MAX_SHIFTS) { 15 | // Prevents infinite shifting attack 16 | byte = data.take() 17 | number += (byte & 0x7f) << (index * 7) 18 | if (byte >> 7 === 0) break 19 | else index++ 20 | } 21 | 22 | return number 23 | } 24 | 25 | export const encodeUnsignedLeb128 = (value: number): Uint8Array => { 26 | if (value < 0) 27 | throw new Error(`Only encodes positive numbers. ${value} is not allowed`) 28 | const array: Array = [] 29 | 30 | let lowerBits: number 31 | let highBit: number 32 | while (value > 0) { 33 | lowerBits = value & 0x7f 34 | value = value >> 7 35 | highBit = value ? 1 : 0 36 | array.push((highBit << 7) + lowerBits) 37 | } 38 | 39 | return new Uint8Array(array) 40 | } 41 | 42 | export const decodeSignedLeb128 = (input: ByteIterator): number => { 43 | let result = 0 44 | let shift = 0 45 | let index = 0 46 | 47 | while (index < MAX_SHIFTS) { 48 | // Prevents infinite shifting attack 49 | index += 1 50 | const byte = input.take() 51 | result |= (byte & 0x7f) << shift 52 | shift += 7 53 | if ((0x80 & byte) === 0) { 54 | if (shift < 32 && (byte & 0x40) !== 0) { 55 | return result | (~0 << shift) 56 | } 57 | return result 58 | } 59 | } 60 | } 61 | 62 | export const encodeSignedLeb128 = (value: number): Uint8Array => { 63 | value |= 0 64 | const result: Array = [] 65 | 66 | while (true) { 67 | // Prevents infinite shifting attack 68 | const byte = value & 0x7f 69 | value >>= 7 70 | if ( 71 | (value === 0 && (byte & 0x40) === 0) || 72 | (value === -1 && (byte & 0x40) !== 0) 73 | ) { 74 | result.push(byte) 75 | return new Uint8Array(result) 76 | } 77 | result.push(byte | 0x80) 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/query/types.ts: -------------------------------------------------------------------------------- 1 | export const MIN_LENGTH = 3 2 | export const END = 255 3 | export const ASCII_OFFSET = 65 4 | 5 | import { DocumentNode, TypeNode } from 'graphql/language/ast' 6 | 7 | export enum Operation { 8 | query = 0 << 0, 9 | mutation = 1 << 1, 10 | subscription = 1 << 2 11 | } 12 | 13 | export enum Flags { 14 | Name = 1 << 3, 15 | Variables = 1 << 4, 16 | Directives = 1 << 5 17 | } 18 | 19 | export type DecodeResult = { 20 | document: DocumentNode 21 | variables: object | null 22 | encodeResponse: (response: Result) => Uint8Array 23 | } 24 | 25 | export interface QueryDecoder { 26 | vector: () => VectorHandler> 27 | variables: () => VariablesHandler 28 | } 29 | 30 | export interface QueryKeyHandler extends KeyHandler { 31 | addArg: (key: string, type: any) => void 32 | addDirective?: (key: string, type: any) => void 33 | commit: () => void 34 | } 35 | 36 | export interface VariablesHandler { 37 | accumulate: AccumulateVariables 38 | commit: () => T 39 | } 40 | 41 | export type AccumulateVariables = (key: string, type: TypeNode) => void 42 | 43 | export interface VectorHandler { 44 | accumulate: (key: string) => KeyHandler 45 | commit: () => T 46 | } 47 | 48 | export interface DataDecoder { 49 | vector: () => VectorHandler> 50 | list: () => ListHandler 51 | } 52 | 53 | export interface KeyHandler { 54 | addValue: (value: T) => void 55 | } 56 | 57 | export interface ListHandler { 58 | accumulate: (value: any) => void 59 | commit: () => T 60 | } 61 | 62 | export type EncodeResult = 63 | | VariablesEncoder 64 | | EncodedQueryWithHandler 65 | 66 | export type VariablesEncoder = ( 67 | variables: Variables 68 | ) => EncodedQueryWithHandler 69 | 70 | export interface EncodedQueryWithHandler { 71 | query: Uint8Array 72 | decodeResponse: (data: Uint8Array) => Result 73 | } 74 | 75 | export enum Config { 76 | SCALAR = 0, 77 | VECTOR = 1 << 0, 78 | ARGUMENT = 1 << 1, 79 | LIST = 1 << 2, 80 | INPUT = 1 << 3, 81 | HAS_ARGUMENTS = 1 << 4, 82 | NON_NULL = 1 << 5 83 | } 84 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GraphQL Binary 2 | GraphQL Binary protocol packs and unpacks GraphQL query into a schema-tied ByteArrays which allows up to 5x traffic reduction and significant parsing (unpack stage) performance boost 3 | 4 | Moreover the response is also optimised by removing the keys, storing integers in bytes, having c-like strings\arrays and so on similarly to Protobuf in terms of schema and to MessagePack in terms of values encoding 5 | 6 | For some developrs the most interesting feature is encoding\decoding custom types, e.g. [Date type with seconds precision taking only 4 bytes](https://github.com/esseswann/graphql-binary/blob/master/src/fixtures/customScalarHandlers.ts#L11) 7 | 8 | # Stage 9 | This project is currently in proof on concept stage. We have no intent on supporting Union and Interface types in the first release. Fragments will be inlined for multiple reasons 10 | 11 | # Concept 12 | ```graphql 13 | query BasicQuery { 14 | int 15 | float 16 | boolean 17 | string 18 | withArgs ( 19 | int: 1 20 | boolean: true 21 | string: "string" 22 | ) 23 | map { 24 | id 25 | map { 26 | id 27 | map { 28 | id 29 | } 30 | } 31 | } 32 | } 33 | ``` 34 | 35 | is converted to this 36 | ```javascript 37 | Uint8Array(30) [ 38 | 0, 1, 2, 3, 5, 6, 1, 1, 8, 1, 39 | 195, 9, 7, 166, 115, 116, 114, 105, 110, 103, 40 | 4, 0, 1, 0, 1, 0, 255, 255, 255, 255 41 | ] 42 | ``` 43 | by using a GraphQL schema where each Field is assigned a 8-bit integer index starting from top level Type definitions and boiling down to each individual type. 44 | Obviously it can be optimised yet 45 | 46 | # Usage ⚗️ 47 | Clone repository and execute 48 | ```shell 49 | yarn && yarn dev 50 | ``` 51 | Then after you're finished 52 | ```shell 53 | yarn test 54 | ``` 55 | Don't forget to force Jest to rerun tests by inputting `a` in the Jest console 56 | 57 | # Limitations 58 | Currently the implementation will break if schema contains a type that has more than 255 fields 59 | 60 | # Support 61 | All contributions are warmly welcome. Please follow issues section or consider these: 62 | - Test coverage compatible to graphql-js 63 | - Documentation 64 | - Ports for other languages 65 | 66 | Please follow the Functional style 67 | -------------------------------------------------------------------------------- /src/query/test.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import { buildSchema, print } from 'graphql' 3 | import compress from 'graphql-query-compress' 4 | import { 5 | BasicDocument, 6 | BasicQuery, 7 | NoArgsDocument, 8 | NoArgsMutation, 9 | NoArgsSubscriptionDocument, 10 | NoArgsSubscriptionSubscription, 11 | WithVariablesDocument, 12 | WithVariablesQuery, 13 | WithVariablesQueryVariables 14 | } from '../fixtures' 15 | import withVariablesVariables from '../fixtures/queryWithVariablesVariables' 16 | import Decoder from './decode' 17 | import Encoder from './encode' 18 | import { EncodedQueryWithHandler, VariablesEncoder } from './types' 19 | 20 | const schemaString = fs.readFileSync('src/fixtures/schema.graphql', 'utf8') 21 | const schema = buildSchema(schemaString) 22 | 23 | const decoder = new Decoder(schema) 24 | const encoder = new Encoder(schema) 25 | 26 | test('decoded query matches encoded', () => { 27 | const result = encoder.encode( 28 | BasicDocument 29 | ) as EncodedQueryWithHandler 30 | expect(decoder.decode(result.query).document).toEqual(BasicDocument) 31 | }) 32 | 33 | test('decoded variables query matches encoded', () => { 34 | const handleVariables = encoder.encode< 35 | WithVariablesQuery, 36 | WithVariablesQueryVariables 37 | >(WithVariablesDocument) as VariablesEncoder< 38 | WithVariablesQuery, 39 | WithVariablesQueryVariables 40 | > 41 | const result = handleVariables(withVariablesVariables) 42 | const decoded = decoder.decode(result.query) 43 | expect(decoded.document).toEqual(WithVariablesDocument) 44 | expect(decoded.variables).toEqual(withVariablesVariables) 45 | }) 46 | 47 | test('decoded mutation matches encoded', () => { 48 | const result = encoder.encode( 49 | NoArgsDocument 50 | ) as EncodedQueryWithHandler 51 | expect(decoder.decode(result.query).document).toEqual(NoArgsDocument) 52 | }) 53 | 54 | test('decoded subscription matches encoded', () => { 55 | const result = encoder.encode( 56 | NoArgsSubscriptionDocument 57 | ) as EncodedQueryWithHandler 58 | expect(decoder.decode(result.query).document).toEqual( 59 | NoArgsSubscriptionDocument 60 | ) 61 | }) 62 | 63 | test('binary representation at least twice smaller than string representation', () => { 64 | const encoded = encoder.encode( 65 | BasicDocument 66 | ) as EncodedQueryWithHandler 67 | const graphql = compress(print(BasicDocument)) 68 | expect(graphql.length / encoded.query.length).toBeGreaterThanOrEqual(0.3) 69 | }) 70 | -------------------------------------------------------------------------------- /src/response/decode.ts: -------------------------------------------------------------------------------- 1 | import { 2 | DocumentNode, 3 | FieldNode, 4 | GraphQLObjectType, 5 | isObjectType, 6 | isScalarType, 7 | Kind, 8 | ListTypeNode, 9 | SelectionNode, 10 | TypeNode 11 | } from 'graphql' 12 | import { ByteIterator, createIterator } from '../iterator' 13 | import Encoder from '../query/encode' 14 | import { END } from '../query/types' 15 | 16 | function decode( 17 | encoder: Encoder, 18 | document: DocumentNode, 19 | data: Uint8Array 20 | ): Result { 21 | const operation = document.definitions[0] 22 | 23 | if ( 24 | document.definitions.length > 1 || 25 | operation.kind !== Kind.OPERATION_DEFINITION 26 | ) 27 | throw new Error('Only single fragmentless operation definition allowed') 28 | 29 | const byteIterator = createIterator(data, END) 30 | const result = decodeVector( 31 | encoder, 32 | encoder.schema.getQueryType(), 33 | operation.selectionSet.selections, 34 | byteIterator 35 | ) 36 | return result as Result 37 | } 38 | 39 | function decodeVector( 40 | encoder: Encoder, 41 | type: GraphQLObjectType, 42 | selections: Readonly, 43 | data: ByteIterator 44 | ) { 45 | const result = {} 46 | for (let index = 0; index < selections.length; index++) { 47 | const element = selections[index] as FieldNode // FIXME support fragments and union spread 48 | const field = type.astNode.fields.find( 49 | (field) => element.name.value === field.name.value 50 | ) 51 | result[element.name.value] = decodeValue(encoder, field.type, element, data) 52 | } 53 | return result 54 | } 55 | 56 | function decodeValue( 57 | encoder: Encoder, 58 | type: TypeNode, 59 | field: FieldNode, 60 | data: ByteIterator 61 | ) { 62 | if (type.kind === Kind.NON_NULL_TYPE) type = type.type 63 | if (type.kind === Kind.NAMED_TYPE) { 64 | const schemaType = encoder.schema.getType(type.name.value) 65 | if (isScalarType(schemaType)) 66 | return encoder.scalarHandlers[schemaType.name].decode(data) 67 | if (isObjectType(schemaType)) 68 | return decodeVector( 69 | encoder, 70 | schemaType, 71 | field.selectionSet.selections, 72 | data 73 | ) 74 | } else return decodeList(encoder, type, field, data) 75 | return null 76 | } 77 | 78 | function decodeList( 79 | encoder: Encoder, 80 | type: ListTypeNode, 81 | field: FieldNode, 82 | data: ByteIterator 83 | ) { 84 | const result = [] 85 | while (!data.atEnd()) 86 | result.push(decodeValue(encoder, type.type, field, data)) 87 | data.take() // FIXME probably wrong 88 | return result 89 | } 90 | 91 | export default decode 92 | -------------------------------------------------------------------------------- /src/response/encode.ts: -------------------------------------------------------------------------------- 1 | import { 2 | DocumentNode, 3 | FieldNode, 4 | GraphQLObjectType, 5 | isObjectType, 6 | isScalarType, 7 | Kind, 8 | ListTypeNode, 9 | SelectionNode, 10 | TypeNode 11 | } from 'graphql' 12 | import mergeArrays from '../mergeArrays' 13 | import Decoder from '../query/decode' 14 | import { END } from '../query/types' 15 | 16 | function encode( 17 | decoder: Decoder, 18 | document: DocumentNode, 19 | data: Response 20 | ): Uint8Array { 21 | const operation = document.definitions[0] 22 | 23 | if ( 24 | document.definitions.length > 1 || 25 | operation.kind !== Kind.OPERATION_DEFINITION 26 | ) 27 | throw new Error('Only single fragmentless operation definition allowed') 28 | 29 | const result = encodeVector( 30 | decoder, 31 | decoder.schema.getQueryType(), 32 | operation.selectionSet.selections, 33 | data 34 | ) 35 | return result 36 | } 37 | 38 | function encodeVector( 39 | decoder: Decoder, 40 | type: GraphQLObjectType, 41 | selections: Readonly, 42 | data: any 43 | ): Uint8Array { 44 | let result = new Uint8Array() 45 | for (let index = 0; index < selections.length; index++) { 46 | const element = selections[index] as FieldNode // FIXME support fragments and union spread 47 | const field = type.astNode.fields.find( 48 | (field) => element.name.value === field.name.value 49 | ) 50 | result = mergeArrays( 51 | result, 52 | encodeValue(decoder, field.type, element, data[element.name.value]) 53 | ) 54 | } 55 | return result 56 | } 57 | 58 | function encodeValue( 59 | decoder: Decoder, 60 | type: TypeNode, 61 | field: FieldNode, 62 | data: any 63 | ): Uint8Array { 64 | if (type.kind === Kind.NON_NULL_TYPE) type = type.type 65 | if (type.kind === Kind.NAMED_TYPE) { 66 | const schemaType = decoder.schema.getType(type.name.value) 67 | if (isScalarType(schemaType)) 68 | return decoder.scalarHandlers[schemaType.name].encode(data) 69 | if (isObjectType(schemaType)) 70 | return encodeVector( 71 | decoder, 72 | schemaType, 73 | field.selectionSet.selections, 74 | data 75 | ) 76 | } else return encodeList(decoder, type, field, data) 77 | return null 78 | } 79 | 80 | function encodeList( 81 | decoder: Decoder, 82 | type: ListTypeNode, 83 | field: FieldNode, 84 | data: any[] 85 | ): Uint8Array { 86 | let result = new Uint8Array() 87 | for (const iterator of data) 88 | result = mergeArrays( 89 | result, 90 | encodeValue(decoder, type.type, field, iterator) 91 | ) 92 | return mergeArrays(result, new Uint8Array([END])) 93 | } 94 | 95 | export default encode 96 | -------------------------------------------------------------------------------- /src/test.ts: -------------------------------------------------------------------------------- 1 | import { addMocksToSchema } from '@graphql-tools/mock' 2 | import { makeExecutableSchema } from '@graphql-tools/schema' 3 | import fs from 'fs' 4 | import { buildSchema, ExecutionResult, graphql, print } from 'graphql' 5 | import { Decoder, Encoder } from '.' 6 | import { 7 | BasicDocument, 8 | BasicQuery, 9 | WithVariablesDocument, 10 | WithVariablesQuery, 11 | WithVariablesQueryVariables 12 | } from './fixtures' 13 | import customScalarHandlers from './fixtures/customScalarHandlers' 14 | import withVariablesVariables from './fixtures/queryWithVariablesVariables' 15 | import { EncodedQueryWithHandler, VariablesEncoder } from './query/types' 16 | 17 | const schemaString = fs.readFileSync('src/fixtures/schema.graphql', 'utf8') 18 | const schema = buildSchema(schemaString) 19 | 20 | const decoder = new Decoder(schema, customScalarHandlers) 21 | const encoder = new Encoder(schema, customScalarHandlers) 22 | 23 | const executableSchema = makeExecutableSchema({ typeDefs: schema }) 24 | const schemaWithMocks = addMocksToSchema({ 25 | schema: executableSchema, 26 | mocks: { 27 | Float: () => 3.5 // FIXME should do something with precision being lost during decoding 28 | } 29 | }) 30 | 31 | test('variables query full pass', async () => { 32 | const handleVariables = encoder.encode< 33 | WithVariablesQuery, 34 | WithVariablesQueryVariables 35 | >(WithVariablesDocument) as VariablesEncoder< 36 | WithVariablesQuery, 37 | WithVariablesQueryVariables 38 | > 39 | const encoded = handleVariables(withVariablesVariables) 40 | const decoded = decoder.decode(encoded.query) 41 | expect(decoded.document).toEqual(WithVariablesDocument) 42 | expect(decoded.variables).toEqual(withVariablesVariables) 43 | 44 | const response: ExecutionResult = await graphql({ 45 | schema: schemaWithMocks, 46 | source: print(WithVariablesDocument) 47 | }) 48 | const encodedResponse = decoded.encodeResponse(response.data) 49 | const decodedResponse = encoded.decodeResponse(encodedResponse) 50 | expect(decodedResponse).toEqual(response.data) 51 | }) 52 | 53 | test('basic query full pass', async () => { 54 | const encoded = encoder.encode( 55 | BasicDocument 56 | ) as EncodedQueryWithHandler 57 | const decoded = decoder.decode(encoded.query) 58 | expect(decoded.document).toEqual(BasicDocument) 59 | const response: ExecutionResult = await graphql({ 60 | schema: schemaWithMocks, 61 | source: print(BasicDocument) 62 | }) 63 | const encodedResponse = decoded.encodeResponse(response.data) 64 | const decodedResponse = encoded.decodeResponse(encodedResponse) 65 | expect(decodedResponse).toEqual(response.data) 66 | }) 67 | -------------------------------------------------------------------------------- /src/query/encode.ts: -------------------------------------------------------------------------------- 1 | import { 2 | DocumentNode, 3 | EnumTypeDefinitionNode, 4 | FieldNode, 5 | GraphQLInputObjectType, 6 | GraphQLObjectType, 7 | GraphQLSchema, 8 | Kind, 9 | ListTypeNode, 10 | OperationDefinitionNode, 11 | SelectionSetNode, 12 | TypeNode, 13 | VariableDefinitionNode 14 | } from 'graphql' 15 | import mergeArrays from '../mergeArrays' 16 | import { decodeResponse } from '../response' 17 | import defaultScalarHandlers, { ScalarHandlers } from '../scalarHandlers' 18 | import extractTargetType from './extractTargetType' 19 | import { 20 | // VariablesEncoder, 21 | // EncodedQueryWithHandler, 22 | EncodeResult, 23 | END, 24 | Flags, 25 | Operation 26 | } from './types' 27 | 28 | class Encoder { 29 | readonly schema: GraphQLSchema 30 | readonly scalarHandlers: ScalarHandlers 31 | 32 | constructor(schema: GraphQLSchema, customScalarHandlers?: ScalarHandlers) { 33 | this.schema = schema 34 | this.scalarHandlers = { ...defaultScalarHandlers, ...customScalarHandlers } 35 | } 36 | 37 | // FIXME not sure if making a default type argument value is a good idea 38 | encode( 39 | query: DocumentNode 40 | ): EncodeResult { 41 | let result: Array = [] 42 | const { definitions } = this.prepareDocument(query) 43 | const { operation, variableDefinitions, selectionSet, name } = 44 | definitions[0] as OperationDefinitionNode 45 | 46 | let configBitmask = Operation[operation] 47 | 48 | if (name.value) { 49 | configBitmask |= Flags.Name 50 | result = Array.from(this.scalarHandlers.String.encode(name.value)) 51 | } 52 | 53 | encodeQueryVector( 54 | this.schema, 55 | result, 56 | this.getOperationType(operation), 57 | selectionSet 58 | ) 59 | 60 | if (variableDefinitions) { 61 | configBitmask |= Flags.Variables 62 | result.unshift(configBitmask) 63 | 64 | return (variables: Variables) => ({ 65 | query: mergeArrays( 66 | new Uint8Array(result), 67 | encodeVariables(this, variableDefinitions, variables) 68 | ), 69 | decodeResponse: (data) => decodeResponse(this, query, data) 70 | }) 71 | } else { 72 | result.unshift(configBitmask) 73 | 74 | return { 75 | query: new Uint8Array(result), 76 | decodeResponse: (data) => decodeResponse(this, query, data) 77 | } 78 | } 79 | } 80 | 81 | private prepareDocument(query: DocumentNode) { 82 | return query 83 | } 84 | 85 | private getOperationType(operation: string): GraphQLObjectType { 86 | if (operation === 'query') 87 | return this.schema.getQueryType() as GraphQLObjectType 88 | else if (operation === 'mutation') 89 | return this.schema.getMutationType() as GraphQLObjectType 90 | else if (operation === 'subscription') 91 | return this.schema.getMutationType() as GraphQLObjectType 92 | else throw new Error(`Unsupported operation type ${operation}`) 93 | } 94 | } 95 | 96 | function encodeQueryVector( 97 | schema: GraphQLSchema, 98 | result: Array, 99 | type: GraphQLObjectType, 100 | selectionSet: SelectionSetNode 101 | ): Array { 102 | const fieldsArray = type.astNode?.fields || [] 103 | for (let index = 0; index < selectionSet.selections.length; index++) { 104 | const selection = selectionSet.selections[index] as FieldNode 105 | const fieldIndex = fieldsArray.findIndex( 106 | ({ name }) => name.value === selection.name.value 107 | ) 108 | result.push(fieldIndex) 109 | 110 | if (selection.arguments) 111 | for (let index = 0; index < selection.arguments.length; index++) 112 | result.push(fieldIndex + index + 1) 113 | 114 | if (selection.selectionSet) { 115 | const typeName = extractTargetType(fieldsArray[fieldIndex].type) 116 | const type = schema.getType(typeName) as GraphQLObjectType 117 | encodeQueryVector(schema, result, type, selection.selectionSet) 118 | } 119 | } 120 | result.push(END) 121 | return result 122 | } 123 | 124 | function encodeVariables( 125 | encoder: Encoder, 126 | variableDefinitions: Readonly>, 127 | data: any 128 | ): Uint8Array { 129 | let result: Uint8Array = new Uint8Array() 130 | for (let index = 0; index < variableDefinitions.length; index++) { 131 | const { type, variable } = variableDefinitions[index] 132 | if (!data.hasOwnProperty(variable.name.value)) 133 | throw new Error(`Variable ${variable.name.value} was not provided`) 134 | result = mergeArrays( 135 | result, 136 | encodeValue(encoder, type, data[variable.name.value]) 137 | ) 138 | } 139 | 140 | return result 141 | } 142 | 143 | function encodeValue(encoder: Encoder, type: TypeNode, data: any): Uint8Array { 144 | let result = new Uint8Array([]) 145 | 146 | if (type.kind === Kind.NON_NULL_TYPE) type = type.type 147 | if (type.kind === Kind.NAMED_TYPE) { 148 | const definition = encoder.schema.getType(type.name.value) 149 | if (!definition) throw new Error(`Unknown type ${type.name.value}`) 150 | 151 | const kind = definition.astNode?.kind 152 | 153 | if (!kind && encoder.scalarHandlers[definition.name]) 154 | result = encoder.scalarHandlers[definition.name].encode(data) 155 | else if ( 156 | kind === Kind.SCALAR_TYPE_DEFINITION && 157 | encoder.scalarHandlers[kind] 158 | ) 159 | result = encoder.scalarHandlers[kind].encode(data) 160 | else if (kind === Kind.ENUM_TYPE_DEFINITION) { 161 | const index = ( 162 | definition.astNode as EnumTypeDefinitionNode 163 | ).values?.findIndex(({ name }) => name.value === data) 164 | if (index === -1 || index === undefined) 165 | throw new Error(`Unknown Enum value ${data} for ${type.name.value}`) 166 | result = new Uint8Array([index]) 167 | } else if (kind === Kind.INPUT_OBJECT_TYPE_DEFINITION) { 168 | result = encodeVector(encoder, definition as GraphQLInputObjectType, data) 169 | } else 170 | throw new Error( 171 | `Unknown or non-input type ${type.name.value} in a variable` 172 | ) 173 | } else result = encodeList(encoder, type, data) 174 | return result 175 | } 176 | 177 | function encodeVector( 178 | encoder: Encoder, 179 | type: GraphQLInputObjectType, 180 | data: { 181 | [key: string]: any 182 | } 183 | ): Uint8Array { 184 | let result = new Uint8Array() 185 | const fields = type.astNode?.fields 186 | if (fields) 187 | for (const key in data) { 188 | const index = fields.findIndex(({ name }) => name.value === key) 189 | if (index === -1) 190 | throw new Error(`No field with name ${key} found in ${type.name}`) 191 | const fieldType = fields[index] 192 | result = mergeArrays( 193 | result, 194 | new Uint8Array([index]), 195 | encodeValue(encoder, fieldType.type, data[key]) 196 | ) 197 | } 198 | result = mergeArrays(result, new Uint8Array([END])) 199 | return result 200 | } 201 | 202 | function encodeList( 203 | encoder: Encoder, 204 | type: ListTypeNode, 205 | data: Array 206 | ): Uint8Array { 207 | let result = new Uint8Array([]) 208 | for (let index = 0; index < data.length; index++) 209 | result = mergeArrays(result, encodeValue(encoder, type.type, data[index])) 210 | return mergeArrays(result, new Uint8Array([END])) 211 | } 212 | 213 | export default Encoder 214 | -------------------------------------------------------------------------------- /src/query/decode.ts: -------------------------------------------------------------------------------- 1 | import { 2 | GraphQLEnumType, 3 | GraphQLObjectType, 4 | GraphQLSchema, 5 | Kind 6 | } from 'graphql' 7 | import { 8 | DocumentNode, 9 | ListTypeNode, 10 | NameNode, 11 | OperationTypeNode, 12 | TypeNode, 13 | VariableDefinitionNode 14 | } from 'graphql/language/ast' 15 | import { ByteIterator, createIterator } from '../iterator' 16 | import { encodeResponse } from '../response' 17 | import defaultScalarHandlers, { ScalarHandlers } from '../scalarHandlers' 18 | import { documentDecoder } from './documentDecoder' 19 | import extractTargetType from './extractTargetType' 20 | import jsonDecoder from './jsonDecoder' 21 | import { 22 | ASCII_OFFSET, 23 | DataDecoder, 24 | DecodeResult, 25 | END, 26 | Flags, 27 | Operation, 28 | QueryDecoder, 29 | VariablesHandler 30 | } from './types' 31 | 32 | class Decoder { 33 | readonly schema: GraphQLSchema 34 | readonly queryDecoder: QueryDecoder 35 | readonly dataDecoder: DataDecoder 36 | readonly scalarHandlers: ScalarHandlers 37 | readonly topLevelTypes = { 38 | // FIXME 39 | query: 'getQueryType', 40 | mutation: 'getMutationType', 41 | subscription: 'getSubscriptionType' 42 | } 43 | 44 | constructor(schema: GraphQLSchema, customScalarHandlers?: ScalarHandlers) { 45 | this.schema = schema 46 | this.queryDecoder = documentDecoder 47 | this.dataDecoder = jsonDecoder 48 | this.scalarHandlers = { ...defaultScalarHandlers, ...customScalarHandlers } 49 | } 50 | 51 | decode(data: Uint8Array): DecodeResult { 52 | const iterator = createIterator(data, END) 53 | 54 | const configBitmask = iterator.take() 55 | // FIXME this should be done more elegantly 56 | const operation = ( // Order is important 57 | (configBitmask & Operation.mutation) === Operation.mutation 58 | ? 'mutation' 59 | : (configBitmask & Operation.subscription) === Operation.subscription 60 | ? 'subscription' 61 | : (configBitmask & Operation.query) === Operation.query 62 | ? 'query' 63 | : null 64 | ) as OperationTypeNode 65 | const name = 66 | (configBitmask & Flags.Name) === Flags.Name 67 | ? ({ 68 | kind: 'Name', 69 | value: this.scalarHandlers.String.decode(iterator) 70 | } as NameNode) 71 | : undefined 72 | 73 | const { selectionSet, variableDefinitions } = decodeQuery( 74 | this, 75 | this.schema[this.topLevelTypes[operation]]() as GraphQLObjectType, 76 | iterator, 77 | this.queryDecoder.variables() 78 | ) 79 | 80 | const hasVariables = variableDefinitions.length 81 | 82 | const document: DocumentNode = { 83 | kind: Kind.DOCUMENT, 84 | definitions: [ 85 | { 86 | name, 87 | kind: Kind.OPERATION_DEFINITION, 88 | operation: operation, 89 | selectionSet: selectionSet, 90 | ...(hasVariables && { variableDefinitions }) 91 | } 92 | ] 93 | } 94 | 95 | return { 96 | encodeResponse: (response: Response) => 97 | encodeResponse(this, document, response), 98 | document, 99 | variables: hasVariables 100 | ? decodeVariables(this, variableDefinitions, iterator) 101 | : null 102 | } 103 | } 104 | } 105 | 106 | function decodeQuery( 107 | decoder: Decoder, 108 | type: GraphQLObjectType, 109 | data: ByteIterator, 110 | variablesHandler: VariablesHandler 111 | ) { 112 | const vector = decoder.queryDecoder.vector() 113 | const fields = type.astNode?.fields 114 | if (fields) 115 | // FIXME should be invariant 116 | while (!data.atEnd()) { 117 | const index = data.take() 118 | const field = fields[index] 119 | const callbacks = vector.accumulate(field.name.value) 120 | 121 | // Arguments 122 | if (field.arguments && field.arguments.length > 0) 123 | while (!data.atEnd()) { 124 | const arg = field.arguments[data.current() - index - 1] 125 | if (arg) { 126 | const variableName = String.fromCharCode( 127 | variablesHandler.commit().length + ASCII_OFFSET 128 | ) 129 | callbacks.addArg(arg.name.value, variableName) 130 | data.take() 131 | // FIXME direct callback with type definition breaks abstraction gap 132 | variablesHandler.accumulate(variableName, cleanLocations(arg.type)) 133 | } else break 134 | } 135 | 136 | const typeName = extractTargetType(field.type) 137 | const fieldType = decoder.schema.getType(typeName) 138 | if ((fieldType as GraphQLObjectType).getFields) { 139 | const children = decodeQuery( 140 | decoder, 141 | fieldType as GraphQLObjectType, 142 | data, 143 | variablesHandler 144 | ) 145 | // FIXME abstraction should work without selectionSet subkey 146 | callbacks.addValue(children.selectionSet) 147 | } 148 | callbacks.commit() 149 | } 150 | data.take() 151 | return { 152 | selectionSet: vector.commit(), 153 | variableDefinitions: variablesHandler.commit() 154 | } 155 | } 156 | 157 | // FIXME this is wrong 158 | function cleanLocations({ loc, ...obj }: TypeNode): TypeNode { 159 | if (obj.kind === Kind.NAMED_TYPE) { 160 | const { loc, ...name } = obj.name 161 | obj.name = name 162 | } 163 | return obj 164 | } 165 | 166 | function decodeVariables( 167 | decoder: Decoder, 168 | dictionary: Array, 169 | data: ByteIterator 170 | ) { 171 | if (data.current() === undefined) 172 | throw new Error('Expected variables data for query') 173 | const vector = decoder.dataDecoder.vector() 174 | for (let index = 0; index < dictionary.length; index++) { 175 | const { type, variable } = dictionary[index] 176 | const { addValue } = vector.accumulate(variable.name.value) 177 | addValue(decodeValue(decoder, type, data)) 178 | } 179 | return vector.commit() 180 | } 181 | 182 | function decodeValue(decoder: Decoder, type: TypeNode, data: ByteIterator) { 183 | if (type.kind === Kind.NON_NULL_TYPE) type = type.type 184 | if (type.kind === Kind.NAMED_TYPE) { 185 | const definition = decoder.schema.getType(type.name.value) 186 | return (definition as GraphQLObjectType).getFields 187 | ? decodeVector(decoder, definition as GraphQLObjectType, data) 188 | : (definition as GraphQLEnumType).getValues 189 | ? (definition as GraphQLEnumType).getValues()[data.take()].value 190 | : decoder.scalarHandlers[type.name.value].decode(data) 191 | } else return decodeList(decoder, type, data) 192 | } 193 | 194 | function decodeList( 195 | decoder: Decoder, 196 | type: ListTypeNode, 197 | data: ByteIterator 198 | ): T { 199 | const list = decoder.dataDecoder.list() 200 | while (!data.atEnd()) list.accumulate(decodeValue(decoder, type.type, data)) 201 | data.take() 202 | return list.commit() 203 | } 204 | 205 | function decodeVector( 206 | decoder: Decoder, 207 | type: GraphQLObjectType, 208 | data: ByteIterator 209 | ): T { 210 | const vector = decoder.dataDecoder.vector() 211 | const fields = type.astNode?.fields 212 | if (fields) 213 | // FIXME should be invariant 214 | while (!data.atEnd()) { 215 | const field = fields[data.take()] 216 | const { addValue } = vector.accumulate(field.name.value) 217 | addValue(decodeValue(decoder, field.type, data)) 218 | } 219 | data.take() 220 | return vector.commit() 221 | } 222 | 223 | export default Decoder 224 | -------------------------------------------------------------------------------- /src/fixtures/index.ts: -------------------------------------------------------------------------------- 1 | import { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core' 2 | export type Maybe = T | null 3 | export type InputMaybe = Maybe 4 | export type Exact = { 5 | [K in keyof T]: T[K] 6 | } 7 | export type MakeOptional = Omit & { 8 | [SubKey in K]?: Maybe 9 | } 10 | export type MakeMaybe = Omit & { 11 | [SubKey in K]: Maybe 12 | } 13 | /** All built-in and custom scalars, mapped to their actual values */ 14 | export type Scalars = { 15 | ID: string 16 | String: string 17 | Boolean: boolean 18 | Int: number 19 | Float: number 20 | UDateS: any 21 | } 22 | 23 | export enum Enumerable { 24 | First = 'FIRST', 25 | Second = 'SECOND', 26 | Third = 'THIRD' 27 | } 28 | 29 | export type InputMap = { 30 | inputListMap?: InputMaybe>> 31 | inputListScalar?: InputMaybe>> 32 | inputMap?: InputMaybe 33 | int?: InputMaybe 34 | } 35 | 36 | export type Map = { 37 | __typename?: 'Map' 38 | id?: Maybe 39 | map?: Maybe 40 | maps?: Maybe>> 41 | } 42 | 43 | export type Mutation = { 44 | __typename?: 'Mutation' 45 | noArgs?: Maybe 46 | } 47 | 48 | export type Query = { 49 | __typename?: 'Query' 50 | boolean?: Maybe 51 | date?: Maybe 52 | float?: Maybe 53 | int?: Maybe 54 | map?: Maybe 55 | mapArray?: Maybe>> 56 | scalarArray?: Maybe>> 57 | string?: Maybe 58 | withArgs?: Maybe 59 | } 60 | 61 | export type QueryWithArgsArgs = { 62 | boolean?: InputMaybe 63 | enum?: InputMaybe 64 | float?: InputMaybe 65 | inputMap?: InputMaybe 66 | int?: InputMaybe 67 | string?: InputMaybe 68 | } 69 | 70 | export type Subscription = { 71 | __typename?: 'Subscription' 72 | noArgs?: Maybe 73 | } 74 | 75 | export type BasicQueryVariables = Exact<{ [key: string]: never }> 76 | 77 | export type BasicQuery = { 78 | __typename?: 'Query' 79 | int?: number | null | undefined 80 | float?: number | null | undefined 81 | boolean?: boolean | null | undefined 82 | string?: string | null | undefined 83 | withArgs?: string | null | undefined 84 | scalarArray?: Array | null | undefined 85 | mapArray?: 86 | | Array< 87 | | { 88 | __typename?: 'Map' 89 | id?: string | null | undefined 90 | map?: 91 | | { 92 | __typename?: 'Map' 93 | id?: string | null | undefined 94 | map?: 95 | | { __typename?: 'Map'; id?: string | null | undefined } 96 | | null 97 | | undefined 98 | } 99 | | null 100 | | undefined 101 | } 102 | | null 103 | | undefined 104 | > 105 | | null 106 | | undefined 107 | map?: 108 | | { 109 | __typename?: 'Map' 110 | id?: string | null | undefined 111 | map?: 112 | | { 113 | __typename?: 'Map' 114 | id?: string | null | undefined 115 | map?: 116 | | { __typename?: 'Map'; id?: string | null | undefined } 117 | | null 118 | | undefined 119 | } 120 | | null 121 | | undefined 122 | } 123 | | null 124 | | undefined 125 | } 126 | 127 | export type WithArgsQueryVariables = Exact<{ [key: string]: never }> 128 | 129 | export type WithArgsQuery = { 130 | __typename?: 'Query' 131 | int?: number | null | undefined 132 | float?: number | null | undefined 133 | boolean?: boolean | null | undefined 134 | string?: string | null | undefined 135 | withArgs?: string | null | undefined 136 | scalarArray?: Array | null | undefined 137 | mapArray?: 138 | | Array< 139 | | { 140 | __typename?: 'Map' 141 | id?: string | null | undefined 142 | map?: 143 | | { 144 | __typename?: 'Map' 145 | id?: string | null | undefined 146 | maps?: 147 | | Array< 148 | | { __typename?: 'Map'; id?: string | null | undefined } 149 | | null 150 | | undefined 151 | > 152 | | null 153 | | undefined 154 | } 155 | | null 156 | | undefined 157 | } 158 | | null 159 | | undefined 160 | > 161 | | null 162 | | undefined 163 | map?: 164 | | { 165 | __typename?: 'Map' 166 | id?: string | null | undefined 167 | map?: 168 | | { 169 | __typename?: 'Map' 170 | id?: string | null | undefined 171 | map?: 172 | | { __typename?: 'Map'; id?: string | null | undefined } 173 | | null 174 | | undefined 175 | } 176 | | null 177 | | undefined 178 | } 179 | | null 180 | | undefined 181 | } 182 | 183 | export type CustomScalarQueryQueryVariables = Exact<{ [key: string]: never }> 184 | 185 | export type CustomScalarQueryQuery = { 186 | __typename?: 'Query' 187 | date?: any | null | undefined 188 | } 189 | 190 | export type NoArgsMutationVariables = Exact<{ [key: string]: never }> 191 | 192 | export type NoArgsMutation = { 193 | __typename?: 'Mutation' 194 | noArgs?: string | null | undefined 195 | } 196 | 197 | export type WithVariablesQueryVariables = Exact<{ 198 | A?: Maybe 199 | B?: Maybe 200 | C?: Maybe 201 | D?: Maybe 202 | E?: Maybe 203 | F?: Maybe 204 | }> 205 | 206 | export type WithVariablesQuery = { 207 | __typename?: 'Query' 208 | withArgs?: string | null | undefined 209 | } 210 | 211 | export type NoArgsSubscriptionSubscriptionVariables = Exact<{ 212 | [key: string]: never 213 | }> 214 | 215 | export type NoArgsSubscriptionSubscription = { 216 | __typename?: 'Subscription' 217 | noArgs?: string | null | undefined 218 | } 219 | 220 | export const BasicDocument = { 221 | kind: 'Document', 222 | definitions: [ 223 | { 224 | kind: 'OperationDefinition', 225 | operation: 'query', 226 | name: { kind: 'Name', value: 'Basic' }, 227 | selectionSet: { 228 | kind: 'SelectionSet', 229 | selections: [ 230 | { kind: 'Field', name: { kind: 'Name', value: 'int' } }, 231 | { kind: 'Field', name: { kind: 'Name', value: 'float' } }, 232 | { kind: 'Field', name: { kind: 'Name', value: 'boolean' } }, 233 | { kind: 'Field', name: { kind: 'Name', value: 'string' } }, 234 | { kind: 'Field', name: { kind: 'Name', value: 'withArgs' } }, 235 | { kind: 'Field', name: { kind: 'Name', value: 'scalarArray' } }, 236 | { 237 | kind: 'Field', 238 | name: { kind: 'Name', value: 'mapArray' }, 239 | selectionSet: { 240 | kind: 'SelectionSet', 241 | selections: [ 242 | { kind: 'Field', name: { kind: 'Name', value: 'id' } }, 243 | { 244 | kind: 'Field', 245 | name: { kind: 'Name', value: 'map' }, 246 | selectionSet: { 247 | kind: 'SelectionSet', 248 | selections: [ 249 | { kind: 'Field', name: { kind: 'Name', value: 'id' } }, 250 | { 251 | kind: 'Field', 252 | name: { kind: 'Name', value: 'map' }, 253 | selectionSet: { 254 | kind: 'SelectionSet', 255 | selections: [ 256 | { 257 | kind: 'Field', 258 | name: { kind: 'Name', value: 'id' } 259 | } 260 | ] 261 | } 262 | } 263 | ] 264 | } 265 | } 266 | ] 267 | } 268 | }, 269 | { 270 | kind: 'Field', 271 | name: { kind: 'Name', value: 'map' }, 272 | selectionSet: { 273 | kind: 'SelectionSet', 274 | selections: [ 275 | { kind: 'Field', name: { kind: 'Name', value: 'id' } }, 276 | { 277 | kind: 'Field', 278 | name: { kind: 'Name', value: 'map' }, 279 | selectionSet: { 280 | kind: 'SelectionSet', 281 | selections: [ 282 | { kind: 'Field', name: { kind: 'Name', value: 'id' } }, 283 | { 284 | kind: 'Field', 285 | name: { kind: 'Name', value: 'map' }, 286 | selectionSet: { 287 | kind: 'SelectionSet', 288 | selections: [ 289 | { 290 | kind: 'Field', 291 | name: { kind: 'Name', value: 'id' } 292 | } 293 | ] 294 | } 295 | } 296 | ] 297 | } 298 | } 299 | ] 300 | } 301 | } 302 | ] 303 | } 304 | } 305 | ] 306 | } as unknown as DocumentNode 307 | export const WithArgsDocument = { 308 | kind: 'Document', 309 | definitions: [ 310 | { 311 | kind: 'OperationDefinition', 312 | operation: 'query', 313 | name: { kind: 'Name', value: 'WithArgs' }, 314 | selectionSet: { 315 | kind: 'SelectionSet', 316 | selections: [ 317 | { kind: 'Field', name: { kind: 'Name', value: 'int' } }, 318 | { kind: 'Field', name: { kind: 'Name', value: 'float' } }, 319 | { kind: 'Field', name: { kind: 'Name', value: 'boolean' } }, 320 | { kind: 'Field', name: { kind: 'Name', value: 'string' } }, 321 | { 322 | kind: 'Field', 323 | name: { kind: 'Name', value: 'withArgs' }, 324 | arguments: [ 325 | { 326 | kind: 'Argument', 327 | name: { kind: 'Name', value: 'int' }, 328 | value: { kind: 'IntValue', value: '1' } 329 | }, 330 | { 331 | kind: 'Argument', 332 | name: { kind: 'Name', value: 'float' }, 333 | value: { kind: 'FloatValue', value: '1.5' } 334 | }, 335 | { 336 | kind: 'Argument', 337 | name: { kind: 'Name', value: 'boolean' }, 338 | value: { kind: 'BooleanValue', value: true } 339 | }, 340 | { 341 | kind: 'Argument', 342 | name: { kind: 'Name', value: 'string' }, 343 | value: { kind: 'StringValue', value: 'string', block: false } 344 | }, 345 | { 346 | kind: 'Argument', 347 | name: { kind: 'Name', value: 'enum' }, 348 | value: { kind: 'EnumValue', value: 'FIRST' } 349 | } 350 | ] 351 | }, 352 | { kind: 'Field', name: { kind: 'Name', value: 'scalarArray' } }, 353 | { 354 | kind: 'Field', 355 | name: { kind: 'Name', value: 'mapArray' }, 356 | selectionSet: { 357 | kind: 'SelectionSet', 358 | selections: [ 359 | { kind: 'Field', name: { kind: 'Name', value: 'id' } }, 360 | { 361 | kind: 'Field', 362 | name: { kind: 'Name', value: 'map' }, 363 | selectionSet: { 364 | kind: 'SelectionSet', 365 | selections: [ 366 | { kind: 'Field', name: { kind: 'Name', value: 'id' } }, 367 | { 368 | kind: 'Field', 369 | name: { kind: 'Name', value: 'maps' }, 370 | selectionSet: { 371 | kind: 'SelectionSet', 372 | selections: [ 373 | { 374 | kind: 'Field', 375 | name: { kind: 'Name', value: 'id' } 376 | } 377 | ] 378 | } 379 | } 380 | ] 381 | } 382 | } 383 | ] 384 | } 385 | }, 386 | { 387 | kind: 'Field', 388 | name: { kind: 'Name', value: 'map' }, 389 | selectionSet: { 390 | kind: 'SelectionSet', 391 | selections: [ 392 | { kind: 'Field', name: { kind: 'Name', value: 'id' } }, 393 | { 394 | kind: 'Field', 395 | name: { kind: 'Name', value: 'map' }, 396 | selectionSet: { 397 | kind: 'SelectionSet', 398 | selections: [ 399 | { kind: 'Field', name: { kind: 'Name', value: 'id' } }, 400 | { 401 | kind: 'Field', 402 | name: { kind: 'Name', value: 'map' }, 403 | selectionSet: { 404 | kind: 'SelectionSet', 405 | selections: [ 406 | { 407 | kind: 'Field', 408 | name: { kind: 'Name', value: 'id' } 409 | } 410 | ] 411 | } 412 | } 413 | ] 414 | } 415 | } 416 | ] 417 | } 418 | } 419 | ] 420 | } 421 | } 422 | ] 423 | } as unknown as DocumentNode 424 | export const CustomScalarQueryDocument = { 425 | kind: 'Document', 426 | definitions: [ 427 | { 428 | kind: 'OperationDefinition', 429 | operation: 'query', 430 | name: { kind: 'Name', value: 'customScalarQuery' }, 431 | selectionSet: { 432 | kind: 'SelectionSet', 433 | selections: [{ kind: 'Field', name: { kind: 'Name', value: 'date' } }] 434 | } 435 | } 436 | ] 437 | } as unknown as DocumentNode< 438 | CustomScalarQueryQuery, 439 | CustomScalarQueryQueryVariables 440 | > 441 | export const NoArgsDocument = { 442 | kind: 'Document', 443 | definitions: [ 444 | { 445 | kind: 'OperationDefinition', 446 | operation: 'mutation', 447 | name: { kind: 'Name', value: 'NoArgs' }, 448 | selectionSet: { 449 | kind: 'SelectionSet', 450 | selections: [{ kind: 'Field', name: { kind: 'Name', value: 'noArgs' } }] 451 | } 452 | } 453 | ] 454 | } as unknown as DocumentNode 455 | export const WithVariablesDocument = { 456 | kind: 'Document', 457 | definitions: [ 458 | { 459 | kind: 'OperationDefinition', 460 | operation: 'query', 461 | name: { kind: 'Name', value: 'WithVariables' }, 462 | variableDefinitions: [ 463 | { 464 | kind: 'VariableDefinition', 465 | variable: { kind: 'Variable', name: { kind: 'Name', value: 'A' } }, 466 | type: { kind: 'NamedType', name: { kind: 'Name', value: 'Int' } } 467 | }, 468 | { 469 | kind: 'VariableDefinition', 470 | variable: { kind: 'Variable', name: { kind: 'Name', value: 'B' } }, 471 | type: { kind: 'NamedType', name: { kind: 'Name', value: 'Float' } } 472 | }, 473 | { 474 | kind: 'VariableDefinition', 475 | variable: { kind: 'Variable', name: { kind: 'Name', value: 'C' } }, 476 | type: { kind: 'NamedType', name: { kind: 'Name', value: 'Boolean' } } 477 | }, 478 | { 479 | kind: 'VariableDefinition', 480 | variable: { kind: 'Variable', name: { kind: 'Name', value: 'D' } }, 481 | type: { kind: 'NamedType', name: { kind: 'Name', value: 'String' } } 482 | }, 483 | { 484 | kind: 'VariableDefinition', 485 | variable: { kind: 'Variable', name: { kind: 'Name', value: 'E' } }, 486 | type: { 487 | kind: 'NamedType', 488 | name: { kind: 'Name', value: 'Enumerable' } 489 | } 490 | }, 491 | { 492 | kind: 'VariableDefinition', 493 | variable: { kind: 'Variable', name: { kind: 'Name', value: 'F' } }, 494 | type: { kind: 'NamedType', name: { kind: 'Name', value: 'InputMap' } } 495 | } 496 | ], 497 | selectionSet: { 498 | kind: 'SelectionSet', 499 | selections: [ 500 | { 501 | kind: 'Field', 502 | name: { kind: 'Name', value: 'withArgs' }, 503 | arguments: [ 504 | { 505 | kind: 'Argument', 506 | name: { kind: 'Name', value: 'int' }, 507 | value: { kind: 'Variable', name: { kind: 'Name', value: 'A' } } 508 | }, 509 | { 510 | kind: 'Argument', 511 | name: { kind: 'Name', value: 'float' }, 512 | value: { kind: 'Variable', name: { kind: 'Name', value: 'B' } } 513 | }, 514 | { 515 | kind: 'Argument', 516 | name: { kind: 'Name', value: 'boolean' }, 517 | value: { kind: 'Variable', name: { kind: 'Name', value: 'C' } } 518 | }, 519 | { 520 | kind: 'Argument', 521 | name: { kind: 'Name', value: 'string' }, 522 | value: { kind: 'Variable', name: { kind: 'Name', value: 'D' } } 523 | }, 524 | { 525 | kind: 'Argument', 526 | name: { kind: 'Name', value: 'enum' }, 527 | value: { kind: 'Variable', name: { kind: 'Name', value: 'E' } } 528 | }, 529 | { 530 | kind: 'Argument', 531 | name: { kind: 'Name', value: 'inputMap' }, 532 | value: { kind: 'Variable', name: { kind: 'Name', value: 'F' } } 533 | } 534 | ] 535 | } 536 | ] 537 | } 538 | } 539 | ] 540 | } as unknown as DocumentNode 541 | export const NoArgsSubscriptionDocument = { 542 | kind: 'Document', 543 | definitions: [ 544 | { 545 | kind: 'OperationDefinition', 546 | operation: 'subscription', 547 | name: { kind: 'Name', value: 'NoArgsSubscription' }, 548 | selectionSet: { 549 | kind: 'SelectionSet', 550 | selections: [{ kind: 'Field', name: { kind: 'Name', value: 'noArgs' } }] 551 | } 552 | } 553 | ] 554 | } as unknown as DocumentNode< 555 | NoArgsSubscriptionSubscription, 556 | NoArgsSubscriptionSubscriptionVariables 557 | > 558 | --------------------------------------------------------------------------------