UNPKG

graphql-request

Version:

Minimal GraphQL client supporting Node and browsers for scripts or simple apps.

270 lines (234 loc) 10.1 kB
import { RootTypeName } from '../../lib/graphql.js' import { assertArray, assertObject, lowerCaseFirstLetter } from '../../lib/prelude.js' import { Schema } from '../1_Schema/__.js' import { readMaybeThunk } from '../1_Schema/core/helpers.js' import type { ReturnModeType } from '../5_client/Config.js' import type { SelectionSet } from './__.js' import { isSelectFieldName } from './helpers.js' import { parseClientDirectiveDefer } from './runtime/directives/defer.js' import { toGraphQLDirective } from './runtime/directives/directive.js' import { parseClientDirectiveInclude } from './runtime/directives/include.js' import { parseClientDirectiveSkip } from './runtime/directives/skip.js' import { parseClientDirectiveStream } from './runtime/directives/stream.js' import { parseClientFieldItem } from './runtime/FieldItem.js' import { parseClientFieldName, toGraphQLFieldName } from './runtime/FieldName.js' import type { Indicator } from './runtime/indicator.js' import { isIndicator, isPositiveIndicator } from './runtime/indicator.js' import { parseClientOn, toGraphQLOn } from './runtime/on.js' type SpecialFields = { // todo - this requires having the schema at runtime to know which fields to select. // $scalars?: SelectionSet.Indicator $include?: SelectionSet.Directive.Include['$include'] $skip?: SelectionSet.Directive.Skip['$skip'] $defer?: SelectionSet.Directive.Defer['$defer'] $stream?: SelectionSet.Directive.Stream['$stream'] $?: Args } type Args = { [k: string]: ArgValue } type ArgValue = string | boolean | null | number | Args export type DocumentObject = Record<string, GraphQLRootSelection> export type GraphQLRootSelection = { query: GraphQLObjectSelection } | { mutation: GraphQLObjectSelection } export type GraphQLObjectSelection = Record<string, Indicator | SS> export type SS = { [k: string]: Indicator | SS } & SpecialFields type FieldValue = SS | Indicator export interface Context { schemaIndex: Schema.Index config: { returnMode: ReturnModeType } } export const rootTypeSelectionSet = ( context: Context, objectDef: Schema.Object$2, selectionSet: GraphQLObjectSelection, operationName: string = ``, ) => { const operationTypeName = lowerCaseFirstLetter(objectDef.fields.__typename.type.type) return `${operationTypeName} ${operationName} { ${resolveObjectLikeFieldValue(context, objectDef, selectionSet)} }` } const resolveDirectives = (fieldValue: FieldValue) => { if (isIndicator(fieldValue)) return `` const { $include, $skip, $defer, $stream } = fieldValue let directives = `` if ($stream !== undefined) { directives += toGraphQLDirective(parseClientDirectiveStream($stream)) } if ($defer !== undefined) { directives += toGraphQLDirective(parseClientDirectiveDefer($defer)) } if ($include !== undefined) { directives += toGraphQLDirective(parseClientDirectiveInclude($include)) } if ($skip !== undefined) { directives += toGraphQLDirective(parseClientDirectiveSkip($skip)) } return directives } const resolveArgValue = ( context: Context, schemaArgTypeMaybeThunk: Schema.Input.Any, argValue: ArgValue, ): string => { if (argValue === null) return String(null) // todo could check if index agrees is nullable. const schemaArgType = readMaybeThunk(schemaArgTypeMaybeThunk) switch (schemaArgType.kind) { case `nullable`: return resolveArgValue(context, schemaArgType.type, argValue) case `list`: { assertArray(argValue) const value = argValue.map(_ => resolveArgValue(context, schemaArgType.type, _ as ArgValue)) return `[${value.join(`, `)}]` } case `InputObject`: { assertObject(argValue) const entries = Object.entries(argValue).map(([argName, argValue]) => { const schemaArgField = schemaArgType.fields[argName] as Schema.Input.Field | undefined if (!schemaArgField) throw new Error(`Arg not found: ${argName}`) return [argName, resolveArgValue(context, schemaArgField.type, argValue)] }) return `{ ${entries.map(([k, v]) => `${k!}: ${v!}`).join(`, `)} }` } case `Enum`: { return String(argValue) } case `Scalar`: { // @ts-expect-error fixme return JSON.stringify(schemaArgType.codec.encode(argValue)) } default: throw new Error(`Unsupported arg kind: ${JSON.stringify(schemaArgType)}`) } } const resolveArgs = (context: Context, schemaField: Schema.SomeField, ss: Indicator | SS) => { if (isIndicator(ss)) return `` const { $ } = ss if ($ === undefined) return `` const schemaArgs = schemaField.args if (!schemaArgs) throw new Error(`Field has no args`) const argEntries = Object.entries($) if (argEntries.length === 0) return `` return `(${ argEntries.map(([argFieldName, v]) => { const schemaArgField = schemaArgs.fields[argFieldName] as Schema.Input.Any | undefined // eslint-disable-line if (!schemaArgField) throw new Error(`Arg field ${argFieldName} not found in schema.`) const valueEncoded = resolveArgValue(context, schemaArgField, v) return `${argFieldName}: ${valueEncoded}` }).join(`, `) })` } const pruneNonSelections = (ss: SS) => { const entries = Object.entries(ss) const selectEntries = entries.filter(_ => !_[0].startsWith(`$`)) return Object.fromEntries(selectEntries) } const resolveFieldValue = ( context: Context, schemaField: Schema.SomeField, fieldValue: null | FieldValue, ): string => { if (fieldValue === null) return `null` // todo test this case if (isIndicator(fieldValue)) return `` const entries = Object.entries(fieldValue) const directives = resolveDirectives(fieldValue) const args = resolveArgs(context, schemaField, fieldValue) const selects = entries.filter(_ => isSelectFieldName(_[0])) if (selects.length === 0) { return `${args} ${directives}` } const selection = Object.fromEntries(selects) as GraphQLObjectSelection // eslint-disable-next-line // @ts-ignore ID error const schemaNamedOutputType = Schema.Output.unwrapToNamed(schemaField.type) as Schema.Object$2 return `${args} ${directives} { ${resolveObjectLikeFieldValue(context, readMaybeThunk(schemaNamedOutputType), selection)} }` } export const resolveObjectLikeFieldValue = ( context: Context, schemaItem: Schema.Object$2 | Schema.Union | Schema.Interface, fieldValue: FieldValue, ): string => { // todo optimize by doing single loop const applicableSelections = Object.entries(fieldValue).filter(([_, ss]) => isPositiveIndicator(ss)) as [ string, FieldValue, ][] switch (schemaItem.kind) { case `Object`: { const rootTypeName = (RootTypeName as Record<string, RootTypeName>)[schemaItem.fields.__typename.type.type] ?? null return applicableSelections.map(([clientFieldName, ss]) => { const fieldName = parseClientFieldName(clientFieldName) const schemaField = schemaItem.fields[fieldName.actual] if (!schemaField) throw new Error(`Field ${clientFieldName} not found in schema object`) /** * Inject __typename field for result fields that are missing it. */ // dprint-ignore if (rootTypeName && context.config.returnMode === `successData` && context.schemaIndex.error.rootResultFields[rootTypeName][fieldName.actual]) { (ss as Record<string, boolean>)[`__typename`] = true } return `${toGraphQLFieldName(fieldName)} ${resolveFieldValue(context, schemaField, ss)}` }).join(`\n`) + `\n` } case `Interface`: { return applicableSelections.map(([ClientFieldName, ss]) => { const fieldItem = parseClientFieldItem(ClientFieldName) switch (fieldItem._tag) { case `FieldName`: { if (fieldItem.actual === `__typename`) { return `${toGraphQLFieldName(fieldItem)} ${resolveDirectives(ss)}` } const schemaField = schemaItem.fields[fieldItem.actual] if (!schemaField) throw new Error(`Field ${ClientFieldName} not found in schema object`) return `${toGraphQLFieldName(fieldItem)} ${resolveFieldValue(context, schemaField, ss)}` } case `On`: { const schemaObject = context.schemaIndex[`objects`][fieldItem.typeOrFragmentName] if (!schemaObject) throw new Error(`Fragment ${fieldItem.typeOrFragmentName} not found in schema`) return `${toGraphQLOn(fieldItem)} ${resolveDirectives(ss)} { ${ resolveObjectLikeFieldValue(context, schemaObject, ss) } }` } default: { throw new Error(`Unknown field item tag`) } } }).join(`\n`) + `\n` } case `Union`: { return applicableSelections.map(([fieldExpression, ss]) => { const fieldItem = parseClientFieldItem(fieldExpression) switch (fieldItem._tag) { case `FieldName`: { if (fieldItem.actual === `__typename`) { return `${toGraphQLFieldName(fieldItem)} ${resolveDirectives(ss)}` } // todo throw new Error(`todo resolve common interface fields from unions`) } case `On`: { const schemaObject = context.schemaIndex[`objects`][fieldItem.typeOrFragmentName] if (!schemaObject) throw new Error(`Fragment ${fieldItem.typeOrFragmentName} not found in schema`) // if (isIndicator(ss)) throw new Error(`Union field must have selection set`) return `${toGraphQLOn(fieldItem)} ${resolveDirectives(ss)} { ${ // @ts-expect-error fixme resolveObjectLikeFieldValue(context, schemaObject, pruneNonSelections(ss))} }` } default: { throw new Error(`Unknown field item tag`) } } }).join(`\n`) + `\n` } default: throw new Error(`Unknown schema item kind`) } } export const resolveOn = (field: string) => { const on = parseClientOn(field) if (on) return toGraphQLOn(on) return field }