UNPKG

graphql-compose

Version:

GraphQL schema builder from different data sources with middleware extensions.

703 lines (605 loc) 22.4 kB
'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;