urql icon indicating copy to clipboard operation
urql copied to clipboard

RFC: Add a way to minify query before sending it

Open LoMonacoSalvatore opened this issue 2 years ago • 8 comments

Summary

I'm trying to send a query to Contentful GraphQL API, which has a query limit of 8Kb. The query in question is slightly above the limit. I've tried to minify it by using https://www.npmjs.com/package/graphql-query-compress this package, and by compressing the query string I've managed to get it to around 4Kb.

After feeding the Urql client the query, it seems like it decompresses it somehow before sending it to the API, since I still see that error in the response.

Is it possible to add a compressQuery option before sending it?

Not sure if this counts as an issue or a question or an RFC, be free to scold me if it's not an RFC :)

Thanks for your time reading this.

Proposed Solution

Add an option to compress/minify the query string before sending it

LoMonacoSalvatore avatar Mar 17 '23 11:03 LoMonacoSalvatore

You can already do this yourself by authoring an exchange that would sit in front of the fetchExchange

EDIT: oh, no it looks like our fetch itself explodes it again before sending it to the server so the minification should happen there https://github.com/urql-graphql/urql/blob/%40urql/core%403.2.2/packages/core/src/internal/fetchOptions.ts#L26

JoviDeCroock avatar Mar 17 '23 12:03 JoviDeCroock

So possibly going from this query: stringifyDocument(request.query) to this query: minifyQuery ? compress(stringifyDocument(request.query)) : stringifyDocument(request.query)

Could easily fix the issue since that's the last point where the query string gets manipulated?

Maybe this could be an easy fix for a yarn patch until you guys discuss the idea at a broader level

LoMonacoSalvatore avatar Mar 17 '23 13:03 LoMonacoSalvatore

From my point of view this isn't really something I would want to put into urql, I would be open to finding a way where stringifyDocument can pick up prior minified work but not adding a library that adds a lot of bundle-size

JoviDeCroock avatar Mar 17 '23 13:03 JoviDeCroock

I understand, and agree with it.

For anyone encountering this issue, before it gets resolved at a stringifyDocument level, I can confirm that patching (with yarn patch) the dist file and adding the compress function from the library stated in the comment above worked like a charm.

@JoviDeCroock if you need any help on how to preserve minification on stringifyDocument let me know.

Also, as a random thought: what if the query gets minified by default? Wouldn't it be better from a performance standpoint? My query dropped from 8Kb to 4Kb, it seems a little but sizeable improvement tbh.

Thanks again for your kind help!

LoMonacoSalvatore avatar Mar 17 '23 13:03 LoMonacoSalvatore

I'm not sure this is something that belongs in @urql/core. To be very honest, I think making several assumptions what a GraphQL document normalizes to is in a way important.

For context, internally a GraphQL query document is always parsed, and it's a "transports" responsibility to stringify it back to a GraphQL query document string. This is why the fetchExchange sends it in its normalized form. This is also important for Persisted Query support.

In fact, I'm very surprised that Contentful imposes a length limit, rather than a field limit. I think their length limit isn't unreasonable, but I'm surprised it's this low. For context, it's only twice as long as our rather low default GET URL length limit.

You can create a custom fetchExchange to circumvent this. We expose utilities in @urql/core/internal to enable this, e.g.:

import { filter, merge, mergeMap, pipe, share, takeUntil } from 'wonka';
import { Exchange } from '@urql/core';

import {
  makeFetchBody,
  makeFetchURL,
  makeFetchOptions,
  makeFetchSource,
} from '@urql/core/internal';

import compress from 'graphql-query-compress'

export const fetchExchange: Exchange = ({ forward }) => {
  return ops$ => {
    const sharedOps$ = share(ops$);
    const fetchResults$ = pipe(
      sharedOps$,
      filter(operation => {
        return operation.kind === 'query' || operation.kind === 'mutation';
      }),
      mergeMap(operation => {
        const body = makeFetchBody(operation);
        if (body.query)
          body.query = compress(body.query);

        return pipe(
          makeFetchSource(
            operation,
            makeFetchURL(operation, body),
            makeFetchOptions(operation, body)
          ),
          takeUntil(
            pipe(
              sharedOps$,
              filter(op => op.kind === 'teardown' && op.key === operation.key)
            )
          )
        );
      })
    );

    const forward$ = pipe(
      sharedOps$,
      filter(operation => {
        return operation.kind !== 'query' && operation.kind !== 'mutation';
      }),
      forward
    );

    return merge([fetchResults$, forward$]);
  };
};

kitten avatar Mar 17 '23 14:03 kitten

Thank you @JoviDeCroock and @kitten!

One question tho, where should I create this custom fetchExchange? I can't find how to use the exposed utilities in the urql doc.

LoMonacoSalvatore avatar Mar 17 '23 16:03 LoMonacoSalvatore

I think I found it https://formidable.com/open-source/urql/docs/advanced/authoring-exchanges/#using-exchanges

LoMonacoSalvatore avatar Mar 17 '23 16:03 LoMonacoSalvatore

Marking this as documentation, since we'll simply have to document @urql/core/internal and its fetch source utilities.

kitten avatar Mar 24 '23 01:03 kitten