npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@iappx/gql-builder

v1.0.0

Published

A dependency-free TypeScript GraphQL document builder: compose queries, mutations and subscriptions as an object tree and print them with variables extracted automatically.

Readme

NPM Version Issues License

About The Project

@iappx/gql-builder builds GraphQL operations at runtime, without a schema, without codegen and without a GraphQL parser in the bundle. You compose an object tree — operation → fields → arguments → values — and the printer turns it into a document you can hand to any transport:

{ query: string, variables: Record<string, unknown>, operationName?: string }

Why it exists:

  • Runtime-shaped queries. Selection sets that depend on data known only at runtime (entity metadata, user-selected columns, dynamic filters) cannot be written as static gql tags.
  • Variables, not string interpolation. Every dynamic value can be lifted into a real GraphQL variable, so the document text stays constant while values change — no escaping bugs, no injection through user input, and a stable document for persisted-query / APQ caching.
  • No dependencies. Nothing is imported, so it runs in the browser, in Node and in workers alike.

It is transport-agnostic on purpose: it never sends anything, it only builds documents.

Installation

npm install @iappx/gql-builder

Requires Node.js 18 or newer when running server-side. The package has no dependencies and uses no Node APIs, so it also runs unchanged in browsers and workers.

Quick Start

import { Gql, GqlOperation, GqlType } from '@iappx/gql-builder'

const operation = GqlOperation.single('query', 'user', Gql.fields(['id', 'name']))
operation.root.addArg('id', Gql.variable(GqlType.int().required(), 42, 'id'))

const document = operation.build()
// document.query         → 'query user($id: Int!) { user(id: $id) { id name } }'
// document.variables     → { id: 42 }
// document.operationName → 'user'

GqlOperation.single(type, rootFieldName, fields?) covers the common case of a single root field; the root field is available as operation.root. For several root fields, create the operation and add fields yourself:

const operation = Gql.query('dashboard')
operation.field('user').addFields(Gql.fields(['id']))
operation.field('project').addFields(Gql.fields(['id']))

Mutations and subscriptions work the same way:

const mutation = GqlOperation.single('mutation', 'createUser', Gql.fields(['id']))
mutation.root.addArg('input', Gql.variable(GqlType.named('UserCreateInput').required(), { name: 'bob' }, 'input'))

const subscription = GqlOperation.single('subscription', 'userChanged', Gql.fields(['id', 'name']))

Variables

A variable carries its GraphQL type, its value and an optional preferred name:

Gql.variable(GqlType.int().required(), 42, 'id')    // → $id, declared as ($id: Int!), value 42
Gql.variable(GqlType.int().required(), 42)          // → auto-named $v1, $v2, …

The type is a GqlType, never a string of GraphQL syntax — the builder assembles the brackets and the !:

GqlType.int()                                   // Int
GqlType.int().required()                        // Int!
GqlType.string().listOf()                       // [String]
GqlType.string().required().listOf()            // [String!]
GqlType.string().listOf().required()            // [String]!
GqlType.named('UserCreateInput').required()     // UserCreateInput!

Built-in scalars have their own factories (int, float, string, boolean, id); everything the schema defines itself — input objects, enums, custom scalars — comes from GqlType.named('…'), whose argument is validated as a GraphQL name. Types are immutable: listOf() and required() return new instances.

Variables are discovered by walking the operation on build(): arguments, directive arguments and values nested inside lists and objects are all scanned. Names are made unique automatically ($id, $id_2, …), and the same GqlVariable instance reused in several places is declared once:

import { GqlVariable } from '@iappx/gql-builder'

const id = new GqlVariable(GqlType.int().required(), 7, 'id')
const operation = Gql.query('user')
const root = operation.field('user')
root.addArg('id', Gql.useVariable(id))
root.field('parent').addArg('id', Gql.useVariable(id)).addFields(Gql.fields(['id']))
// query user($id: Int!) { user(id: $id) { parent(id: $id) { id } } }

Because values live outside the document, two calls that differ only in values produce byte-identical query text — which is what makes automatic persisted queries effective.

Values

Values can be built explicitly or converted from plain JavaScript:

Gql.string('text')          // "text"  — properly escaped, including U+2028 / U+2029
Gql.int(10)                 // 10      — rejects non-integers
Gql.float(1.5)              // 1.5     — rejects NaN / Infinity
Gql.boolean(true)           // true
Gql.nullValue()             // null
Gql.enumValue('DESC')       // DESC    — printed unquoted
Gql.list([Gql.int(1)])      // [1]
Gql.object({ a: Gql.int(1) })    // { a: 1 }

Gql.from(someValue)         // infers the value type
Gql.fromObject({ name: 'bob', tags: ['a', 'b'], createdAt: new Date() })

Gql.from maps number to Int or Float by Number.isInteger, Date to an ISO string, null and undefined to null, arrays to lists (element by element) and objects to input objects. Values that are already GqlValue instances pass through untouched, so you can mix them into plain data.

Field, argument, directive, alias, enum and variable names are validated against the GraphQL name grammar — an invalid name throws instead of producing a broken document.

Selections

Selection sets can be built from names, from a declarative tree, or field by field:

Gql.fields(['id', 'name'])

Gql.selection([
    { name: 'id' },
    { name: 'name', alias: 'title' },
    { name: 'posts', args: { limit: Gql.int(10) }, fields: [{ name: 'id' }] },
])

const field = Gql.field('user')
field.field('profile').addFields(Gql.fields(['avatar']))

TSelectionNode ({ name, alias?, args?, directives?, fields? }) is a plain data shape, which makes it the integration point for code that derives selections from its own metadata — an ORM, an entity registry, a column picker — without that code depending on the builder classes.

Directives

const directive = Gql.directive('orderBy', [
    Gql.arg('direction', Gql.enumValue('DESC')),
    Gql.arg('index', Gql.int(0)),
])
Gql.field('createdAt').addDirective(directive)
// createdAt @orderBy(direction: DESC, index: 0)

Directive arguments accept variables just like field arguments:

Gql.directive('filters', [
    Gql.arg('items', Gql.variable(GqlType.named('FilterInput').listOf(), filterItems, 'filters')),
])

Printing

operation.build()                    // { query, variables, operationName? }
operation.print()                    // compact, single line
operation.print({ pretty: true })    // indented
operation.print({ pretty: true, indent: '    ' })

The compact form is what you send; the pretty form is for logs and debugging. Printing is deterministic: the same tree always yields the same text, including variable names.

API

| Entry point | Purpose | |---|---| | Gql | Static facade over everything below — values, fields, directives, operations | | GqlOperation | Operation root: query / mutation / subscription, single, field, root, build, print | | GqlField | Field node: field, addField(s), addArg(s), addDirective(s), alias | | GqlArgument, GqlDirective | Argument and directive nodes | | GqlValue and subclasses | GqlStringValue, GqlIntValue, GqlFloatValue, GqlBooleanValue, GqlNullValue, GqlEnumValue, GqlListValue, GqlObjectValue, GqlVariableValue | | GqlValueFactory | fromJs, fromObject | | GqlVariable, GqlVariableRegistry | Variable declaration, naming and value collection | | GqlType | Variable types: int, float, string, boolean, id, named, listOf, required | | GqlSelectionFactory, TSelectionNode | Declarative selection trees | | GqlPrinter, TGqlPrintOptions | Document printing | | TGqlDocument | { query, variables, operationName? } |

Not supported (by design, for now): fragments, inline fragments and schema validation. The builder trusts the tree you give it and only checks what it can check without a schema.

License

Distributed under the MIT License. See LICENSE for more information.