graphql-compose
Version:
GraphQL schema builder from different data sources with middleware extensions.
703 lines (605 loc) • 22.4 kB
JavaScript
'use strict';
var _map = require('babel-runtime/core-js/map');
var _map2 = _interopRequireDefault(_map);
var _promise = require('babel-runtime/core-js/promise');
var _promise2 = _interopRequireDefault(_promise);
var _keys = require('babel-runtime/core-js/object/keys');
var _keys2 = _interopRequireDefault(_keys);
var _from = require('babel-runtime/core-js/array/from');
var _from2 = _interopRequireDefault(_from);
var _defineProperty2 = require('babel-runtime/core-js/object/define-property');
var _defineProperty3 = _interopRequireDefault(_defineProperty2);
var _assign = require('babel-runtime/core-js/object/assign');
var _assign2 = _interopRequireDefault(_assign);
var _isIterable2 = require('babel-runtime/core-js/is-iterable');
var _isIterable3 = _interopRequireDefault(_isIterable2);
var _getIterator2 = require('babel-runtime/core-js/get-iterator');
var _getIterator3 = _interopRequireDefault(_getIterator2);
Object.defineProperty(exports, "__esModule", {
value: true
});
var _slicedToArray = function () { function sliceIterator(arr, i) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = (0, _getIterator3.default)(arr), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"]) _i["return"](); } finally { if (_d) throw _e; } } return _arr; } return function (arr, i) { if (Array.isArray(arr)) { return arr; } else if ((0, _isIterable3.default)(Object(arr))) { return sliceIterator(arr, i); } else { throw new TypeError("Invalid attempt to destructure non-iterable instance"); } }; }();
var _extends = _assign2.default || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; (0, _defineProperty3.default)(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
var _graphql = require('graphql');
var _misc = require('./utils/misc');
var _is = require('./utils/is');
var _configAsThunk = require('./utils/configAsThunk');
var _debug = require('./utils/debug');
var _resolver = require('./resolver');
var _resolver2 = _interopRequireDefault(_resolver);
var _toInputObjectType = require('./toInputObjectType');
var _inputTypeComposer = require('./inputTypeComposer');
var _inputTypeComposer2 = _interopRequireDefault(_inputTypeComposer);
var _typeMapper = require('./typeMapper');
var _typeMapper2 = _interopRequireDefault(_typeMapper);
var _typeByPath = require('./typeByPath');
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } else { return (0, _from2.default)(arr); } }
function _defineProperty(obj, key, value) { if (key in obj) { (0, _defineProperty3.default)(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
var TypeComposer = function () {
_createClass(TypeComposer, null, [{
key: 'create',
value: function create(opts) {
var TC = void 0;
if ((0, _is.isString)(opts)) {
// $FlowFixMe
var typeName = opts;
var NAME_RX = /^[_a-zA-Z][_a-zA-Z0-9]*$/;
if (NAME_RX.test(typeName)) {
TC = new TypeComposer(new _graphql.GraphQLObjectType({
name: typeName,
fields: function fields() {
return {};
}
}));
} else {
var type = _typeMapper2.default.createType(typeName);
if (!(type instanceof _graphql.GraphQLObjectType)) {
throw new Error('You should provide correct GraphQLObjectType type definition.');
}
TC = new TypeComposer(type);
}
} else if (opts instanceof _graphql.GraphQLObjectType) {
TC = new TypeComposer(opts);
} else if ((0, _is.isObject)(opts)) {
// $FlowFixMe
var _type = new _graphql.GraphQLObjectType(_extends({}, opts, {
fields: function fields() {
return {};
}
}));
TC = new TypeComposer(_type);
// $FlowFixMe
if ((0, _is.isObject)(opts.fields)) {
// $FlowFixMe
TC.addFields(opts.fields);
}
} else {
throw new Error('You should provide GraphQLObjectTypeConfig or string with type name to TypeComposer.create(opts)');
}
return TC;
}
}]);
function TypeComposer(gqType) {
_classCallCheck(this, TypeComposer);
this.gqType = gqType;
}
/**
* Get fields from a GraphQL type
* WARNING: this method read an internal GraphQL instance variable.
*/
_createClass(TypeComposer, [{
key: 'getFields',
value: function getFields() {
var fields = this.gqType._typeConfig.fields;
// $FlowFixMe
var fieldMap = (0, _configAsThunk.keepConfigsAsThunk)((0, _misc.resolveMaybeThunk)(fields));
if ((0, _is.isObject)(fieldMap)) {
// $FlowFixMe
return (0, _assign2.default)({}, fieldMap);
}
return {};
}
}, {
key: 'getFieldNames',
value: function getFieldNames() {
return (0, _keys2.default)(this.getFields());
}
/**
* Completely replace all fields in GraphQL type
* WARNING: this method rewrite an internal GraphQL instance variable.
*/
}, {
key: 'setFields',
value: function setFields(fields) {
var _this = this;
var prepearedFields = _typeMapper2.default.convertOutputFieldConfigMap(fields, this.getTypeName());
// if field has a projection option, then add it to projection mapper
(0, _keys2.default)(prepearedFields).forEach(function (name) {
if (prepearedFields[name].projection) {
// $FlowFixMe
var projection = prepearedFields[name].projection;
_this.addProjectionMapper(name, projection);
}
});
this.gqType._typeConfig.fields = function () {
return (0, _configAsThunk.resolveOutputConfigsAsThunk)(prepearedFields, _this.getTypeName());
};
delete this.gqType._fields; // clear builded fields in type
return this;
}
}, {
key: 'hasField',
value: function hasField(fieldName) {
var fields = this.getFields();
return !!fields[fieldName];
}
}, {
key: 'setField',
value: function setField(fieldName, fieldConfig) {
this.addFields(_defineProperty({}, fieldName, fieldConfig));
return this;
}
/**
* @deprecated 2.0.0
*/
}, {
key: 'addField',
value: function addField(fieldName, fieldConfig) {
(0, _debug.deprecate)('Use TypeComposer.setField() or plural addFields({}) instead.');
this.addFields(_defineProperty({}, fieldName, fieldConfig));
}
/**
* Add new fields or replace existed in a GraphQL type
*/
}, {
key: 'addFields',
value: function addFields(newFields) {
this.setFields((0, _assign2.default)({}, this.getFields(), newFields));
return this;
}
/**
* Get fieldConfig by name
*/
}, {
key: 'getField',
value: function getField(fieldName) {
var fields = this.getFields();
if (fields[fieldName]) {
return fields[fieldName];
}
return undefined;
}
}, {
key: 'removeField',
value: function removeField(fieldNameOrArray) {
var fieldNames = Array.isArray(fieldNameOrArray) ? fieldNameOrArray : [fieldNameOrArray];
var fields = this.getFields();
fieldNames.forEach(function (fieldName) {
return delete fields[fieldName];
});
this.setFields((0, _assign2.default)({}, fields)); // immutability
return this;
}
}, {
key: 'removeOtherFields',
value: function removeOtherFields(fieldNameOrArray) {
var keepFieldNames = Array.isArray(fieldNameOrArray) ? fieldNameOrArray : [fieldNameOrArray];
var fields = this.getFields();
(0, _keys2.default)(fields).forEach(function (fieldName) {
if (!keepFieldNames.includes(fieldName)) {
delete fields[fieldName];
}
});
this.setFields(fields);
return this;
}
}, {
key: 'extendField',
value: function extendField(name, parialFieldConfig) {
var fieldConfig = (0, _assign2.default)({}, this.getField(name), parialFieldConfig);
this.setField(name, fieldConfig);
return this;
}
}, {
key: 'reorderFields',
value: function reorderFields(names) {
var orderedFields = {};
var fields = this.getFields();
names.forEach(function (name) {
if (fields[name]) {
orderedFields[name] = fields[name];
delete fields[name];
}
});
this.setFields(_extends({}, orderedFields, fields));
return this;
}
}, {
key: 'addRelation',
value: function addRelation(fieldName, relationFn) {
if (!this.gqType._gqcRelations) {
this.gqType._gqcRelations = {};
}
this.gqType._gqcRelations[fieldName] = relationFn;
return this;
}
}, {
key: 'getRelations',
value: function getRelations() {
if (!this.gqType._gqcRelations) {
this.gqType._gqcRelations = {};
}
return this.gqType._gqcRelations;
}
}, {
key: 'buildRelations',
value: function buildRelations() {
var _this2 = this;
var relationFields = {};
var names = (0, _keys2.default)(this.getRelations());
names.forEach(function (fieldName) {
relationFields[fieldName] = _this2.buildRelation(fieldName);
});
return this;
}
}, {
key: 'buildRelation',
value: function buildRelation(fieldName) {
if (!this.gqType._gqcRelations || !(0, _is.isFunction)(this.gqType._gqcRelations[fieldName])) {
throw new Error('Cannot call buildRelation() for type ' + this.getTypeName() + '. ' + ('Relation with name \'' + fieldName + '\' does not exist.'));
}
var relationFn = this.gqType._gqcRelations[fieldName];
// $FlowFixMe
var relationOpts = relationFn();
if (relationOpts.resolver) {
if (!(relationOpts.resolver instanceof _resolver2.default)) {
throw new Error('You should provide correct Resolver object for relation ' + (this.getTypeName() + '.' + fieldName));
}
if (relationOpts.type) {
throw new Error('You can not use `resolver` and `type` properties simultaneously for relation ' + (this.getTypeName() + '.' + fieldName));
}
if (relationOpts.resolve) {
throw new Error('You can not use `resolver` and `resolve` properties simultaneously for relation ' + (this.getTypeName() + '.' + fieldName));
}
this.addRelationWithResolver(fieldName, relationOpts.resolver, relationOpts);
} else if (relationOpts.type) {
this.setField(fieldName, _extends({}, relationOpts, {
_gqcIsRelation: true
}));
}
return this;
}
}, {
key: 'addRelationWithResolver',
value: function addRelationWithResolver(fieldName, resolver, opts) {
var _this3 = this;
var resolverFieldConfig = resolver.getFieldConfig();
var argsConfig = (0, _assign2.default)({}, resolverFieldConfig.args);
var argsProto = {};
var argsRuntime = [];
// remove args from config, if arg name provided in args
// if `argMapVal`
// is `undefined`, then keep arg field in config
// is `null`, then just remove arg field from config
// is `function`, then remove arg field and run it in resolve
// is any other value, then put it to args prototype for resolve
var optsArgs = opts.args || {};
(0, _keys2.default)(optsArgs).forEach(function (argName) {
var argMapVal = optsArgs[argName];
if (argMapVal !== undefined) {
delete argsConfig[argName];
if ((0, _is.isFunction)(argMapVal)) {
// $FlowFixMe
argsRuntime.push([argName, argMapVal]);
} else if (argMapVal !== null) {
argsProto[argName] = argMapVal;
}
}
});
// if opts.catchErrors is undefined then set true, otherwise take it value
var _opts$catchErrors = opts.catchErrors,
catchErrors = _opts$catchErrors === undefined ? true : _opts$catchErrors;
var resolve = function resolve(source, args, context, info) {
var newArgs = (0, _assign2.default)({}, args, argsProto);
argsRuntime.forEach(function (_ref) {
var _ref2 = _slicedToArray(_ref, 2),
argName = _ref2[0],
argFn = _ref2[1];
newArgs[argName] = argFn(source, args, context, info);
});
var payload = resolverFieldConfig.resolve(source, newArgs, context, info);
return catchErrors ? _promise2.default.resolve(payload).catch(function (e) {
console.log(
// eslint-disable-line
'GQC ERROR: relation for ' + _this3.getTypeName() + '.' + fieldName + ' throws error:');
console.log(e); // eslint-disable-line
return null;
}) : payload;
};
this.setField(fieldName, {
type: resolverFieldConfig.type,
description: opts.description,
deprecationReason: opts.deprecationReason,
args: argsConfig,
resolve: resolve,
projection: opts.projection,
_gqcIsRelation: true
});
return this;
}
/**
* Get fields from a GraphQL type
* WARNING: this method read an internal GraphQL instance variable.
*/
}, {
key: 'getInterfaces',
value: function getInterfaces() {
var interfaces = this.gqType._typeConfig.interfaces;
if (typeof interfaces === 'function') {
return interfaces() || [];
}
return interfaces || [];
}
/**
* Completely replace all interfaces in GraphQL type
* WARNING: this method rewrite an internal GraphQL instance variable.
*/
}, {
key: 'setInterfaces',
value: function setInterfaces(interfaces) {
this.gqType._typeConfig.interfaces = interfaces;
delete this.gqType._interfaces; // if schema was builded, delete _interfaces
return this;
}
}, {
key: 'hasInterface',
value: function hasInterface(interfaceObj) {
return this.getInterfaces().indexOf(interfaceObj) > -1;
}
}, {
key: 'addInterface',
value: function addInterface(interfaceObj) {
if (!this.hasInterface(interfaceObj)) {
this.setInterfaces([].concat(_toConsumableArray(this.getInterfaces()), [interfaceObj]));
}
return this;
}
}, {
key: 'removeInterface',
value: function removeInterface(interfaceObj) {
var interfaces = this.getInterfaces();
var idx = interfaces.indexOf(interfaceObj);
if (idx > -1) {
interfaces.splice(idx, 1);
this.setInterfaces(interfaces);
}
return this;
}
}, {
key: 'clone',
value: function clone(newTypeName) {
if (!newTypeName) {
throw new Error('You should provide newTypeName:string for TypeComposer.clone()');
}
var fields = this.getFields();
var newFields = {};
(0, _keys2.default)(fields).forEach(function (fieldName) {
newFields[fieldName] = (0, _assign2.default)({}, fields[fieldName]);
});
var cloned = new TypeComposer(new _graphql.GraphQLObjectType({
name: newTypeName,
fields: newFields
}));
cloned.gqType._gqcProjectionMapper = this.gqType._gqcProjectionMapper;
cloned.setDescription(this.getDescription());
try {
cloned.setRecordIdFn(this.getRecordIdFn());
} catch (e) {
// no problem, clone without resolveIdFn
}
this.getResolvers().forEach(function (resolver) {
var newResolver = resolver.clone(cloned);
cloned.addResolver(newResolver);
});
return cloned;
}
/**
* Get fieldType by name
*/
}, {
key: 'getFieldType',
value: function getFieldType(fieldName) {
var field = this.getField(fieldName);
if (field) {
return field.type;
}
return undefined;
}
}, {
key: 'getType',
value: function getType() {
return this.gqType;
}
}, {
key: 'getTypePlural',
value: function getTypePlural() {
return new _graphql.GraphQLList(this.gqType);
}
}, {
key: 'getInputType',
value: function getInputType() {
return this.getInputTypeComposer().getType();
}
}, {
key: 'hasInputTypeComposer',
value: function hasInputTypeComposer() {
return !!this.gqType._gqcInputTypeComposer;
}
}, {
key: 'getInputTypeComposer',
value: function getInputTypeComposer() {
if (!this.gqType._gqcInputTypeComposer) {
this.gqType._gqcInputTypeComposer = (0, _toInputObjectType.toInputObjectType)(this);
}
return this.gqType._gqcInputTypeComposer;
}
}, {
key: 'getResolvers',
value: function getResolvers() {
if (!this.gqType._gqcResolvers) {
this.gqType._gqcResolvers = new _map2.default();
}
return this.gqType._gqcResolvers;
}
}, {
key: 'hasResolver',
value: function hasResolver(name) {
if (!this.gqType._gqcResolvers) {
return false;
}
return this.gqType._gqcResolvers.has(name);
}
}, {
key: 'getResolver',
value: function getResolver(name) {
if (this.hasResolver(name) && this.gqType._gqcResolvers) {
return this.gqType._gqcResolvers.get(name);
}
return undefined;
}
}, {
key: 'setResolver',
value: function setResolver(name, resolver) {
if (!this.gqType._gqcResolvers) {
this.gqType._gqcResolvers = new _map2.default();
}
if (!(resolver instanceof _resolver2.default)) {
throw new Error('setResolver() accept only Resolver instance');
}
this.gqType._gqcResolvers.set(name, resolver);
return this;
}
}, {
key: 'addResolver',
value: function addResolver(resolver) {
if (!(resolver instanceof _resolver2.default)) {
resolver = new _resolver2.default(resolver); // eslint-disable-line no-param-reassign
}
if (!resolver.name) {
throw new Error('resolver should have non-empty name property');
}
this.setResolver(resolver.name, resolver);
return this;
}
}, {
key: 'removeResolver',
value: function removeResolver(resolverName) {
if (resolverName) {
this.getResolvers().delete(resolverName);
}
return this;
}
}, {
key: 'getTypeName',
value: function getTypeName() {
return this.gqType.name;
}
}, {
key: 'setTypeName',
value: function setTypeName(name) {
this.gqType.name = name;
return this;
}
}, {
key: 'getDescription',
value: function getDescription() {
return this.gqType.description || '';
}
}, {
key: 'setDescription',
value: function setDescription(description) {
this.gqType.description = description;
return this;
}
}, {
key: 'setRecordIdFn',
value: function setRecordIdFn(fn) {
this.gqType._gqcGetRecordIdFn = fn;
return this;
}
}, {
key: 'hasRecordIdFn',
value: function hasRecordIdFn() {
return !!this.gqType._gqcGetRecordIdFn;
}
}, {
key: 'getRecordIdFn',
value: function getRecordIdFn() {
if (!this.gqType._gqcGetRecordIdFn) {
throw new Error('Type ' + this.getTypeName() + ' does not have RecordIdFn');
}
return this.gqType._gqcGetRecordIdFn;
}
/**
* Get function that returns record id, from provided object.
*/
}, {
key: 'getRecordId',
value: function getRecordId(source, args, context) {
return this.getRecordIdFn()(source, args, context);
}
}, {
key: 'getFieldArgs',
value: function getFieldArgs(fieldName) {
var field = this.getField(fieldName);
if (field) {
return field.args;
}
return null;
}
}, {
key: 'getFieldArg',
value: function getFieldArg(fieldName, argName) {
var fieldArgs = this.getFieldArgs(fieldName) || {};
return fieldArgs[argName] ? fieldArgs[argName] : undefined;
}
/**
* @deprecated 2.0.0
*/
}, {
key: 'getByPath',
value: function getByPath(path) {
(0, _debug.deprecate)('Use TypeComposer.get() instead.');
return this.get(path);
}
}, {
key: 'get',
value: function get(path) {
return (0, _typeByPath.typeByPath)(this, path);
}
// Sometimes, when you create relations or some tricky fields,
// you should have a data from additional fields, that not in a query projection.
// E.g. for obtaining `friendList` you also should add `friendIds` to projection.
// or for `fullname` field you should request `firstname` and `lastname` from DB.
// this _gqcProjectionMapper used in `projection` module
}, {
key: 'addProjectionMapper',
value: function addProjectionMapper(fieldName, sourceProjection) {
if (!this.gqType._gqcProjectionMapper) {
this.gqType._gqcProjectionMapper = {};
}
this.gqType._gqcProjectionMapper[fieldName] = sourceProjection;
return this;
}
}, {
key: 'getProjectionMapper',
value: function getProjectionMapper() {
return this.gqType._gqcProjectionMapper || {};
}
}]);
return TypeComposer;
}();
exports.default = TypeComposer;