@benzene/core
Version:
Fast, minimal, agnostic GraphQL Libraries
187 lines (183 loc) • 6.4 kB
JavaScript
import { execute, subscribe, GraphQLError, validate, GraphQLSchema, validateSchema, print, parse, getOperationAST } from 'graphql';
import lru from 'tiny-lru';
function isAsyncIterator(val) {
return typeof Object(val)[Symbol.asyncIterator] === "function";
}
/**
* Create a compileQuery function using graphql-js
* @returns CompileQuery
*/
function makeCompileQuery() {
return function compileQuery(schema) {
return {
execute(args) {
return execute({ ...args, schema });
},
subscribe(args) {
return subscribe({ ...args, schema });
},
};
};
}
function isExecutionResult(val) {
return (typeof val === "object" &&
val !== null &&
(Array.isArray(val.errors) ||
(typeof val.data === "object" &&
!Array.isArray(val.data))));
}
/**
* Validate whether an operation does not exist
* or operationName is missing. Even though
* execution will realize this, we need to provide
* this hint mainly for handlers to avoid execution
* @param operation
* @param operationName
*/
function validateOperationName(operation, operationName) {
if (operation)
return [];
if (!operationName) {
return [
new GraphQLError("Must provide operation name if query contains multiple operations."),
];
}
return [new GraphQLError(`Unknown operation named "${operationName}".`)];
}
/**
* Given a GraphQLError, format it according to the rules described by the
* Response Format, Errors section of the GraphQL Specification.
*/
function formatError(error) {
const formattedError = {
message: error.message,
};
if (error.locations != null) {
formattedError.locations = error.locations;
}
if (error.path != null) {
formattedError.path = error.path;
}
if (error.extensions != null && Object.keys(error.extensions).length > 0) {
formattedError.extensions = error.extensions;
}
return formattedError;
}
class Benzene {
constructor(options) {
if (!options)
throw new TypeError("GQL must be initialized with options");
this.validateFn = options.validateFn || validate;
this.validationRules = options.validationRules;
this.formatErrorFn = options.formatErrorFn || formatError;
this.contextFn = options.contextFn;
// build cache
this.lru = lru(1024);
// construct schema and validate
if (!(options.schema instanceof GraphQLSchema)) {
throw new Error(`Expected ${options.schema} to be a GraphQL schema.`);
}
const schemaValidationErrors = validateSchema(options.schema);
if (schemaValidationErrors.length > 0) {
throw schemaValidationErrors;
}
this.schema = options.schema;
this.compileQuery = options.compileQuery || makeCompileQuery();
}
compile(query, operationName) {
let document;
if (typeof query === "object") {
// query is DocumentNode
document = query;
query = print(document);
}
const key = query + (operationName ? `:${operationName}` : "");
let cached = this.lru.get(key);
if (cached) {
return cached;
}
else {
if (!document) {
if (!query)
throw new Error("Must provide document.");
try {
document = parse(query);
}
catch (syntaxErr) {
return {
errors: [syntaxErr],
};
}
}
const validationErrors = this.validateFn(this.schema, document, this.validationRules);
if (validationErrors.length > 0) {
return {
errors: validationErrors,
};
}
const compiled = this.compileQuery(this.schema, document, operationName);
// Compilation is a failure since its result is ExecutionResult
if (isExecutionResult(compiled))
return compiled;
cached = compiled;
cached.document = document;
const operation = getOperationAST(document, operationName)?.operation;
if (operation) {
// If we could not determine the operation, it is unsafe to cache
cached.operation = operation;
this.lru.set(key, cached);
}
return cached;
}
}
formatExecutionResult(result) {
const o = {};
if (result.data)
o.data = result.data;
if (result.errors)
o.errors = result.errors.map(this.formatErrorFn);
return o;
}
async graphql({ source, contextValue, variableValues, operationName, rootValue, }) {
const cachedOrResult = this.compile(source, operationName);
if (isExecutionResult(cachedOrResult))
return cachedOrResult;
return this.execute({
document: cachedOrResult.document,
contextValue,
variableValues,
rootValue,
operationName,
compiled: cachedOrResult,
});
}
execute(args) {
if (!args.compiled) {
if (!args.document)
throw new Error("Must provide document.");
const compiledOrResult = this.compile(args.document);
if (isExecutionResult(compiledOrResult))
return compiledOrResult;
args.compiled = compiledOrResult;
}
else {
args.document = args.compiled.document;
}
return args.compiled.execute(args);
}
async subscribe(args) {
if (!args.compiled) {
if (!args.document)
throw new Error("Must provide document.");
const compiledOrResult = this.compile(args.document);
if (isExecutionResult(compiledOrResult))
return compiledOrResult;
args.compiled = compiledOrResult;
}
else {
args.document = args.compiled.document;
}
return args.compiled.subscribe(args);
}
}
export { Benzene, isAsyncIterator, isExecutionResult, makeCompileQuery, validateOperationName };