@pothos/plugin-add-graphql
Version:
A Pothos plugin for adding existing GraphQL types to a Pothos schema
334 lines (291 loc) • 9.89 kB
text/typescript
import './global-types';
import SchemaBuilder, {
type ArgumentRef,
type EnumRef,
type EnumValueConfigMap,
type InputFieldRef,
InputListRef,
type InputType,
type InputTypeParam,
ListRef,
type ObjectParam,
type ObjectRef,
type OutputType,
type SchemaTypes,
type TypeParam,
} from '@pothos/core';
import {
defaultFieldResolver,
type GraphQLEnumType,
type GraphQLInputObjectType,
type GraphQLInputType,
type GraphQLInterfaceType,
type GraphQLObjectType,
type GraphQLOutputType,
type GraphQLType,
type GraphQLUnionType,
isListType,
isNonNullType,
} from 'graphql';
import type {
AddGraphQLEnumTypeOptions,
AddGraphQLInputTypeOptions,
AddGraphQLInterfaceTypeOptions,
AddGraphQLObjectTypeOptions,
AddGraphQLUnionTypeOptions,
EnumValuesWithShape,
} from './types';
import { addReferencedType } from './utils';
const proto = SchemaBuilder.prototype as PothosSchemaTypes.SchemaBuilder<SchemaTypes>;
function resolveNullableOutputRef(
builder: PothosSchemaTypes.SchemaBuilder<SchemaTypes>,
type: GraphQLOutputType,
): OutputType<SchemaTypes> {
if (isNonNullType(type)) {
throw new Error('Expected a nullable type');
}
if (isListType(type)) {
const nullable = !isNonNullType(type.ofType);
const listType = nullable ? type.ofType : type.ofType.ofType;
return new ListRef(resolveNullableOutputRef(builder, listType), nullable);
}
addReferencedType(builder, type);
return type.name as OutputType<SchemaTypes>;
}
function resolveNullableInputRef(
builder: PothosSchemaTypes.SchemaBuilder<SchemaTypes>,
type: GraphQLType,
): InputTypeParam<SchemaTypes> {
if (isNonNullType(type)) {
throw new Error('Expected a nullable type');
}
if (isListType(type)) {
const required = isNonNullType(type.ofType);
const listType = required ? type.ofType.ofType : type.ofType;
return new InputListRef(resolveNullableInputRef(builder, listType), required);
}
addReferencedType(builder, type);
return type.name as InputType<SchemaTypes>;
}
function resolveOutputType(
builder: PothosSchemaTypes.SchemaBuilder<SchemaTypes>,
type: GraphQLOutputType,
): { type: TypeParam<SchemaTypes>; nullable: boolean } {
const isNullable = !isNonNullType(type);
const nonNullable = isNonNullType(type) ? type.ofType : type;
const typeRef = resolveNullableOutputRef(builder, nonNullable);
return {
type: typeRef,
nullable: isNullable,
};
}
function resolveInputType(
builder: PothosSchemaTypes.SchemaBuilder<SchemaTypes>,
type: GraphQLInputType,
): { type: InputTypeParam<SchemaTypes>; required: boolean } {
const isNullable = !isNonNullType(type);
const nonNullable = isNonNullType(type) ? type.ofType : type;
const typeRef = resolveNullableInputRef(builder, nonNullable);
return {
type: typeRef,
required: !isNullable,
};
}
proto.addGraphQLObject = function addGraphQLObject<Shape>(
type: GraphQLObjectType<Shape>,
{ fields, extensions, ...options }: AddGraphQLObjectTypeOptions<SchemaTypes, Shape> = {},
) {
const typeOptions = {
...options,
description: type.description ?? undefined,
isTypeOf: type.isTypeOf as never,
extensions: { ...type.extensions, ...extensions },
interfaces: () => type.getInterfaces().map((i) => resolveNullableOutputRef(this, i)) as [],
fields: (t: PothosSchemaTypes.ObjectFieldBuilder<SchemaTypes, Shape>) => {
const existingFields = type.getFields();
const newFields = fields?.(t) ?? {};
const combinedFields: typeof newFields = {
...newFields,
};
for (const [fieldName, field] of Object.entries(existingFields)) {
if (newFields[fieldName] !== undefined) {
if (newFields[fieldName] === null) {
delete combinedFields[fieldName];
}
continue;
}
const args: Record<string, ArgumentRef<SchemaTypes>> = {};
for (const { name, ...arg } of field.args) {
const input = resolveInputType(this, arg.type);
args[name] = t.arg({
...input,
description: arg.description ?? undefined,
deprecationReason: arg.deprecationReason ?? undefined,
defaultValue: arg.defaultValue,
extensions: arg.extensions,
});
}
combinedFields[fieldName] = t.field({
...resolveOutputType(this, field.type),
args,
description: field.description ?? undefined,
deprecationReason: field.deprecationReason ?? undefined,
extensions: field.extensions,
resolve: (field.resolve ?? defaultFieldResolver) as never,
...(field.subscribe ? { subscribe: field.subscribe } : {}),
});
}
return combinedFields as {};
},
};
switch (type.name) {
case 'Query':
this.queryType(typeOptions as never);
return 'Query' as never;
case 'Mutation':
this.mutationType(typeOptions as never);
return 'Mutation' as never;
case 'Subscription':
this.subscriptionType(typeOptions as never);
return 'Subscription' as never;
default:
return this.objectRef<Shape>(options?.name ?? type.name).implement(typeOptions as never);
}
};
proto.addGraphQLInterface = function addGraphQLInterface<Shape = unknown>(
type: GraphQLInterfaceType,
{ fields, extensions, ...options }: AddGraphQLInterfaceTypeOptions<SchemaTypes, Shape> = {},
) {
const ref = this.interfaceRef<Shape>(options?.name ?? type.name);
ref.implement({
...options,
description: type.description ?? undefined,
resolveType: type.resolveType as never,
extensions: { ...type.extensions, ...extensions },
interfaces: () => type.getInterfaces().map((i) => resolveNullableOutputRef(this, i)) as [],
fields: (t) => {
const existingFields = type.getFields();
const newFields = fields?.(t) ?? {};
const combinedFields: typeof newFields = {
...newFields,
};
for (const [fieldName, field] of Object.entries(existingFields)) {
if (newFields[fieldName] !== undefined) {
if (newFields[fieldName] === null) {
delete combinedFields[fieldName];
}
continue;
}
const args: Record<string, ArgumentRef<SchemaTypes>> = {};
for (const { name, ...arg } of field.args) {
args[name] = t.arg({
...resolveInputType(this, arg.type),
description: arg.description ?? undefined,
deprecationReason: arg.deprecationReason ?? undefined,
defaultValue: arg.defaultValue,
extensions: arg.extensions,
});
}
combinedFields[fieldName] = t.field({
...resolveOutputType(this, field.type),
args,
description: field.description ?? undefined,
deprecationReason: field.deprecationReason ?? undefined,
resolve: field.resolve as never,
extensions: field.extensions,
});
}
return combinedFields as {};
},
});
return ref;
};
proto.addGraphQLUnion = function addGraphQLUnion<Shape>(
type: GraphQLUnionType,
{
types,
extensions,
...options
}: AddGraphQLUnionTypeOptions<SchemaTypes, ObjectRef<SchemaTypes, Shape>> = {},
) {
return this.unionType<ObjectParam<SchemaTypes>, Shape>(options?.name ?? type.name, {
...options,
description: type.description ?? undefined,
resolveType: type.resolveType as never,
extensions: { ...type.extensions, ...extensions },
types: types ?? (type.getTypes().map((t) => resolveNullableOutputRef(this, t)) as []),
});
};
proto.addGraphQLEnum = function addGraphQLEnum<Shape extends number | string>(
type: GraphQLEnumType,
{
values,
extensions,
...options
}: AddGraphQLEnumTypeOptions<SchemaTypes, EnumValuesWithShape<SchemaTypes, Shape>> = {},
) {
const newValues =
values ??
type.getValues().reduce<EnumValueConfigMap<SchemaTypes>>((acc, value) => {
acc[value.name] = {
value: value.value as never,
description: value.description ?? undefined,
deprecationReason: value.deprecationReason ?? undefined,
extensions: value.extensions,
};
return acc;
}, {});
const ref: EnumRef<SchemaTypes, Shape> = this.enumType<
never,
EnumValuesWithShape<SchemaTypes, Shape>
>(
(options?.name ?? type.name) as never,
{
...options,
description: type.description ?? undefined,
extensions: { ...type.extensions, ...extensions },
values: newValues,
} as never,
);
return ref;
};
proto.addGraphQLInput = function addGraphQLInput<Shape extends {}>(
type: GraphQLInputObjectType,
{
name = type.name,
fields,
extensions,
...options
}: AddGraphQLInputTypeOptions<SchemaTypes, Shape> = {},
) {
const ref = this.inputRef<Shape>(name);
return ref.implement({
...options,
description: type.description ?? undefined,
extensions: { ...type.extensions, ...extensions },
isOneOf: type.isOneOf,
fields: (t) => {
const existingFields = type.getFields();
const newFields: Record<string, InputFieldRef<SchemaTypes, unknown> | null> =
fields?.(t) ?? {};
const combinedFields: typeof newFields = {
...newFields,
};
for (const [fieldName, field] of Object.entries(existingFields)) {
if (newFields[fieldName] !== undefined) {
if (newFields[fieldName] === null) {
delete combinedFields[fieldName];
}
continue;
}
combinedFields[fieldName] = t.field({
...resolveInputType(this, field.type),
description: field.description ?? undefined,
defaultValue: field.defaultValue,
extensions: field.extensions,
});
}
return combinedFields as never;
},
}) as never;
};