Common Utilities
Here are some common utilities that you might find useful when working with Pigeon.
frg
export const frg = (
strings: readonly string[] | ArrayLike<string>,
...values: any[]
) => {
const templated = String.raw({ raw: strings }, ...values)
.replace(/\n/g, "")
.replace(/\s{1,}/g, " ");
return templated.startsWith("__typename ")
? templated
: `__typename ${templated}`;
};
Utilises Tagged Templates to create a GraphQL fragment.
This will append __typename
to the start of the fragment and remove all new lines and multiple spaces. If the fragment
starts with __typename
it will not be appended. If the fragment contains __typename
anywhere else, we will still
append it to the start. We will not remove duplicate __typename
‘s. This is a simple string append, we don’t analyse
the fragment.
Example
const registration = createRegistration({
fragment: frg`
id
name
`
})
// registration.fragment === "__typename id name"
dependenciesToMappedQuery
export function dependenciesToMappedQuery(
registrations: (RegistrationStruct | DependencyStruct)[]
): string {
return registrations
.map((value) => `... on ${value.__typename} { ...${value.fragmentName} }`)
.join("\n");
}
Sometimes a field can have multiple options, for example, a Component
field that can be a HeroBanner
or Image
. In that case you would use dependenciesToMappedQuery
to map the dependencies to a query.
Before
const query = `
component {
__typename
... on HeroBanner { ...${HeroBannerFragment.fragmentName} }
... on Asset { ...${ImageFragment.fragmentName} }
}
`
After
const query = `
component {
__typename
${dependenciesToMappedQuery([HeroBannerFragment, ImageFragment])}
}
`
Struct
import type { RegistrationStruct, DependencyStruct } from '@adrocodes/pigeon';
import { z } from 'zod';
type Struct<R extends RegistrationStruct | DependencyStruct> = z.infer<R["schema"]>
This is a utility type that will infer the schema of a createRegistration
or createDependency
.
Example
type ImageStruct = Struct<typeof ImageFragment>;
resolver
import { z, type ZodError, type ZodSchema } from "zod";
// IMPORTANT: Replace with YOUR client
import { getClient, type QueryInput } from "./client";
type Resolver<T extends ZodSchema, QE extends unknown, ZE extends unknown> = {
schema: T;
query: QueryInput;
onQueryError: (error: Error) => QE;
onZodError: (error: ZodError) => ZE;
label?: string;
};
export async function resolver<
T extends ZodSchema,
QE extends unknown,
ZE extends unknown
>(
input: Resolver<T, QE, ZE>
): Promise<
| z.infer<T>
| ReturnType<Resolver<T, QE, ZE>["onQueryError"]>
| ReturnType<Resolver<T, QE, ZE>["onZodError"]>
> {
const { query, schema, onQueryError, onZodError, label = "Unknown" } = input;
const { data, error } = await getClient().query(query);
if (error) {
console.error(`[${label}] - Query Error`, {
error,
message: error.message,
});
return onQueryError(error);
}
const parsed = await schema.safeParseAsync(data);
if (!parsed.success) {
console.error(`[${label}] - Zod Error`, {
error: parsed.error,
});
return onZodError(parsed.error);
}
return parsed.data;
}
This is a utility function that will resolve a query using a Zod schema. It will return the data if successful, or call the onQueryError
or onZodError
functions if there is an error.
You’ll need to provide your own getClient
function and QueryInput
type.
Example
const data = await resolver({
query: {},
schema: z.object({...}),
onQueryError: (error) => {...},
onZodError: (error) => {...},
label: "GetPageData"
})