graphql-compose-mongoose
Version:
Plugin for `graphql-compose` which derive a graphql types from a mongoose model.
378 lines (320 loc) • 10.8 kB
Flow
/* @flow */
/* eslint-disable no-use-before-define */
import mongoose from 'mongoose';
import type { Schema, MongooseModel, MongooseSchemaField } from 'mongoose';
import objectPath from 'object-path';
import type {
SchemaComposer,
ObjectTypeComposer,
EnumTypeComposer,
ComposeOutputTypeDefinition,
} from 'graphql-compose';
import { upperFirst } from 'graphql-compose';
import type { GraphQLScalarType } from 'graphql-compose/lib/graphql';
import GraphQLMongoID from './types/mongoid';
import GraphQLBSONDecimal from './types/bsonDecimal';
type MongooseFieldT = MongooseSchemaField<any>;
type MongooseFieldMapT = { [fieldName: string]: MongooseFieldT };
type ComposeScalarType = string | GraphQLScalarType;
export type MongoosePseudoModelT = {
schema: Schema<any>,
};
export const ComplexTypes = {
ARRAY: 'ARRAY',
EMBEDDED: 'EMBEDDED',
DOCUMENT_ARRAY: 'DOCUMENT_ARRAY',
ENUM: 'ENUM',
REFERENCE: 'REFERENCE',
SCALAR: 'SCALAR',
MIXED: 'MIXED',
DECIMAL: 'DECIMAL',
};
function _getFieldName(field: MongooseFieldT): string {
return field.path || '__unknownField__';
}
function _getFieldType(field: MongooseFieldT): string {
return field.instance;
}
function _getFieldDescription(field: MongooseFieldT): ?string {
if (field.options && field.options.description) {
return field.options.description;
}
return undefined;
}
function _getFieldEnums(field: MongooseFieldT): ?(string[]) {
if (field.enumValues && field.enumValues.length > 0) {
return field.enumValues;
}
return undefined;
}
export function dotPathsToEmbedded(fields: MongooseFieldMapT): MongooseFieldMapT {
// convert only one dot-level on this step to EmbeddedModel
// further when converting EmbeddedModel to GQL, it internally
// call this method to extract deep fields with dots
const result: MongooseFieldMapT = {};
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: MongooseFieldT = {
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] = { ...fields[fieldName], path: subName };
}
});
return result;
}
export function getFieldsFromModel(model: MongooseModel | MongoosePseudoModelT): MongooseFieldMapT {
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<TSource, TContext>(
model: MongooseModel | MongoosePseudoModelT,
typeName: string,
schemaComposer: SchemaComposer<TContext>
): ObjectTypeComposer<TSource, TContext> {
const sc = schemaComposer;
if (!typeName) {
throw new Error('You provide empty name for type. `name` argument should be non-empty string.');
}
// if model already has generated ObjectTypeComposer early, then return it
if (sc.has(model.schema)) {
return sc.getOTC(model.schema);
}
const typeComposer = sc.getOrCreateOTC(typeName);
sc.set(model.schema, typeComposer);
sc.set(typeName, typeComposer);
const mongooseFields = getFieldsFromModel(model);
const graphqlFields = {};
Object.keys(mongooseFields).forEach(fieldName => {
const mongooseField: MongooseFieldT = mongooseFields[fieldName];
graphqlFields[fieldName] = {
type: convertFieldToGraphQL(mongooseField, typeName, sc),
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: Schema<any>,
typeName: string,
schemaComposer: SchemaComposer<any>
): ObjectTypeComposer<any, any> {
const sc = schemaComposer;
if (!typeName) {
throw new Error('You provide empty name for type. `name` argument should be non-empty string.');
}
if (sc.has(schema)) {
return sc.getOTC(schema);
}
const tc = convertModelToGraphQL({ schema }, typeName, sc);
// also generate InputType
tc.getInputTypeComposer();
sc.set(schema, tc);
return tc;
}
export function convertFieldToGraphQL(
field: MongooseFieldT,
prefix?: string = '',
schemaComposer: SchemaComposer<any>
): ComposeOutputTypeDefinition<any> {
if (!schemaComposer.has('MongoID')) {
schemaComposer.add(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): any);
case ComplexTypes.MIXED:
return 'JSON';
case ComplexTypes.DECIMAL:
if (!schemaComposer.has('BSONDecimal')) {
schemaComposer.add(GraphQLBSONDecimal);
}
return 'BSONDecimal';
default:
return scalarToGraphQL(field);
}
}
export function deriveComplexType(field: MongooseFieldT): $Keys<typeof ComplexTypes> {
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 ||
(fieldType === 'Array' && objectPath.has(field, 'schema.paths'))
) {
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;
} else if (fieldType === 'Decimal128') {
return ComplexTypes.DECIMAL;
}
const enums = _getFieldEnums(field);
if (enums) {
return ComplexTypes.ENUM;
}
return ComplexTypes.SCALAR;
}
export function scalarToGraphQL(field: MongooseFieldT): ComposeScalarType {
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: MongooseFieldT,
prefix?: string = '',
schemaComposer: SchemaComposer<any>
): ComposeOutputTypeDefinition<any> {
if (!field || !field.caster) {
throw new Error(
'You provide incorrect mongoose field to `arrayToGraphQL()`. ' +
'Correct field should contain `caster` property.'
);
}
const unwrappedField = { ...field.caster };
const outputType: any = convertFieldToGraphQL(unwrappedField, prefix, schemaComposer);
return [outputType];
}
export function embeddedToGraphQL(
field: MongooseFieldT,
prefix?: string = '',
schemaComposer: SchemaComposer<any>
): ObjectTypeComposer<any, any> {
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: MongooseFieldT,
prefix?: string = '',
schemaComposer: SchemaComposer<any>
): EnumTypeComposer<any> {
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: MongooseFieldT,
prefix?: string = '',
schemaComposer: SchemaComposer<any>
): [ObjectTypeComposer<any, any>] {
if (
!(field instanceof mongoose.Schema.Types.DocumentArray) &&
!objectPath.has(field, 'schema.paths')
) {
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: any), typeName, schemaComposer);
return [tc];
}
export function referenceToGraphQL(field: MongooseFieldT): ComposeScalarType {
return scalarToGraphQL(field);
}