# GraphQL Client

Execute GraphQL queries and mutations against configured GraphQL endpoints with full type safety and runtime validation.

## Methods

| Method                                                 | Description                                                |
| ------------------------------------------------------ | ---------------------------------------------------------- |
| `query<T>(query, schema, variables?, metadata?)`       | Execute a GraphQL query with required schema validation    |
| `mutation<T>(mutation, schema, variables?, metadata?)` | Execute a GraphQL mutation with required schema validation |

## Usage

### Basic Query

```typescript
import { api, z, graphql } from "@superblocksteam/sdk-api";

// Integration ID from the integrations panel
const PROD_GRAPHQL = "a1b2c3d4-5678-90ab-cdef-graphql00001";

// Schema must include the `data` wrapper that GraphQL returns
const GetUserResponseSchema = z.object({
  data: z.object({
    user: z.object({
      id: z.string(),
      name: z.string(),
      email: z.string().email(),
    }),
  }),
});

export default api({
  integrations: {
    graphql: graphql(PROD_GRAPHQL),
  },
  name: "GraphQLExample",
  input: z.object({
    userId: z.string(),
  }),
  output: z.object({
    user: z.object({
      id: z.string(),
      name: z.string(),
      email: z.string(),
    }),
  }),
  async run(ctx, { userId }) {
    const result = await ctx.integrations.graphql.query(
      `query GetUser($id: ID!) {
        user(id: $id) {
          id
          name
          email
        }
      }`,
      { response: GetUserResponseSchema },
      { id: userId },
    );

    // Access data through the data property
    return { user: result.data.user };
  },
});
```

### Query with List Results

```typescript
const ListUsersResponseSchema = z.object({
  data: z.object({
    users: z.array(
      z.object({
        id: z.string(),
        name: z.string(),
        role: z.enum(["admin", "user", "guest"]),
      }),
    ),
    totalCount: z.number(),
  }),
});

const result = await ctx.integrations.graphql.query(
  `query ListUsers($limit: Int, $offset: Int) {
    users(limit: $limit, offset: $offset) {
      id
      name
      role
    }
    totalCount
  }`,
  { response: ListUsersResponseSchema },
  { limit: 10, offset: 0 },
);

console.log(`Found ${result.data.totalCount} users`);
result.data.users.forEach((user) => {
  console.log(`${user.name} (${user.role})`);
});
```

### Basic Mutation

```typescript
const CreateUserResponseSchema = z.object({
  data: z.object({
    createUser: z.object({
      id: z.string(),
      name: z.string(),
      createdAt: z.string(),
    }),
  }),
});

const result = await ctx.integrations.graphql.mutation(
  `mutation CreateUser($input: CreateUserInput!) {
    createUser(input: $input) {
      id
      name
      createdAt
    }
  }`,
  { response: CreateUserResponseSchema },
  {
    input: {
      name: "John Doe",
      email: "john@example.com",
    },
  },
);

console.log(`Created user: ${result.data.createUser.id}`);
```

### Mutation with Complex Input Types

```typescript
const UpdateOrderResponseSchema = z.object({
  data: z.object({
    updateOrder: z.object({
      id: z.string(),
      status: z.string(),
      items: z.array(
        z.object({
          productId: z.string(),
          quantity: z.number(),
        }),
      ),
    }),
  }),
});

const result = await ctx.integrations.graphql.mutation(
  `mutation UpdateOrder($id: ID!, $input: UpdateOrderInput!) {
    updateOrder(id: $id, input: $input) {
      id
      status
      items {
        productId
        quantity
      }
    }
  }`,
  { response: UpdateOrderResponseSchema },
  {
    id: "order-123",
    input: {
      status: "shipped",
      items: [
        { productId: "prod-1", quantity: 2 },
        { productId: "prod-2", quantity: 1 },
      ],
    },
  },
);
```

### Handling Optional and Nullable Fields

```typescript
const UserProfileResponseSchema = z.object({
  data: z.object({
    user: z.object({
      id: z.string(),
      name: z.string(),
      bio: z.string().nullable(), // Field can be null
      avatar: z.string().optional(), // Field may not be present
      settings: z
        .object({
          theme: z.string(),
          notifications: z.boolean(),
        })
        .nullable(), // Entire object can be null
    }),
  }),
});
```

## Trace Metadata

