npm package: @soda-gql/colocation-tools
Description: Colocation utilities for soda-gql fragments
Author: Shota Hatada
Latest version: 0.14.2

# @soda-gql/colocation-tools

Utilities for colocating GraphQL fragments with components in soda-gql. This package provides tools for fragment composition and data masking patterns.

## Features

- **Fragment colocation** - Keep GraphQL fragments close to components that use them
- **Data projection** - Create typed projections from fragment data
- **Type safety** - Full TypeScript support for fragment composition

## Installation

```bash
npm install @soda-gql/colocation-tools
# or
bun add @soda-gql/colocation-tools
```

## Usage

### Fragment Colocation Pattern

```typescript
import { createProjection, createExecutionResultParser } from "@soda-gql/colocation-tools";
import { userFragment } from "./graphql-system";

// Create a projection with paths and handle function
const userProjection = createProjection(userFragment, {
  paths: ["$.user"],
  handle: (result) => {
    if (result.isError()) return { error: result.error, user: null };
    if (result.isEmpty()) return { error: null, user: null };
    const [user] = result.unwrap(); // tuple of values for each path
    return { error: null, user };
  },
});

// Use with execution result parser
const parser = createExecutionResultParser({
  user: userProjection,
});
```

### Spreading Fragments

Fragments can be spread in operations:

```typescript
import { gql } from "./graphql-system";
import { userFragment } from "./UserCard";

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

### Using with $colocate

When composing multiple fragments in a single operation, use `$colocate` to prefix field selections with labels. The `createExecutionResultParser` will use these same labels to extract the corresponding data.

#### Complete Workflow

**Step 1: Define component fragments**

```typescript
// UserCard.tsx
export const userCardFragment = gql.default(({ fragment }) =>
  fragment("UserCard", "Query")`($userId: ID!) {
    user(id: $userId) {
      id
      name
      email
    }
  }`(),
);

export const userCardProjection = createProjection(userCardFragment, {
  paths: ["$.user"],
  handle: (result) => {
    if (result.isError()) return { error: result.error, user: null };
    if (result.isEmpty()) return { error: null, user: null };
    const [user] = result.unwrap();
    return { error: null, user };
  },
});
```

**Step 2: Compose operation with $colocate**

```typescript
// UserPage.tsx
import { userCardFragment, userCardProjection } from "./UserCard";
import { postListFragment, postListProjection } from "./PostList";

export const userPageQuery = gql.default(({ query, $colocate }) =>
  query("UserPage")({
    variables: `($userId: ID!)`,
    fields: ({ $ }) => $colocate({
      userCard: userCardFragment.spread({ userId: $.userId }),
      postList: postListFragment.spread({ userId: $.userId }),
    }),
  })({}),
);
```

**Step 3: Create parser with matching labels**

```typescript
const parseUserPageResult = createExecutionResultParser({
  userCard: userCardProjection,
  postList: postListProjection,
});
```

**Step 4: Parse execution result**

```typescript
const result = await executeQuery(userPageQuery);
const { userCard, postList } = parseUserPageResult(result);
// userCard and postList contain the projected data
```

The labels in `$colocate` (`userCard`, `postList`) must match the labels in `createExecutionResultParser` for proper data routing.

## API

### createProjection

Creates a typed projection from a fragment definition with specified paths and handler.

```typescript
import { createProjection } from "@soda-gql/colocation-tools";

const projection = createProjection(fragment, {
  // Field paths to extract (must start with "$.")
  paths: ["$.user"],
  // Handler to transform the sliced result (receives tuple of values for each path)
  handle: (result) => {
    if (result.isError()) return { error: result.error, data: null };
    if (result.isEmpty()) return { error: null, data: null };
    const [user] = result.unwrap();
    return { error: null, data: user };
  },
});
```

### createProjectionAttachment

Combines fragment definition and projection into a single export using `attach()`. This eliminates the need for separate projection definitions.

```typescript
import { createProjectionAttachment } from "@soda-gql/colocation-tools";
import { gql } from "./graphql-system";

export const postListFragment = gql
  .default(({ fragment }) =>
    fragment("PostList", "Query")`($userId: ID!) {
      user(id: $userId) {
        posts {
          id
          title
        }
      }
    }`(),
  )
  .attach(
    createProjectionAttachment({
      paths: ["$.user.posts"],
      handle: (result) => {
        if (result.isError()) return { error: result.error, posts: null };
        if (result.isEmpty()) return { error: null, posts: null };
        const [posts] = result.unwrap();
        return { error: null, posts: posts ?? [] };
      },
    }),
  );

// The fragment now has a .projection property
postListFragment.projection;
```

**Benefits**:
- Single export for both fragment and projection
- Fragment can be passed directly to `createExecutionResultParser`
- Reduces boilerplate when projection logic is simple

**Using with createExecutionResultParser**:

```typescript
const parseResult = createExecutionResultParser({
  userCard: { projection: userCardProjection }, // Explicit projection
  postList: postListFragment,                    // Fragment with attached projection
});
```

Both patterns work with the parser - it automatically detects fragments with attached projections.

### createExecutionResultParser

Creates a parser from labeled projections to process GraphQL execution results.

```typescript
import { createExecutionResultParser } from "@soda-gql/colocation-tools";

const parser = createExecutionResultParser({
  userData: userProjection,
  postsData: postsProjection,
});

const results = parser(executionResult);
// results.userData, results.postsData
```

### createDirectParser

For single fragment operations (like mutations), use `createDirectParser` for simpler parsing without `$colocate`:

```typescript
import { createDirectParser } from "@soda-gql/colocation-tools";

// Fragment with attached projection
const productFragment = gql
  .default(({ fragment }) => fragment("ProductFields", "Product")`{ ... }`())
  .attach(createProjectionAttachment({ ... }));

// Direct parser - no labels needed
const parseResult = createDirectParser(productFragment);
const result = parseResult(executionResult);
// result is the projected type directly
```

## Related Packages

- [@soda-gql/core](../core) - Core types and fragment definitions
- [@soda-gql/core](../core) - Core types, runtime (`@soda-gql/core/runtime`)

## License

MIT