UNPKG

graphqly

Version:
518 lines (458 loc) 13.3 kB
const { makeExecutableSchema, addResolveFunctionsToSchema } = require("graphql-tools"); const _ = require("lodash"); const { PubSub, withFilter } = require("graphql-subscriptions"); const Type = require("./type"); const Query = require("./query"); const Mutation = require("./mutation"); const Subscription = require("./subscription"); const Input = require("./input"); const Interface = require("./interface"); const Enum = require("./enum"); const { Definition, Hookable, Logger, LoggerFactory } = require("../base"); const { omitNil, hasFields } = require("../utils"); const pubsub = new PubSub(); class SchemaBuilder { constructor(options = {}) { this._types = {}; this._enums = {}; this._inputs = {}; this._ifaces = {}; this._queries = {}; this._mutations = {}; this._subscriptions = {}; this.use = this.use.bind(this); this._clear(); this.configure(options); } getLogger(name) { if (this._loggerFactory) { return this._loggerFactory.getLogger(name); } return undefined; } configure(options = {}) { if (options.loggerFactory) { if (!(options.loggerFactory instanceof LoggerFactory)) { throw new Error("Invalid configuration for LoggerFactory"); } this._loggerFactory = options.loggerFactory; } } /** * Just a simpple wrapper. It's gonna be overriden by `Hookable` * @param {*} fn */ wrap(fn) { return fn; } /** * Use definitions provided by providers * * @param {any} providers A function or a collection of functions which `this` is binded to the current instance of SchemaBuilder * @return {SchemaBuilder} The schema * @memberof SchemaBuilder */ use(providers) { if (_.isFunction(providers)) { providers(this); return this; } if (_.isArray(providers)) { _.forEach(providers, provider => this.use(provider)); return this; } throw new Error("Invalid providers"); } type(name) { if (_.has(this._types, name)) { throw new Error(`Redefinition of type "${name}"`); } const t = new Type(name); this._types[name] = t; return t; } enum(name) { if (_.has(this._enums, name)) { throw new Error(`Redefinition of enum "${name}"`); } const e = new Enum(name); this._enums[name] = e; return e; } input(name) { if (_.has(this._inputs, name)) { throw new Error(`Redefinition of input "${name}"`); } const i = new Input(name); this._inputs[name] = i; return i; } iface(name) { if (_.has(this._ifaces, name)) { throw new Error(`Redefinition of interface "${name}"`); } const i = new Interface(name); this._ifaces[name] = i; return i; } query(def) { const q = new Query(def); const name = q._name; if (_.isNil(name)) { throw new Error("Invalid query"); } if (_.has(this._queries, name)) { throw new Error(`Redefinition of query "${name}"`); } this._queries[name] = q; return q; } mutation(def) { const m = new Mutation(def); const name = m._name; if (_.isNil(name)) { throw new Error("Invalid mutation"); } if (_.has(this._mutations, name)) { throw new Error(`Redefinition of mutation "${name}"`); } this._mutations[name] = m; return m; } subscription(def) { const m = new Subscription(def); const name = m._name; if (_.isNil(name)) { throw new Error("Invalid subscription"); } if (_.has(this._subscriptions, name)) { throw new Error(`Redefinition of subscription "${name}"`); } this._subscriptions[name] = m; return m; } _validateStructures() { const validate = structure => { _.forEach(structure, s => { if (_.isNil(s._def) && _.isNil(s._parent)) { throw new Error(`${s._kind} "${s._name}" must provide a definition`); } }); }; validate(this._ifaces); validate(this._enums); validate(this._inputs); validate(this._types); } _resolveStructures() { this._validateStructures(); const resolving = _.merge( {}, this._ifaces, this._enums, this._inputs, this._types ); const defs = {}; let keys, found; const _getError = () => { // print unresolvable structures return _.reduce( _.values(resolving), (acc, structure) => { if (structure._dependencies.length != 0) { acc += `${structure._kind} "${structure._name}" (depends on ${structure._dependencies.join( ", " )})\n`; } else { acc += `${structure._kind} "${structure._name}"\n`; } return acc; }, "Can not resolve following structures\n" ); }; const _isResolvable = names => { if (_.isNil(names)) { return true; } if (!_.isArray(names)) { names = [names]; } for (let name of names) { if (!_.has(defs, name)) { return false; } } return true; }; // first, we dertermine the resolving order of types const _resolve = key => { let extendable = resolving[key]; if ( !_isResolvable(extendable._parent) || !_isResolvable(extendable._iface) || !_isResolvable(extendable._dependencies) ) { return; } // this definition can be resolved this._updateDefs(defs, key, extendable); _.unset(resolving, key); found = true; }; while (true) { keys = _.keys(resolving); if (keys.length === 0) { break; } found = false; _.forEach(keys, _resolve); if (!found) { throw new Error(_getError()); } } this._structures = defs; } _resolveQueries() { const resolveQuery = query => { if (!_.isFunction(query._fn)) { throw new Error( `Query "${query._name}" must provide a resolving function` ); } // verify if its dependencies are resolved const resolveDependency = dependency => { if (!_.has(this._structures, dependency)) { throw new Error( `Unresolved dependency "${dependency}" in query "${query._name}"` ); } }; _.forEach(query._dependencies, resolveDependency); _.set(query, "_pubsub", pubsub); query.setLogger(this.getLogger(`Query ${query._name}`)); // update _.set( this._resolvers, `Query.${query._name}`, this.wrapQuery(query.wrap(query._fn), query) ); }; _.forEach(this._queries, resolveQuery); } _resolveMutations() { const resolveMutation = mutation => { if (!_.isFunction(mutation._fn)) { throw new Error( `Mutation "${mutation._name}" must provide a resolving function` ); } // verify if its dependencies are resolved const resolveDependency = dependency => { if (!_.has(this._structures, dependency)) { throw new Error( `Unresolved dependency "${dependency}" in mutation "${mutation._name}"` ); } }; _.forEach(mutation._dependencies, resolveDependency); _.set(mutation, "_pubsub", pubsub); mutation.setLogger(this.getLogger(`Mutation ${mutation._name}`)); // update _.set( this._resolvers, `Mutation.${mutation._name}`, this.wrapMutation(mutation.wrap(mutation._fn), mutation) ); }; _.forEach(this._mutations, resolveMutation); } _resolveSubscriptions() { const resolveSubscription = subscription => { if (!_.isFunction(subscription._fn)) { // throw new Error( // `Subscription "${subscription._name}" must provide a resolving function` // ); // strictly speaking, subscriptions does NOT need to provide resolving functions // we're gonna provide it with a simple one subscription._fn = payload => payload; } if (!_.isFunction(subscription._filterFn)) { // https://github.com/apollographql/graphql-subscriptions#filters subscription._filterFn = () => true; } // verify if its dependencies are resolved const resolveDependency = dependency => { if (!_.has(this._structures, dependency)) { throw new Error( `Unresolved dependency "${dependency}" in subscription "${subscription._name}"` ); } }; _.forEach(subscription._dependencies, resolveDependency); subscription.setLogger( this.getLogger(`Subscription ${subscription._name}`) ); const resolve = this.wrapSubscription( subscription.wrap(subscription._fn), subscription ); const subscribe = withFilter( () => pubsub.asyncIterator(subscription._name), subscription._filterFn.bind(subscription) ); // update _.set(this._resolvers, `Subscription.${subscription._name}`, { resolve, subscribe }); }; _.forEach(this._subscriptions, resolveSubscription); } _beautify(schema) { // replace multiple line breaks with a single one return schema.replace(/\n\s*\n/g, "\n"); } _generateStructures() { let structures = _.values(this._structures).map(structure => structure.toString() ); return structures.join("\n"); } _generateQueries() { let queries = _.values(this._queries).map(query => query._def); if (queries.length === 0) { return ""; } return `type Query {\n${queries.join("\n")}\n}`; } _generateMutations() { let mutations = _.values(this._mutations).map(mutation => mutation._def); if (mutations.length === 0) { return ""; } return `type Mutation {\n${mutations.join("\n")}\n}`; } _generateSubscriptions() { let subscriptions = _.values(this._subscriptions).map( subscription => subscription._def ); if (subscriptions.length === 0) { return ""; } return `type Subscription {\n${subscriptions.join("\n")}\n}`; } _generateResolverMap() { const resolved = {}; _.keys(this._resolversMap).map(iface => { const names = _.keys(_.get(this._resolversMap, iface)); const checks = _.map(names, name => { const fields = _.get(this._types, name)._fields; if (fields.length === 0) { throw new Error(`Type "${name}" must have its own fields`); } return object => hasFields(object, fields); }); const __resolveType = (object, context, info) => { for (var index = 0; index < names.length; index += 1) { if (checks[index](object)) { return names[index]; } } return null; }; _.set(resolved, iface, { __resolveType }); }); this._resolversMap = resolved; } _clear() { this._structures = {}; // resolved structures this._resolvers = {}; this._resolversMap = {}; this._schemaText = ""; this._schema = undefined; } /** * Build schema, resolver, resolver map * @return {[type]} [description] */ build() { this._clear(); this._resolveStructures(); this._resolveQueries(); this._resolveMutations(); this._resolveSubscriptions(); this._schemaText = this._beautify( [ this._generateStructures(), this._generateQueries(), this._generateMutations(), this._generateSubscriptions() ].join("\n") ); this._generateResolverMap(); try { this._schema = makeExecutableSchema({ typeDefs: [this._schemaText], resolvers: this._resolvers }); addResolveFunctionsToSchema(this._schema, this._resolversMap); return this._schema; } catch (e) { console.log("***************** SCHEMA EXCEPTION *****************"); console.log(e); process.exit(-1); } } getText() { return this._schemaText; } getSchema() { return this._schema; } _updateDefs(defs, name, ext) { let def = new Definition(); const hasIface = !_.isNil(ext._iface); const hasParent = !_.isNil(ext._parent); if (hasIface && hasParent) { throw new Error( `${name} can not implement an interface or extend an existing type as the same time.` ); } // update `head` if (!hasIface) { def.head = `${ext._kind} ${ext._name}`; } else { if (!ext.isType()) { throw new Error("Only `type` can implement an interface"); } else { def.head = `${ext._kind} ${ext._name} implements ${ext._iface}`; // just flag it to resolve in our resolvers map _.set(this._resolversMap, `${ext._iface}.${ext._name}`, true); } } // update `body` let body = []; if (hasIface) { body.push(defs[ext._iface].body); } if (hasParent) { body.push(defs[ext._parent].body); } body.push(ext._def); def.body = body.join("\n"); defs[name] = def; } } const HookableBuilder = Hookable(SchemaBuilder); HookableBuilder.create = (options = {}) => { return new HookableBuilder(options); }; module.exports = { createBuilder: HookableBuilder.create, pubsub };