@soda-gql/core
Core GraphQL types, utilities, and primitives for soda-gql.
Installation
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
| Parameter | Description |
|---|
name | The GraphQL scalar name |
options or callback | Type configuration |
Callback Parameters
| Property | Type | Description |
|---|
input | type<T>() | TypeScript type for input (variables) |
output | type<T>() | TypeScript type for output (responses) |
directives | object | Directive 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
| Property | Type | Description |
|---|
headers | Record<string, string> | HTTP headers |
custom | Record<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
| Specifier | GraphQL | TypeScript |
|---|
"ID:!" | ID! | string |
"ID:?" | ID | string | undefined |
"String:!" | String! | string |
"String:?" | String | string | undefined |
"Int:!" | Int! | number |
"Int:?" | Int | number | undefined |
"Float:!" | Float! | number |
"Float:?" | Float | number | undefined |
"Boolean:!" | Boolean! | boolean |
"Boolean:?" | Boolean | boolean | undefined |
List Types
| Specifier | GraphQL | Description |
|---|
"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
| Specifier | GraphQL |
|---|
"Int:![]![]!" | [[Int!]!]! |
"String:?[]?[]?" | [[String]] |
Custom Types
$var("input").CreateUserInput("!")
$var("filters").FilterInput("![]?")
Field Selection Patterns Reference
Complete reference for field selection API:
| Pattern | Example | Description |
|---|
| Basic field | f.id() | Select a scalar field |
| With arguments | f.posts({ limit: 10 }) | Field with arguments |
| Nested (curried) | f.posts()(({ f }) => ({ ... })) | Nested selections |
| With alias | f.id(null, { alias: "userId" }) | Renamed field |
| Fragment spread | userFragment.spread({}) | Spread fragment fields |
| Fragment with vars | userFragment.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:
| Property | Type | Description |
|---|
document | DocumentNode | The GraphQL document to transform |
operationName | string | The operation name |
operationType | OperationType | "query", "mutation", or "subscription" |
variableNames | readonly string[] | Variable names defined for this operation |
schemaLevel | TSchemaLevel | undefined | Schema-level configuration |
fragmentMetadata | TAggregatedFragmentMetadata | undefined | Aggregated fragment metadata |
OperationDocumentTransformArgs
Arguments passed to operation-level transformDocument:
| Property | Type | Description |
|---|
document | DocumentNode | The GraphQL document to transform |
metadata | TOperationMetadata | undefined | Typed 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