UNPKG

40.4 kBJavaScriptView Raw
1/** @namespace GQLBaseEnv */
2// @flow
3
4import Path from 'path'
5import fs from 'fs'
6
7import { Deferred, joinLines } from './utils'
8import { typeOf } from 'ne-types'
9import { SyntaxTree } from './SyntaxTree'
10import { Properties } from './decorators/ModelProperties'
11import { GraphQLObjectType, GraphQLEnumType } from 'graphql'
12import { IDLFileHandler } from './IDLFileHandler'
13import { merge } from 'lodash'
14import { LatticeLogs as ll } from './utils'
15import { dedent } from 'ne-tag-fns'
16
17import AsyncFunctionExecutionError from './errors/AsyncFunctionExecutionError'
18import FunctionExecutionError from './errors/FunctionExecutionError'
19import AwaitingPromiseError from './errors/AwaitingPromiseError'
20
21import EventEmitter from 'events'
22
23/* Internal implementation to detect the existence of proxies. When present
24 * additional functionality is enabled. Proxies are native in Node >= 6 */
25const hasProxy = typeof global.Proxy !== 'undefined';
26
27/* Internal Symbol referring to real accessor to GQLBase model object */
28const _MODEL_KEY = Symbol.for('data-model-contents-value');
29
30/* Internal Symbol referring to the static object containing a proxy handler */
31const _PROXY_HANDLER = Symbol.for('internal-base-proxy-handler')
32
33/**
34 * Simple function to check if a supplied key matches a string of your
35 * choosing and that string is not a defined property on the instance
36 * passed to the check.
37 *
38 * @method GQLBaseEnv~notDefined
39 * @memberof GQLBaseEnv
40 * @since 2.5.0
41 *
42 * @param {string} keyToTest a String denoting the property you wish to test
43 * @param {mixed} keySupplied a value, coerced `toString()`, to compare to
44 * `keyToTest`
45 * @param {mixed} instance an object instance to check `hasOwnProperty` on for
46 * the `keyToTest` supplied.
47 * @return {Boolean} true if the property matches the supplied key and that
48 * property is not an ownedProperty of the instance supplied.
49 */
50export function notDefined(
51 keyToTest: string,
52 keySupplied: Object | string,
53 instance: Object
54) {
55 return (
56 new RegExp("^" + keyToTest + "$").test(keySupplied.toString())
57 && !instance.hasOwnProperty(keyToTest)
58 );
59}
60
61/**
62 * A `Symbol` used as a key to store the backing model data. Designed as a
63 * way to separate model data and GraphQL property accessors into logical bits.
64 *
65 * @type {Symbol}
66 * @memberof GQLBaseEnv
67 * @const
68 */
69export const MODEL_KEY = Symbol.for('data-model-contents-key');
70
71/**
72 * A `Symbol` used as a key to store the request data for an instance of the
73 * GQLBase object in question.
74 *
75 * @type {Symbol}
76 * @const
77 * @inner
78 * @memberof GQLBaseEnv
79 */
80export const REQ_DATA_KEY = Symbol.for('request-data-object-key');
81
82/**
83 * A nameless Symbol for use as a key to the internal decorator storage
84 *
85 * @type {Symbol}
86 * @const
87 * @inner
88 * @memberof GQLBaseEnv
89 */
90export const META_KEY = Symbol();
91
92/**
93 * A Symbol used to identify calls to @Properties for properties generated
94 * automatically upon instance creation.
95 *
96 * @type {Symbol}
97 * @const
98 * @inner
99 * @memberOf GQLBaseEnv
100 */
101export const AUTO_PROPS = Symbol.for('auto-props')
102
103/**
104 * A Symbol used to identify calls to @Getters for properties generated
105 * via decorator. These are stored in <class>[META_KEY][GETTERS]
106 *
107 * @type {Symbol}
108 * @const
109 * @inner
110 * @memberOf GQLBaseEnv
111 */
112export const GETTERS = Symbol.for('getters')
113
114/**
115 * A Symbol used to identify calls to @Setters for properties generated
116 * via decorator. These are stored in <class>[META_KEY][SETTERS]
117 *
118 * @type {Symbol}
119 * @const
120 * @inner
121 * @memberOf GQLBaseEnv
122 */
123export const SETTERS = Symbol.for('setters')
124
125/**
126 * A Symbol used to identify calls to @Properties for properties generated
127 * via decorator. These are stored in <class>[META_KEY][PROPS]
128 *
129 * @type {Symbol}
130 * @const
131 * @inner
132 * @memberOf GQLBaseEnv
133 */
134export const PROPS = Symbol.for('props')
135
136/**
137 * All GraphQL Type objects used in this system are assumed to have extended
138 * from this class. An instance of this class can be used to wrap an existing
139 * structure if you have one.
140 *
141 * @class GQLBase
142 */
143export class GQLBase extends EventEmitter {
144 fileHandler: ?IDLFileHandler;
145
146 /**
147 * Request data is passed to this object when constructed. Typically these
148 * objects, and their children, are instantiated by its own static MUTATORS
149 * and RESOLVERS. They should contain request specific state if any is to
150 * be shared.
151 *
152 * These can be considered request specific controllers for the object in
153 * question. The base class takes a single object which should contain all
154 * the HTTP/S request data and the graphQLParams is provided as the object
155 * { query, variables, operationName, raw }.
156 *
157 * When used with express-graphql, the requestData object has the format
158 * { req, res, gql } where
159 * • req is an Express 4.x request object
160 * • res is an Express 4.x response object
161 * • gql is the graphQLParams object in the format of
162 * { query, variables, operationName, raw }
163 * See https://github.com/graphql/express-graphql for more info
164 *
165 * @memberof GQLBase
166 * @method ⎆⠀constructor
167 * @constructor
168 *
169 * @param {mixed} modelData this, typically an object, although anything
170 * really is supported, represents the model data for our GraphQL object
171 * instance.
172 * @param {Object} requestData see description above
173 */
174 constructor(
175 modelData: Object = {},
176 requestData: ?Object = null,
177 options: Object = { autoProps: true }
178 ) {
179 super();
180
181 const Class = this.constructor;
182 const tree = SyntaxTree.from(Class.SCHEMA);
183 const outline = tree && tree.outline || null;
184
185 if (!outline) {
186 throw new FunctionExecutionError(
187 new Error(dedent`
188 The SDL is unparsable. Please check your SCHEMA and make sure
189 it is valid GraphQL SDL/IDL. Your SCHEMA is defined as:
190
191 ${this.SCHEMA}
192 `)
193 )
194 }
195
196 if (outline && !(Class.name in outline)) {
197 throw new FunctionExecutionError(
198 new Error(dedent`
199 The class name "${Class.name}" does not match any of the types,
200 enums, scalars, unions or interfaces defined in the SCHEMA for
201 this class (${Object.keys(outline)}).
202
203 \x1b[1mIn most clases this is because your class name and SCHEMA
204 type do not match.\x1b[0m
205 `)
206 )
207 }
208
209 GQLBase.setupModel(this);
210 this.setModel(modelData);
211 this.requestData = requestData || {};
212 this.fileHandler = new IDLFileHandler(this.constructor);
213
214 if (options && !!options.autoProps !== false) {
215 this.applyAutoProps()
216 }
217
218 // @ComputedType
219 return hasProxy ? new Proxy(this, GQLBase[_PROXY_HANDLER]) : this;
220 }
221
222 /**
223 * Since reading the Schema for a given GraphQL Lattice type or
224 * interface is simple enough, we should be able to automatically
225 * apply one to one GraphQL:Model properties.
226 *
227 * @instance
228 * @method ⌾⠀applyAutoProps
229 * @memberof GQLBase
230 */
231 applyAutoProps() {
232 if (!this.constructor.SCHEMA || !this.constructor.SCHEMA.length) {
233 ll.warn(joinLines`
234 There is no SCHEMA for ${this.constructor.name}!! This will likely
235 end in an error. Proceed with caution. Skipping \`applyAutoProps\`
236 `)
237 return
238 }
239
240 // Individual property getters do not need to be auto-created for enum
241 // types. Potentially do some checks for Interfaces and Unions as well
242 if (this.constructor.GQL_TYPE === GraphQLEnumType) {
243 return
244 }
245
246 let Class = this.constructor
247 let tree = SyntaxTree.from(Class.SCHEMA)
248 let outline = tree ? tree.outline : {}
249 let props = []
250
251 // $FlowFixMe
252 for (let propName of Object.keys(outline[Class.name])) {
253 // $FlowFixMe
254 let desc = Object.getOwnPropertyDescriptor(Class.prototype, propName)
255 let hasCustomImpl = !!(
256 // We have a descriptor for the property name
257 desc && (
258 // We have a getter function defined
259 typeof desc.get !== 'undefined'
260 ||
261 // ...or we have a function, async or not, defined
262 typeof desc.value === 'function'
263 )
264 )
265
266 // Only create auto-props for non custom implementations
267 if (!hasCustomImpl) {
268 props.push(propName)
269 }
270 }
271
272 if (props.length) {
273 ll.info(`Creating auto-props for [${Class.name}]: `, props)
274 try {
275 Properties(...props)(Class, [AUTO_PROPS])
276 }
277 catch(error) {
278 let parsed = /Cannot redefine property: (\w+)/.exec(error.message)
279 if (parsed) {
280 ll.warn(`Skipping auto-prop '${Class.name}.${parsed[1]}'`)
281 }
282 else {
283 ll.error(`Failed to apply auto-properties\nReason: `)
284 ll.error(error);
285 }
286 }
287 }
288 }
289
290 /**
291 * Getter for the internally stored model data. The contents of this
292 * object are abstracted away behind a `Symbol` key to prevent collision
293 * between the underlying model and any GraphQL Object Definition properties.
294 *
295 * @instance
296 * @memberof GQLBase
297 * @method ⌾⠀getModel
298 * @since 2.5
299 *
300 * @param {Object} value any object you wish to use as a data store
301 */
302 getModel() {
303 // @ComputedType
304 return this[MODEL_KEY];
305 }
306
307 /**
308 * Setter for the internally stored model data. The contents of this
309 * object are abstracted away behind a `Symbol` key to prevent collision
310 * between the underlying model and any GraphQL Object Definition properties.
311 *
312 * @instance
313 * @memberof GQLBase
314 * @method ⌾⠀setModel
315 * @since 2.5
316 *
317 * @param {Object} value any object you wish to use as a data store
318 */
319 setModel(value: Object): GQLBase {
320 // @ComputedType
321 this[MODEL_KEY] = value;
322 return this;
323 }
324
325 /**
326 * Uses `_.merge()` to modify the internal backing data store for the
327 * object instance. This is a shortcut for
328 * `_.merge()(instance[MODEL_KEY], ...extensions)`
329 *
330 * @instance
331 * @memberof GQLBase
332 * @method ⌾⠀extendModel
333 * @since 2.5
334 *
335 * @param {mixed} extensions n-number of valid `_.merge()` parameters
336 * @return {GQLBase} this is returned
337 */
338 extendModel(...extensions: Array<mixed>): GQLBase {
339 // $FlowFixMe
340 merge(this[MODEL_KEY], ...extensions);
341 return this;
342 }
343
344 /**
345 * A getter that retrieves the inner request data object. When used with
346 * GQLExpressMiddleware, this is an object matching {req, res, gql}.
347 *
348 * @instance
349 * @memberof GQLBase
350 * @method ⬇︎⠀requestData
351 *
352 * @return {Object} an object, usually matching { req, res, gql }
353 */
354 get requestData(): Object | null {
355 // @ComputedType
356 return this[REQ_DATA_KEY];
357 }
358
359 /**
360 * A setter that assigns a value to the inner request data object. When
361 * used with GQLExpressMiddleware, this is an object matching {req, res, gql}.
362 *
363 * @instance
364 * @memberof GQLBase
365 * @method ⬆︎⠀requestData
366 *
367 * @param {Object} value an object, usually matching { req, res, gql }
368 */
369 set requestData(value: Object): void {
370 // @ComputedType
371 this[REQ_DATA_KEY] = value;
372 }
373
374 /**
375 * Returns the `constructor` name. If invoked as the context, or `this`,
376 * object of the `toString` method of `Object`'s `prototype`, the resulting
377 * value will be `[object MyClass]`, given an instance of `MyClass`
378 *
379 * @method ⌾⠀[Symbol.toStringTag]
380 * @memberof ModuleParser
381 *
382 * @return {string} the name of the class this is an instance of
383 * @ComputedType
384 */
385 get [Symbol.toStringTag]() { return this.constructor.name }
386
387 /**
388 * Properties defined for GraphQL types in Lattice can be defined as
389 * a getter, a function or an async function. In the case of standard
390 * functions, if they return a promise they will be handled as though
391 * they were async
392 *
393 * Given the variety of things a GraphQL type can actually be, obtaining
394 * its value can annoying. This method tends to lessen that boilerplate.
395 * Errors raised will be thrown.
396 *
397 * @instance
398 * @memberof GQLBase
399 * @method ⌾⠀getProp
400 *
401 * @param {string|Symbol} propName the name of the property in question
402 * @param {boolean} bindGetters true, by default, if the `get` or
403 * `initializer` descriptor values should be bound to the current instance
404 * or an object of the programmers choice before returning
405 * @param {mixed} bindTo the `this` object to use for binding when
406 * `bindGetters` is set to true.
407 * @return {mixed} the value of the `propName` as a Function or something
408 * else when the requested property name exists
409 *
410 * @throws {Error} errors raised in awaiting results will be thrown
411 */
412 getProp(propName: string, bindGetters: boolean = true, bindTo: mixed) {
413 // $FlowFixMe
414 let proto = Object.getPrototypeOf(this)
415 let descriptor = Object.getOwnPropertyDescriptor(proto, propName)
416 let result
417
418 if (!descriptor) {
419 return null;
420 }
421
422 if (descriptor) {
423 if (descriptor.initializer || descriptor.get) {
424 let what = descriptor.initializer || descriptor.get
425
426 if (bindGetters) {
427 result = what.bind(bindTo || this)
428 }
429 else {
430 result = what
431 }
432 }
433 else if (descriptor.value) {
434 result = descriptor.value
435 }
436 }
437
438 return result
439 }
440
441 /**
442 * Properties defined for GraphQL types in Lattice can be defined as
443 * a getter, a function or an async function. In the case of standard
444 * functions, if they return a promise they will be handled as though
445 * they were async. In addition to fetching the property, or field
446 * resolver, its resulting function or getter will be invoked.
447 *
448 * Given the variety of things a GraphQL type can actually be, obtaining
449 * its value can annoying. This method tends to lessen that boilerplate.
450 * Errors raised will be thrown.
451 *
452 * @instance
453 * @memberof GQLBase
454 * @method ⌾⠀callProp
455 *
456 * @param {string} propName the name of the property in question
457 * @param {Array<mixed>} args the arguments array that will be passed
458 * to `.apply()` should the property evaluate to a `function`
459 * @return {mixed} the return value of any resulting function or
460 * value returned by a getter; wrapped in a promise as all async
461 * functions do.
462 *
463 * @throws {Error} errors raised in awaiting results will be thrown
464 */
465 async callProp(propName: string, ...args: Array<mixed>) {
466 // $FlowFixMe
467 let prop = this.getProp(propName, ...args);
468 let result
469
470 if (prop && typeOf(prop) === 'AsyncFunction') {
471 try {
472 result = await prop.apply(this, args);
473 }
474 catch (error) {
475 throw new AsyncFunctionExecutionError(error, prop, args, result)
476 }
477 }
478 else if (prop && typeOf(prop) === Function.name) {
479 try {
480 result = prop.apply(this, args)
481 }
482 catch (error) {
483 throw new FunctionExecutionError(error, prop, args, result)
484 }
485
486 if (typeOf(result) === Promise.name) {
487 try {
488 result = await result
489 }
490 catch (error) {
491 throw new AwaitingPromiseError(error).setPromise(result)
492 }
493 }
494 }
495
496 return result
497 }
498
499 /**
500 * A pass-thru method to the static function of the same name. The
501 * difference being that if `requestData` is not specified, the
502 * `requestData` object from this instance will be used to build the
503 * resolvers in question.
504 *
505 * @instance
506 * @method ⌾⠀getResolver
507 * @memberof GQLBase
508 *
509 * @param {string} resolverName the name of the resolver as a string
510 * @param {Object} requestData the requestData used to build the
511 * resolver methods from which to choose
512 * @return {Function} returns either a `function` representing the
513 * resolver requested or null if there wasn't one to be found
514 */
515 async getResolver(resolverName: string, requestData: Object) {
516 return await this.constructor.getResolver(
517 resolverName,
518 requestData || this.requestData
519 )
520 }
521
522 /**
523 * Resolvers are created in a number of different ways. OOP design
524 * dictates that instances of a created class will handle field
525 * resolvers, but query, mutation and subscription resolvers are
526 * typically what creates these instances.
527 *
528 * Since a resolver can be created using `@mutator/@subscriptor/@resolver`
529 * or via method on a object returned from `RESOLVERS()`, `MUTATORS()` or
530 * `SUBSCRIPTIONS()`, there should be an easy to use way to fetch a
531 * resolver by name; if for nothing else, code reuse.
532 *
533 * Pass the name of the resolver to the function and optionally pass a
534 * requestData object. The `getMergedRoot()` method will build an object
535 * containing all the root resolvers for the type, bound to the supplied
536 * `requestData` object. It is from this object that `resolverName` will
537 * be used to fetch the function in question. If one exists, it will be
538 * returned, ready for use. Otherwise, null will be your answer.
539 *
540 *
541 * @static
542 * @method ⌾⠀getResolver
543 * @memberof GQLBase
544 *
545 * @param {string} resolverName the name of the resolver as a string
546 * @param {Object} requestData the requestData used to build the
547 * resolver methods from which to choose
548 * @return {Function} returns either a `function` representing the
549 * resolver requested or null if there wasn't one to be found
550 */
551 static async getResolver(resolverName: string, requestData: Object) {
552 const reqData = requestData || null
553 const rootObj = await this.getMergedRoot(reqData)
554
555 return rootObj[resolverName] || null
556 }
557
558 /**
559 * The static version of getProp reads into the prototype to find the field
560 * that is desired. If the field is either a getter or a initializer (see
561 * class properties descriptors), then the option to bind that to either the
562 * prototype object or one of your choosing is available.
563 *
564 * @memberof GQLBase
565 * @method ⌾⠀getProp
566 * @static
567 *
568 * @param {string|Symbol} propName a string or Symbol denoting the name of
569 * the property or field you desire
570 * @param {boolean} bindGetters true if a resulting `getter` or `initializer`
571 * should be bound to the prototype or other object
572 * @param {mixed} bindTo the object to which to bind the `getter` or
573 * `initializer` functions to if other than the class prototype.
574 * @return {mixed} a `Function` or other mixed value making up the property
575 * name requested
576 */
577 static getProp(
578 propName: string,
579 bindGetters: boolean = false,
580 bindTo: mixed
581 ) {
582 let descriptor = Object.getOwnPropertyDescriptor(this.prototype, propName)
583
584 if (descriptor) {
585 if (descriptor.get || descriptor.initializer) {
586 let what = descriptor.initializer || descriptor.get
587
588 if (bindGetters) {
589 bindTo = bindTo || this.prototype
590
591 return what.bind(bindTo)
592 }
593 else {
594 return what
595 }
596 }
597 else {
598 return descriptor.value
599 }
600 }
601 else {
602 return null
603 }
604 }
605
606 /**
607 * Until such time as the reference implementation of Facebook's GraphQL
608 * SDL AST parser supports comments, or until we take advantage of Apollo's
609 * AST parser, this is how comments will be applied to a built schema.
610 *
611 * Several constants are defined on the GQLBase object itself, and thereby
612 * all its subclasses. They pertain to how to define description fields
613 * for various parts of your GQL implementation.
614 *
615 * ```
616 * // To define a description on the top level class
617 * [this.DOC_CLASS]: string
618 *
619 * // To define a description on a field (getter, function or async function)
620 * [this.DOC_FIELDS]: {
621 * fieldName: string
622 * }
623 *
624 * // To define a description on a query, mutation or subscription field
625 * [this.DOC_QUERIES || this.DOC_MUTATORS || this.DOC_SUBSCRIPTIONS]: {
626 * fieldName: string
627 * }
628 * ```
629 *
630 * To make writing code easier, the `joinLines()` template function is
631 * available so your source code can look nice and neat and your descriptions
632 * won't get annoying line breaks and spaces as part of that process.
633 *
634 * @static
635 * @memberof GQLBase
636 * @method apiDocs
637 *
638 * @return {Object} an object with various keys and values denoting
639 * description fields that should be applied to the final schema object
640 */
641 static apiDocs(): Object {
642 return {
643 [this.DOC_CLASS]: joinLines`
644 GQLBase class implementation. GQLBase is the root class used in
645 graphql-lattice to describe a GraphQLObjectType. If you are reading
646 this, the person using lattice failed to provide documentation for
647 their type. :)
648 `,
649
650 [this.DOC_QUERY]: joinLines`
651 ## Welcome to GraphQL Lattice
652 **Query**
653
654 You will want to define a \`DOC_QUERY\` apiDoc comment with something
655 more meaningful to your particular Schema here.
656 `,
657
658 [this.DOC_MUTATION]: joinLines`
659 ## Welcome to GraphQL Lattice
660 **Mutation**
661
662 You will want to define a \`DOC_MUTATION\` apiDoc comment with
663 something more meaningful to your particular Schema here.
664 `,
665
666 [this.DOC_SUBSCRIPTION]: joinLines`
667 ## Welcome to GraphQL Lattice
668 **Subscription**
669
670 You will want to define a \`DOC_SUBSCRIPTION\` apiDoc comment with
671 something more meaningful to your particular Schema here.
672 `,
673
674 [this.DOC_FIELDS]: {
675 // fieldName: `fieldDescription`,
676 },
677
678 [this.DOC_QUERIES]: {
679 // queryName: `queryDescription`,
680 },
681
682 [this.DOC_MUTATORS]: {
683 // mutatorName: `mutatorDescription`
684 },
685
686 [this.DOC_SUBSCRIPTIONS]: {
687 // subscriptionName: `subscriptionDescription`
688 }
689 }
690 }
691
692 /**
693 * Defined in a base class, this getter should return either a String
694 * detailing the full IDL schema of a GraphQL handler or one of two
695 * types of Symbols.
696 *
697 * The first Symbol type is the constant `ADJACENT_FILE`. If this Symbol is
698 * returned, the system assumes that next to the source file in question is
699 * a file of the same name with a .graphql extension. This file should be
700 * made of the GraphQL IDL schema definitions for the object types being
701 * created.
702 *
703 * Example:
704 * ```js
705 * static get SCHEMA(): string | Symbol {
706 * return GQLBase.ADJACENT_FILE
707 * }
708 * ```
709 *
710 * The primary advantage of this approach is allowing an outside editor that
711 * provides syntax highlighting rather than returning a string from the
712 * SCHEMA getter.
713 *
714 * Alternatively, the static method IDLFilePath can be used to point to an
715 * alternate location where the GraphQL IDL file resides. The extension can
716 * also be changed from .graphql to something else if need be using this
717 * method.
718 *
719 * Example:
720 * ```js
721 * static get SCHEMA(): string | Symbol {
722 * return GQLBase.IDLFilePath('/path/to/file', '.idl')
723 * }
724 * ```
725 *
726 * @instance
727 * @memberof GQLBase
728 * @method ⬇︎⠀SCHEMA
729 * @readonly
730 * @static
731 *
732 * @return {string|Symbol} a valid IDL string or one of the Symbols
733 * described above.
734 *
735 * @see {@link GQLBase#ADJACENT_FILE}
736 * @see {@link GQLBase#IDLFilePath}
737 */
738 static get SCHEMA(): string | Symbol {
739 return ''
740 }
741
742 /**
743 * This method should return a promise that resolves to an object of
744 * functions matching the names of the mutation operations. These are to be
745 * injected into the root object when used by `GQLExpressMiddleware`.
746 *
747 * @instance
748 * @memberof GQLBase
749 * @method ⌾⠀MUTATORS
750 * @readonly
751 * @static
752 *
753 * @param {Object} requestData typically an object containing three
754 * properties; {req, res, gql}
755 * @return {Promise} a promise that resolves to an object; see above for more
756 * information.
757 */
758 static async MUTATORS(requestData: Object): Promise<Object> {
759 // define in base class
760 return {};
761 }
762
763 /**
764 * This method should return a promise that resolves to an object of
765 * functions matching the names of the query operations. These are to be
766 * injected into the root object when used by `GQLExpressMiddleware`.
767 *
768 * @instance
769 * @memberof GQLBase
770 * @method ⌾⠀RESOLVERS
771 * @readonly
772 * @static
773 *
774 * @param {Object} requestData typically an object containing three
775 * properties; {req, res, gql}
776 * @return {Promise} a promise that resolves to an object; see above for more
777 * information.
778 */
779 static async RESOLVERS(requestData: Object): Promise<Object> {
780 // define in base class
781 return {};
782 }
783
784 /**
785 * @see {@link GQLBase#SCHEMA}
786 *
787 * @memberof GQLBase
788 * @method ⬇︎⠀ADJACENT_FILE
789 * @static
790 * @const
791 *
792 * @return {Symbol} the Symbol, when returned from SCHEMA, causes
793 * the logic to load an IDL Schema from an associated file with a .graphql
794 * extension and bearing the same name.
795 */
796 static get ADJACENT_FILE(): Symbol {
797 return Symbol.for('.graphql file located adjacent to source')
798 }
799
800 /**
801 * Determines the default type targeted by this GQLBase class. Any
802 * type will technically be valid but only will trigger special behavior
803 *
804 * @memberof GQLBase
805 * @method ⬇︎⠀GQL_TYPE
806 * @static
807 * @const
808 *
809 * @return {Function} a type, such as `GraphQLObjectType` or
810 * `GraphQLInterfaceType`
811 */
812 static get GQL_TYPE(): Function {
813 return GraphQLObjectType;
814 }
815
816 /**
817 * Creates an appropriate Symbol crafted with the right data for use by
818 * the IDLFileHandler class below.
819 *
820 * @static
821 * @memberof GQLBase
822 * @method ⌾⠀IDLFilePath
823 *
824 * @param {string} path a path to the IDL containing file
825 * @param {string} [extension='.graphql'] an extension, including the
826 * prefixed period, that will be added to the supplied path should it not
827 * already exist.
828 * @return Symbol
829 *
830 * @see {@link GQLBase#SCHEMA}
831 */
832 static IDLFilePath(path: string, extension: string = '.graphql'): Symbol {
833 return Symbol.for(`Path ${path} Extension ${extension}`);
834 }
835
836 /**
837 * A file handler for fetching the IDL schema string from the file system
838 * for those `GQLBase` extended classes that have indicated to do so by
839 * returning a `Symbol` for their `SCHEMA` property.
840 *
841 * @static
842 * @memberof GQLBase
843 * @method ⬇︎⠀handler
844 *
845 * @return {IDLFileHandler} instance of IDLFileHandler, created if one does
846 * not already exist, for fetching the contents from disk.
847 */
848 static get handler(): IDLFileHandler {
849 const key = Symbol.for(`${IDLFileHandler.name}.${this.name}`);
850
851 // @ComputedType
852 if (!this[key]) {
853 // @ComputedType
854 this[key] = new IDLFileHandler(this);
855 }
856
857 // @ComputedType
858 return this[key];
859 }
860
861 /**
862 * Returns the module object where your class is created. This needs to be
863 * defined on your class, as a static getter, in the FILE where you are
864 * defining your Class definition.
865 *
866 * @static
867 * @memberof GQLBase
868 * @method ⬇︎⠀module
869 * @const
870 *
871 * @return {Object} the reference to the module object defined and injected
872 * by node.js' module loading system.
873 *
874 * @see https://nodejs.org/api/modules.html
875 */
876 static get module(): Object {
877 return module;
878 }
879
880 /**
881 * The internal data model has some custom `EventEmitter` code wrapped
882 * it here. When the data model is set via `setModel` or by accessing it
883 * via `instance[MODEL_KEY]`, an event `EVENT_MODEL_SET` is emitted. Any
884 * listener listening for this event receives an object with two keys
885 * ```
886 * {
887 * model: The actual model being set; changes are persisted
888 * instance: The GQLBase instance the model is associated with
889 * }
890 * ```
891 *
892 * Subsequently, the events `EVENT_MODEL_PROP_CHANGE` and
893 * `EVENT_MODEL_PROP_DELETE` can be listened to if your version of node
894 * supports Proxy objects. They allow you to be notified whenever your
895 * model has a property changed or deleted, respectively.
896 *
897 * The callback for `change` receives an object with four properties
898 * ```
899 * {
900 * model: The model object the value is being changed on
901 * old: The old value being replaced; undefined if it is the first time
902 * key: The property key for the value being changed
903 * value: The new value being set
904 * }
905 * ```
906 *
907 * The callback for `delete` receives an object with four properties
908 * ```
909 * {
910 * model: The model object the value is deleted from
911 * key: The property key for the deleted value
912 * deleted: The deleted value
913 * }
914 * ```
915 *
916 * @static
917 * @memberof GQLBase
918 * @method ⌾⠀setupModel
919 *
920 * @param {GQLBase} instance typically `this` as passed in from a call in
921 * the constructor
922 */
923 static setupModel(instance: GQLBase) {
924 const changeHandler: Object = {
925 /**
926 * Proxy set() handler. This is where the change events are fired from
927 *
928 * @method GQLBase~set
929 * @param {Object} target the `GQLBase` model object
930 * @param {string} key the property name
931 * @param {mixed} value the new property value
932 */
933 set(target, key, value) {
934 const old = target[key];
935
936 target[key] = value;
937 instance.emit(GQLBase.EVENT_MODEL_PROP_CHANGE, {
938 model: target,
939 old,
940 key,
941 value
942 })
943 },
944
945 /**
946 * Proxy deleteProperty() handler. This is where the delete property
947 * events are fired from
948 *
949 * @method GQLBase~deleteProperty
950 * @param {Object} target the `GQLBase` model object
951 * @param {string} key the property name
952 */
953 deleteProperty(target, key) {
954 const deleted = target[key];
955
956 delete target[key];
957 instance.emit(GQLBase.EVENT_MODEL_PROP_DELETE, {
958 model: target,
959 key,
960 deleted
961 })
962 }
963 }
964
965 /**
966 * 'Publicly' the Symbol for accessing the `GQLBase` model is `MODEL_KEY`.
967 * In truth it is stored under a Symbol defined in `setupModel` and
968 * referred to as `_MODEL_KEY` in this code. This is done so a getter and
969 * setter can be wrapped around the usage of the instance's data model.
970 *
971 * When being read, if `Proxy` exists in the node environment and if there
972 * are any registered `EVENT_MODEL_PROP_CHANGE` or `EVENT_MODEL_PROP_DELETE`
973 * events, then the returned model is a Proxy around the real model that
974 * allows us to capture the changes and deletion of keys
975 *
976 * When being assigned, the event `EVENT_MODEL_WILL_BE_SET` and the event
977 * `EVENT_MODEL_HAS_BEEN_SET` are emitted to allow listeners to modify and
978 * see the final data around the setting of a model object. Both events
979 * receive an object with two keys
980 *
981 * ```
982 * {
983 * model: The object being or having been set
984 * instance: The GQLBase instance receiving the model
985 * }
986 * ```
987 */
988 Object.defineProperty(instance, MODEL_KEY, {
989 get: function() {
990 let model = this[_MODEL_KEY]
991 let hasListeners =
992 this.listenerCount(GQLBase.EVENT_MODEL_PROP_CHANGE) +
993 this.listenerCount(GQLBase.EVENT_MODEL_PROP_DELETE)
994
995 if (hasProxy && hasListeners) {
996 model = new Proxy(model, changeHandler);
997 }
998
999 return model
1000 },
1001
1002 set: function(model) {
1003 const instance = this;
1004
1005 this.emit(GQLBase.EVENT_MODEL_WILL_BE_SET, { model, instance });
1006 instance[_MODEL_KEY] = model;
1007 this.emit(GQLBase.EVENT_MODEL_HAS_BEEN_SET, { model, instance })
1008 }
1009 });
1010 }
1011
1012 /**
1013 * If ES6 Proxies are supported in your execution environment, all GQLBase
1014 * extended classes are also proxies. By default the internal proxy handler
1015 * provides backwards compatibility with the removal of the default getters
1016 * and setters for the 'model' property as long as you do not define a
1017 * top level 'model' property of your own.
1018 *
1019 * @method ⬇︎⠀[_PROXY_HANDLER]
1020 * @memberof GQLBase
1021 * @static
1022 * @const
1023 * @since 2.5.0
1024 *
1025 * @type {Object}
1026 * @ComputedType
1027 */
1028 static get [_PROXY_HANDLER]() {
1029 return {
1030 get(target, key, lastResult) {
1031 const model = target[_MODEL_KEY];
1032
1033 // Allow backwards compatibility for 'model' property if one is not
1034 // explicitly defined on your instance.
1035 if (notDefined('model', key, target)) {
1036 // Be sure to use the public MODEL_KEY to ensure events fire
1037 return target[MODEL_KEY];
1038 }
1039
1040 return target[key]
1041 }
1042 }
1043 }
1044
1045 /**
1046 * Applies the same logic as {@link #[Symbol.toStringTag]} but on a static
1047 * scale. So, if you perform `Object.prototype.toString.call(MyClass)`
1048 * the result would be `[object MyClass]`.
1049 *
1050 * @method ⌾⠀[Symbol.toStringTag]
1051 * @memberof ModuleParser
1052 * @static
1053 *
1054 * @return {string} the name of this class
1055 * @ComputedType
1056 */
1057 static get [Symbol.toStringTag]() { return this.name }
1058
1059 /**
1060 * A constant used to register an event listener for when the internal
1061 * model object is assigned a new value. This event fires before the model
1062 * is set. Changes to the model value at this point will affect the contents
1063 * before the value assignment takes place.
1064 *
1065 * @static
1066 * @memberof GQLBase
1067 * @method ⬇︎⠀EVENT_MODEL_WILL_BE_SET
1068 * @const
1069 *
1070 * @type {string}
1071 */
1072 static get EVENT_MODEL_WILL_BE_SET() { return 'E: Int. model will be set' }
1073
1074 /**
1075 * A constant used to register an event listener for when the internal
1076 * model object is assigned a new value. This event fires after the model
1077 * is set.
1078 *
1079 * @static
1080 * @memberof GQLBase
1081 * @method ⬇︎⠀EVENT_MODEL_HAS_BEEN_SET
1082 * @const
1083 *
1084 * @type {string}
1085 */
1086 static get EVENT_MODEL_HAS_BEEN_SET() { return 'E: Int. model has been set' }
1087
1088 /**
1089 * A constant used to register an event listener for when a property of the
1090 * internal model object is set to a new or intial value.
1091 *
1092 * @static
1093 * @memberof GQLBase
1094 * @method ⬇︎⠀EVENT_MODEL_PROP_CHANGE
1095 * @const
1096 *
1097 * @type {string}
1098 */
1099 static get EVENT_MODEL_PROP_CHANGE() { return 'E: Int. model prop changed' }
1100
1101 /**
1102 * A constant used to register an event listener for when a property of the
1103 * internal model object has been deleted. This event fires after the value
1104 * has been deleted.
1105 *
1106 * @static
1107 * @memberof GQLBase
1108 * @method ⬇︎⠀EVENT_MODEL_PROP_DELETE
1109 * @const
1110 *
1111 * @type {string}
1112 */
1113 static get EVENT_MODEL_PROP_DELETE() { return 'E: Int. model prop deleted' }
1114
1115 /**
1116 * A constant key used to identify a comment for a class description
1117 *
1118 * @static
1119 * @memberof GQLBase
1120 * @method ⬇︎⠀DOC_CLASS
1121 * @const
1122 *
1123 * @type {string}
1124 */
1125 static get DOC_CLASS() { return 'class' }
1126
1127 /**
1128 * A constant key used to identify a comment for a type field description
1129 *
1130 * @static
1131 * @memberof GQLBase
1132 * @method ⬇︎⠀DOC_FIELDS
1133 * @const
1134 *
1135 * @type {string}
1136 */
1137 static get DOC_FIELDS() { return 'fields' }
1138
1139 /**
1140 * A constant key used to identify a comment for the top level query
1141 * description
1142 *
1143 * @static
1144 * @memberof GQLBase
1145 * @method ⬇︎⠀DOC_QUERY
1146 * @const
1147 *
1148 * @type {string}
1149 */
1150 static get DOC_QUERY() { return 'query' }
1151
1152 /**
1153 * A constant key used to identify a comment for a query description
1154 *
1155 * @static
1156 * @memberof GQLBase
1157 * @method ⬇︎⠀DOC_QUERIES
1158 * @const
1159 *
1160 * @type {string}
1161 */
1162 static get DOC_QUERIES() { return 'queries' }
1163
1164 /**
1165 * A constant key used to identify a comment for the top level mutation
1166 * description
1167 *
1168 * @static
1169 * @memberof GQLBase
1170 * @method ⬇︎⠀DOC_MUTATION
1171 * @const
1172 *
1173 * @type {string}
1174 */
1175 static get DOC_MUTATION() { return 'mutation' }
1176
1177 /**
1178 * A constant key used to identify a comment for a mutator description
1179 *
1180 * @static
1181 * @memberof GQLBase
1182 * @method ⬇︎⠀DOC_MUTATORS
1183 * @const
1184 * @deprecated Use `DOC_MUTATIONS` instead
1185 *
1186 * @type {string}
1187 */
1188 static get DOC_MUTATORS() { return 'mutators' }
1189
1190 /**
1191 * A constant key used to identify a comment for a mutator description
1192 *
1193 * @static
1194 * @memberof GQLBase
1195 * @method ⬇︎⠀DOC_MUTATORS
1196 * @const
1197 *
1198 * @type {string}
1199 */
1200 static get DOC_MUTATIONS() { return 'mutators' }
1201
1202 /**
1203 * A constant key used to identify a comment for the top level subscription
1204 * description
1205 *
1206 * @static
1207 * @memberof GQLBase
1208 * @method ⬇︎⠀DOC_SUBSCRIPTION
1209 * @const
1210 *
1211 * @type {string}
1212 */
1213 static get DOC_SUBSCRIPTION() { return 'subscription' }
1214
1215 /**
1216 * A constant key used to identify a comment for a subscription description
1217 *
1218 * @static
1219 * @memberof GQLBase
1220 * @method ⬇︎⠀DOC_SUBSCRIPTIONS
1221 * @const
1222 *
1223 * @type {string}
1224 */
1225 static get DOC_SUBSCRIPTIONS() { return 'subscriptions' }
1226
1227 /**
1228 * A shortcut to the utils/joinLines function to make it easier to get
1229 * the tools to write docs for your types in a friendly fashion.
1230 *
1231 * @memberof GQLBase
1232 * @method ⬇︎⠀joinLines
1233 * @static
1234 * @const
1235 *
1236 * @type {Function}
1237 */
1238 static get joinLines(): Function { return joinLines }
1239
1240 /**
1241 * An simple pass-thru method for fetching a types merged root object.
1242 *
1243 * @method ⌾⠀getMergedRoot
1244 * @memberof GQLBase
1245 * @static
1246 *
1247 * @param {Object} requestData an object containing the request data such as
1248 * request, response or graphql context info that should be passed along to
1249 * each of the resolver creators
1250 * @return {Object} the merged root object with all the query, mutation and
1251 * subscription resolvers defined and created within.
1252 */
1253 static async getMergedRoot(
1254 requestData: Object,
1255 separateByType: boolean = false
1256 ): Object {
1257 const root = {};
1258 const Class = this;
1259
1260 let _ = {
1261 // $FlowFixMe
1262 resolvers: Class[META_KEY].resolvers || [],
1263 // $FlowFixMe
1264 mutators: Class[META_KEY].mutators || [],
1265 // $FlowFixMe
1266 subscriptors: Class[META_KEY].subscriptors || []
1267 }
1268
1269 let convert = f => {
1270 let isFactoryClass = (c) => {
1271 return !!Class[META_KEY][Symbol.for('Factory Class')]
1272 }
1273
1274 if (isFactoryClass(Class)) {
1275 return {
1276 [f.name]: function(...args) {
1277 return f.apply(Class, [Class, requestData, ...args])
1278 }
1279 }
1280 }
1281 else {
1282 return {
1283 [f.name]: function(...args) {
1284 return f.apply(Class, [requestData, ...args])
1285 }
1286 }
1287 }
1288 }
1289 let reduce = (p, c) => merge(p, c)
1290
1291 _.resolvers = _.resolvers.map(convert).reduce(reduce, {})
1292 _.mutators = _.mutators.map(convert).reduce(reduce, {})
1293 _.subscriptors = _.subscriptors.map(convert).reduce(reduce, {})
1294
1295 if (separateByType) {
1296 // Apollo wants all the resolvers to grouped by top level type.
1297 // The field resolvers aren't an issue in Lattice defined types
1298 // but the root types do need to be sorted; so let's do that here
1299 merge(
1300 root,
1301 { Query: await Class.RESOLVERS(requestData) },
1302 { Mutation: await Class.MUTATORS(requestData) },
1303 { Query: _.resolvers },
1304 { Mutation: _.mutators },
1305 { Subscription: _.subscriptors }
1306 );
1307
1308 // When using lattice with apollo server, it is quite particular about
1309 // empty Query, Mutation or Subscription resolver maps.
1310 if (!Object.keys(root.Query).length) delete root.Query
1311 if (!Object.keys(root.Mutation).length) delete root.Mutation
1312 if (!Object.keys(root.Subscription).length) delete root.Subscription
1313 }
1314 else {
1315 merge(
1316 root,
1317 await Class.RESOLVERS(requestData),
1318 await Class.MUTATORS(requestData),
1319 _.resolvers,
1320 _.mutators,
1321 _.subscriptors
1322 );
1323 }
1324
1325 return root;
1326 }
1327
1328 /**
1329 * An object used to store data used by decorators and other internal
1330 * proccesses.
1331 * @ComputedType
1332 */
1333 static get [META_KEY]() {
1334 let storage = this[Symbol.for(this.name)]
1335
1336 if (!storage) {
1337 storage = (this[Symbol.for(this.name)] = {})
1338 }
1339
1340 return storage;
1341 }
1342}
1343
1344export default GQLBase;