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

feat(core): Reuse stringifyDocument and cached prints across @urql/core (#2847)

+8
.changeset/quiet-mugs-travel.md
···
···
+
---
+
'@urql/core': patch
+
---
+
+
Reuse output of `stringifyDocument` in place of repeated `print`. This will mean that we now prevent calling `print` repeatedly for identical operations and are instead only reusing the result once.
+
+
This change has a subtle consequence of our internals. Operation keys will change due to this
+
refactor and we will no longer sanitise strip newlines from queries that `@urql/core` has printed.
+41 -20
exchanges/multipart-fetch/src/__snapshots__/multipartFetchExchange.test.ts.snap
···
"key": 2,
"kind": "query",
"query": {
-
"__key": 3521976120,
"definitions": [
{
"directives": [],
···
],
"kind": "Document",
"loc": {
-
"end": 86,
"source": {
-
"body": "# getUser
-
query getUser($name: String) { user(name: $name) { id firstName lastName } }",
"locationOffset": {
"column": 1,
"line": 1,
···
"key": 2,
"kind": "query",
"query": {
-
"__key": 3521976120,
"definitions": [
{
"directives": [],
···
],
"kind": "Document",
"loc": {
-
"end": 86,
"source": {
-
"body": "# getUser
-
query getUser($name: String) { user(name: $name) { id firstName lastName } }",
"locationOffset": {
"column": 1,
"line": 1,
···
"key": 2,
"kind": "query",
"query": {
-
"__key": 3521976120,
"definitions": [
{
"directives": [],
···
],
"kind": "Document",
"loc": {
-
"end": 86,
"source": {
-
"body": "# getUser
-
query getUser($name: String) { user(name: $name) { id firstName lastName } }",
"locationOffset": {
"column": 1,
"line": 1,
···
"key": 3,
"kind": "mutation",
"query": {
-
"__key": 4034972436,
"definitions": [
{
"directives": [],
···
],
"kind": "Document",
"loc": {
-
"end": 125,
"source": {
-
"body": "# uploadProfilePicture
-
mutation uploadProfilePicture($picture: File) { uploadProfilePicture(picture: $picture) { location } }",
"locationOffset": {
"column": 1,
"line": 1,
···
"key": 3,
"kind": "mutation",
"query": {
-
"__key": 2033658603,
"definitions": [
{
"directives": [],
···
],
"kind": "Document",
"loc": {
-
"end": 132,
"source": {
-
"body": "# uploadProfilePictures
-
mutation uploadProfilePictures($pictures: [File]) { uploadProfilePicture(pictures: $pictures) { location } }",
"locationOffset": {
"column": 1,
"line": 1,
···
"key": 2,
"kind": "query",
"query": {
+
"__key": -2395444236,
"definitions": [
{
"directives": [],
···
],
"kind": "Document",
"loc": {
+
"end": 92,
"source": {
+
"body": "query getUser($name: String) {
+
user(name: $name) {
+
id
+
firstName
+
lastName
+
}
+
}",
"locationOffset": {
"column": 1,
"line": 1,
···
"key": 2,
"kind": "query",
"query": {
+
"__key": -2395444236,
"definitions": [
{
"directives": [],
···
],
"kind": "Document",
"loc": {
+
"end": 92,
"source": {
+
"body": "query getUser($name: String) {
+
user(name: $name) {
+
id
+
firstName
+
lastName
+
}
+
}",
"locationOffset": {
"column": 1,
"line": 1,
···
"key": 2,
"kind": "query",
"query": {
+
"__key": -2395444236,
"definitions": [
{
"directives": [],
···
],
"kind": "Document",
"loc": {
+
"end": 92,
"source": {
+
"body": "query getUser($name: String) {
+
user(name: $name) {
+
id
+
firstName
+
lastName
+
}
+
}",
"locationOffset": {
"column": 1,
"line": 1,
···
"key": 3,
"kind": "mutation",
"query": {
+
"__key": 8029062428,
"definitions": [
{
"directives": [],
···
],
"kind": "Document",
"loc": {
+
"end": 110,
"source": {
+
"body": "mutation uploadProfilePicture($picture: File) {
+
uploadProfilePicture(picture: $picture) {
+
location
+
}
+
}",
"locationOffset": {
"column": 1,
"line": 1,
···
"key": 3,
"kind": "mutation",
"query": {
+
"__key": -6039055341,
"definitions": [
{
"directives": [],
···
],
"kind": "Document",
"loc": {
+
"end": 116,
"source": {
+
"body": "mutation uploadProfilePictures($pictures: [File]) {
+
uploadProfilePicture(pictures: $pictures) {
+
location
+
}
+
}",
"locationOffset": {
"column": 1,
"line": 1,
+27 -12
packages/core/src/exchanges/__snapshots__/fetch.test.ts.snap
···
"key": 2,
"kind": "query",
"query": {
-
"__key": 3521976120,
"definitions": [
{
"directives": [],
···
],
"kind": "Document",
"loc": {
-
"end": 86,
"source": {
-
"body": "# getUser
-
query getUser($name: String) { user(name: $name) { id firstName lastName } }",
"locationOffset": {
"column": 1,
"line": 1,
···
"key": 2,
"kind": "query",
"query": {
-
"__key": 3521976120,
"definitions": [
{
"directives": [],
···
],
"kind": "Document",
"loc": {
-
"end": 86,
"source": {
-
"body": "# getUser
-
query getUser($name: String) { user(name: $name) { id firstName lastName } }",
"locationOffset": {
"column": 1,
"line": 1,
···
"key": 2,
"kind": "query",
"query": {
-
"__key": 3521976120,
"definitions": [
{
"directives": [],
···
],
"kind": "Document",
"loc": {
-
"end": 86,
"source": {
-
"body": "# getUser
-
query getUser($name: String) { user(name: $name) { id firstName lastName } }",
"locationOffset": {
"column": 1,
"line": 1,
···
"key": 2,
"kind": "query",
"query": {
+
"__key": -2395444236,
"definitions": [
{
"directives": [],
···
],
"kind": "Document",
"loc": {
+
"end": 92,
"source": {
+
"body": "query getUser($name: String) {
+
user(name: $name) {
+
id
+
firstName
+
lastName
+
}
+
}",
"locationOffset": {
"column": 1,
"line": 1,
···
"key": 2,
"kind": "query",
"query": {
+
"__key": -2395444236,
"definitions": [
{
"directives": [],
···
],
"kind": "Document",
"loc": {
+
"end": 92,
"source": {
+
"body": "query getUser($name: String) {
+
user(name: $name) {
+
id
+
firstName
+
lastName
+
}
+
}",
"locationOffset": {
"column": 1,
"line": 1,
···
"key": 2,
"kind": "query",
"query": {
+
"__key": -2395444236,
"definitions": [
{
"directives": [],
···
],
"kind": "Document",
"loc": {
+
"end": 92,
"source": {
+
"body": "query getUser($name: String) {
+
user(name: $name) {
+
id
+
firstName
+
lastName
+
}
+
}",
"locationOffset": {
"column": 1,
"line": 1,
+7 -4
packages/core/src/exchanges/__snapshots__/subscription.test.ts.snap
···
"key": 4,
"kind": "subscription",
"query": {
-
"__key": 2088253569,
"definitions": [
{
"directives": [],
···
],
"kind": "Document",
"loc": {
-
"end": 92,
"source": {
-
"body": "# subscribeToUser
-
subscription subscribeToUser($user: String) { user(user: $user) { name } }",
"locationOffset": {
"column": 1,
"line": 1,
···
"key": 4,
"kind": "subscription",
"query": {
+
"__key": 7623921801,
"definitions": [
{
"directives": [],
···
],
"kind": "Document",
"loc": {
+
"end": 82,
"source": {
+
"body": "subscription subscribeToUser($user: String) {
+
user(user: $user) {
+
name
+
}
+
}",
"locationOffset": {
"column": 1,
"line": 1,
+5 -2
packages/core/src/exchanges/subscription.test.ts
···
-
import { print } from 'graphql';
import { vi, expect, it } from 'vitest';
import {
empty,
publish,
···
import { Client } from '../client';
import { subscriptionOperation, subscriptionResult } from '../test-utils';
import { OperationResult } from '../types';
import { subscriptionExchange, SubscriptionForwarder } from './subscription';
···
const unsubscribe = vi.fn();
const forwardSubscription: SubscriptionForwarder = operation => {
-
expect(operation.query).toBe(print(subscriptionOperation.query));
expect(operation.variables).toBe(subscriptionOperation.variables);
expect(operation.context).toEqual(subscriptionOperation.context);
···
import { vi, expect, it } from 'vitest';
+
import {
empty,
publish,
···
import { Client } from '../client';
import { subscriptionOperation, subscriptionResult } from '../test-utils';
+
import { stringifyDocument } from '../utils';
import { OperationResult } from '../types';
import { subscriptionExchange, SubscriptionForwarder } from './subscription';
···
const unsubscribe = vi.fn();
const forwardSubscription: SubscriptionForwarder = operation => {
+
expect(operation.query).toBe(
+
stringifyDocument(subscriptionOperation.query)
+
);
expect(operation.variables).toBe(subscriptionOperation.variables);
expect(operation.context).toEqual(subscriptionOperation.context);
+7 -4
packages/core/src/exchanges/subscription.ts
···
-
import { print } from 'graphql';
-
import {
filter,
make,
···
takeUntil,
} from 'wonka';
-
import { makeResult, makeErrorResult, makeOperation } from '../utils';
import {
Exchange,
···
// This excludes the query's name as a field although subscription-transport-ws does accept it since it's optional
const observableish = forwardSubscription({
key: operation.key.toString(36),
-
query: print(operation.query),
variables: operation.variables!,
context: { ...operation.context },
});
···
import {
filter,
make,
···
takeUntil,
} from 'wonka';
+
import {
+
stringifyDocument,
+
makeResult,
+
makeErrorResult,
+
makeOperation,
+
} from '../utils';
import {
Exchange,
···
// This excludes the query's name as a field although subscription-transport-ws does accept it since it's optional
const observableish = forwardSubscription({
key: operation.key.toString(36),
+
query: stringifyDocument(operation.query),
variables: operation.variables!,
context: { ...operation.context },
});
+2 -2
packages/core/src/gql.test.ts
···
parse('{ gql testing }', { noLocation: true }).definitions
);
-
expect(doc).toBe(keyDocument('{ gql testing }'));
expect(doc.loc).toEqual({
start: 0,
-
end: 15,
source: expect.anything(),
});
});
···
parse('{ gql testing }', { noLocation: true }).definitions
);
+
expect(doc).toBe(keyDocument('{\n gql\n testing\n}'));
expect(doc.loc).toEqual({
start: 0,
+
end: 19,
source: expect.anything(),
});
});
+45 -20
packages/core/src/internal/__snapshots__/fetchSource.test.ts.snap
···
"key": 2,
"kind": "query",
"query": {
-
"__key": 3521976120,
"definitions": [
{
"directives": [],
···
],
"kind": "Document",
"loc": {
-
"end": 86,
"source": {
-
"body": "# getUser
-
query getUser($name: String) { user(name: $name) { id firstName lastName } }",
"locationOffset": {
"column": 1,
"line": 1,
···
"key": 2,
"kind": "query",
"query": {
-
"__key": 3521976120,
"definitions": [
{
"directives": [],
···
],
"kind": "Document",
"loc": {
-
"end": 86,
"source": {
-
"body": "# getUser
-
query getUser($name: String) { user(name: $name) { id firstName lastName } }",
"locationOffset": {
"column": 1,
"line": 1,
···
"key": 2,
"kind": "query",
"query": {
-
"__key": 3521976120,
"definitions": [
{
"directives": [],
···
],
"kind": "Document",
"loc": {
-
"end": 86,
"source": {
-
"body": "# getUser
-
query getUser($name: String) { user(name: $name) { id firstName lastName } }",
"locationOffset": {
"column": 1,
"line": 1,
···
"key": 2,
"kind": "query",
"query": {
-
"__key": 3521976120,
"definitions": [
{
"directives": [],
···
],
"kind": "Document",
"loc": {
-
"end": 86,
"source": {
-
"body": "# getUser
-
query getUser($name: String) { user(name: $name) { id firstName lastName } }",
"locationOffset": {
"column": 1,
"line": 1,
···
"key": 2,
"kind": "query",
"query": {
-
"__key": 3521976120,
"definitions": [
{
"directives": [],
···
],
"kind": "Document",
"loc": {
-
"end": 86,
"source": {
-
"body": "# getUser
-
query getUser($name: String) { user(name: $name) { id firstName lastName } }",
"locationOffset": {
"column": 1,
"line": 1,
···
"key": 2,
"kind": "query",
"query": {
+
"__key": -2395444236,
"definitions": [
{
"directives": [],
···
],
"kind": "Document",
"loc": {
+
"end": 92,
"source": {
+
"body": "query getUser($name: String) {
+
user(name: $name) {
+
id
+
firstName
+
lastName
+
}
+
}",
"locationOffset": {
"column": 1,
"line": 1,
···
"key": 2,
"kind": "query",
"query": {
+
"__key": -2395444236,
"definitions": [
{
"directives": [],
···
],
"kind": "Document",
"loc": {
+
"end": 92,
"source": {
+
"body": "query getUser($name: String) {
+
user(name: $name) {
+
id
+
firstName
+
lastName
+
}
+
}",
"locationOffset": {
"column": 1,
"line": 1,
···
"key": 2,
"kind": "query",
"query": {
+
"__key": -2395444236,
"definitions": [
{
"directives": [],
···
],
"kind": "Document",
"loc": {
+
"end": 92,
"source": {
+
"body": "query getUser($name: String) {
+
user(name: $name) {
+
id
+
firstName
+
lastName
+
}
+
}",
"locationOffset": {
"column": 1,
"line": 1,
···
"key": 2,
"kind": "query",
"query": {
+
"__key": -2395444236,
"definitions": [
{
"directives": [],
···
],
"kind": "Document",
"loc": {
+
"end": 92,
"source": {
+
"body": "query getUser($name: String) {
+
user(name: $name) {
+
id
+
firstName
+
lastName
+
}
+
}",
"locationOffset": {
"column": 1,
"line": 1,
···
"key": 2,
"kind": "query",
"query": {
+
"__key": -2395444236,
"definitions": [
{
"directives": [],
···
],
"kind": "Document",
"loc": {
+
"end": 92,
"source": {
+
"body": "query getUser($name: String) {
+
user(name: $name) {
+
id
+
firstName
+
lastName
+
}
+
}",
"locationOffset": {
"column": 1,
"line": 1,
+7 -5
packages/core/src/internal/fetchOptions.ts
···
-
import { print } from 'graphql';
-
import { getOperationName, stringifyVariables } from '../utils';
import { AnyVariables, GraphQLRequest, Operation } from '../types';
export interface FetchBody {
···
Variables extends AnyVariables = AnyVariables
>(request: Omit<GraphQLRequest<Data, Variables>, 'key'>): FetchBody {
return {
-
query: print(request.query),
operationName: getOperationName(request.query),
variables: request.variables || undefined,
extensions: undefined,
···
const url = new URL(operation.context.url);
const search = url.searchParams;
if (body.operationName) search.set('operationName', body.operationName);
-
if (body.query)
-
search.set('query', body.query.replace(/#[^\n\r]+/g, ' ').trim());
if (body.variables)
search.set('variables', stringifyVariables(body.variables));
if (body.extensions)
···
+
import {
+
stringifyDocument,
+
getOperationName,
+
stringifyVariables,
+
} from '../utils';
import { AnyVariables, GraphQLRequest, Operation } from '../types';
export interface FetchBody {
···
Variables extends AnyVariables = AnyVariables
>(request: Omit<GraphQLRequest<Data, Variables>, 'key'>): FetchBody {
return {
+
query: stringifyDocument(request.query),
operationName: getOperationName(request.query),
variables: request.variables || undefined,
extensions: undefined,
···
const url = new URL(operation.context.url);
const search = url.searchParams;
if (body.operationName) search.set('operationName', body.operationName);
+
if (body.query) search.set('query', body.query);
if (body.variables)
search.set('variables', stringifyVariables(body.variables));
if (body.extensions)
+14
packages/core/src/utils/hash.test.ts
···
···
+
import { HashValue, phash } from './hash';
+
import { expect, it } from 'vitest';
+
+
it('hashes given strings', () => {
+
expect(phash('hello')).toMatchInlineSnapshot('261238937');
+
});
+
+
it('hashes given strings and seeds', () => {
+
let hash: HashValue;
+
expect((hash = phash('hello'))).toMatchInlineSnapshot('261238937');
+
expect((hash = phash('world', hash))).toMatchInlineSnapshot('-152191');
+
expect((hash = phash('!', hash))).toMatchInlineSnapshot('-5022270');
+
expect(typeof hash).toBe('number');
+
});
+5 -5
packages/core/src/utils/hash.ts
···
// When we have separate strings it's useful to run a progressive
// version of djb2 where we pretend that we're still looping over
// the same string
-
export const phash = (h: number, x: string): number => {
for (let i = 0, l = x.length | 0; i < l; i++)
h = (h << 5) + h + x.charCodeAt(i);
-
return h | 0;
};
-
-
// This is a djb2 hashing function
-
export const hash = (x: string): number => phash(5381 | 0, x) >>> 0;
···
+
export type HashValue = number & { readonly _opaque: unique symbol };
+
// When we have separate strings it's useful to run a progressive
// version of djb2 where we pretend that we're still looping over
// the same string
+
export const phash = (x: string, seed?: HashValue): HashValue => {
+
let h = typeof seed === 'number' ? seed | 0 : 5381;
for (let i = 0, l = x.length | 0; i < l; i++)
h = (h << 5) + h + x.charCodeAt(i);
+
return h as HashValue;
};
+109 -82
packages/core/src/utils/request.test.ts
···
-
import { vi, expect, it, describe } from 'vitest';
-
-
vi.mock('./hash', async () => {
-
const hash = await vi.importActual<typeof import('./hash')>('./hash');
-
return {
-
...hash,
-
phash: (x: number) => x,
-
};
-
});
import { parse, print } from 'graphql';
import { gql } from '../gql';
import { createRequest, stringifyDocument } from './request';
-
-
it('should hash identical queries identically', () => {
-
const reqA = createRequest('{ test }', undefined);
-
const reqB = createRequest('{ test }', undefined);
-
expect(reqA.key).toBe(reqB.key);
-
});
-
it('should hash identical DocumentNodes identically', () => {
-
const reqA = createRequest(parse('{ testB }'), undefined);
-
const reqB = createRequest(parse('{ testB }'), undefined);
-
expect(reqA.key).toBe(reqB.key);
-
expect(reqA.query).toBe(reqB.query);
-
});
-
it('should use the hash from a key if available', () => {
-
const doc = parse('{ testC }');
-
(doc as any).__key = 1234;
-
const req = createRequest(doc, undefined);
-
expect(req.key).toBe(1234);
-
});
-
it('should hash DocumentNodes and strings identically', () => {
-
const docA = parse('{ field }');
-
const docB = print(docA).replace(/\s/g, ' ');
-
const reqA = createRequest(docA, undefined);
-
const reqB = createRequest(docB, undefined);
-
expect(reqA.key).toBe(reqB.key);
-
expect(reqA.query).toBe(reqB.query);
-
});
-
it('should hash graphql-tag documents correctly', () => {
-
const doc = gql`
-
{
-
testD
-
}
-
`;
-
createRequest(doc, undefined);
-
expect((doc as any).__key).not.toBe(undefined);
-
});
-
it('should return a valid query object', () => {
-
const doc = gql`
-
{
-
testE
-
}
-
`;
-
const val = createRequest(doc, undefined);
-
expect(val).toMatchObject({
-
key: expect.any(Number),
-
query: expect.any(Object),
-
variables: {},
});
-
});
-
it('should return a valid query object with variables', () => {
-
const doc = print(
-
gql`
{
-
testF
}
-
`
-
);
-
const val = createRequest(doc, { test: 5 });
-
expect(print(val.query)).toBe(doc);
-
expect(val).toMatchObject({
-
key: expect.any(Number),
-
query: expect.any(Object),
-
variables: { test: 5 },
});
});
-
describe('stringifyDocument (internal API)', () => {
it('should remove comments', () => {
const doc = `
{ #query
···
test
}
`;
-
expect(stringifyDocument(createRequest(doc, undefined).query)).toBe(
-
'{ test }'
-
);
});
it('should remove duplicate spaces', () => {
···
abc ,, test
}
`;
-
expect(stringifyDocument(createRequest(doc, undefined).query)).toBe(
-
'{ abc test }'
-
);
});
it('should not sanitize within strings', () => {
···
field(arg: "test #1")
}
`;
-
expect(stringifyDocument(createRequest(doc, undefined).query)).toBe(
-
'{ field(arg:"test #1") }'
-
);
});
it('should not sanitize within block strings', () => {
···
field(
arg: """
hello
-
hello
"""
)
}
`;
-
expect(stringifyDocument(createRequest(doc, undefined).query)).toBe(
-
'{ field(arg:"""\n hello\n hello\n """) }'
-
);
});
});
···
+
import { expect, it, describe } from 'vitest';
import { parse, print } from 'graphql';
import { gql } from '../gql';
import { createRequest, stringifyDocument } from './request';
+
import { formatDocument } from './typenames';
+
describe('createRequest', () => {
+
it('should hash identical queries identically', () => {
+
const reqA = createRequest('{ test }', undefined);
+
const reqB = createRequest('{ test }', undefined);
+
expect(reqA.key).toBe(reqB.key);
+
});
+
it('should hash identical queries identically', () => {
+
const reqA = createRequest('{ test }', undefined);
+
const reqB = createRequest('{ test }', undefined);
+
expect(reqA.key).toBe(reqB.key);
+
});
+
it('should hash identical DocumentNodes identically', () => {
+
const reqA = createRequest(parse('{ testB }'), undefined);
+
const reqB = createRequest(parse('{ testB }'), undefined);
+
expect(reqA.key).toBe(reqB.key);
+
expect(reqA.query).toBe(reqB.query);
+
});
+
it('should use the hash from a key if available', () => {
+
const doc = parse('{ testC }');
+
(doc as any).__key = 1234;
+
const req = createRequest(doc, undefined);
+
expect(req.key).toBe(1234);
+
});
+
it('should hash DocumentNodes and strings identically', () => {
+
const docA = parse('{ field }');
+
const docB = print(docA);
+
const reqA = createRequest(docA, undefined);
+
const reqB = createRequest(docB, undefined);
+
expect(reqA.key).toBe(reqB.key);
+
expect(reqA.query).toBe(reqB.query);
+
});
+
it('should hash graphql-tag documents correctly', () => {
+
const doc = gql`
+
{
+
testD
+
}
+
`;
+
createRequest(doc, undefined);
+
expect((doc as any).__key).not.toBe(undefined);
});
+
it('should return a valid query object', () => {
+
const doc = gql`
{
+
testE
}
+
`;
+
const val = createRequest(doc, undefined);
+
expect(val).toMatchObject({
+
key: expect.any(Number),
+
query: expect.any(Object),
+
variables: {},
+
});
+
});
+
+
it('should return a valid query object with variables', () => {
+
const doc = print(
+
gql`
+
{
+
testF
+
}
+
`
+
);
+
const val = createRequest(doc, { test: 5 });
+
+
expect(print(val.query)).toBe(doc);
+
expect(val).toMatchObject({
+
key: expect.any(Number),
+
query: expect.any(Object),
+
variables: { test: 5 },
+
});
});
});
+
describe('stringifyDocument ', () => {
+
it('should reprint formatted documents', () => {
+
const doc = parse('{ test { field } }');
+
const formatted = formatDocument(doc);
+
expect(stringifyDocument(formatted)).toBe(print(formatted));
+
});
+
it('should remove comments', () => {
const doc = `
{ #query
···
test
}
`;
+
expect(stringifyDocument(createRequest(doc, undefined).query))
+
.toMatchInlineSnapshot(`
+
"{
+
test
+
}"
+
`);
});
it('should remove duplicate spaces', () => {
···
abc ,, test
}
`;
+
expect(stringifyDocument(createRequest(doc, undefined).query))
+
.toMatchInlineSnapshot(`
+
"{
+
abc
+
test
+
}"
+
`);
});
it('should not sanitize within strings', () => {
···
field(arg: "test #1")
}
`;
+
expect(stringifyDocument(createRequest(doc, undefined).query))
+
.toMatchInlineSnapshot(`
+
"{
+
field(arg:
+
+
\\"test #1\\")
+
}"
+
`);
});
it('should not sanitize within block strings', () => {
···
field(
arg: """
hello
+
#hello
"""
)
}
`;
+
expect(stringifyDocument(createRequest(doc, undefined).query))
+
.toMatchInlineSnapshot(`
+
"{
+
field(arg:
+
+
\\"\\"\\"
+
hello
+
#hello
+
\\"\\"\\")
+
}"
+
`);
});
});
+52 -44
packages/core/src/utils/request.ts
···
print,
} from 'graphql';
-
import { hash, phash } from './hash';
import { stringifyVariables } from './stringifyVariables';
import { TypedDocumentNode, AnyVariables, GraphQLRequest } from '../types';
···
}
export interface KeyedDocumentNode extends DocumentNode {
-
__key: number;
}
const GRAPHQL_STRING_RE = /("{3}[\s\S]*"{3}|"(?:\\.|[^"])*")/g;
-
const REPLACE_CHAR_RE = /([\s,]|#[^\n\r]+)+/g;
const replaceOutsideStrings = (str: string, idx: number) =>
-
idx % 2 === 0 ? str.replace(REPLACE_CHAR_RE, ' ').trim() : str;
export const stringifyDocument = (
node: string | DefinitionNode | DocumentNode
): string => {
-
let str = (typeof node !== 'string'
-
? (node.loc && node.loc.source.body) || print(node)
-
: node
-
)
-
.split(GRAPHQL_STRING_RE)
-
.map(replaceOutsideStrings)
-
.join('');
-
-
if (typeof node !== 'string') {
-
const operationName = 'definitions' in node && getOperationName(node);
-
if (operationName) {
-
str = `# ${operationName}\n${str}`;
-
}
-
if (!node.loc) {
-
(node as WritableLocation).loc = {
-
start: 0,
-
end: str.length,
-
source: {
-
body: str,
-
name: 'gql',
-
locationOffset: { line: 1, column: 1 },
-
},
-
} as Location;
-
}
}
-
return str;
};
-
const docs = new Map<number, KeyedDocumentNode>();
-
export const keyDocument = (q: string | DocumentNode): KeyedDocumentNode => {
-
let key: number;
let query: DocumentNode;
-
if (typeof q === 'string') {
-
key = hash(stringifyDocument(q));
-
query = docs.get(key) || parse(q, { noLocation: true });
} else {
-
key = (q as KeyedDocumentNode).__key || hash(stringifyDocument(q));
-
query = docs.get(key) || q;
}
// Add location information if it's missing
···
Variables extends AnyVariables = AnyVariables
>(
q: string | DocumentNode | TypedDocumentNode<Data, Variables>,
-
vars: Variables
): GraphQLRequest<Data, Variables> => {
-
if (!vars) vars = {} as Variables;
const query = keyDocument(q);
-
return {
-
key: phash(query.__key, stringifyVariables(vars)) >>> 0,
-
query,
-
variables: vars as Variables,
-
};
};
/**
···
print,
} from 'graphql';
+
import { HashValue, phash } from './hash';
import { stringifyVariables } from './stringifyVariables';
import { TypedDocumentNode, AnyVariables, GraphQLRequest } from '../types';
···
}
export interface KeyedDocumentNode extends DocumentNode {
+
__key: HashValue;
}
+
const SOURCE_NAME = 'gql';
const GRAPHQL_STRING_RE = /("{3}[\s\S]*"{3}|"(?:\\.|[^"])*")/g;
+
const REPLACE_CHAR_RE = /(#[^\n\r]+)?(?:\n|\r\n?|$)+/g;
const replaceOutsideStrings = (str: string, idx: number) =>
+
idx % 2 === 0 ? str.replace(REPLACE_CHAR_RE, '\n') : str;
+
+
const sanitizeDocument = (node: string): string =>
+
node.split(GRAPHQL_STRING_RE).map(replaceOutsideStrings).join('').trim();
export const stringifyDocument = (
node: string | DefinitionNode | DocumentNode
): string => {
+
const printed = sanitizeDocument(
+
typeof node === 'string'
+
? node
+
: node.loc && node.loc.source.name === SOURCE_NAME
+
? node.loc.source.body
+
: print(node)
+
);
+
if (typeof node !== 'string' && !node.loc) {
+
(node as WritableLocation).loc = {
+
start: 0,
+
end: printed.length,
+
source: {
+
body: printed,
+
name: SOURCE_NAME,
+
locationOffset: { line: 1, column: 1 },
+
},
+
} as Location;
}
+
return printed;
};
+
const hashDocument = (
+
node: string | DefinitionNode | DocumentNode
+
): HashValue => {
+
let key = phash(stringifyDocument(node));
+
// Add the operation name to the produced hash
+
if (typeof node === 'object' && 'definitions' in node) {
+
const operationName = getOperationName(node);
+
if (operationName) key = phash(`\n# ${operationName}`, key);
+
}
+
return key;
+
};
+
const docs = new Map<HashValue, KeyedDocumentNode>();
+
+
export const keyDocument = (node: string | DocumentNode): KeyedDocumentNode => {
+
let key: HashValue;
let query: DocumentNode;
+
if (typeof node === 'string') {
+
key = hashDocument(node);
+
query = docs.get(key) || parse(node, { noLocation: true });
} else {
+
key = (node as KeyedDocumentNode).__key || hashDocument(node);
+
query = docs.get(key) || node;
}
// Add location information if it's missing
···
Variables extends AnyVariables = AnyVariables
>(
q: string | DocumentNode | TypedDocumentNode<Data, Variables>,
+
variables: Variables
): GraphQLRequest<Data, Variables> => {
+
if (!variables) variables = {} as Variables;
const query = keyDocument(q);
+
const printedVars = stringifyVariables(variables);
+
let key = query.__key;
+
if (printedVars !== '{}') key = phash(printedVars, key);
+
return { key, query, variables };
};
/**
+8 -1
packages/svelte-urql/src/mutationStore.test.ts
···
it('fills the store with correct values', () => {
expect(get(store).operation.kind).toBe('mutation');
expect(get(store).operation.context.url).toBe('https://example.com');
-
expect(get(store).operation.query.loc?.source.body).toBe(query);
expect(get(store).operation.variables).toBe(variables);
});
});
···
it('fills the store with correct values', () => {
expect(get(store).operation.kind).toBe('mutation');
expect(get(store).operation.context.url).toBe('https://example.com');
expect(get(store).operation.variables).toBe(variables);
+
+
expect(get(store).operation.query.loc?.source.body).toMatchInlineSnapshot(`
+
"mutation ($input: Example!) {
+
doExample(input: $input) {
+
id
+
}
+
}"
+
`);
});
});
+6 -1
packages/svelte-urql/src/queryStore.test.ts
···
it('fills the store with correct values', () => {
expect(get(store).operation.kind).toBe('query');
expect(get(store).operation.context.url).toBe('https://example.com');
-
expect(get(store).operation.query.loc?.source.body).toBe(query);
expect(get(store).operation.variables).toBe(variables);
});
it('adds pause handles', () => {
···
it('fills the store with correct values', () => {
expect(get(store).operation.kind).toBe('query');
expect(get(store).operation.context.url).toBe('https://example.com');
expect(get(store).operation.variables).toBe(variables);
+
+
expect(get(store).operation.query.loc?.source.body).toMatchInlineSnapshot(`
+
"{
+
test
+
}"
+
`);
});
it('adds pause handles', () => {
+8 -1
packages/svelte-urql/src/subscriptionStore.test.ts
···
it('fills the store with correct values', () => {
expect(get(store).operation.kind).toBe('subscription');
expect(get(store).operation.context.url).toBe('https://example.com');
-
expect(get(store).operation.query.loc?.source.body).toBe(query);
expect(get(store).operation.variables).toBe(variables);
});
it('adds pause handles', () => {
···
it('fills the store with correct values', () => {
expect(get(store).operation.kind).toBe('subscription');
expect(get(store).operation.context.url).toBe('https://example.com');
expect(get(store).operation.variables).toBe(variables);
+
+
expect(get(store).operation.query.loc?.source.body).toMatchInlineSnapshot(`
+
"subscription ($input: ExampleInput) {
+
exampleSubscribe(input: $input) {
+
data
+
}
+
}"
+
`);
});
it('adds pause handles', () => {