@soda-gql/colocation-tools
v0.10.2
Published
Colocation utilities for soda-gql fragments
Maintainers
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-toolsUsage
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.operation({
name: "GetUser",
fields: ({ f }) => ({ ...f.user({ id: "1" })(({ f }) => ({ ...userFragment.spread() })) }),
}),
);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, $var }) =>
fragment.Query({
variables: { ...$var("userId").ID("!") },
fields: ({ f, $ }) => ({ ...f.user({ id: $.userId })(({ f }) => ({ ...f.id(), ...f.name(), ...f.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, $var, $colocate }) =>
query.operation({
name: "UserPage",
variables: { ...$var("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 dataThe 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, $var }) =>
fragment.Query({
variables: { ...$var("userId").ID("!") },
fields: ({ f, $ }) => ({ ...f.user({ id: $.userId })(({ f }) => ({ ...f.posts({})(({ f }) => ({ ...f.id(), ...f.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.postsDatacreateDirectParser
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.Mutation({ ... }))
.attach(createProjectionAttachment({ ... }));
// Direct parser - no labels needed
const parseResult = createDirectParser(productFragment);
const result = parseResult(executionResult);
// result is the projected type directlyRelated Packages
- @soda-gql/core - Core types and fragment definitions
- @soda-gql/runtime - Runtime operation handling
License
MIT
