@soda-gql/core

Core GraphQL types, utilities, and primitives for soda-gql.

Installation

bun add @soda-gql/core

Overview

@soda-gql/core provides the foundational types and utilities for defining GraphQL fragments and operations.

defineScalar

Define custom scalar types with input/output transformations:

import { defineScalar } from "@soda-gql/core";

export const scalar = {
  // Simple syntax
  ...defineScalar<"ID", string, string>("ID"),
  ...defineScalar<"String", string, string>("String"),

  // Callback syntax with directives
  ...defineScalar("DateTime", ({ type }) => ({
    input: type<string>(),
    output: type<Date>(),
    directives: {},
  })),
} as const;

Parameters

ParameterDescription
nameThe GraphQL scalar name
options or callbackType configuration

Callback Parameters

PropertyTypeDescription
inputtype<T>()TypeScript type for input (variables)
outputtype<T>()TypeScript type for output (responses)
directivesobjectDirective definitions

gql (Generated)

The gql object is generated per-schema and provides builders:

import { gql } from "@/graphql-system";

// Fragment builder
gql.default(({ fragment }) => fragment.User({ fields: ({ f }) => ({ ... }) }));

// Query operation
gql.default(({ query }) => query.operation({ name: "...", fields: ({ f, $ }) => ({ ... }) }));

// Mutation operation
gql.default(({ mutation }) => mutation.operation({ name: "...", fields: ({ f, $ }) => ({ ... }) }));

// Subscription operation (planned)
gql.default(({ subscription }) => subscription.operation({ name: "...", fields: ({ f, $ }) => ({ ... }) }));

Element Extensions (attach)

The attach() method extends gql elements with custom properties:

import type { GqlElementAttachment } from "@soda-gql/core";

export const userFragment = gql
  .default(({ fragment }) =>
    fragment.User({
      fields: ({ f }) => ({
        ...f.id(),
        ...f.name(),
      }),
    }),
  )
  .attach({
    name: "utils",
    createValue: (element) => ({
      getDisplayName: (user: typeof element.$infer.output) =>
        user.name.toUpperCase(),
    }),
  });

// Usage
userFragment.utils.getDisplayName(userData);

GqlElementAttachment Interface

interface GqlElementAttachment<TElement, TName extends string, TValue> {
  name: TName;
  createValue: (element: TElement) => TValue;
}

Chaining Attachments

Multiple attachments can be chained:

const fragment = gql
  .default(...)
  .attach(attachment1)
  .attach(attachment2);

// Access both
fragment.attachment1Name;
fragment.attachment2Name;

Metadata API

Define runtime metadata on operations:

gql.default(({ query, $var }) =>
  query.operation({
    name: "GetUser",
    variables: { ...$var("id").ID("!") },
    metadata: ({ $, document, $var }) => ({
      headers: { "X-Request-ID": "get-user" },
      custom: { requiresAuth: true, hash: hashDocument(document) },
    }),
    fields: ({ f, $ }) => ({
      ...
    }),
  }),
);

Metadata Structure

PropertyTypeDescription
headersRecord<string, string>HTTP headers
customRecord<string, unknown>Application-specific values

Accessing Metadata

const meta = operation.metadata({ id: "123" });
console.log(meta.headers);
console.log(meta.custom);

$var Helper Methods

The $var object provides methods for inspecting VarRef values. These are available in the metadata callback and can be used to extract information from variable references.

$var.getName(ref)

Get the variable name from a VarRef.

$var.getName($.userId)  // Returns "userId"

Throws: If the VarRef contains a nested-value instead of a variable reference.

$var.getValue(ref)

Get the const value from a VarRef when the variable was assigned a literal value.

// When $.status was assigned a literal value like "active"
$var.getValue($.status)  // Returns "active"

Throws: If the VarRef contains a variable reference, or if the nested-value contains any VarRef inside.

$var.getInner(ref)

Get the raw inner structure of a VarRef.

$var.getInner($.userId)
// Returns { type: "variable", name: "userId" }

$var.getNameAt(ref, selector)

Get the variable name at a specific path within an input type variable.

// Given a variable defined with an input type containing nested VarRefs
// e.g., $var("filter").UserFilter("!") where UserFilter has { userId: $.id }
$var.getNameAt($.filter, p => p.userId)  // Returns "id"

Parameters:

  • ref: A VarRef (typically from $ in metadata callback)
  • selector: A function that navigates to the target path, e.g., p => p.userId

Throws: If the path doesn't lead to a VarRef with type "variable".

$var.getValueAt(ref, selector)

Get the const value at a specific path within an input type variable.