All methods accept an optional `metadata` parameter as the last argument for diagnostics labeling. See the [root SDK README](../../../README.md#trace-metadata) for details.

## Common Pitfalls

### Response Must Include `data` Wrapper

GraphQL responses are wrapped in a `data` object. Your schema must account for this:

```typescript
// WRONG - Missing data wrapper
const schema = z.object({
  user: z.object({
    id: z.string(),
    name: z.string(),
  }),
});

// CORRECT - Include data wrapper
const schema = z.object({
  data: z.object({
    user: z.object({
      id: z.string(),
      name: z.string(),
    }),
  }),
});
```

### Access Results Through `data` Property

The result object includes the `data` wrapper, so you must access fields through it:

```typescript
const result = await ctx.integrations.graphql.query(
  `query { user(id: "123") { name } }`,
  {
    response: ResponseSchema,
  },
);

// WRONG - Direct access
console.log(result.user.name); // undefined or error

// CORRECT - Access through data
console.log(result.data.user.name);
```

### Schema Parameter is Required

Both `query()` and `mutation()` require a schema for validation:

```typescript
// WRONG - Missing schema
const result = await ctx.integrations.graphql.query(
  `query { users { id } }`,
  { id: "123" }, // This looks like variables, not schema
);

// CORRECT - Schema is required
const result = await ctx.integrations.graphql.query(
  `query GetUser($id: ID!) { user(id: $id) { id } }`,
  { response: ResponseSchema }, // Schema is required
  { id: "123" }, // Variables are optional third parameter
);
```

### Handling GraphQL Errors

GraphQL errors are returned in the response, not thrown as exceptions. Consider including them in your schema:

```typescript
const ResponseSchema = z.object({
  data: z
    .object({
      user: z.object({
        id: z.string(),
        name: z.string(),
      }),
    })
    .nullable(), // data can be null if errors occur
  errors: z
    .array(
      z.object({
        message: z.string(),
        locations: z
          .array(
            z.object({
              line: z.number(),
              column: z.number(),
            }),
          )
          .optional(),
        path: z.array(z.union([z.string(), z.number()])).optional(),
      }),
    )
    .optional(),
});

const result = await ctx.integrations.graphql.query(query, {
  response: ResponseSchema,
});

if (result.errors && result.errors.length > 0) {
  console.error("GraphQL errors:", result.errors);
}

if (result.data) {
  console.log("User:", result.data.user);
}
```

### Variables Must Match GraphQL Types

Ensure variable values match the expected GraphQL types:

```typescript
// GraphQL schema expects: query GetUser($id: ID!)

// WRONG - Number instead of string
const result = await ctx.integrations.graphql.query(
  query,
  { response: schema },
  { id: 123 },
);

// CORRECT - ID type expects string
const result = await ctx.integrations.graphql.query(
  query,
  { response: schema },
  { id: "123" },
);
```

### Handling Unions and Interfaces

For GraphQL unions or interfaces, use discriminated unions in Zod:

```typescript
const SearchResultSchema = z.object({
  data: z.object({
    search: z.array(
      z.discriminatedUnion("__typename", [
        z.object({
          __typename: z.literal("User"),
          id: z.string(),
          name: z.string(),
        }),
        z.object({
          __typename: z.literal("Post"),
          id: z.string(),
          title: z.string(),
        }),
      ]),
    ),
  }),
});

const result = await ctx.integrations.graphql.query(
  `query Search($term: String!) {
    search(term: $term) {
      __typename
      ... on User {
        id
        name
      }
      ... on Post {
        id
        title
      }
    }
  }`,
  { response: SearchResultSchema },
  { term: "john" },
);

result.data.search.forEach((item) => {
  if (item.__typename === "User") {
    console.log(`User: ${item.name}`);
  } else {
    console.log(`Post: ${item.title}`);
  }
});
```

## Error Handling

### RestApiValidationError

Thrown when the GraphQL response fails schema validation:

```typescript
import { RestApiValidationError } from "@superblocksteam/sdk-api";

try {
  const result = await ctx.integrations.graphql.query(query, {
    response: schema,
  });
} catch (error) {
  if (error instanceof RestApiValidationError) {
    console.error("Validation failed:", error.message);
    console.error("Zod errors:", error.details.zodError);
    console.error("Actual response:", error.details.data);
  }
}
```

## API Reference

- [GraphQL Specification](https://spec.graphql.org/)
- [GraphQL Learn](https://graphql.org/learn/)
