@nyteshade/lattice-legacy
Version:
OO Underpinnings for ease of GraphQL Implementation
822 lines (723 loc) • 25.8 kB
JavaScript
'use strict';
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.SyntaxTree = undefined;
var _escape = require('babel-runtime/core-js/regexp/escape');
var _escape2 = _interopRequireDefault(_escape);
var _toStringTag = require('babel-runtime/core-js/symbol/to-string-tag');
var _toStringTag2 = _interopRequireDefault(_toStringTag);
var _iterator = require('babel-runtime/core-js/symbol/iterator');
var _iterator2 = _interopRequireDefault(_iterator);
var _set = require('babel-runtime/core-js/set');
var _set2 = _interopRequireDefault(_set);
var _for = require('babel-runtime/core-js/symbol/for');
var _for2 = _interopRequireDefault(_for);
var _neTypes = require('ne-types');
var _graphql = require('graphql');
var _lodash = require('lodash');
var _utils = require('./utils');
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
// Shorthand for the key storing the internal AST
// @prop
// @module SyntaxTree
const AST_KEY = (0, _for2.default)('Internal AST Storage Key');
/**
* A parser and processor of GraphQL IDL Abstract Syntax Trees. Used to combine
* a set of {@link GQLBase} class instances.
*
* @class SyntaxTree
*/
let SyntaxTree = exports.SyntaxTree = class SyntaxTree {
/**
* Constructs a new `SyntaxTree` object. If a string schema is supplied or
* an already parsed AST object, either of which is valid GraphQL IDL, then
* its parsed AST will be the internals of this object.
*
* @constructor
* @memberof SyntaxTree
* @method ⎆⠀constructor
*
* @param {string|Object|SyntaxTree} schemaOrASTOrST if supplied the tree
* will be constructed with the contents of the data. If a string of IDL is
* given, it will be parsed. If an AST is given, it will be verified. If a
* SyntaxTree is supplied, it will be copied.
*/
constructor(schemaOrASTOrST) {
// $ComputedType
this[AST_KEY] = {};
if (schemaOrASTOrST) {
this.setAST(schemaOrASTOrST);
}
}
/**
* Getter that retrieves the abstract syntax tree created by `graphql.parse`
* when it is presented with a valid string of IDL.
*
* @instance
* @memberof SyntaxTree
* @method ⬇︎⠀ast
*
* @return {Object} a GraphQL AST object
*/
get ast() {
// $ComputedType
return this[AST_KEY];
}
/**
* Setter that assigns the abstract syntax tree, typically created by
* `graphql.parse` when given a valid string of IDL.
*
* @instance
* @memberof SyntaxTree
* @method ⬆︎⠀ast
*
* @param {Object} value a valid AST object. Other operations will act
* in an undefined manner should this object not be a valid AST
*/
set ast(value) {
// $ComputedType
this[AST_KEY] = value;
}
/**
* Sets the underlying AST object with either schema which will be parsed
* into a valid AST or an existing AST. Previous ast values will be erased.
*
* @instance
* @memberof SyntaxTree
* @method ⌾⠀setAST
*
* @param {string|Object} schemaOrAST a valid GraphQL IDL schema or a
* previosuly parsed or compatible GraphQL IDL AST object.
* @return {SyntaxTree} this for inlining.
*/
setAST(schemaOrASTOrST) {
// $ComputedType
this[AST_KEY] = {};
const type = (0, _neTypes.typeOf)(schemaOrASTOrST);
let ast;
let st;
switch (type) {
case String.name:
try {
ast = (0, _graphql.parse)(schemaOrASTOrST);
(0, _lodash.merge)(this.ast, ast);
} catch (ignore) {/* Ignore this error */}
break;
case Object.name:
ast = schemaOrASTOrST;
try {
ast = (0, _graphql.parse)((0, _graphql.print)(ast));
(0, _lodash.merge)(this.ast, ast);
} catch (ignore) {/* Ignore this error */}
break;
case SyntaxTree.name:
st = schemaOrASTOrST;
(0, _lodash.merge)(this.ast, st.ast);
break;
}
return this;
}
/**
* As passthru update method that works on the internal AST object. If
* an error occurs, the update is skipped. An error can occur if adding the
* changes would make the AST invalid. In such a case, the error is logged
* to the error console.
*
* @instance
* @memberof SyntaxTree
* @method ⌾⠀updateAST
*
* @param {Object} ast an existing GraphQL IDL AST object that will be
* merged on top of the existing tree using _.merge()
* @return {SyntaxTree} this for inlining.
*/
updateAST(ast) {
if ((0, _neTypes.typeOf)(ast) === Object.name) {
let newAST = (0, _lodash.merge)({}, this.ast, ast);
try {
(0, _graphql.print)(newAST);
this.ast = (0, _lodash.merge)(this.ast, ast);
} catch (error) {
_utils.LatticeLogs.error('[SyntaxTree] Failed to updateAST with %o', ast);
_utils.LatticeLogs.error('Resulting object would be %o', newAST);
_utils.LatticeLogs.error(error);
}
}
return this;
}
/**
* Appends all definitions from another AST to this one. The method will
* actually create a copy using SyntaxTree.from() so the input types can
* be any one of a valid GraphQL IDL schema string, a GraphQL IDL AST or
* another SyntaxTree object instance.
*
* Definitions of the same name but different kinds will be replaced by the
* new copy. Those of the same kind and name will be merged (TODO handle more
* than ObjectTypeDefinition kinds when merging; currently other types are
* overwritten).
*
* @instance
* @memberof SyntaxTree
* @method ⌾⠀appendDefinitions
*
* @param {string|Object|SyntaxTree} schemaOrASTOrST an instance of one of
* the valid types for SyntaxTree.from() that can be used to create or
* duplicate the source from which to copy definitions.
* @return {SyntaxTree} this for inlining
*/
appendDefinitions(schemaOrASTOrST) {
const source = SyntaxTree.from(schemaOrASTOrST);
const set = new _set2.default();
this.ast.definitions.map(definition => {
set.add(definition.name.value);
});
if (source && source.ast.definitions && this.ast.definitions) {
for (let theirs of source) {
let name = theirs.name.value;
let ours = this.find(name);
let index = ours && this.ast.definitions.indexOf(ours) || -1;
// We don't yet have one with that name
if (!set.has(name)) {
set.add(name);
this.ast.definitions.push(theirs);
}
// We do have one with that name
else {
// The kinds aren't the same, just replace theirs with ours
if (theirs.kind !== ours.kind) {
// replace with the new one
this.ast.definitions[index] = theirs;
}
// The kinds are the same, lets just merge their fields
else {
// merge the properties of the same types.
switch (theirs.kind) {
case 'ObjectTypeDefinition':
ours.interfaces = [].concat(ours.interfaces, theirs.interfaces);
ours.directives = [].concat(ours.directives, theirs.directives);
ours.fields = [].concat(ours.fields, theirs.fields);
break;
default:
// Since we don't support other types yet. Let's replace
this.ast.definitions[index] = theirs;
break;
}
}
}
}
}
return this;
}
/**
* This method finds the Query type definitions in the supplied AST or
* SyntaxTree objects, takes its defined fields and adds it to the current
* instances. If this instance does not have a Query type defined but the
* supplied object does, then the supplied one is moved over. If neither
* has a query handler, then nothing happens.
*
* NOTE this *removes* the Query type definition from the supplied AST or
* SyntaxTree object.
*
* @instance
* @memberof SyntaxTree
* @method ⌾⠀consumeDefinition
*
* @param {Object|SyntaxTree} astOrSyntaxTree a valid GraphQL IDL AST or
* an instance of SyntaxTree that represents one.
* @param {string|RegExp} definitionType a valid search input as would be
* accepted for the #find() method of this object.
* @return {SyntaxTree} returns this for inlining
*/
consumeDefinition(astOrSyntaxTree, definitionType = "Query") {
if (!astOrSyntaxTree || !this.ast || !this.ast.definitions) {
return this;
}
const tree = (0, _neTypes.typeOf)(SyntaxTree) === SyntaxTree.name ? astOrSyntaxTree : SyntaxTree.from(astOrSyntaxTree);
let left = this.find(definitionType);
let right = tree && tree.find(definitionType) || null;
if (!tree) {
_utils.LatticeLogs.error('There seems to be something wrong with your tree');
_utils.LatticeLogs.error(new Error('Missing tree; continuing...'));
return this;
}
if (!right) {
return this;
}
if (!left) {
this.ast.definitions.push(right);
// Remove the copied definition from the source
tree.ast.definitions.splice(tree.ast.definitions.indexOf(right), 1);
return this;
}
// TODO support other types aside from ObjectTypeDefinitions
// TODO see if there is a better way to achieve this with built-in
// graphql code someplace
switch (left.kind) {
case 'ObjectTypeDefinition':
if (left.interfaces && right.interfaces) {
left.interfaces = [].concat(left.interfaces, right.interfaces);
}
if (left.directives && right.directives) {
left.directives = [].concat(left.directives, right.directives);
}
if (left.fields && right.fields) {
left.fields = [].concat(left.fields, right.fields);
}
break;
default:
break;
}
// Remove the copied definition from the source
tree.ast.definitions.splice(tree.ast.definitions.indexOf(right), 1);
return this;
}
/**
* When iterating over an instance of SyntaxTree, you are actually
* iterating over the definitions of the SyntaxTree if there are any;
*
* @instance
* @memberof SyntaxTree
* @method *[Symbol.iterator]
*
* @return {TypeDefinitionNode} an instance of a TypeDefinitionNode; see
* graphql/language/ast.js.flow for more information
* @ComputedType
*/
*[_iterator2.default]() {
if (this[AST_KEY].definitions) {
return yield* this[AST_KEY].definitions;
} else {
return yield* this;
}
}
/**
* Getter that builds a small outline object denoting the schema being
* processed. If you have a schema that looks like the following:
*
* ```javascript
* let st = SyntaxTree.from(`
* type Contrived {
* name: String
* age: Int
* }
*
* type Query {
* getContrived: Contrived
* }
* `)
* let outline = st.outline
* ```
*
* You will end up with an object that looks like the following:
*
* ```javascript
* {
* Contrived: { name: 'String', age: 'Int' },
* Query: { getContrived: 'Contrived' }
* }
* ```
*
* As may be evidenced by the example above, the name of the type is
* represented by an object where the name of each field (sans arguments)
* is mapped to a string denoting the type.
*/
get outline() {
let outline = {};
let interfaces = (0, _for2.default)('interfaces');
// $FlowFixMe
for (let definition of this) {
let out;
switch (definition.kind) {
case 'InterfaceTypeDefinition':
case 'ObjectTypeDefinition':
out = outline[definition.name.value] = {};
definition.fields.forEach(field => {
if (field.type.kind === 'NamedType') out[field.name.value] = field.type.name.value;else if (field.type.kind === 'ListType') out[field.name.value] = field.type.type.name.value;
});
if (definition.interfaces) {
// $FlowFixMe
out = out[interfaces] = out[interfaces] || [];
definition.interfaces.forEach(_interface => out.push(_interface.name.value));
}
break;
case 'EnumTypeDefinition':
out = outline[definition.name.value] = [];
definition.values.forEach(value => out[value.name.value] = value.name.value);
break;
case 'UnionTypeDefinition':
out = outline[definition.name.value] = [];
definition.types.forEach(type => out.push(type.name.value));
break;
}
}
return outline;
}
/**
* Iterate through the definitions of the AST if there are any. For each
* definition the name property's value field is compared to the supplied
* definitionName. The definitionName can be a string or a regular
* expression if finer granularity is desired.
*
* @instance
* @memberof SyntaxTree
* @method ⌾⠀find
*
* @param {string|RegExp} definitionName a string or regular expression used
* to match against the definition name field in a given AST.
* @return {Object|null} a reference to the internal definition field or
* null if one with a matching name could not be found.
*/
find(definitionName) {
// $ComputedType
return SyntaxTree.findDefinition(this[AST_KEY], definitionName);
}
/**
* SyntaxTree instances that are toString()'ed will have the graphql method
* print() called on them to convert their internal structures back to a
* GraphQL IDL schema syntax. If the object is in an invalid state, it WILL
* throw an error.
*
* @instance
* @memberof SyntaxTree
* @method ⌾⠀toString
*
* @return {string} the AST for the tree parsed back into a string
*/
toString() {
// $ComputedType
return (0, _graphql.print)(this[AST_KEY]);
}
/**
* A runtime constant denoting a query type.
*
* @type {string}
* @static
* @memberof SyntaxTree
* @method ⬇︎⠀QUERY
* @readonly
* @const
*/
static get QUERY() {
return 'Query';
}
/**
* A runtime constant denoting a mutation type.
*
* @type {string}
* @static
* @memberof SyntaxTree
* @method ⬇︎⠀MUTATION
* @readonly
* @const
*/
static get MUTATION() {
return 'Mutation';
}
/**
* A runtime constant denoting a subscription type.
*
* @type {string}
* @static
* @memberof SyntaxTree
* @method SUBSCRIPTION
* @readonly
* @const
*/
static get SUBSCRIPTION() {
return 'Subscription';
}
/**
* 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 SyntaxTree
*
* @return {string} the name of the class this is an instance of
* @ComputedType
*/
get [_toStringTag2.default]() {
return this.constructor.name;
}
/**
* 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 SyntaxTree
* @static
*
* @return {string} the name of this class
* @ComputedType
*/
static get [_toStringTag2.default]() {
return this.name;
}
/**
* Given one of, a valid GraphQL IDL schema string, a valid GraphQL AST or
* an instance of SyntaxTree, the static from() method will create a new
* instance of the SyntaxTree with the values you provide.
*
* @static
* @memberof SyntaxTree
* @method ⌾⠀from
*
* @param {String|Object|SyntaxTree} mixed an instance of one of the valid
* types specified above. Everything else will result in a null value.
* @return {SyntaxTree} a newly created and populated instance of SyntaxTree
* or null if an invalid type was supplied for mixed.
*/
static from(mixed) {
let schema;
let ast;
switch ((0, _neTypes.typeOf)(mixed)) {
case String.name:
schema = mixed;
try {
(0, _graphql.parse)(schema);
} catch (error) {
_utils.LatticeLogs.error(error);return null;
}
return SyntaxTree.fromSchema(String(schema));
case Object.name:
ast = mixed;
try {
(0, _graphql.print)(ast);
} catch (error) {
return null;
}
return SyntaxTree.fromAST(ast);
case SyntaxTree.name:
schema = mixed.toString();
return SyntaxTree.from(schema);
default:
return null;
}
}
/**
* Generates a new instance of SyntaxTree from the supplied, valid, GraphQL
* schema. This method does not perform try/catch validation and if an
* invalid GraphQL schema is supplied an error will be thrown.
*
* @static
* @memberof SyntaxTree
* @method ⌾⠀fromSchema
*
* @param {string} schema a valid GraphQL IDL schema string.
* @return {SyntaxTree} a new instance of SyntaxTree initialized with a
* parsed response from require('graphql').parse().
*/
static fromSchema(schema) {
const ast = (0, _graphql.parse)(schema);
let tree = new SyntaxTree(ast);
return tree;
}
/**
* Generates a new instance of SyntaxTree from the supplied, valid, GraphQL
* schema. This method does not perform try/catch validation and if an
* invalid GraphQL schema is supplied an error will be thrown.
*
* @static
* @memberof SyntaxTree
* @method ⌾⠀fromAST
*
* @param {object} ast a valid GraphQL AST object.
* @return {SyntaxTree} a new instance of SyntaxTree initialized with a
* supplied abstract syntax tree generated by require('graphql').parse() or
* other compatible method.
*/
static fromAST(ast) {
const source = (0, _graphql.parse)((0, _graphql.print)(ast));
let tree = new SyntaxTree(source);
return source ? tree : null;
}
/**
* Iterate through the definitions of the AST if there are any. For each
* definition the name property's value field is compared to the supplied
* definitionName. The definitionName can be a string or a regular
* expression if finer granularity is desired.
*
* @static
* @memberof SyntaxTree
* @method ⌾⠀findDefinition
*
* @param {Object} ast an abstract syntax tree object created from a GQL SDL
* @param {string|RegExp} definitionName a string or regular expression used
* to match against the definition name field in a given AST.
* @return {Object|null} a reference to the internal definition field or
* null if one with a matching name could not be found.
*/
static findDefinition(ast, definitionName) {
return this.findInASTArrayByNameValue(ast.definitions, definitionName);
}
/**
* Iterate through the fields of a definition AST if there are any. For each
* field, the name property's value field is compared to the supplied
* fieldName. The fieldName can be a string or a regular expression if
* finer granularity is desired.
*
* Before iterating over the fields, however, the definition is found using
* `SyntaxTree#findDefinition`. If either the field or definition are not
* found, null is returned.
*
* @static
* @memberof SyntaxTree
* @method ⌾⠀findField
* @since 2.7.0
*
* @param {Object} ast an abstract syntax tree object created from a GQL SDL
* @param {string|RegExp} definitionName a string or regular expression used
* to match against the definition name field in a given AST.
* @param {string|RegExp} fieldName a string or regular expression used
* to match against the field name field in a given AST.
* @return {Object|null} an object containing two keys, the first being
* `field` which points to the requested AST definition field. The second
* being `meta` which contains three commonly requested bits of data; `name`,
* `type` and `nullable`. Non-nullable fields have their actual type wrapped
* in a `NonNullType` GraphQL construct. The actual field type is contained
* within. The meta object surfaces those values for easy use.
*/
static findField(ast, definitionName, fieldName) {
const definition = this.findDefinition(ast, definitionName);
let meta;
if (!definition || !definition.fields) {
return null;
}
const field = this.findInASTArrayByNameValue(definition.fields, fieldName);
if (field) {
meta = {
name: field.name && field.name.value || null,
type: field.type && field.type.kind === 'NonNullType' ? field.type.type.name.value : field.type && field.type.name && field.type.name.value || null,
nullable: !!(field.type && field.type.kind !== 'NonNullType')
};
}
return { field, meta };
}
/**
* Enum AST definitions operate differently than object type definitions
* do. Namely, they do not have a `fields` array but instead have a `values`
* array. This wrapper method, first finds the enum definition in the ast
* and then searches the values for the named node desired and returns that
* or null, if one could not be found.
*
* @method SyntaxTree#⌾⠀findEnumDefinition
* @since 2.7.0
*
* @param {Object} ast the abstract syntax tree parsed by graphql
* @param {string|RegExp} enumDefinitionName a string or regular expression
* used to locate the enum definition in the AST.
* @param {string|RegExp} enumValueName a string or regular expression used
* to locate the value by name in the values of the enum definition.
* @return {Object|null} the desired AST node or null if one does not exist
*/
static findEnumDefinition(ast, enumDefinitionName, enumValueName) {
// Fetch the enum definition
const definition = this.findDefinition(ast, enumDefinitionName);
// Ensure we have one or that it has a values array
if (!definition || !definition.values) {
return null;
}
// Return the results of an `findInASTArrayByNameValue()` search of the
// aforementioned 'values' array.
return this.findInASTArrayByNameValue(definition.values, enumValueName);
}
/**
* A lot of searching in ASTs is filtering through arrays and matching on
* subobject properties on each iteration. A common theme is find something
* by its `.name.value`. This method simplifies that by taking an array of
* AST nodes and searching them for a `.name.value` property that exists
* within.
*
* @static
* @memberof SyntaxTree
* @method ⌾⠀findInASTArrayByNameValue
* @since 2.7.0
*
* @param {Array} array of mixed AST object nodes containing `name.value`s
* @param {string|RegExp} name a string or regular expression used
* to match against the node name value
* @return {Object|null} the AST leaf if one matches or null otherwise.
*/
static findInASTArrayByNameValue(array, name) {
const isRegExp = /RegExp/.test((0, _neTypes.typeOf)(name));
const regex = !isRegExp
// $FlowFixMe
? new RegExp((0, _escape2.default)(name.toString()))
// $FlowFixMe
: name;
const flags = regex.flags;
const source = regex.source;
const reducer = (last, cur, i) => {
if (last !== -1) return last;
if (!cur || !cur.name || !cur.name.value) return -1;
return new RegExp(source, flags).test(cur.name.value) ? i : -1;
};
const index = array.reduce(reducer, -1);
return ~index ? array[index] : null;
}
/**
* Query types in GraphQL are an ObjectTypeDefinition of importance for
* placement on the root object. There is utility in creating an empty
* one that can be injected with the fields of other GraphQL object query
* entries.
*
* @static
* @memberof SyntaxTree
* @method ⌾⠀EmptyQuery
*
* @return {SyntaxTree} an instance of SyntaxTree with a base AST generated
* by parsing the graph query, "type Query {}"
*/
static EmptyQuery() {
return SyntaxTree.from(`type ${this.QUERY} {}`);
}
/**
* Mutation types in GraphQL are an ObjectTypeDefinition of importance for
* placement on the root object. There is utility in creating an empty
* one that can be injected with the fields of other GraphQL object mutation
* entries.
*
* @static
* @memberof SyntaxTree
* @method ⌾⠀EmptyMutation
*
* @return {SyntaxTree} an instance of SyntaxTree with a base AST generated
* by parsing the graph query, "type Mutation {}"
*/
static EmptyMutation() {
return SyntaxTree.from(`type ${this.MUTATION} {}`);
}
/**
* The starting point for a SyntaxTree that will be built up programmatically.
*
* @static
* @memberof SyntaxTree
* @method ⌾⠀EmptyDocument
*
* @param {string|Object|SyntaxTree} schemaOrASTOrST any valid type taken by
* SyntaxTree.from() used to further populate the new empty document
* @return {SyntaxTree} an instance of SyntaxTree with no definitions and a
* kind set to 'Document'
*/
static EmptyDocument(schemaOrASTOrST) {
let tree = new SyntaxTree();
// Due to normal validation methods with ASTs (i.e. converting to string
// and then back to an AST object), doing this with an empty document
// fails. Therefore, we manually set the document contents here. This allows
// toString(), consumeDefinition() and similar methods to still work.
tree.ast = {
kind: 'Document',
definitions: [],
loc: { start: 0, end: 0 }
};
if (schemaOrASTOrST) {
tree.appendDefinitions(schemaOrASTOrST);
}
return tree;
}
};
exports.default = SyntaxTree;
//# sourceMappingURL=SyntaxTree.js.map