UNPKG

graphql-compose-mongoose

Version:

Plugin for `graphql-compose` which derive a graphql types from a mongoose model.

301 lines (242 loc) 9.3 kB
var _extends = Object.assign || 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; }; /* eslint-disable no-use-before-define */ import mongoose from 'mongoose'; import objectPath from 'object-path'; import { upperFirst, schemaComposer as globalSchemaComposer } from 'graphql-compose'; import GraphQLMongoID from './types/mongoid'; export const ComplexTypes = { ARRAY: 'ARRAY', EMBEDDED: 'EMBEDDED', DOCUMENT_ARRAY: 'DOCUMENT_ARRAY', ENUM: 'ENUM', REFERENCE: 'REFERENCE', SCALAR: 'SCALAR', MIXED: 'MIXED' }; function _getFieldName(field) { return field.path || '__unknownField__'; } function _getFieldType(field) { return field.instance; } function _getFieldDescription(field) { if (field.options && field.options.description) { return field.options.description; } return undefined; } function _getFieldEnums(field) { if (field.enumValues && field.enumValues.length > 0) { return field.enumValues; } return undefined; } export function dotPathsToEmbedded(fields) { // convert only one dot-level on this step to EmbeddedModel // further when converting EmbeddedModel to GQL, it internally const result = {}; Object.keys(fields).forEach(fieldName => { const dotIdx = fieldName.indexOf('.'); if (dotIdx === -1) { result[fieldName] = fields[fieldName]; } else { // create pseudo sub-model const name = fieldName.substr(0, dotIdx); if (!result[name]) { const embeddedField = { instance: 'Embedded', path: name, schema: { paths: {} } }; result[name] = embeddedField; } const subName = fieldName.substr(dotIdx + 1); const fieldSchema = result[name].schema; if (!fieldSchema) { throw new Error(`Field ${name} does not have schema property`); } fieldSchema.paths[subName] = _extends({}, fields[fieldName], { path: subName }); } }); return result; } export function getFieldsFromModel(model) { if (!model || !model.schema || !model.schema.paths) { throw new Error('You provide incorrect mongoose model to `getFieldsFromModel()`. ' + 'Correct model should contain `schema.paths` properties.'); } const fields = {}; const paths = dotPathsToEmbedded(model.schema.paths); Object.keys(paths).filter(path => !path.startsWith('__')) // skip hidden fields .forEach(path => { fields[path] = paths[path]; }); return fields; } export function convertModelToGraphQL(model, typeName, sc) { const schemaComposer = sc || globalSchemaComposer; // if model already has generated TypeComposer early, then return it const modelSchema = model.schema; if (modelSchema && modelSchema._gqcTypeComposer) { return modelSchema._gqcTypeComposer; } if (!typeName) { throw new Error('You provide empty name for type. `name` argument should be non-empty string.'); } const typeComposer = schemaComposer.getOrCreateTC(typeName); modelSchema._gqcTypeComposer = typeComposer; // eslint-disable-line no-param-reassign const mongooseFields = getFieldsFromModel(model); const graphqlFields = {}; Object.keys(mongooseFields).forEach(fieldName => { const mongooseField = mongooseFields[fieldName]; graphqlFields[fieldName] = { type: convertFieldToGraphQL(mongooseField, typeName, schemaComposer), description: _getFieldDescription(mongooseField) }; if (deriveComplexType(mongooseField) === ComplexTypes.EMBEDDED) { // https://github.com/nodkz/graphql-compose-mongoose/issues/7 graphqlFields[fieldName].resolve = source => { if (source) { if (source.toObject) { const obj = source.toObject(); return obj[fieldName]; } return source[fieldName]; } return null; }; } }); typeComposer.addFields(graphqlFields); return typeComposer; } export function convertSchemaToGraphQL(schema, // MongooseModelSchemaT, TODO use Model from mongoose_v4.x.x definition when it will be public typeName, sc) { const schemaComposer = sc || globalSchemaComposer; if (!typeName) { throw new Error('You provide empty name for type. `name` argument should be non-empty string.'); } if (schema._gqcTypeComposer) { return schema._gqcTypeComposer; } const tc = convertModelToGraphQL({ schema }, typeName, schemaComposer); // also generate InputType tc.getInputTypeComposer(); schema._gqcTypeComposer = tc; // eslint-disable-line return tc; } export function convertFieldToGraphQL(field, prefix = '', schemaComposer) { if (!schemaComposer.has('MongoID')) { schemaComposer.set('MongoID', GraphQLMongoID); } const complexType = deriveComplexType(field); switch (complexType) { case ComplexTypes.SCALAR: return scalarToGraphQL(field); case ComplexTypes.ARRAY: return arrayToGraphQL(field, prefix, schemaComposer); case ComplexTypes.EMBEDDED: return embeddedToGraphQL(field, prefix, schemaComposer); case ComplexTypes.ENUM: return enumToGraphQL(field, prefix, schemaComposer); case ComplexTypes.REFERENCE: return referenceToGraphQL(field); case ComplexTypes.DOCUMENT_ARRAY: return documentArrayToGraphQL(field, prefix, schemaComposer); case ComplexTypes.MIXED: return 'JSON'; default: return scalarToGraphQL(field); } } export function deriveComplexType(field) { if (!field || !field.path || !field.instance) { throw new Error('You provide incorrect mongoose field to `deriveComplexType()`. ' + 'Correct field should contain `path` and `instance` properties.'); } const fieldType = _getFieldType(field); if (field instanceof mongoose.Schema.Types.DocumentArray) { return ComplexTypes.DOCUMENT_ARRAY; } else if (field instanceof mongoose.Schema.Types.Embedded || fieldType === 'Embedded') { return ComplexTypes.EMBEDDED; } else if (field instanceof mongoose.Schema.Types.Array || objectPath.has(field, 'caster.instance')) { return ComplexTypes.ARRAY; } else if (field instanceof mongoose.Schema.Types.Mixed) { return ComplexTypes.MIXED; } else if (fieldType === 'ObjectID') { return ComplexTypes.REFERENCE; } const enums = _getFieldEnums(field); if (enums) { return ComplexTypes.ENUM; } return ComplexTypes.SCALAR; } export function scalarToGraphQL(field) { const typeName = _getFieldType(field); switch (typeName) { case 'String': return 'String'; case 'Number': return 'Float'; case 'Date': return 'Date'; case 'Buffer': return 'Buffer'; case 'Boolean': return 'Boolean'; case 'ObjectID': return 'MongoID'; default: return 'JSON'; } } export function arrayToGraphQL(field, prefix = '', schemaComposer) { if (!field || !field.caster) { throw new Error('You provide incorrect mongoose field to `arrayToGraphQL()`. ' + 'Correct field should contain `caster` property.'); } const unwrappedField = _extends({}, field.caster); objectPath.set(unwrappedField, 'options.ref', objectPath.get(field, 'options.ref', undefined)); const outputType = convertFieldToGraphQL(unwrappedField, prefix, schemaComposer); return [outputType]; } export function embeddedToGraphQL(field, prefix = '', schemaComposer) { const fieldName = _getFieldName(field); const fieldType = _getFieldType(field); if (fieldType !== 'Embedded') { throw new Error(`You provide incorrect field '${prefix}.${fieldName}' to 'embeddedToGraphQL()'. ` + 'This field should has `Embedded` type. '); } const fieldSchema = field.schema; if (!fieldSchema) { throw new Error(`Mongoose field '${prefix}.${fieldName}' should have 'schema' property`); } const typeName = `${prefix}${upperFirst(fieldName)}`; return convertSchemaToGraphQL(fieldSchema, typeName, schemaComposer); } export function enumToGraphQL(field, prefix = '', schemaComposer) { const valueList = _getFieldEnums(field); if (!valueList) { throw new Error('You provide incorrect mongoose field to `enumToGraphQL()`. ' + 'Correct field should contain `enumValues` property'); } const typeName = `Enum${prefix}${upperFirst(_getFieldName(field))}`; return schemaComposer.getOrCreateETC(typeName, etc => { const desc = _getFieldDescription(field); if (desc) etc.setDescription(desc); const fields = valueList.reduce((result, val) => { result[val] = { value: val }; // eslint-disable-line no-param-reassign return result; }, {}); etc.setFields(fields); }); } export function documentArrayToGraphQL(field, prefix = '', schemaComposer) { if (!(field instanceof mongoose.Schema.Types.DocumentArray)) { throw new Error('You provide incorrect mongoose field to `documentArrayToGraphQL()`. ' + 'Correct field should be instance of `mongoose.Schema.Types.DocumentArray`'); } const typeName = `${prefix}${upperFirst(_getFieldName(field))}`; const tc = convertModelToGraphQL(field, typeName, schemaComposer); return [tc]; } export function referenceToGraphQL(field) { return scalarToGraphQL(field); }