frg

frg.ts
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

dependenciesToMappedQuery.ts
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

struct.ts
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

resolver.ts
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"
})