// Given a variable like $var("categoryId").String_comparison_exp("?")
// When called with { _eq: "tech", _neq: "spam" }
$var.getValueAt($.categoryId, p => p._eq)   // Returns "tech"
$var.getValueAt($.categoryId, p => p._neq)  // Returns "spam"

Parameters:

  • ref: A VarRef (typically from $ in metadata callback)
  • selector: A function that navigates to the target path, e.g., p => p._eq

Throws: If the path leads to a VarRef, or if the value at the path contains any nested VarRef.

Variable Type Syntax Reference

Complete reference for the $var().Type() type specifier:

Basic Types

SpecifierGraphQLTypeScript
"ID:!"ID!string
"ID:?"IDstring | undefined
"String:!"String!string
"String:?"Stringstring | undefined
"Int:!"Int!number
"Int:?"Intnumber | undefined
"Float:!"Float!number
"Float:?"Floatnumber | undefined
"Boolean:!"Boolean!boolean
"Boolean:?"Booleanboolean | undefined

List Types

SpecifierGraphQLDescription
"String:![]!"[String!]!Required list of required strings
"String:![]?"[String!]Optional list of required strings
"String:?[]!"[String]!Required list of optional strings
"String:?[]?"[String]Optional list of optional strings

Nested Lists

SpecifierGraphQL
"Int:![]![]!"[[Int!]!]!
"String:?[]?[]?"[[String]]

Custom Types

$var("input").CreateUserInput("!")
$var("filters").FilterInput("![]?")

Field Selection Patterns Reference

Complete reference for field selection API:

PatternExampleDescription
Basic fieldf.id()Select a scalar field
With argumentsf.posts({ limit: 10 })Field with arguments
Nested (curried)f.posts()(({ f }) => ({ ... }))Nested selections
With aliasf.id(null, { alias: "userId" })Renamed field
Fragment spreaduserFragment.spread({})Spread fragment fields
Fragment with varsuserFragment.spread({ a: $.b })Pass variables

Type Inference

Extract TypeScript types using $infer:

// Fragment types
type UserInput = typeof userFragment.$infer.input;
type UserOutput = typeof userFragment.$infer.output;

// Operation types
type QueryVariables = typeof query.$infer.input;
type QueryResult = typeof query.$infer.output.projected;

// Metadata type
type QueryMeta = typeof query.$infer.metadata;

Runtime Exports

The /runtime subpath provides runtime utilities:

import { gqlRuntime } from "@soda-gql/core/runtime";

// Get registered operation
const operation = gqlRuntime.getOperation("canonicalId");

TypeScript Requirements

  • TypeScript 5.x or later for full type inference
  • Strict mode recommended for best type safety

defineAdapter

Create a typed adapter with helpers, metadata configuration, and document transformation:

import { defineAdapter } from "@soda-gql/core/adapter";

const adapter = defineAdapter({
  helpers: {
    auth: {
      requiresLogin: () => ({ requiresAuth: true }),
    },
  },
  metadata: {
    aggregateFragmentMetadata: (fragments) => ({
      count: fragments.length,
    }),
    schemaLevel: { apiVersion: "v2" },
  },
  transformDocument: ({ document, operationType }) => {
    // Modify document AST
    return document;
  },
});

Adapter Type

type Adapter<THelpers, TFragmentMetadata, TAggregatedFragmentMetadata, TSchemaLevel> = {
  helpers?: THelpers;
  metadata?: MetadataAdapter<TFragmentMetadata, TAggregatedFragmentMetadata, TSchemaLevel>;
  transformDocument?: DocumentTransformer<TSchemaLevel, TAggregatedFragmentMetadata>;
};

DocumentTransformArgs

Arguments passed to adapter-level transformDocument:

PropertyTypeDescription
documentDocumentNodeThe GraphQL document to transform
operationNamestringThe operation name
operationTypeOperationType"query", "mutation", or "subscription"
variableNamesreadonly string[]Variable names defined for this operation
schemaLevelTSchemaLevel | undefinedSchema-level configuration
fragmentMetadataTAggregatedFragmentMetadata | undefinedAggregated fragment metadata

OperationDocumentTransformArgs

Arguments passed to operation-level transformDocument:

PropertyTypeDescription
documentDocumentNodeThe GraphQL document to transform
metadataTOperationMetadata | undefinedTyped operation metadata

Operation transformDocument Option

Operations can define their own document transform with typed metadata:

gql.default(({ query, $var }) =>
  query.operation({
    name: "GetUser",
    variables: { ...$var("id").ID("!") },
    metadata: () => ({ cacheHint: 300 }),
    transformDocument: ({ document, metadata }) => {
      // metadata is typed as { cacheHint: number }
      return document;
    },
    fields: ({ f, $ }) => ({ ... }),
  }),
);

Transform Order: Operation transform runs first, then adapter transform.

See Also