/** @namespace GQLBaseEnv */
// @flow
import Path from 'path'
import fs from 'fs'
import { Deferred, joinLines } from './utils'
import { typeOf } from 'ne-types'
import { SyntaxTree } from './SyntaxTree'
import { Properties } from './decorators/ModelProperties'
import { GraphQLObjectType, GraphQLEnumType } from 'graphql'
import { IDLFileHandler } from './IDLFileHandler'
import { merge } from 'lodash'
import { LatticeLogs as ll } from './utils'
import { dedent } from 'ne-tag-fns'
import AsyncFunctionExecutionError from './errors/AsyncFunctionExecutionError'
import FunctionExecutionError from './errors/FunctionExecutionError'
import AwaitingPromiseError from './errors/AwaitingPromiseError'
import EventEmitter from 'events'
/* Internal implementation to detect the existence of proxies. When present
* additional functionality is enabled. Proxies are native in Node >= 6 */
const hasProxy = typeof global.Proxy !== 'undefined';
/* Internal Symbol referring to real accessor to GQLBase model object */
const _MODEL_KEY = Symbol.for('data-model-contents-value');
/* Internal Symbol referring to the static object containing a proxy handler */
const _PROXY_HANDLER = Symbol.for('internal-base-proxy-handler')
/**
* Simple function to check if a supplied key matches a string of your
* choosing and that string is not a defined property on the instance
* passed to the check.
*
* @method GQLBaseEnv~notDefined
* @memberof GQLBaseEnv
* @since 2.5.0
*
* @param {string} keyToTest a String denoting the property you wish to test
* @param {mixed} keySupplied a value, coerced `toString()`, to compare to
* `keyToTest`
* @param {mixed} instance an object instance to check `hasOwnProperty` on for
* the `keyToTest` supplied.
* @return {Boolean} true if the property matches the supplied key and that
* property is not an ownedProperty of the instance supplied.
*/
export function notDefined(
keyToTest: string,
keySupplied: Object | string,
instance: Object
) {
return (
new RegExp("^" + keyToTest + "$").test(keySupplied.toString())
&& !instance.hasOwnProperty(keyToTest)
);
}
/**
* A `Symbol` used as a key to store the backing model data. Designed as a
* way to separate model data and GraphQL property accessors into logical bits.
*
* @type {Symbol}
* @memberof GQLBaseEnv
* @const
*/
export const MODEL_KEY = Symbol.for('data-model-contents-key');
/**
* A `Symbol` used as a key to store the request data for an instance of the
* GQLBase object in question.
*
* @type {Symbol}
* @const
* @inner
* @memberof GQLBaseEnv
*/
export const REQ_DATA_KEY = Symbol.for('request-data-object-key');
/**
* A nameless Symbol for use as a key to the internal decorator storage
*
* @type {Symbol}
* @const
* @inner
* @memberof GQLBaseEnv
*/
export const META_KEY = Symbol();
/**
* A Symbol used to identify calls to @Properties for properties generated
* automatically upon instance creation.
*
* @type {Symbol}
* @const
* @inner
* @memberOf GQLBaseEnv
*/
export const AUTO_PROPS = Symbol.for('auto-props')
/**
* A Symbol used to identify calls to @Getters for properties generated
* via decorator. These are stored in <class>[META_KEY][GETTERS]
*
* @type {Symbol}
* @const
* @inner
* @memberOf GQLBaseEnv
*/
export const GETTERS = Symbol.for('getters')
/**
* A Symbol used to identify calls to @Setters for properties generated
* via decorator. These are stored in <class>[META_KEY][SETTERS]
*
* @type {Symbol}
* @const
* @inner
* @memberOf GQLBaseEnv
*/
export const SETTERS = Symbol.for('setters')
/**
* A Symbol used to identify calls to @Properties for properties generated
* via decorator. These are stored in <class>[META_KEY][PROPS]
*
* @type {Symbol}
* @const
* @inner
* @memberOf GQLBaseEnv
*/
export const PROPS = Symbol.for('props')
/**
* All GraphQL Type objects used in this system are assumed to have extended
* from this class. An instance of this class can be used to wrap an existing
* structure if you have one.
*
* @class GQLBase
*/
export class GQLBase extends EventEmitter {
fileHandler: ?IDLFileHandler;
/**
* Request data is passed to this object when constructed. Typically these
* objects, and their children, are instantiated by its own static MUTATORS
* and RESOLVERS. They should contain request specific state if any is to
* be shared.
*
* These can be considered request specific controllers for the object in
* question. The base class takes a single object which should contain all
* the HTTP/S request data and the graphQLParams is provided as the object
* { query, variables, operationName, raw }.
*
* When used with express-graphql, the requestData object has the format
* { req, res, gql } where
* • req is an Express 4.x request object
* • res is an Express 4.x response object
* • gql is the graphQLParams object in the format of
* { query, variables, operationName, raw }
* See https://github.com/graphql/express-graphql for more info
*
* @memberof GQLBase
* @method ⎆⠀constructor
* @constructor
*
* @param {mixed} modelData this, typically an object, although anything
* really is supported, represents the model data for our GraphQL object
* instance.
* @param {Object} requestData see description above
*/
constructor(
modelData: Object = {},
requestData: ?Object = null,
options: Object = { autoProps: true }
) {
super();
const Class = this.constructor;
const tree = SyntaxTree.from(Class.SCHEMA);
const outline = tree && tree.outline || null;
if (!outline) {
throw new FunctionExecutionError(
new Error(dedent`
The SDL is unparsable. Please check your SCHEMA and make sure
it is valid GraphQL SDL/IDL. Your SCHEMA is defined as:
${this.SCHEMA}
`)
)
}
if (outline && !(Class.name in outline)) {
throw new FunctionExecutionError(
new Error(dedent`
The class name "${Class.name}" does not match any of the types,
enums, scalars, unions or interfaces defined in the SCHEMA for
this class (${Object.keys(outline)}).
\x1b[1mIn most clases this is because your class name and SCHEMA
type do not match.\x1b[0m
`)
)
}
GQLBase.setupModel(this);
this.setModel(modelData);
this.requestData = requestData || {};
this.fileHandler = new IDLFileHandler(this.constructor);
if (options && !!options.autoProps !== false) {
this.applyAutoProps()
}
// @ComputedType
return hasProxy ? new Proxy(this, GQLBase[_PROXY_HANDLER]) : this;
}
/**
* Since reading the Schema for a given GraphQL Lattice type or
* interface is simple enough, we should be able to automatically
* apply one to one GraphQL:Model properties.
*
* @instance
* @method ⌾⠀applyAutoProps
* @memberof GQLBase
*/
applyAutoProps() {
if (!this.constructor.SCHEMA || !this.constructor.SCHEMA.length) {
ll.warn(joinLines`
There is no SCHEMA for ${this.constructor.name}!! This will likely
end in an error. Proceed with caution. Skipping \`applyAutoProps\`
`)
return
}
// Individual property getters do not need to be auto-created for enum
// types. Potentially do some checks for Interfaces and Unions as well
if (this.constructor.GQL_TYPE === GraphQLEnumType) {
return
}
let Class = this.constructor
let tree = SyntaxTree.from(Class.SCHEMA)
let outline = tree ? tree.outline : {}
let props = []
// $FlowFixMe
for (let propName of Object.keys(outline[Class.name])) {
// $FlowFixMe
let desc = Object.getOwnPropertyDescriptor(Class.prototype, propName)
let hasCustomImpl = !!(
// We have a descriptor for the property name
desc && (
// We have a getter function defined
typeof desc.get !== 'undefined'
||
// ...or we have a function, async or not, defined
typeof desc.value === 'function'
)
)
// Only create auto-props for non custom implementations
if (!hasCustomImpl) {
props.push(propName)
}
}
if (props.length) {
ll.info(`Creating auto-props for [${Class.name}]: `, props)
try {
Properties(...props)(Class, [AUTO_PROPS])
}
catch(error) {
let parsed = /Cannot redefine property: (\w+)/.exec(error.message)
if (parsed) {
ll.warn(`Skipping auto-prop '${Class.name}.${parsed[1]}'`)
}
else {
ll.error(`Failed to apply auto-properties\nReason: `)
ll.error(error);
}
}
}
}
/**
* Getter for the internally stored model data. The contents of this
* object are abstracted away behind a `Symbol` key to prevent collision
* between the underlying model and any GraphQL Object Definition properties.
*
* @instance
* @memberof GQLBase
* @method ⌾⠀getModel
* @since 2.5
*
* @param {Object} value any object you wish to use as a data store
*/
getModel() {
// @ComputedType
return this[MODEL_KEY];
}
/**
* Setter for the internally stored model data. The contents of this
* object are abstracted away behind a `Symbol` key to prevent collision
* between the underlying model and any GraphQL Object Definition properties.
*
* @instance
* @memberof GQLBase
* @method ⌾⠀setModel
* @since 2.5
*
* @param {Object} value any object you wish to use as a data store
*/
setModel(value: Object): GQLBase {
// @ComputedType
this[MODEL_KEY] = value;
return this;
}
/**
* Uses `_.merge()` to modify the internal backing data store for the
* object instance. This is a shortcut for
* `_.merge()(instance[MODEL_KEY], ...extensions)`
*
* @instance
* @memberof GQLBase
* @method ⌾⠀extendModel
* @since 2.5
*
* @param {mixed} extensions n-number of valid `_.merge()` parameters
* @return {GQLBase} this is returned
*/
extendModel(...extensions: Array<mixed>): GQLBase {
// $FlowFixMe
merge(this[MODEL_KEY], ...extensions);
return this;
}
/**
* A getter that retrieves the inner request data object. When used with
* GQLExpressMiddleware, this is an object matching {req, res, gql}.
*
* @instance
* @memberof GQLBase
* @method ⬇︎⠀requestData
*
* @return {Object} an object, usually matching { req, res, gql }
*/
get requestData(): Object | null {
// @ComputedType
return this[REQ_DATA_KEY];
}
/**
* A setter that assigns a value to the inner request data object. When
* used with GQLExpressMiddleware, this is an object matching {req, res, gql}.
*
* @instance
* @memberof GQLBase
* @method ⬆︎⠀requestData
*
* @param {Object} value an object, usually matching { req, res, gql }
*/
set requestData(value: Object): void {
// @ComputedType
this[REQ_DATA_KEY] = value;
}
/**
* Returns the `constructor` name. If invoked as the context, or `this`,
* object of the `toString` method of `Object`'s `prototype`, the resulting
* value will be `[object MyClass]`, given an instance of `MyClass`
*
* @method ⌾⠀[Symbol.toStringTag]
* @memberof ModuleParser
*
* @return {string} the name of the class this is an instance of
* @ComputedType
*/
get [Symbol.toStringTag]() { return this.constructor.name }
/**
* Properties defined for GraphQL types in Lattice can be defined as
* a getter, a function or an async function. In the case of standard
* functions, if they return a promise they will be handled as though
* they were async
*
* Given the variety of things a GraphQL type can actually be, obtaining
* its value can annoying. This method tends to lessen that boilerplate.
* Errors raised will be thrown.
*
* @instance
* @memberof GQLBase
* @method ⌾⠀getProp
*
* @param {string|Symbol} propName the name of the property in question
* @param {boolean} bindGetters true, by default, if the `get` or
* `initializer` descriptor values should be bound to the current instance
* or an object of the programmers choice before returning
* @param {mixed} bindTo the `this` object to use for binding when
* `bindGetters` is set to true.
* @return {mixed} the value of the `propName` as a Function or something
* else when the requested property name exists
*
* @throws {Error} errors raised in awaiting results will be thrown
*/
getProp(propName: string, bindGetters: boolean = true, bindTo: mixed) {
// $FlowFixMe
let proto = Object.getPrototypeOf(this)
let descriptor = Object.getOwnPropertyDescriptor(proto, propName)
let result
if (!descriptor) {
return null;
}
if (descriptor) {
if (descriptor.initializer || descriptor.get) {
let what = descriptor.initializer || descriptor.get
if (bindGetters) {
result = what.bind(bindTo || this)
}
else {
result = what
}
}
else if (descriptor.value) {
result = descriptor.value
}
}
return result
}
/**
* Properties defined for GraphQL types in Lattice can be defined as
* a getter, a function or an async function. In the case of standard
* functions, if they return a promise they will be handled as though
* they were async. In addition to fetching the property, or field
* resolver, its resulting function or getter will be invoked.
*
* Given the variety of things a GraphQL type can actually be, obtaining
* its value can annoying. This method tends to lessen that boilerplate.
* Errors raised will be thrown.
*
* @instance
* @memberof GQLBase
* @method ⌾⠀callProp
*
* @param {string} propName the name of the property in question
* @param {Array<mixed>} args the arguments array that will be passed
* to `.apply()` should the property evaluate to a `function`
* @return {mixed} the return value of any resulting function or
* value returned by a getter; wrapped in a promise as all async
* functions do.
*
* @throws {Error} errors raised in awaiting results will be thrown
*/
async callProp(propName: string, ...args: Array<mixed>) {
// $FlowFixMe
let prop = this.getProp(propName, ...args);
let result
if (prop && typeOf(prop) === 'AsyncFunction') {
try {
result = await prop.apply(this, args);
}
catch (error) {
throw new AsyncFunctionExecutionError(error, prop, args, result)
}
}
else if (prop && typeOf(prop) === Function.name) {
try {
result = prop.apply(this, args)
}
catch (error) {
throw new FunctionExecutionError(error, prop, args, result)
}
if (typeOf(result) === Promise.name) {
try {
result = await result
}
catch (error) {
throw new AwaitingPromiseError(error).setPromise(result)
}
}
}
return result
}
/**
* A pass-thru method to the static function of the same name. The
* difference being that if `requestData` is not specified, the
* `requestData` object from this instance will be used to build the
* resolvers in question.
*
* @instance
* @method ⌾⠀getResolver
* @memberof GQLBase
*
* @param {string} resolverName the name of the resolver as a string
* @param {Object} requestData the requestData used to build the
* resolver methods from which to choose
* @return {Function} returns either a `function` representing the
* resolver requested or null if there wasn't one to be found
*/
async getResolver(resolverName: string, requestData: Object) {
return await this.constructor.getResolver(
resolverName,
requestData || this.requestData
)
}
/**
* Resolvers are created in a number of different ways. OOP design
* dictates that instances of a created class will handle field
* resolvers, but query, mutation and subscription resolvers are
* typically what creates these instances.
*
* Since a resolver can be created using `@mutator/@subscriptor/@resolver`
* or via method on a object returned from `RESOLVERS()`, `MUTATORS()` or
* `SUBSCRIPTIONS()`, there should be an easy to use way to fetch a
* resolver by name; if for nothing else, code reuse.
*
* Pass the name of the resolver to the function and optionally pass a
* requestData object. The `getMergedRoot()` method will build an object
* containing all the root resolvers for the type, bound to the supplied
* `requestData` object. It is from this object that `resolverName` will
* be used to fetch the function in question. If one exists, it will be
* returned, ready for use. Otherwise, null will be your answer.
*
*
* @static
* @method ⌾⠀getResolver
* @memberof GQLBase
*
* @param {string} resolverName the name of the resolver as a string
* @param {Object} requestData the requestData used to build the
* resolver methods from which to choose
* @return {Function} returns either a `function` representing the
* resolver requested or null if there wasn't one to be found
*/
static async getResolver(resolverName: string, requestData: Object) {
const reqData = requestData || null
const rootObj = await this.getMergedRoot(reqData)
return rootObj[resolverName] || null
}
/**
* The static version of getProp reads into the prototype to find the field
* that is desired. If the field is either a getter or a initializer (see
* class properties descriptors), then the option to bind that to either the
* prototype object or one of your choosing is available.
*
* @memberof GQLBase
* @method ⌾⠀getProp
* @static
*
* @param {string|Symbol} propName a string or Symbol denoting the name of
* the property or field you desire
* @param {boolean} bindGetters true if a resulting `getter` or `initializer`
* should be bound to the prototype or other object
* @param {mixed} bindTo the object to which to bind the `getter` or
* `initializer` functions to if other than the class prototype.
* @return {mixed} a `Function` or other mixed value making up the property
* name requested
*/
static getProp(
propName: string,
bindGetters: boolean = false,
bindTo: mixed
) {
let descriptor = Object.getOwnPropertyDescriptor(this.prototype, propName)
if (descriptor) {
if (descriptor.get || descriptor.initializer) {
let what = descriptor.initializer || descriptor.get
if (bindGetters) {
bindTo = bindTo || this.prototype
return what.bind(bindTo)
}
else {
return what
}
}
else {
return descriptor.value
}
}
else {
return null
}
}
/**
* Until such time as the reference implementation of Facebook's GraphQL
* SDL AST parser supports comments, or until we take advantage of Apollo's
* AST parser, this is how comments will be applied to a built schema.
*
* Several constants are defined on the GQLBase object itself, and thereby
* all its subclasses. They pertain to how to define description fields
* for various parts of your GQL implementation.
*
* ```
* // To define a description on the top level class
* [this.DOC_CLASS]: string
*
* // To define a description on a field (getter, function or async function)
* [this.DOC_FIELDS]: {
* fieldName: string
* }
*
* // To define a description on a query, mutation or subscription field
* [this.DOC_QUERIES || this.DOC_MUTATORS || this.DOC_SUBSCRIPTIONS]: {
* fieldName: string
* }
* ```
*
* To make writing code easier, the `joinLines()` template function is
* available so your source code can look nice and neat and your descriptions
* won't get annoying line breaks and spaces as part of that process.
*
* @static
* @memberof GQLBase
* @method apiDocs
*
* @return {Object} an object with various keys and values denoting
* description fields that should be applied to the final schema object
*/
static apiDocs(): Object {
return {
[this.DOC_CLASS]: joinLines`
GQLBase class implementation. GQLBase is the root class used in
graphql-lattice to describe a GraphQLObjectType. If you are reading
this, the person using lattice failed to provide documentation for
their type. :)
`,
[this.DOC_QUERY]: joinLines`
## Welcome to GraphQL Lattice
**Query**
You will want to define a \`DOC_QUERY\` apiDoc comment with something
more meaningful to your particular Schema here.
`,
[this.DOC_MUTATION]: joinLines`
## Welcome to GraphQL Lattice
**Mutation**
You will want to define a \`DOC_MUTATION\` apiDoc comment with
something more meaningful to your particular Schema here.
`,
[this.DOC_SUBSCRIPTION]: joinLines`
## Welcome to GraphQL Lattice
**Subscription**
You will want to define a \`DOC_SUBSCRIPTION\` apiDoc comment with
something more meaningful to your particular Schema here.
`,
[this.DOC_FIELDS]: {
// fieldName: `fieldDescription`,
},
[this.DOC_QUERIES]: {
// queryName: `queryDescription`,
},
[this.DOC_MUTATORS]: {
// mutatorName: `mutatorDescription`
},
[this.DOC_SUBSCRIPTIONS]: {
// subscriptionName: `subscriptionDescription`
}
}
}
/**
* Defined in a base class, this getter should return either a String
* detailing the full IDL schema of a GraphQL handler or one of two
* types of Symbols.
*
* The first Symbol type is the constant `ADJACENT_FILE`. If this Symbol is
* returned, the system assumes that next to the source file in question is
* a file of the same name with a .graphql extension. This file should be
* made of the GraphQL IDL schema definitions for the object types being
* created.
*
* Example:
* ```js
* static get SCHEMA(): string | Symbol {
* return GQLBase.ADJACENT_FILE
* }
* ```
*
* The primary advantage of this approach is allowing an outside editor that
* provides syntax highlighting rather than returning a string from the
* SCHEMA getter.
*
* Alternatively, the static method IDLFilePath can be used to point to an
* alternate location where the GraphQL IDL file resides. The extension can
* also be changed from .graphql to something else if need be using this
* method.
*
* Example:
* ```js
* static get SCHEMA(): string | Symbol {
* return GQLBase.IDLFilePath('/path/to/file', '.idl')
* }
* ```
*
* @instance
* @memberof GQLBase
* @method ⬇︎⠀SCHEMA
* @readonly
* @static
*
* @return {string|Symbol} a valid IDL string or one of the Symbols
* described above.
*
* @see {@link GQLBase#ADJACENT_FILE}
* @see {@link GQLBase#IDLFilePath}
*/
static get SCHEMA(): string | Symbol {
return ''
}
/**
* This method should return a promise that resolves to an object of
* functions matching the names of the mutation operations. These are to be
* injected into the root object when used by `GQLExpressMiddleware`.
*
* @instance
* @memberof GQLBase
* @method ⌾⠀MUTATORS
* @readonly
* @static
*
* @param {Object} requestData typically an object containing three
* properties; {req, res, gql}
* @return {Promise} a promise that resolves to an object; see above for more
* information.
*/
static async MUTATORS(requestData: Object): Promise<Object> {
// define in base class
return {};
}
/**
* This method should return a promise that resolves to an object of
* functions matching the names of the query operations. These are to be
* injected into the root object when used by `GQLExpressMiddleware`.
*
* @instance
* @memberof GQLBase
* @method ⌾⠀RESOLVERS
* @readonly
* @static
*
* @param {Object} requestData typically an object containing three
* properties; {req, res, gql}
* @return {Promise} a promise that resolves to an object; see above for more
* information.
*/
static async RESOLVERS(requestData: Object): Promise<Object> {
// define in base class
return {};
}
/**
* @see {@link GQLBase#SCHEMA}
*
* @memberof GQLBase
* @method ⬇︎⠀ADJACENT_FILE
* @static
* @const
*
* @return {Symbol} the Symbol, when returned from SCHEMA, causes
* the logic to load an IDL Schema from an associated file with a .graphql
* extension and bearing the same name.
*/
static get ADJACENT_FILE(): Symbol {
return Symbol.for('.graphql file located adjacent to source')
}
/**
* Determines the default type targeted by this GQLBase class. Any
* type will technically be valid but only will trigger special behavior
*
* @memberof GQLBase
* @method ⬇︎⠀GQL_TYPE
* @static
* @const
*
* @return {Function} a type, such as `GraphQLObjectType` or
* `GraphQLInterfaceType`
*/
static get GQL_TYPE(): Function {
return GraphQLObjectType;
}
/**
* Creates an appropriate Symbol crafted with the right data for use by
* the IDLFileHandler class below.
*
* @static
* @memberof GQLBase
* @method ⌾⠀IDLFilePath
*
* @param {string} path a path to the IDL containing file
* @param {string} [extension='.graphql'] an extension, including the
* prefixed period, that will be added to the supplied path should it not
* already exist.
* @return Symbol
*
* @see {@link GQLBase#SCHEMA}
*/
static IDLFilePath(path: string, extension: string = '.graphql'): Symbol {
return Symbol.for(`Path ${path} Extension ${extension}`);
}
/**
* A file handler for fetching the IDL schema string from the file system
* for those `GQLBase` extended classes that have indicated to do so by
* returning a `Symbol` for their `SCHEMA` property.
*
* @static
* @memberof GQLBase
* @method ⬇︎⠀handler
*
* @return {IDLFileHandler} instance of IDLFileHandler, created if one does
* not already exist, for fetching the contents from disk.
*/
static get handler(): IDLFileHandler {
const key = Symbol.for(`${IDLFileHandler.name}.${this.name}`);
// @ComputedType
if (!this[key]) {
// @ComputedType
this[key] = new IDLFileHandler(this);
}
// @ComputedType
return this[key];
}
/**
* Returns the module object where your class is created. This needs to be
* defined on your class, as a static getter, in the FILE where you are
* defining your Class definition.
*
* @static
* @memberof GQLBase
* @method ⬇︎⠀module
* @const
*
* @return {Object} the reference to the module object defined and injected
* by node.js' module loading system.
*
* @see https://nodejs.org/api/modules.html
*/
static get module(): Object {
return module;
}
/**
* The internal data model has some custom `EventEmitter` code wrapped
* it here. When the data model is set via `setModel` or by accessing it
* via `instance[MODEL_KEY]`, an event `EVENT_MODEL_SET` is emitted. Any
* listener listening for this event receives an object with two keys
* ```
* {
* model: The actual model being set; changes are persisted
* instance: The GQLBase instance the model is associated with
* }
* ```
*
* Subsequently, the events `EVENT_MODEL_PROP_CHANGE` and
* `EVENT_MODEL_PROP_DELETE` can be listened to if your version of node
* supports Proxy objects. They allow you to be notified whenever your
* model has a property changed or deleted, respectively.
*
* The callback for `change` receives an object with four properties
* ```
* {
* model: The model object the value is being changed on
* old: The old value being replaced; undefined if it is the first time
* key: The property key for the value being changed
* value: The new value being set
* }
* ```
*
* The callback for `delete` receives an object with four properties
* ```
* {
* model: The model object the value is deleted from
* key: The property key for the deleted value
* deleted: The deleted value
* }
* ```
*
* @static
* @memberof GQLBase
* @method ⌾⠀setupModel
*
* @param {GQLBase} instance typically `this` as passed in from a call in
* the constructor
*/
static setupModel(instance: GQLBase) {
const changeHandler: Object = {
/**
* Proxy set() handler. This is where the change events are fired from
*
* @method GQLBase~set
* @param {Object} target the `GQLBase` model object
* @param {string} key the property name
* @param {mixed} value the new property value
*/
set(target, key, value) {
const old = target[key];
target[key] = value;
instance.emit(GQLBase.EVENT_MODEL_PROP_CHANGE, {
model: target,
old,
key,
value
})
},
/**
* Proxy deleteProperty() handler. This is where the delete property
* events are fired from
*
* @method GQLBase~deleteProperty
* @param {Object} target the `GQLBase` model object
* @param {string} key the property name
*/
deleteProperty(target, key) {
const deleted = target[key];
delete target[key];
instance.emit(GQLBase.EVENT_MODEL_PROP_DELETE, {
model: target,
key,
deleted
})
}
}
/**
* 'Publicly' the Symbol for accessing the `GQLBase` model is `MODEL_KEY`.
* In truth it is stored under a Symbol defined in `setupModel` and
* referred to as `_MODEL_KEY` in this code. This is done so a getter and
* setter can be wrapped around the usage of the instance's data model.
*
* When being read, if `Proxy` exists in the node environment and if there
* are any registered `EVENT_MODEL_PROP_CHANGE` or `EVENT_MODEL_PROP_DELETE`
* events, then the returned model is a Proxy around the real model that
* allows us to capture the changes and deletion of keys
*
* When being assigned, the event `EVENT_MODEL_WILL_BE_SET` and the event
* `EVENT_MODEL_HAS_BEEN_SET` are emitted to allow listeners to modify and
* see the final data around the setting of a model object. Both events
* receive an object with two keys
*
* ```
* {
* model: The object being or having been set
* instance: The GQLBase instance receiving the model
* }
* ```
*/
Object.defineProperty(instance, MODEL_KEY, {
get: function() {
let model = this[_MODEL_KEY]
let hasListeners =
this.listenerCount(GQLBase.EVENT_MODEL_PROP_CHANGE) +
this.listenerCount(GQLBase.EVENT_MODEL_PROP_DELETE)
if (hasProxy && hasListeners) {
model = new Proxy(model, changeHandler);
}
return model
},
set: function(model) {
const instance = this;
this.emit(GQLBase.EVENT_MODEL_WILL_BE_SET, { model, instance });
instance[_MODEL_KEY] = model;
this.emit(GQLBase.EVENT_MODEL_HAS_BEEN_SET, { model, instance })
}
});
}
/**
* If ES6 Proxies are supported in your execution environment, all GQLBase
* extended classes are also proxies. By default the internal proxy handler
* provides backwards compatibility with the removal of the default getters
* and setters for the 'model' property as long as you do not define a
* top level 'model' property of your own.
*
* @method ⬇︎⠀[_PROXY_HANDLER]
* @memberof GQLBase
* @static
* @const
* @since 2.5.0
*
* @type {Object}
* @ComputedType
*/
static get [_PROXY_HANDLER]() {
return {
get(target, key, lastResult) {
const model = target[_MODEL_KEY];
// Allow backwards compatibility for 'model' property if one is not
// explicitly defined on your instance.
if (notDefined('model', key, target)) {
// Be sure to use the public MODEL_KEY to ensure events fire
return target[MODEL_KEY];
}
return target[key]
}
}
}
/**
* Applies the same logic as {@link #[Symbol.toStringTag]} but on a static
* scale. So, if you perform `Object.prototype.toString.call(MyClass)`
* the result would be `[object MyClass]`.
*
* @method ⌾⠀[Symbol.toStringTag]
* @memberof ModuleParser
* @static
*
* @return {string} the name of this class
* @ComputedType
*/
static get [Symbol.toStringTag]() { return this.name }
/**
* A constant used to register an event listener for when the internal
* model object is assigned a new value. This event fires before the model
* is set. Changes to the model value at this point will affect the contents
* before the value assignment takes place.
*
* @static
* @memberof GQLBase
* @method ⬇︎⠀EVENT_MODEL_WILL_BE_SET
* @const
*
* @type {string}
*/
static get EVENT_MODEL_WILL_BE_SET() { return 'E: Int. model will be set' }
/**
* A constant used to register an event listener for when the internal
* model object is assigned a new value. This event fires after the model
* is set.
*
* @static
* @memberof GQLBase
* @method ⬇︎⠀EVENT_MODEL_HAS_BEEN_SET
* @const
*
* @type {string}
*/
static get EVENT_MODEL_HAS_BEEN_SET() { return 'E: Int. model has been set' }
/**
* A constant used to register an event listener for when a property of the
* internal model object is set to a new or intial value.
*
* @static
* @memberof GQLBase
* @method ⬇︎⠀EVENT_MODEL_PROP_CHANGE
* @const
*
* @type {string}
*/
static get EVENT_MODEL_PROP_CHANGE() { return 'E: Int. model prop changed' }
/**
* A constant used to register an event listener for when a property of the
* internal model object has been deleted. This event fires after the value
* has been deleted.
*
* @static
* @memberof GQLBase
* @method ⬇︎⠀EVENT_MODEL_PROP_DELETE
* @const
*
* @type {string}
*/
static get EVENT_MODEL_PROP_DELETE() { return 'E: Int. model prop deleted' }
/**
* A constant key used to identify a comment for a class description
*
* @static
* @memberof GQLBase
* @method ⬇︎⠀DOC_CLASS
* @const
*
* @type {string}
*/
static get DOC_CLASS() { return 'class' }
/**
* A constant key used to identify a comment for a type field description
*
* @static
* @memberof GQLBase
* @method ⬇︎⠀DOC_FIELDS
* @const
*
* @type {string}
*/
static get DOC_FIELDS() { return 'fields' }
/**
* A constant key used to identify a comment for the top level query
* description
*
* @static
* @memberof GQLBase
* @method ⬇︎⠀DOC_QUERY
* @const
*
* @type {string}
*/
static get DOC_QUERY() { return 'query' }
/**
* A constant key used to identify a comment for a query description
*
* @static
* @memberof GQLBase
* @method ⬇︎⠀DOC_QUERIES
* @const
*
* @type {string}
*/
static get DOC_QUERIES() { return 'queries' }
/**
* A constant key used to identify a comment for the top level mutation
* description
*
* @static
* @memberof GQLBase
* @method ⬇︎⠀DOC_MUTATION
* @const
*
* @type {string}
*/
static get DOC_MUTATION() { return 'mutation' }
/**
* A constant key used to identify a comment for a mutator description
*
* @static
* @memberof GQLBase
* @method ⬇︎⠀DOC_MUTATORS
* @const
* @deprecated Use `DOC_MUTATIONS` instead
*
* @type {string}
*/
static get DOC_MUTATORS() { return 'mutators' }
/**
* A constant key used to identify a comment for a mutator description
*
* @static
* @memberof GQLBase
* @method ⬇︎⠀DOC_MUTATORS
* @const
*
* @type {string}
*/
static get DOC_MUTATIONS() { return 'mutators' }
/**
* A constant key used to identify a comment for the top level subscription
* description
*
* @static
* @memberof GQLBase
* @method ⬇︎⠀DOC_SUBSCRIPTION
* @const
*
* @type {string}
*/
static get DOC_SUBSCRIPTION() { return 'subscription' }
/**
* A constant key used to identify a comment for a subscription description
*
* @static
* @memberof GQLBase
* @method ⬇︎⠀DOC_SUBSCRIPTIONS
* @const
*
* @type {string}
*/
static get DOC_SUBSCRIPTIONS() { return 'subscriptions' }
/**
* A shortcut to the utils/joinLines function to make it easier to get
* the tools to write docs for your types in a friendly fashion.
*
* @memberof GQLBase
* @method ⬇︎⠀joinLines
* @static
* @const
*
* @type {Function}
*/
static get joinLines(): Function { return joinLines }
/**
* An simple pass-thru method for fetching a types merged root object.
*
* @method ⌾⠀getMergedRoot
* @memberof GQLBase
* @static
*
* @param {Object} requestData an object containing the request data such as
* request, response or graphql context info that should be passed along to
* each of the resolver creators
* @return {Object} the merged root object with all the query, mutation and
* subscription resolvers defined and created within.
*/
static async getMergedRoot(
requestData: Object,
separateByType: boolean = false
): Object {
const root = {};
const Class = this;
let _ = {
// $FlowFixMe
resolvers: Class[META_KEY].resolvers || [],
// $FlowFixMe
mutators: Class[META_KEY].mutators || [],
// $FlowFixMe
subscriptors: Class[META_KEY].subscriptors || []
}
let convert = f => {
let isFactoryClass = (c) => {
return !!Class[META_KEY][Symbol.for('Factory Class')]
}
if (isFactoryClass(Class)) {
return {
[f.name]: function(...args) {
return f.apply(Class, [Class, requestData, ...args])
}
}
}
else {
return {
[f.name]: function(...args) {
return f.apply(Class, [requestData, ...args])
}
}
}
}
let reduce = (p, c) => merge(p, c)
_.resolvers = _.resolvers.map(convert).reduce(reduce, {})
_.mutators = _.mutators.map(convert).reduce(reduce, {})
_.subscriptors = _.subscriptors.map(convert).reduce(reduce, {})
if (separateByType) {
// Apollo wants all the resolvers to grouped by top level type.
// The field resolvers aren't an issue in Lattice defined types
// but the root types do need to be sorted; so let's do that here
merge(
root,
{ Query: await Class.RESOLVERS(requestData) },
{ Mutation: await Class.MUTATORS(requestData) },
{ Query: _.resolvers },
{ Mutation: _.mutators },
{ Subscription: _.subscriptors }
);
// When using lattice with apollo server, it is quite particular about
// empty Query, Mutation or Subscription resolver maps.
if (!Object.keys(root.Query).length) delete root.Query
if (!Object.keys(root.Mutation).length) delete root.Mutation
if (!Object.keys(root.Subscription).length) delete root.Subscription
}
else {
merge(
root,
await Class.RESOLVERS(requestData),
await Class.MUTATORS(requestData),
_.resolvers,
_.mutators,
_.subscriptors
);
}
return root;
}
/**
* An object used to store data used by decorators and other internal
* proccesses.
* @ComputedType
*/
static get [META_KEY]() {
let storage = this[Symbol.for(this.name)]
if (!storage) {
storage = (this[Symbol.for(this.name)] = {})
}
return storage;
}
}
export default GQLBase;