@nyteshade/lattice-legacy
Version:
OO Underpinnings for ease of GraphQL Implementation
471 lines (413 loc) • 15.5 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.DirectTypeManager = undefined;
var _defineProperty = require('babel-runtime/core-js/object/define-property');
var _defineProperty2 = _interopRequireDefault(_defineProperty);
var _getOwnPropertyDescriptor = require('babel-runtime/core-js/object/get-own-property-descriptor');
var _getOwnPropertyDescriptor2 = _interopRequireDefault(_getOwnPropertyDescriptor);
var _for = require('babel-runtime/core-js/symbol/for');
var _for2 = _interopRequireDefault(_for);
exports.DirectTypeAdd = DirectTypeAdd;
exports.applyTags = applyTags;
exports.Getters = Getters;
exports.Setters = Setters;
exports.Properties = Properties;
var _GQLBase = require('../GQLBase');
var _neTypes = require('ne-types');
var _util = require('util');
var _graphql = require('graphql');
var _SyntaxTree = require('../SyntaxTree');
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
/**
* For each of the decorators, Getters, Setters, and Properties, we take a
* list of property names used to create the appropriate accessor types. In
* some cases, however, the instance of GQLBase's data model may have a
* different name. Finally if the return type for the getter should be wrapped
* in a another GQLBase class type, we will need a way to specify those things
* too.
*
* The `extractBits()` takes a single argument value from the decorator as it
* parses them and converts it into an object, properly sorted, into values that
* allow the above described behavior.
*
* Examples:
*
* ```
* // Create a class with a name and age property that map directly to the
* // underlying data model
* @Getters('name', 'age')
* class MyType extends GQLBase {...}
*
* // Create a class with a name property that maps to a different property
* // name in the underlying data model
* @Getters(['name', '_fake_name'])
* class MyMockType extends GQLBase {...}
*
* // Create a class with an employee property that returns an Employee
* @Getters(['employee', Employee])
* class MyRoleType extends GQLBase {...}
*
* // Finally create a class with an employe property that returns an Employee
* // with data under a different name in the underlying data model.
* @Getters(['employee', '_worker', Employee])
* class MyMockRoleType extends GQLBase {...}
* ```
*
* @memberof decorators
* @method ⌾⠀extractBits
* @since 2.5
*
* @param {String|Array<String|Function>} property name of a property, or list
* of property names and a Class.
* @return {Object} an object with the following format ```
* {
* fieldName: name of root instance property to create
* modelName: name of its associated internal model property
* typeClass: an optional class to wrap around the results in a getter
* }
* ```
*/
function extractBits(property) {
let array = (0, _neTypes.isArray)(property) ? property : [property, property, null];
let reply;
if (!property) {
let error = new Error('Invalid property. Given\n %o', (0, _util.inspect)(property, { depth: 2 }));
return {
fieldName: 'anErrorOccurred',
modelName: 'anErrorOccurred',
typeClass: null,
getterMaker: function () {
return () => error;
},
setterMaker: function () {
return v => undefined;
}
};
}
//
if (array.length === 3) {
reply = {
fieldName: array[0],
modelName: array[1],
typeClass: typeof array[2] === 'function' && array[2] || null
};
}
//
else if (array.length === 2) {
reply = {
fieldName: array[0],
modelName: typeof array[1] === 'string' ? array[1] : array[0],
typeClass: typeof array[1] === 'function' && array[1] || null
};
}
//
else {
reply = {
fieldName: array[0],
modelName: array[0],
typeClass: array[0]
};
}
reply.getterMaker = function () {
let { modelName, fieldName, typeClass } = reply;
return function () {
const thisClass = this.constructor;
const model = this[_GQLBase.MODEL_KEY] || null;
let val;
if (!(0, _neTypes.extendsFrom)(thisClass, _GQLBase.GQLBase)) {
console.error(`${thisClass.name} is not derived from GQLBase`);
return undefined;
}
if (!thisClass.SCHEMA) {
throw new Error(`
All GQLBase extended classes should have a defined SCHEMA. Please
manually define a static get SCHEMA() in your class or use the
@Schema() decorator to do so.
`);
}
if (typeClass) {
// If the value of the model is already the type of class we expect
// we do not need to do any processing and we can just grab it and
// go.
if (model[modelName] && (0, _neTypes.extendsFrom)(model[modelName], typeClass)) {
val = model[modelName];
}
// Otherwise we need to return an instance of the determined typeClass
// and pass that back instead; as requested.
else {
const results = _SyntaxTree.SyntaxTree.findField((0, _graphql.parse)(this.constructor.SCHEMA), this.constructor.name, modelName);
const { meta } = results || { meta: null };
let args = [model[modelName], this.requestData];
if (meta && !meta.nullable && !model) {
throw new Error(`
Using @Getters or @Properties decorators with a null or
undefined model when the schema states that this field
cannot be null.
Type : ${typeClass.name}
Field (AST data)
name : ${meta.name}
type : ${meta.type}
nullable: ${meta.nullable}
[getter] : ${fieldName}
[maps to] : ${modelName}
[model ] : ${model}
`);
}
// If the following is true, it means that despite allowing nulls
// for this field in the schema, we do have a valid model and should
// proceed.
if (model) {
if (extractBits.DIRECT_TYPES.includes(typeClass.name)) {
val = typeClass(...args);
} else {
val = new typeClass(...args);
}
if (typeClass.GQL_TYPE === _graphql.GraphQLEnumType) {
return val.value;
}
}
}
} else {
val = model[modelName];
}
if (val === 'undefined' || val === undefined) {
val = null;
}
return val;
};
};
reply.setterMaker = function () {
let { modelName } = reply;
return function (value) {
this[_GQLBase.MODEL_KEY][modelName] = value;
};
};
return reply;
}
/**
* An array of proper class names that are used to test for cases where the
* proper usage of instantiating an instance should preclude the use of `new`
*
* @memberof decorators
* @type {Array<String>}
*/
/** @namespace decorators */
extractBits.DIRECT_TYPES = [String.name];
/**
* A small suite of functions a getter that allows easy manipulation of the
* the DIRECT_TYPES workaround needed for some types of complex class
* wrapping allowed by the @Getters and @Properties decorators. Namely the
* ability to do something like @Getters('name', String) which would wrap the
* contents of whatever is in the objects model in a String call.
*
* Direct types are those that need to be called without `new` in order for the
* desired behavior to present itself.
*
* @memberof decorators
* @type {Object}
* @since 2.7.0
*/
const DirectTypeManager = exports.DirectTypeManager = {
/**
* A getter that retrieves the array of direct types
*
* @method DirectTypeManager#types
* @member {Array<String>} types
*
* @return {Array<String>} an array of class name strings.
*/
get types() {
return extractBits.DIRECT_TYPES;
},
/**
* Appends the supplied class name to the list of registered direct types. If
* a class or function is passed, rather than a String,
*
* @method DirectTypeManager#types
*
* @param {Function|string|RegExp} className the name of the class to append.
* Typically it is best to pass the name property of the class in question
* such as `RegExp.name` or `MyClass.name`.
*/
add(className) {
if (typeof className === 'function') {
className = className.name;
}
extractBits.DIRECT_TYPES.push(className);
},
/**
* Foricbly empties the contents of the extractBits.DIRECT_TYPES array. This
* is not recommended as it can have unintended consequences. It is
* recommended to use `reset` instead
*
* @method DirectTypeManager#clear
*
* @return {Array<string>} an array of class name Strings that were removed
* when cleared.
*/
clear() {
return extractBits.DIRECT_TYPES.splice(0, extractBits.DIRECT_TYPES.length);
},
/**
* The recommended way to reset the DIRECT_TYPES list. This removes all
* changed values, returns the removed bits, and adds back in the defaults.
*
* @method DirectTypeManager#reset
*
* @return {Array<string>} an array of class name Strings that were removed
* during the reset process.
*/
reset() {
return extractBits.DIRECT_TYPES.splice(0, extractBits.DIRECT_TYPES.length, String.name);
}
};
/**
* This decorator allows you to add a Class method to the DirectTypeManager
* as a function that should not be invoked with the `new` keyword. For all
* intents and purposes the function should be declared `static`.
*
* @method DirectTypeAdd
* @param {Function} target [description]
* @constructor
*/
function DirectTypeAdd(target) {
DirectTypeManager.add(target);
return target;
}
/**
* When applying multiple property getters and setters, knowing some info
* about what was applied elsewhere can be important. "Tags" can be applied
* that store the fieldName and descriptor applied via one of these decorators.
*
* Multiple "tags" are supported to allow for detecting the difference between
* decorators applied by the developer using lattice and something auto
* generated such as auto-props.
*
* @param {GQLBase} Class an instance of GQLBase to apply the tags tp
* @param {Array<string|Symbol>} addTags an array of Symbols or strings to be
* wrapped in Symbols that will be used as tag keys
* @param {string} fieldName the name of the field being decorated
* @param {Object} descriptor the JavaScript descriptor object to associate
* with this tagged field.
*/
function applyTags(Class, addTags, fieldName, descriptor) {
let tags = (Array.isArray(addTags) && addTags || []).map(tag => typeof tag === 'string' && (0, _for2.default)(tag) || tag).filter(tag => typeof tag === 'symbol');
tags.forEach(tag => {
Class[_GQLBase.META_KEY][tag] = Class[_GQLBase.META_KEY][tag] || {};
Class[_GQLBase.META_KEY][tag][fieldName] = descriptor;
});
}
/**
* When working with `GQLBase` instances that expose properties
* that have a 1:1 mapping to their own model property of the
* same name, adding the getters manually can be annoying. This
* takes an indeterminate amount of strings representing the
* properties for which getters should be injected.
*
* @function 🏷⠀Getters
* @memberof! decorators
*
* @param {Array<String|Array<String>>} propertyNames if the model has 'name'
* and 'age' as properties, then passing those two strings will result
* in getters that surface those properties as GraphQL fields.
* @return {Function} a class decorator method.s
*/
function Getters(...propertyNames) {
return function (target, addTags = []) {
for (let property of propertyNames) {
let { fieldName, getterMaker } = extractBits(property);
let desc = (0, _getOwnPropertyDescriptor2.default)(target.prototype, fieldName);
let hasImpl = desc && (desc.get || typeof desc.value === 'function');
let tags = [_GQLBase.GETTERS].concat(Array.isArray(addTags) && addTags || []);
if (!hasImpl) {
let descriptor = {
get: getterMaker()
};
applyTags(target, tags, fieldName, descriptor);
(0, _defineProperty2.default)(target.prototype, fieldName, descriptor);
} else {
console.warn(`Skipping getter for ${target.name}.${fieldName}; already exists`);
}
}
return target;
};
}
/**
* When working with `GQLBase` instances that expose properties
* that have a 1:1 mapping to their own model property of the
* same name, adding the setters manually can be annoying. This
* takes an indeterminate amount of strings representing the
* properties for which setters should be injected.
*
* @function 🏷⠀Setters
* @memberof! decorators
* @since 2.1.0
*
* @param {Array<String|Array<String>>} propertyNames if the model has
* 'name' and 'age' as properties, then passing those two strings will
* result in setters that surface those properties as GraphQL fields.
* @return {Function} a class decorator method
*/
function Setters(...propertyNames) {
return function (target, addTags = []) {
for (let property of propertyNames) {
let { fieldName, setterMaker } = extractBits(property);
let desc = (0, _getOwnPropertyDescriptor2.default)(target.prototype, fieldName);
let hasImpl = desc && (desc.get || typeof desc.value === 'function');
let tags = [_GQLBase.SETTERS].concat(Array.isArray(addTags) && addTags || []);
if (!hasImpl) {
let descriptor = {
set: setterMaker()
};
applyTags(target, tags, fieldName, descriptor);
(0, _defineProperty2.default)(target.prototype, fieldName, descriptor);
} else {
console.warn(`Skipping setter for ${target.name}.${fieldName}; already exists`);
}
}
return target;
};
}
/**
* When working with `GQLBase` instances that expose properties
* that have a 1:1 mapping to their own model property of the
* same name, adding the getters manually can be annoying. This
* takes an indeterminate amount of strings representing the
* properties for which getters should be injected.
*
* This method creates both getters and setters
*
* @function 🏷⠀Properties
* @memberof! decorators
* @since 2.1.0
*
* @param {Array<String|Array<String>>} propertyNames if the model has 'name'
* and 'age' as properties, then passing those two strings will result
* in getters and setters that surface those properties as GraphQL fields.
* @return {Function} a class decorator method
*/
function Properties(...propertyNames) {
return function (target, addTags = []) {
for (let property of propertyNames) {
let { fieldName, getterMaker, setterMaker } = extractBits(property);
let desc = (0, _getOwnPropertyDescriptor2.default)(target.prototype, fieldName);
let hasImpl = desc && (desc.get || typeof desc.value === 'function');
let tags = [_GQLBase.PROPS].concat(Array.isArray(addTags) && addTags || []);
if (!hasImpl) {
let descriptor = {
set: setterMaker(),
get: getterMaker()
};
applyTags(target, tags, fieldName, descriptor);
(0, _defineProperty2.default)(target.prototype, fieldName, descriptor);
} else {
console.warn(`Skipping properties for ${target.name}.${fieldName}; already exists`);
}
}
return target;
};
}
exports.default = Properties;
//# sourceMappingURL=ModelProperties.js.map