JSPM

  • Created
  • Published
  • Downloads 353
  • Score
    100M100P100Q116282F
  • License MIT

Colocation utilities for soda-gql fragments

Package Exports

  • @soda-gql/colocation-tools
  • @soda-gql/colocation-tools/package.json

Readme

@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

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

Usage

Fragment Colocation Pattern

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:

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

// 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

// 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

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

Step 4: Parse execution result

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.

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.

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:

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.

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:

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

License

MIT