GQL fragments from schema
What does this do?
When creating your fragments you will need to define both the schema which is a zod schema and the fragment for the GraphQL query. These two shapes are usually identical so writing it out multiple times can get annoying. This utility aims to make it easier while still allowing you to customise it as needed.
Usage
Once you have your schema you’ll want to call this method, passing in the .shape property into the function.
const SCHEMA = z.object({ name: z.string(), age: z.number(), author: AuthorFrg.schema, profileUrl: z.string().url(), profileImage: z.union([ImageFrg.schema, GifFrg.schema])})
const FRAGMENT = fragmentFromShape(SCHEMA.shape, { author: AuthorFrg, profileUrl: `profileUrl: url`, profileImage: [ImageFrg, GifFrg]})This will output the following fragment (pretty much):
nameageauthor { ...AuthorFragment}profileUrl: urlprofileImage { __typename ... on Image { ...ImageFragment } ... on Gif { ...GifFragment }}The second parameter is completely optional. However, there are a few times you will need to add it:
- If you’re using additional pigeon definitions you’ll need to add it to the object - see
author - If you want to use field renaming OR you want to override what is used for a specific key - see
profileUrl - If you have a
unionof pigeon definitions - seeprofileImage
For pigeon definitions, if you were to not provide it in the second parameter the function will attempt to crawl through those schemas as well for as long as it can. Which kinda defeats the purpose of creating the fragments, but there might be a situation where you want that.
Source
// TODO: Grab the `frg` and `dependenciesToMappedQuery` utilities
/** * This will generate a fragment based on the shape of a zod shape. * * The second optional param is used to inject dependencies to use * their fragments instead of continuing down the object tree. * * You need to specifiy the key in the query and the dependency you * want to use. For example `image: ImageFrg`, will result in the * following: * * `image { ...ImageFrg.fragmentName }`. * * You can pass in an array to utilise the functionality of the * `dependenciesToMappedQuery` utility, useful when a single key * can have multiple types (unions for example). * * You can also pass in a string which will use that value "as-is". * **So it is important to include the key in the value as well!!** * * ```ts * const s = z.object({ renamed: z... }) * fragmentFromShape(s.shape, { * renamed: `renamed: original` * }) * ``` */export function fragmentFromShape( shape: z.ZodRawShape, dependencies?: Record<string, Dependency | Dependency[] | string>,) { const fragment: string[] = []
const handleObjectType = (key: string, obj: z.ZodObject<any>) => { fragment.push( `${key} { ${fragmentFromShape(obj.shape as z.ZodRawShape, dependencies)} }`, ) }
const handleWrappedSchema = (key: string, obj: z.ZodTypeAny) => { switch (true) { case obj instanceof z.ZodObject: handleObjectType(key, obj) break case obj instanceof z.ZodArray: handleZodShape(key, obj._def.type) break case obj instanceof z.ZodOptional: case obj instanceof z.ZodNullable: const unwrapped = unwrapOptional(obj) if (unwrapped) { handleZodShape(key, unwrapped) } break default: handleZodShape(key, obj) } }
const handleZodShape = (key: string, obj: z.ZodTypeAny) => { if (dependencies && key in dependencies) { const dependency = dependencies[key] if (Array.isArray(dependency)) { fragment.push( `${key} { ${frg(dependenciesToMappedQuery(dependency))} }`, ) } else if (typeof dependency === "string") { fragment.push(dependency) } else { fragment.push(`${key} { ...${dependency.fragmentName} }`) } return }
switch (true) { case obj instanceof z.ZodOptional: case obj instanceof z.ZodNullable: case obj instanceof z.ZodArray: handleWrappedSchema(key, obj) break case obj instanceof z.ZodEffects: handleWrappedSchema(key, obj._def.schema) break case obj instanceof z.ZodObject: handleObjectType(key, obj) break default: fragment.push(key) break } }
for (const [key, obj] of Object.entries(shape)) { handleZodShape(key, obj) }
return fragment.join(" ").replace(/\s+|\\n|\\t/gm, " ")}
/** * Some types can be either optional or nullable, when this happens * we want to drill down to the concrete type to know what we * should do next. For example; a nullable string will most likely * just use the key in the shape, however you may have a nullable object * with keys that needs to be processed as well, without this unwrapping * step it will ignore those child keys entirely. */function unwrapOptional(schema: z.ZodTypeAny) { const unwrapLimit = 10 let object: z.ZodTypeAny = schema
for (let i = 0; i < unwrapLimit; i++) { try { object = (object as any).unwrap() } catch { return object } }
return undefined}