1.0.0 • Published 2 years ago

duckql v1.0.0

Weekly downloads
-
License
Apache-2.0
Repository
github
Last release
2 years ago

Tests

DuckQL is an untyped GraphQL server that lets you write JS to resolve queries without a schema. It's a useful layer to build custom resolvers or as a way of unifying other GraphQL servers.

Why?

GraphQL is an overloaded concept. It consists of two wholly unrelated parts:

  • a simple query language
  • a complex type system

GraphQL queries don't know or care about the underlying types that they might resolve. For example, take a list query:

query Foo($filter: Filter!) {
  listFoo(filter: $filter) {
    items {
      id
      name
      fooProp
    }
  }
}

This query knows nothing about what a Foo is, does not specify that items must be returned as a list, nor the Filter type we expect as a variable. Yet, using Apollo requires us to specify a whole schema just to handle a request like this.

Usage

DuckQL parses incoming GraphQL queries (via the core graphql package) and parses them into a ResolverContext type:

import { DuckQLServer } from 'duckql';

const gqlServer = new DuckQLServer({
  resolver(context) {
    const out = { data: {} };

    if ('me' in context.selection.sub) {
      out.data['me'] = { firstName: 'Sam', lastName: 'Thor' };
    }

    return out;
  },
});

const out = await gqlServer.handle({
  query: `query { me { firstName lastName }}`,
});

Other Helpers

DuckQL can also process a query synchronously into a ResolverContext:

import { buildContext } from 'duckql';
const context = buildContext({ query: `{ foo }` });

Or it can handle HTTP requests directly (on "/graphql" with method "POST"), using e.g., Polka:

import polka from 'polka';
import { DuckQLServer } from 'duckql';
const gqlServer = new DuckQLServer({
  resolver(context) { /* TODO */ },
});

polka()
  .post('/graphql', gqlServer.httpHandle)
  // or
  .use(gqlServer.buildMiddlware())
  .listen(3000);

Variable Interpolation

DuckQL interpolates any GraphQL variables it finds, like $foo. For example, for a request like:

const request = {
  variables: {
    'x': 'hi!',
  },
  query: `query($x: String, $y: Number = 123) { listFoo(message: $x, size: $y) }`,
}

The processed selection of listFoo will already contain args { message: "hi!", size: 123 }. Missing or unresolved variables are a parse error and will through GraphQLQueryError from this package.

API

The ResolverContext is an object which wraps up the selections of your query in a structured way. Most importantly, it has a property selection, which contains a recursive type SelectionNode:

export type SelectionNode = {
  args?: { [key: string]: GraphQLType };
  directives?: any[];
  sub?: SelectionSet;
  node: FieldNode;
};
export type SelectionSet = { [key: string]: SelectionNode };

For example, if the user made a query for { listBar { bar(x: 123) { zing } } }, then context.selection will look like:

({
  node: ...,
  sub: {
    'listBar': {
      node: ...,
      sub: {
        'bar': {
          node: ...,
          args: { 'x': 123 },
          sub: {
            'zing': {
              node: ...,
            },
          },
        },
      },
    },
  },
})

Importantly, each sub-tree contains a node which can be used to reproduce a sub-tree of the original query. This can be useful to forward these queries to another server without having to care about the schema. For example:

import { print } from 'graphql';
const q = print(context.sub['listBar'].sub['bar'].node);
q === `bar(x: 123) {
  zing
}`;

Other Context Properties

As well as the selection, the context also contains:

  • operation: one of 'query', 'mutation' or 'subscription'
  • operationName: the operation name in e.g., "query Foo {" would be "Foo", or the blank string for default/none
  • maxDepth: the maximum depth of selection (useful to catch abuse via deeply nested queries)
  • node: the original GraphQL AST node, without variable interpolation

Missing Features

DuckQL does not yet support:

  • Fragments: it will treat these as an invalid query
  • Directives: these are silently ignored, but remain in the AST to be forwarded