@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. Two syntax styles are available:

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

// Fragment
gql.default(({ fragment }) =>
  fragment("UserFields", "User")`{ id name email }`()
);

// Query
gql.default(({ query }) =>
  query("GetUser")`($id: ID!) { user(id: $id) { id name } }`()
);

// Mutation
gql.default(({ mutation }) =>
  mutation("CreateUser")`($input: CreateUserInput!) { createUser(input: $input) { id } }`()
);

// Subscription
gql.default(({ subscription }) =>
  subscription("OnUserCreated")`{ userCreated { id name } }`()
);

Callback Builder Syntax

For advanced features (field aliases, directives, $colocate):

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

// Fragment (tagged template)
gql.default(({ fragment }) =>
  fragment("UserFields", "User")`{ id name }`(),
);

// Query (options-object path — useful for $dir, aliases, programmatic field control)
gql.default(({ query }) =>
  query("GetUser")({ variables: `($id: ID!)`, fields: ({ f, $ }) => ({ ... }) })({}),
);

// Mutation (options-object path)
gql.default(({ mutation }) =>
  mutation("CreateUser")({ variables: `($input: CreateUserInput!)`, fields: ({ f, $ }) => ({ ... }) })({}),
);

See the Tagged Template Syntax Guide for a complete comparison.

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("UserFields", "User")`{ id 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 }) =>
  query("GetUser")`($id: ID!) { user(id: $id) { id name } }`({
    metadata: ({ $, document, $var }) => ({
      headers: { "X-Request-ID": "get-user" },
      custom: { requiresAuth: true, hash: hashDocument(document) },
    }),
  }),
);

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., a variable declared as ($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 declared as ($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

Variables are declared using inline GraphQL syntax in the variables template string or the variables option:

Inline Syntax (Tagged Template)

Variables are written directly in the tagged template using standard GraphQL variable declaration syntax:

query("GetUser")`($id: ID!) { user(id: $id) { id name } }`()

Options-Object Syntax

When using the options-object path, pass a template literal string to variables:

query("GetUser")({ variables: `($id: ID!)`, fields: ({ f, $ }) => ({ ... }) })({})

Basic Types

GraphQLTypeScript
$id: ID!string
$id: IDstring | undefined
$name: String!string
$name: Stringstring | undefined
$count: Int!number
$count: Intnumber | undefined
$score: Float!number
$score: Floatnumber | undefined
$active: Boolean!boolean
$active: Booleanboolean | undefined

List Types

GraphQLDescription
$tags: [String!]!Required list of required strings
$tags: [String!]Optional list of required strings
$tags: [String]!Required list of optional strings
$tags: [String]Optional list of optional strings

Nested Lists

GraphQL
$matrix: [[Int!]!]!
$matrix: [[String]]

Custom Types

query("CreateUser")`($input: CreateUserInput!) { createUser(input: $input) { id } }`()
query("Search")`($filters: [FilterInput!]) { search(filters: $filters) { id } }`()

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 }) =>
  query("GetUser")`($id: ID!) { user(id: $id) { id name } }`({
    metadata: () => ({ cacheHint: 300 }),
    transformDocument: ({ document, metadata }) => {
      // metadata is typed as { cacheHint: number }
      return document;
    },
  }),
);

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

See Also