Skip to content

Commit

Permalink
feat: add custom json serializer support
Browse files Browse the repository at this point in the history
  • Loading branch information
mkazlauskas committed Feb 9, 2022
1 parent cbfa1fb commit 05910a8
Show file tree
Hide file tree
Showing 8 changed files with 131 additions and 20 deletions.
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
# graphql-request-configurable-serializer

Fork of `graphql-request`, adding a `jsonSerializer` option.

# graphql-request

Minimal GraphQL client supporting Node and browsers for scripts or simple apps
Expand Down
12 changes: 8 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "graphql-request",
"version": "0.0.0-dripip",
"name": "graphql-request-configurable-serializer",
"version": "4.0.0",
"main": "dist/index.js",
"files": [
"dist"
Expand All @@ -13,7 +13,7 @@
],
"repository": {
"type": "git",
"url": "https://github.com/prisma/graphql-request.git"
"url": "https://github.com/input-output-hk/graphql-request.git"
},
"keywords": [
"graphql",
Expand All @@ -23,9 +23,13 @@
"apollo"
],
"author": "Prisma Labs Team",
"contributors": [
"Martynas Kazlauskas <martynas.kazlauskas@iohk.io>",
"Rhys Bartels-Waller <rhys.bartelswaller@iohk.io>"
],
"license": "MIT",
"bugs": {
"url": "https://github.com/prisma/graphql-request/issues"
"url": "https://github.com/input-output-hk/graphql-request/issues"
},
"homepage": "https://github.com/prisma/graphql-request",
"scripts": {
Expand Down
12 changes: 7 additions & 5 deletions src/createRequestBody.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { isExtractableFile, extractFiles, ExtractableFile } from 'extract-files'
import FormDataNode from 'form-data'
import { defaultJsonSerializer } from './defaultJsonSerializer'

import { Variables } from './types'

Expand All @@ -19,13 +20,14 @@ const isExtractableFileEnhanced = (value: any): value is ExtractableFile | { pip
export default function createRequestBody(
query: string | string[],
variables?: Variables | Variables[],
operationName?: string
operationName?: string,
jsonSerializer = defaultJsonSerializer
): string | FormData {
const { clone, files } = extractFiles({ query, variables, operationName }, '', isExtractableFileEnhanced)

if (files.size === 0) {
if (!Array.isArray(query)) {
return JSON.stringify(clone)
return jsonSerializer.stringify(clone)
}

if (typeof variables !== 'undefined' && !Array.isArray(variables)) {
Expand All @@ -41,21 +43,21 @@ export default function createRequestBody(
[]
)

return JSON.stringify(payload)
return jsonSerializer.stringify(payload)
}

const Form = typeof FormData === 'undefined' ? FormDataNode : FormData

const form = new Form()

form.append('operations', JSON.stringify(clone))
form.append('operations', jsonSerializer.stringify(clone))

const map: { [key: number]: string[] } = {}
let i = 0
files.forEach((paths) => {
map[++i] = paths
})
form.append('map', JSON.stringify(map))
form.append('map', jsonSerializer.stringify(map))

i = 0
files.forEach((paths, file) => {
Expand Down
6 changes: 6 additions & 0 deletions src/defaultJsonSerializer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { JsonSerializer } from "./types.dom";

export const defaultJsonSerializer: JsonSerializer = {
parse: JSON.parse,
stringify: JSON.stringify
}
22 changes: 12 additions & 10 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import crossFetch, * as CrossFetch from 'cross-fetch'
import { OperationDefinitionNode } from 'graphql/language/ast'
import { print } from 'graphql/language/printer'
import createRequestBody from './createRequestBody'
import { defaultJsonSerializer } from './defaultJsonSerializer'
import {
parseBatchRequestArgs,
parseRawRequestArgs,
Expand Down Expand Up @@ -69,8 +70,8 @@ const resolveHeaders = (headers: Dom.RequestInit['headers']): Record<string, str
const queryCleanner = (str: string): string => str.replace(/([\s,]|#[^\n\r]+)+/g, ' ').trim()

type TBuildGetQueryParams<V> =
| { query: string; variables: V | undefined; operationName: string | undefined }
| { query: string[]; variables: V[] | undefined; operationName: undefined }
| { query: string; variables: V | undefined; operationName: string | undefined; jsonSerializer: Dom.JsonSerializer }
| { query: string[]; variables: V[] | undefined; operationName: undefined; jsonSerializer: Dom.JsonSerializer }

/**
* Create query string for GraphQL request
Expand All @@ -81,12 +82,12 @@ type TBuildGetQueryParams<V> =
* @param {string|undefined} param0.operationName the GraphQL operation name
* @param {any|any[]} param0.variables the GraphQL variables to use
*/
const buildGetQueryParams = <V>({ query, variables, operationName }: TBuildGetQueryParams<V>): string => {
const buildGetQueryParams = <V>({ query, variables, operationName, jsonSerializer }: TBuildGetQueryParams<V>): string => {
if (!Array.isArray(query)) {
const search: string[] = [`query=${encodeURIComponent(queryCleanner(query))}`]

if (variables) {
search.push(`variables=${encodeURIComponent(JSON.stringify(variables))}`)
search.push(`variables=${encodeURIComponent(jsonSerializer.stringify(variables))}`)
}

if (operationName) {
Expand All @@ -105,14 +106,14 @@ const buildGetQueryParams = <V>({ query, variables, operationName }: TBuildGetQu
(accu, currentQuery, index) => {
accu.push({
query: queryCleanner(currentQuery),
variables: variables ? JSON.stringify(variables[index]) : undefined,
variables: variables ? jsonSerializer.stringify(variables[index]) : undefined,
})
return accu
},
[]
)

return `query=${encodeURIComponent(JSON.stringify(payload))}`
return `query=${encodeURIComponent(jsonSerializer.stringify(payload))}`
}

/**
Expand All @@ -135,7 +136,7 @@ const post = async <V = Variables>({
headers?: Dom.RequestInit['headers']
operationName?: string
}) => {
const body = createRequestBody(query, variables, operationName)
const body = createRequestBody(query, variables, operationName, fetchOptions.jsonSerializer)

return await fetch(url, {
method: 'POST',
Expand Down Expand Up @@ -172,6 +173,7 @@ const get = async <V = Variables>({
query,
variables,
operationName,
jsonSerializer: fetchOptions.jsonSerializer
} as TBuildGetQueryParams<V>)

return await fetch(`${url}?${queryParams}`, {
Expand Down Expand Up @@ -377,7 +379,7 @@ async function makeRequest<T = any, V = Variables>({
fetch,
fetchOptions,
})
const result = await getResult(response)
const result = await getResult(response, fetchOptions.jsonSerializer)

const successfullyReceivedData =
isBathchingQuery && Array.isArray(result) ? !result.some(({ data }) => !data) : !!result.data
Expand Down Expand Up @@ -534,7 +536,7 @@ export default request
/**
* todo
*/
function getResult(response: Dom.Response): Promise<any> {
async function getResult(response: Dom.Response, jsonSerializer = defaultJsonSerializer): Promise<any> {
let contentType: string | undefined

response.headers.forEach((value, key) => {
Expand All @@ -544,7 +546,7 @@ function getResult(response: Dom.Response): Promise<any> {
})

if (contentType && contentType.toLowerCase().startsWith('application/json')) {
return response.json()
return jsonSerializer.parse(await response.text())
} else {
return response.text()
}
Expand Down
6 changes: 6 additions & 0 deletions src/types.dom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,11 @@ interface AbortSignal extends EventTarget {
): void
}

export interface JsonSerializer {
stringify(obj: any): string;
parse(obj: string): unknown;
}

export interface RequestInit {
body?: BodyInit | null
cache?: RequestCache
Expand All @@ -294,6 +299,7 @@ export interface RequestInit {
timeout?: number
window?: any
fetch?: any
jsonSerializer?: JsonSerializer
}

interface Body {
Expand Down
2 changes: 1 addition & 1 deletion tests/general.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -177,5 +177,5 @@ test('case-insensitive content-type header for custom fetch', async () => {
const client = new GraphQLClient(ctx.url, options)
const result = await client.request('{ test }')

expect(result).toBe(testData.data)
expect(result).toEqual(testData.data)
})
87 changes: 87 additions & 0 deletions tests/json-serializer.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { createReadStream } from 'fs'
import { join } from 'path'
import { GraphQLClient } from '../src'
import { setupTestServer } from './__helpers'
import * as Dom from '../src/types.dom'

const ctx = setupTestServer()

describe('jsonSerializer option', () => {
let serializer: Dom.JsonSerializer;
const testData = { data: { test: { name: 'test' } } }
let fetch: any;

beforeEach(() => {
serializer = {
stringify: jest.fn(JSON.stringify),
parse: jest.fn(JSON.parse)
}
fetch = (url: string) => Promise.resolve({
headers: new Map([['Content-Type', 'application/json; charset=utf-8']]),
data: testData,
text: function () {
return JSON.stringify(testData)
},
ok: true,
status: 200,
url,
});
})

test('is used for parsing response body', async () => {
const options: Dom.RequestInit = { jsonSerializer: serializer, fetch };
const client: GraphQLClient = new GraphQLClient(ctx.url, options);

const result = await client.request('{ test { name } }')
expect(result).toEqual(testData.data)
expect(serializer.parse).toBeCalledTimes(1)
})

describe('is used for serializing variables', () => {
const document = 'query getTest($name: String!) { test(name: $name) { name } }'
const simpleVariable = { name: 'test' }

let options: Dom.RequestInit
let client: GraphQLClient

const testSingleQuery = (expectedNumStringifyCalls = 1, variables: any = simpleVariable) => async () => {
await client.request(document, variables)
expect(serializer.stringify).toBeCalledTimes(expectedNumStringifyCalls)
}

const testBatchQuery = (expectedNumStringifyCalls: number, variables: any = simpleVariable) => async () => {
await client.batchRequests([{document, variables}])
expect(serializer.stringify).toBeCalledTimes(expectedNumStringifyCalls)
}

describe('request body', () => {
beforeEach(() => {
options = { jsonSerializer: serializer, fetch }
client = new GraphQLClient(ctx.url, options)
})

describe('without files', () => {
test('single query', testSingleQuery())
test('batch query', testBatchQuery(1))
})

describe('with files', () => {
const fileName = 'upload.test.ts'
const file = createReadStream(join(__dirname, fileName))

test('single query', testSingleQuery(2, {...simpleVariable, file}))
test('batch query', testBatchQuery(2, {...simpleVariable, file}))
})
})

describe('query string', () => {
beforeEach(() => {
options = { jsonSerializer: serializer, fetch, method: 'GET' }
client = new GraphQLClient(ctx.url, options)
})

test('single query', testSingleQuery())
test('batch query', testBatchQuery(2)) // once for variable and once for query batch array
})
})
})

0 comments on commit 05910a8

Please sign in to comment.