@graphql-ts/extend

Search for an npm package
'use strict';
Object.defineProperty(exports, '__esModule', { value: true });
var graphql = require('graphql');
var schema = require('@graphql-ts/schema');
/**
* An API to extend an arbitrary {@link GraphQLSchema} with `@graphql-ts/schema`.
* Note if you're building a schema entirely with `@graphql-ts/schema`, you
* shouldn't use this package. This is useful when you have a
* {@link GraphQLSchema} from somewhere else and you want to some fields to
* various places in it.
*
* See {@link extend} for more details.
*
* @module
*/
const builtinScalars = new Set(graphql.specifiedScalarTypes.map(x => x.name));
/**
* `extend` allows you to extend a {@link GraphQLSchema} with
* `@graphql-ts/schema`.
*
* ```ts
* const originalSchema = new GraphQLSchema({ ...etc });
*
* const extendedSchema = extend({
* query: {
* hello: g.field({
* type: g.String,
* resolve() {
* return "Hello!";
* },
* }),
* },
* })(originalSchema);
* ```
*
* To use existing types from the schema you're extending, you can provide a
* function and use the {@link BaseSchemaMeta} passed into the function to use
* existing types in the schema.
*
* ```ts
* const originalSchema = new GraphQLSchema({ ...etc });
*
* const extendedSchema = extend((base) => ({
* query: {
* something: g.field({
* type: base.object("Something"),
* resolve() {
* return { something: true };
* },
* }),
* },
* }))(originalSchema);
* ```
*
* See {@link BaseSchemaMeta} for how to get other types from the schema
*
* `extend` will currently throw an error if the query or mutation types are
* used in other types like this. This will be allowed in a future version.
*
* ```graphql
* type Query {
* thing: Query
* }
* ```
*/
function extend(extension) {
return schema => {
const getType = name => {
const graphQLType = schema.getType(name);
if (graphQLType == null) {
throw new Error(`No type named ${JSON.stringify(name)} exists in the schema that is being extended`);
}
return graphQLType;
};
const resolvedExtension = flattenExtensions(typeof extension === "function" ? extension({
schema,
object(name) {
const graphQLType = getType(name);
if (!graphql.isObjectType(graphQLType)) {
throw new Error(`There is a type named ${JSON.stringify(name)} in the schema being extended but it is not an object type`);
}
return graphQLType;
},
inputObject(name) {
const graphQLType = getType(name);
if (!graphql.isInputObjectType(graphQLType)) {
throw new Error(`There is a type named ${JSON.stringify(name)} in the schema being extended but it is not an input object type`);
}
return graphQLType;
},
enum(name) {
const graphQLType = getType(name);
if (!graphql.isEnumType(graphQLType)) {
throw new Error(`There is a type named ${JSON.stringify(name)} in the schema being extended but it is not an enum type`);
}
return graphQLType;
},
interface(name) {
const graphQLType = getType(name);
if (!graphql.isInterfaceType(graphQLType)) {
throw new Error(`There is a type named ${JSON.stringify(name)} in the schema being extended but it is not an interface type`);
}
return graphQLType;
},
scalar(name) {
if (builtinScalars.has(name)) {
throw new Error(`The names of built-in scalars cannot be passed to BaseSchemaInfo.scalar but ${name} was passed`);
}
const graphQLType = getType(name);
if (!graphql.isScalarType(graphQLType)) {
throw new Error(`There is a type named ${JSON.stringify(name)} in the schema being extended but it is not a scalar type`);
}
return graphQLType;
},
union(name) {
const graphQLType = getType(name);
if (!graphql.isUnionType(graphQLType)) {
throw new Error(`There is a type named ${JSON.stringify(name)} in the schema being extended but it is not a union type`);
}
return graphQLType;
}
}) : extension);
const queryType = schema.getQueryType();
const mutationType = schema.getMutationType();
const typesToFind = new Set();
if (queryType) {
typesToFind.add(queryType);
}
if (mutationType) {
typesToFind.add(mutationType);
}
const usages = findObjectTypeUsages(schema, typesToFind);
if (usages.size) {
throw new Error(`@graphql-ts/extend doesn't yet support using the query and mutation types in other types but\n${[...usages].map(([type, usages]) => {
return `- ${JSON.stringify(type)} is used at ${usages.map(x => JSON.stringify(x)).join(", ")}`;
}).join("\n")}`);
}
if (!resolvedExtension.mutation && !resolvedExtension.query) {
return schema;
}
const newQueryType = extendObjectType(queryType, resolvedExtension.query || {}, "Query");
const newMutationType = extendObjectType(mutationType, resolvedExtension.mutation || {}, "Mutation");
const schemaConfig = schema.toConfig();
let types = [...(queryType || !newQueryType ? [] : [newQueryType]), ...(mutationType || !newMutationType ? [] : [newMutationType]), ...schemaConfig.types.map(type => {
if (newQueryType && type.name === (queryType === null || queryType === void 0 ? void 0 : queryType.name)) {
return newQueryType;
}
if (newMutationType && type.name === (mutationType === null || mutationType === void 0 ? void 0 : mutationType.name)) {
return newMutationType;
}
return type;
})];
const updatedSchema = new graphql.GraphQLSchema({
...schemaConfig,
query: newQueryType,
mutation: newMutationType,
types
});
return updatedSchema;
};
}
function printFieldOnType(type, fieldName) {
const printed = graphql.printType(type);
const document = graphql.parse(printed);
const parsed = document.definitions[0];
const parsedField = parsed.fields.find(x => x.name.value === fieldName);
return graphql.print(parsedField);
}
function extendObjectType(existingType, fieldsToAdd, defaultName) {
const hasNewFields = Object.entries(fieldsToAdd).length;
if (!hasNewFields) {
return existingType;
}
const existingTypeConfig = existingType === null || existingType === void 0 ? void 0 : existingType.toConfig();
const newFields = {
...(existingTypeConfig === null || existingTypeConfig === void 0 ? void 0 : existingTypeConfig.fields)
};
for (const [key, val] of Object.entries(fieldsToAdd)) {
if (newFields[key]) {
var _name;
throw new Error(`The schema extension defines a field ${JSON.stringify(key)} on the ${JSON.stringify((_name = existingType.name) !== null && _name !== void 0 ? _name : defaultName)} type but that type already defines a field with that name.\nThe original field:\n${printFieldOnType(existingType, key)}\nThe field added by the extension:\n${printFieldOnType(new graphql.GraphQLObjectType({
name: "ForError",
fields: {
[key]: val
}
}), key)}`);
}
newFields[key] = val;
}
return new graphql.GraphQLObjectType({
name: defaultName,
...existingTypeConfig,
fields: newFields
});
}
// https://github.com/microsoft/TypeScript/issues/17002
const isReadonlyArray = Array.isArray;
const operations = ["query", "mutation"];
function flattenExtensions(extensions) {
if (isReadonlyArray(extensions)) {
const resolvedExtension = {
mutation: {},
query: {}
};
for (const extension of extensions) {
for (const operation of operations) {
const fields = extension[operation];
if (fields) {
for (const [key, val] of Object.entries(fields)) {
if (resolvedExtension[operation][key]) {
throw new Error(`More than one extension defines a field named ${JSON.stringify(key)} on the ${operation} type.\nThe first field:\n${printFieldOnType(new schema.GObjectType({
name: "ForError",
fields: {
[key]: val
}
}), key)}\nThe second field:\n${printFieldOnType(new schema.GObjectType({
name: "ForError",
fields: {
[key]: resolvedExtension[operation][key]
}
}), key)}`);
}
resolvedExtension[operation][key] = val;
}
}
}
}
return resolvedExtension;
}
return extensions;
}
/**
* Any
*
* Note the distinct usages of `any` vs `unknown` is intentional.
*
* - The `unknown` used for the source type is because the source isn't known and
* it shouldn't generally be used here because these fields are on the query
* and mutation types
* - The first `any` used for the `Args` type parameter is used because `Args` is
* invariant so only `Record<string, Arg<InputType, boolean>>` would work with
* it. The arguable unsafety here doesn't really matter because people will
* always use `g.field`
* - The `any` in `OutputType` and the last type argument mean that a field that
* requires any context can be provided. This is unsafe, the only way this
* could arguably be made more "safe" is by making this unknown which would
* requiring casting or make `extend` and etc. generic over a `Context` but
* given this is immediately used on an arbitrary {@link GraphQLSchema} so the
* type would immediately be thrown away, it would be pretty much pointless.
*/
/**
* An extension to a GraphQL schema. This currently only supports adding fields
* to the query and mutation types. Extending other types will be supported in
* the future.
*/
/**
* This object contains the schema being extended and functions to get GraphQL
* types from the schema.
*/
function findObjectTypeUsages(schema, types) {
const usages = new Map();
for (const [name, type] of Object.entries(schema.getTypeMap())) {
if (graphql.isInterfaceType(type) || graphql.isObjectType(type)) {
for (const [fieldName, field] of Object.entries(type.getFields())) {
const namedType = graphql.getNamedType(field.type);
if (graphql.isObjectType(namedType) && types.has(namedType)) {
getOrDefault(usages, namedType, []).push(`${name}.${fieldName}`);
}
}
}
if (graphql.isUnionType(type)) {
for (const member of type.getTypes()) {
if (types.has(member)) {
getOrDefault(usages, member, []).push(name);
}
}
}
}
return usages;
}
function getOrDefault(input, key, defaultValue) {
if (!input.has(key)) {
input.set(key, defaultValue);
return defaultValue;
}
return input.get(key);
}
exports.extend = extend;