/** @namespace GQLBaseEnv */
// @flow
import Path from 'path'
import fs from 'fs'
import { Deferred } from './utils'
import { typeOf } from './types'
import { SyntaxTree } from './SyntaxTree'
/**
* Constant referring to the nodejs module in which this code is defined.
*
* @memberof GQLBaseEnv
* @type {Object}
* @const
*/
const GQLBaseModule = module;
/**
* 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');
/**
* 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 {
/**
* 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) {
const Class = this.constructor;
this.model = modelData;
this.requestData = requestData;
this.fileHandler = new IDLFileHandler(this.constructor);
}
/**
* 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 ⬇︎⠀model
*
* @param {Object} value any object you wish to use as a data store
*/
get model() {
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 ⬆︎⠀model
*
* @param {Object} value any object you wish to use as a data store
*/
set model(value: Object) {
this[MODEL_KEY] = value;
}
/**
* 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 {
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 {
this[REQ_DATA_KEY] = value;
}
/**
* 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')
* }
* ```
*
* NOTE - Important!
* When not returning a direct string based IDL schema, the call to super()
* from a child class must include `module` as the second parameter or an
* error will be thrown upon object creation.
*
* @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 {
// define in base class
}
/**
* 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
* @deprecated Place all resolvers in RESOLVERS()
*
* @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')
}
/**
* 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}`);
if (!this[key]) {
this[key] = new IDLFileHandler(this);
}
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 GQLBaseModule;
}
}
/**
* The handler, an instance of which is created for every instance of GQLBase.
* The handler manages the fetching and decoding of files bearing the IDL
* schema associated with the class represented by this instance of GQLBase.
*
* @class IDLFileHandler
*/
export class IDLFileHandler {
/**
* The IDLFileHandler checks the SCHEMA value returned by the class type
* of the supplied instance. If the resulting value is a Symbol, then the
* handler's responsibility is to find the file, load it from disk and
* provide various means of using its contents; i.e. as a Buffer, a String
* or wrapped in a SyntaxTree instance.
*
* @memberof IDLFileHandler
* @method ⎆⠀constructor
* @constructor
*
* @param {Function} Class a function or class definition that presumably
* extends from GQLBase were it an instance.
*/
constructor(Class: Function) {
const symbol = typeof Class.SCHEMA === 'symbol' && Class.SCHEMA || null;
const pattern = /Symbol\(Path (.*?) Extension (.*?)\)/;
if (symbol) {
let symbolString = symbol.toString();
if (symbol === Class.ADJACENT_FILE) {
if (Class.module === GQLBaseModule) {
throw new Error(`
The a static getter for 'module' on ${Class.name} must be present
that returns the module object where the Class is defined. Try the
following:
// your ${Class.name}.js file
import { GQLBase } from 'graphql-lattice'
const ${Class.name}Module = module;
class ${Class.name} extends GQLBase {
...
static get module() {
return ${Class.name}Module;
}
}
`);
}
const filename = Class.module.filename;
const extension = Path.extname(filename)
const dir = Path.dirname(filename)
const filefixed = Path.basename(filename, extension)
const build = Path.resolve(Path.join(dir, `${filefixed}.graphql`))
this.path = build;
this.extension = '.graphql';
}
else if (pattern.test(symbolString)) {
const parsed = pattern.exec(symbolString);
const extension = parsed[2] || '.graphql'
const dir = Path.dirname(parsed[1])
const file = Path.basename(parsed[1], extension)
const build = Path.resolve(Path.join(dir, `${file}${extension}`))
this.path = build;
this.extension = extension;
console.log(`Path ${this.path} Ext ${this.extension}`)
console.log(`Resolved ${Path.resolve(this.path)}`)
}
}
else {
this.path = this.extension = null;
}
}
/**
* Loads the calculated file determined by the decoding of the meaning of
* the Symbol returned by the SCHEMA property of the instance supplied to
* the IDLFileHandler upon creation.
*
* @instance
* @memberof IDLFileHandler
* @method ⌾⠀getFile
*
* @return {Buffer|null} returns the Buffer containing the file base IDL
* schema or null if none was found or a direct string schema is returned
* by the SCHEMA property
*/
getFile(): Buffer {
return fs.readFileSync(this.path);
}
/**
* If getFile() returns a Buffer, this is the string representation of the
* underlying file contents. As a means of validating the contents of the
* file, the string contents are parsed into an AST and back to a string.
*
* @instance
* @memberof IDLFileHandler
* @method ⌾⠀getSchema
*
* @return {string|null} the string contents of the Buffer containing the
* file based IDL schema.
*/
getSchema(): string {
if (!this.path) { return null; }
const tree = this.getSyntaxTree();
return tree.toString();
}
/**
* If getFile() returns a Buffer, the string contents are passed to a new
* instance of SyntaxTree which parses this into an AST for manipulation.
*
* @instance
* @memberof IDLFileHandler
* @method ⌾⠀getSyntaxTree
*
* @return {SyntaxTree|null} a SyntaxTree instance constructed from the IDL
* schema contents loaded from disk. Null is returned if a calculated path
* cannot be found; always occurs when SCHEMA returns a string.
*/
getSyntaxTree(): SyntaxTree {
const buffer = this.getFile();
const tree = new SyntaxTree(buffer.toString());
return tree;
}
}
export default GQLBase;