@nyteshade/lattice-legacy
Version:
OO Underpinnings for ease of GraphQL Implementation
1,408 lines (1,253 loc) • 43.1 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.GQLBase = exports.PROPS = exports.SETTERS = exports.GETTERS = exports.AUTO_PROPS = exports.META_KEY = exports.REQ_DATA_KEY = exports.MODEL_KEY = undefined;
var _defineProperty = require('babel-runtime/core-js/object/define-property');
var _defineProperty2 = _interopRequireDefault(_defineProperty);
var _promise = require('babel-runtime/core-js/promise');
var _promise2 = _interopRequireDefault(_promise);
var _asyncToGenerator2 = require('babel-runtime/helpers/asyncToGenerator');
var _asyncToGenerator3 = _interopRequireDefault(_asyncToGenerator2);
var _getPrototypeOf = require('babel-runtime/core-js/object/get-prototype-of');
var _getPrototypeOf2 = _interopRequireDefault(_getPrototypeOf);
var _toStringTag = require('babel-runtime/core-js/symbol/to-string-tag');
var _toStringTag2 = _interopRequireDefault(_toStringTag);
var _getOwnPropertyDescriptor = require('babel-runtime/core-js/object/get-own-property-descriptor');
var _getOwnPropertyDescriptor2 = _interopRequireDefault(_getOwnPropertyDescriptor);
var _keys = require('babel-runtime/core-js/object/keys');
var _keys2 = _interopRequireDefault(_keys);
var _symbol = require('babel-runtime/core-js/symbol');
var _symbol2 = _interopRequireDefault(_symbol);
var _for = require('babel-runtime/core-js/symbol/for');
var _for2 = _interopRequireDefault(_for);
exports.notDefined = notDefined;
var _path = require('path');
var _path2 = _interopRequireDefault(_path);
var _fs = require('fs');
var _fs2 = _interopRequireDefault(_fs);
var _utils = require('./utils');
var _neTypes = require('ne-types');
var _SyntaxTree = require('./SyntaxTree');
var _ModelProperties = require('./decorators/ModelProperties');
var _graphql = require('graphql');
var _IDLFileHandler = require('./IDLFileHandler');
var _lodash = require('lodash');
var _neTagFns = require('ne-tag-fns');
var _AsyncFunctionExecutionError = require('./errors/AsyncFunctionExecutionError');
var _AsyncFunctionExecutionError2 = _interopRequireDefault(_AsyncFunctionExecutionError);
var _FunctionExecutionError = require('./errors/FunctionExecutionError');
var _FunctionExecutionError2 = _interopRequireDefault(_FunctionExecutionError);
var _AwaitingPromiseError = require('./errors/AwaitingPromiseError');
var _AwaitingPromiseError2 = _interopRequireDefault(_AwaitingPromiseError);
var _events = require('events');
var _events2 = _interopRequireDefault(_events);
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
/* 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 */
/** @namespace GQLBaseEnv */
const _MODEL_KEY = (0, _for2.default)('data-model-contents-value');
/* Internal Symbol referring to the static object containing a proxy handler */
const _PROXY_HANDLER = (0, _for2.default)('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.
*/
function notDefined(keyToTest, keySupplied, instance) {
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
*/
const MODEL_KEY = exports.MODEL_KEY = (0, _for2.default)('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
*/
const REQ_DATA_KEY = exports.REQ_DATA_KEY = (0, _for2.default)('request-data-object-key');
/**
* A nameless Symbol for use as a key to the internal decorator storage
*
* @type {Symbol}
* @const
* @inner
* @memberof GQLBaseEnv
*/
const META_KEY = exports.META_KEY = (0, _symbol2.default)();
/**
* A Symbol used to identify calls to @Properties for properties generated
* automatically upon instance creation.
*
* @type {Symbol}
* @const
* @inner
* @memberOf GQLBaseEnv
*/
const AUTO_PROPS = exports.AUTO_PROPS = (0, _for2.default)('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
*/
const GETTERS = exports.GETTERS = (0, _for2.default)('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
*/
const SETTERS = exports.SETTERS = (0, _for2.default)('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
*/
const PROPS = exports.PROPS = (0, _for2.default)('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
*/
let GQLBase = exports.GQLBase = class GQLBase extends _events2.default {
/**
* 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 = {}, requestData = null, options = { autoProps: true }) {
super();
const Class = this.constructor;
const tree = _SyntaxTree.SyntaxTree.from(Class.SCHEMA);
const outline = tree && tree.outline || null;
if (!outline) {
throw new _FunctionExecutionError2.default(new Error(_neTagFns.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 _FunctionExecutionError2.default(new Error(_neTagFns.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 (${(0, _keys2.default)(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.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) {
_utils.LatticeLogs.warn(_utils.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 === _graphql.GraphQLEnumType) {
return;
}
let Class = this.constructor;
let tree = _SyntaxTree.SyntaxTree.from(Class.SCHEMA);
let outline = tree ? tree.outline : {};
let props = [];
// $FlowFixMe
for (let propName of (0, _keys2.default)(outline[Class.name])) {
// $FlowFixMe
let desc = (0, _getOwnPropertyDescriptor2.default)(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) {
_utils.LatticeLogs.info(`Creating auto-props for [${Class.name}]: `, props);
try {
(0, _ModelProperties.Properties)(...props)(Class, [AUTO_PROPS]);
} catch (error) {
let parsed = /Cannot redefine property: (\w+)/.exec(error.message);
if (parsed) {
_utils.LatticeLogs.warn(`Skipping auto-prop '${Class.name}.${parsed[1]}'`);
} else {
_utils.LatticeLogs.error(`Failed to apply auto-properties\nReason: `);
_utils.LatticeLogs.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) {
// @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) {
// $FlowFixMe
(0, _lodash.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() {
// @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) {
// @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 [_toStringTag2.default]() {
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, bindGetters = true, bindTo) {
// $FlowFixMe
let proto = (0, _getPrototypeOf2.default)(this);
let descriptor = (0, _getOwnPropertyDescriptor2.default)(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
*/
callProp(propName, ...args) {
var _this = this;
return (0, _asyncToGenerator3.default)(function* () {
// $FlowFixMe
let prop = _this.getProp(propName, ...args);
let result;
if (prop && (0, _neTypes.typeOf)(prop) === 'AsyncFunction') {
try {
result = yield prop.apply(_this, args);
} catch (error) {
throw new _AsyncFunctionExecutionError2.default(error, prop, args, result);
}
} else if (prop && (0, _neTypes.typeOf)(prop) === Function.name) {
try {
result = prop.apply(_this, args);
} catch (error) {
throw new _FunctionExecutionError2.default(error, prop, args, result);
}
if ((0, _neTypes.typeOf)(result) === _promise2.default.name) {
try {
result = yield result;
} catch (error) {
throw new _AwaitingPromiseError2.default(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
*/
getResolver(resolverName, requestData) {
var _this2 = this;
return (0, _asyncToGenerator3.default)(function* () {
return yield _this2.constructor.getResolver(resolverName, requestData || _this2.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 getResolver(resolverName, requestData) {
var _this3 = this;
return (0, _asyncToGenerator3.default)(function* () {
const reqData = requestData || null;
const rootObj = yield _this3.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, bindGetters = false, bindTo) {
let descriptor = (0, _getOwnPropertyDescriptor2.default)(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() {
return {
[this.DOC_CLASS]: _utils.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]: _utils.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]: _utils.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]: _utils.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() {
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 MUTATORS(requestData) {
return (0, _asyncToGenerator3.default)(function* () {
// 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 RESOLVERS(requestData) {
return (0, _asyncToGenerator3.default)(function* () {
// 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() {
return (0, _for2.default)('.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() {
return _graphql.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, extension = '.graphql') {
return (0, _for2.default)(`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() {
const key = (0, _for2.default)(`${_IDLFileHandler.IDLFileHandler.name}.${this.name}`);
// @ComputedType
if (!this[key]) {
// @ComputedType
this[key] = new _IDLFileHandler.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() {
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) {
const changeHandler = {
/**
* 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
* }
* ```
*/
(0, _defineProperty2.default)(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 [_toStringTag2.default]() {
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() {
return _utils.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 getMergedRoot(requestData, separateByType = false) {
var _this4 = this;
return (0, _asyncToGenerator3.default)(function* () {
const root = {};
const Class = _this4;
let _ = {
// $FlowFixMe
resolvers: Class[META_KEY].resolvers || [],
// $FlowFixMe
mutators: Class[META_KEY].mutators || [],
// $FlowFixMe
subscriptors: Class[META_KEY].subscriptors || []
};
let convert = function (f) {
let isFactoryClass = function (c) {
return !!Class[META_KEY][(0, _for2.default)('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 = function (p, c) {
return (0, _lodash.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
(0, _lodash.merge)(root, { Query: yield Class.RESOLVERS(requestData) }, { Mutation: yield 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 (!(0, _keys2.default)(root.Query).length) delete root.Query;
if (!(0, _keys2.default)(root.Mutation).length) delete root.Mutation;
if (!(0, _keys2.default)(root.Subscription).length) delete root.Subscription;
} else {
(0, _lodash.merge)(root, (yield Class.RESOLVERS(requestData)), (yield 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[(0, _for2.default)(this.name)];
if (!storage) {
storage = this[(0, _for2.default)(this.name)] = {};
}
return storage;
}
};
exports.default = GQLBase;
//# sourceMappingURL=GQLBase.js.map