Operations

Operations are complete GraphQL queries, mutations, or subscriptions. They define variables, select fields, and produce executable GraphQL documents.

How soda-gql Operations Differ from GraphQL

In standard GraphQL, operations are written as query strings:

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

soda-gql operations are TypeScript functions with two syntax options:

AspectGraphQLsoda-gql (Tagged Template)soda-gql (Options-Object Path)
DefinitionString-basedTemplate literals with GraphQL syntaxTypeScript builder functions
Variables$name: Type!($name: Type!) in templatevariables: \($name: Type!)``
Field SelectionsImplicitGraphQL syntax in templateObject spread: ({ ...f("id")() })
Type CheckingRequires codegenBuild-time validationBuild-time validation
Best forSimple queries/mutations$colocate, programmatic control
Recommended Syntax

Use tagged templates for most operations. Switch to the options-object path when you need $colocate for fragment colocation or programmatic field control. See the Tagged Template Syntax Guide for details.

Operation Types

soda-gql supports three operation types:

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

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

// Subscription - real-time updates
gql.default(({ subscription }) =>
  subscription("UserUpdated")`($userId: ID!) {
    userUpdated(userId: $userId) { id name }
  }`()
);

Defining an Operation

The simplest way to define an operation uses tagged template syntax — write GraphQL directly as a template literal:

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

export const getUserQuery = gql.default(({ query }) =>
  query("GetUser")`($userId: ID!) {
    user(id: $userId) {
      id
      name
      email
    }
  }`(),
);

This is concise, readable, and familiar to developers who know GraphQL syntax. The trailing () finalizes the operation.

Options-Object Path

For advanced features like field aliases, directives, or $colocate, use the options-object path:

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

export const getUserQuery = gql.default(({ query }) =>
  query("GetUser")({
    variables: `($userId: ID!)`,                      // Variable declarations
    fields: ({ f, $ }) => ({
      // Field selections
      ...f("user", { id: $.userId })(({ f }) => ({
        ...f("id")(),
        ...f("name")(),
        ...f("email")(),
      })),
    }),
  })({}),
);

Operation Options

OptionTypeDescription
variables`($name: Type!)`Template literal with GraphQL variable syntax
metadatafunctionOptional. Runtime metadata (see Metadata)
fieldsfunctionRequired. Field selection builder function

Field Selections

Field selections in operations work the same as in fragments:

({ f, $ }) => ({
  // Scalar fields
  ...f("id")(),
  ...f("createdAt")(),

  // Fields with arguments
  ...f("posts", { limit: 10 })(({ f }) => ({
    ...f("id")(),
    ...f("title")(),
  })),

  // Nested selections
  ...f("user", { id: $.userId })(({ f }) => ({
    ...f("id")(),
    ...f("profile")(({ f }) => ({
      ...f("avatarUrl")(),
      ...f("bio")(),
    })),
  })),
})

Spreading Fragments

Tagged Template (Interpolation)

In tagged templates, use ${...} interpolation to spread fragments:

import { userFragment } from "./user.fragment";

export const getUserQuery = gql.default(({ query }) =>
  query("GetUser")`($userId: ID!) {
    user(id: $userId) {
      ...${userFragment}
    }
  }`(),
);

For fragments with variables, use a callback function to pass variable bindings:

export const getUserQuery = gql.default(({ query }) =>
  query("GetUser")`($userId: ID!, $includeEmail: Boolean) {
    user(id: $userId) {
      ...${({ $ }) => userFragment.spread({ includeEmail: $.includeEmail })}
    }
  }`(),
);

Options-Object Path (.spread())

In the options-object path, use .spread():

import { userFragment } from "./user.fragment";

export const getUserQuery = gql.default(({ query }) =>
  query("GetUser")({
    variables: `($userId: ID!, $includeEmail: Boolean)`,
    fields: ({ f, $ }) => ({
      ...f("user", { id: $.userId })(({ f }) => ({
        // Spread fragment with variable passing
        ...userFragment.spread({ includeEmail: $.includeEmail }),
      })),
    }),
  })({}),
);

When a fragment has variables, you must pass values for them. These can be:

  • Literal values: { includeEmail: true }
  • Operation variables: { includeEmail: $.includeEmail }

Operation Output

Every operation provides two key properties:

.document

The compiled GraphQL document string, ready to send to a GraphQL server:

console.log(getUserQuery.document);
// query GetUser($userId: ID!) {
//   user(id: $userId) {
//     id
//     name
//     email
//   }
// }

Type Inference

Extract TypeScript types from operations:

// Input type (variables required for this operation)
type GetUserVariables = typeof getUserQuery.$infer.input;
// { userId: string }

// Output type (parsed response structure)
type GetUserResult = typeof getUserQuery.$infer.output.projected;
// { user: { id: string; name: string; email: string } }

Mutations

Mutations follow the same pattern as queries:

export const createUserMutation = gql.default(({ mutation }) =>
  mutation("CreateUser")`($input: CreateUserInput!) {
    createUser(input: $input) {
      id
      name
    }
  }`(),
);

// Usage
const result = await graphqlClient({
  document: createUserMutation.document,
  variables: {
    input: { name: "Alice", email: "alice@example.com" },
  },
});

Next Steps