mgs-graphql
Version:
The simple way to generates GraphQL schemas and Sequelize models from your models definition,microservice supported
804 lines (720 loc) • 25 kB
JavaScript
// @flow
const Sequelize = require('sequelize')
const relay = require('graphql-relay')
const _ = require('lodash')
const {
GraphQLObjectType,
GraphQLNonNull,
GraphQLFloat,
GraphQLID,
GraphQLString,
responsePathAsArray
} = require('graphql')
const camelcase = require('camelcase')
const DataLoader = require('dataloader')
const parseFields = require('graphql-parse-fields')
const StringHelper = require('./utils/StringHelper')
const invariant = require('./utils/invariant')
const Transformer = require('./transformer')
const {mergeNQueryBulk} = require('./sequelize/mergeNQuery')
const SequelizeContext = require('./sequelize/SequelizeContext')
const {buildBindings} = require('./utils/remote')
const helper = require('./utils/helper')
module.exports = class Context {
constructor (sequelize, options, remoteCfg) {
this.dbContext = new SequelizeContext(sequelize)
this.options = {
dataLoader: true,
remoteLoader: true
}
_.assign(this.options, options)
this.dbModels = {}
this.schemas = {}
this.services = {}
this.graphQLObjectTypes = {}
this.queries = {}
this.mutations = {}
this.subscriptions = {}
this.loaders = {}
this.schemasFieldsAndLinks = {}
this.connectionDefinitions = {}
const self = this
this.nodeInterface = relay.nodeDefinitions((globalId) => {
var {type, id} = relay.fromGlobalId(globalId)
console.log('Warning-------------------- node id Fetcher not implement' + type + ' ' + id)
}, (obj) => {
const type = obj._type
return self.graphQLObjectTypes[type]
}).nodeInterface
this.resolvers = {
Query: {},
Mutation: {}
}
this.remoteInfo = buildBindings(remoteCfg, {headerKeys: options.headerKeys})
this.remotePrefix = '_remote_'
// 暂时只开启一个remoteLoader,可考虑开启多个
this.remoteLoader = this.options.remoteLoader !== false ? this.initRemoteLoader() : null
this.getSGContext = (function () {
let unique
function getInstance() {
if (!unique) {
unique = SGContext(this)
}
return unique
}
function SGContext(self) {
return {
sequelize: self.dbContext.sequelize,
loaders: self.loaders,
remoteLoader: self.remoteLoader,
dataLoader: self.options.dataLoader,
models: self.dbModels,
services: _.mapValues(self.services, (service) => service.config.statics),
bindings: {
toGId: (type, id) => relay.toGlobalId(type, id),
toDbId: (type, id) => {
const gid = relay.fromGlobalId(id)
if (gid.type !== type) {
throw new Error(`错误的global id,type:${type},gid:${id}`)
}
return gid.id
},
...self.remoteInfo['binding']
},
getTargetBinding: (modeName) => {
if (!self.remoteInfo['schema']) {
return
}
let target
_.forOwn(self.remoteInfo['schema'], (value, key) => {
if (value && value.getType(modeName)) {
target = key
return false
}
})
return target ? self.remoteInfo['binding'][target] : null
}
}
}
return getInstance
})()
}
getTargetSchema (modeName) {
if (!this.remoteInfo['schema']) {
return
}
let target = {
schema: null,
type: null
}
_.forOwn(this.remoteInfo['schema'], (value, key) => {
let type = value && value.getType(modeName)
if (type) {
if (target && target.type) {
if (helper.calcRemoteLevels(target.type.description) > helper.calcRemoteLevels(type.description)) {
target.schema = value
target.type = type
}
} else {
target.schema = value
target.type = type
}
} else {
// console.error('getTargetSchema:',modeName,key,type.name,type.description)
}
})
return target.schema
}
addRemoteResolver (schemaName, fieldName, linkId, target) {
if (!this.resolvers[schemaName]) {
this.resolvers[schemaName] = {}
}
// console.log('addRemoteResolver:',schemaName,fieldName,linkId,target)
const self = this
this.resolvers[schemaName][fieldName] = {
fragment: `... on ${schemaName} { ${linkId} }`,
resolve: (root, args, context, info) => {
const targetSchema = self.getTargetSchema(target)
if (_.isEmpty(targetSchema)) {
console.error(`addRemoteResolver:can't find remote object ${target} in schema ${schemaName}:${fieldName}`)
return root[fieldName]
}
const fn = self.wrapFieldResolve({
name: fieldName,
path: fieldName,
$type: self.remoteGraphQLObjectType(target),
resolve: async function (root, args, context, info, sgContext) {
if (!root) return
const id = root[linkId]
if (id === undefined) { return null }
if (typeof id === 'object' && id === null) { return null }// db 对应字段为null
if (context.qid && mergeNQueryBulk[context.qid]) {
// const apiName = helper.pluralQueryName(target)
const pathArr = responsePathAsArray(info.path)
const skipIndex = pathArr.length - 3 // eg: [ 'patients', 'edges', 0, 'node', 'city' ] 去掉0,与mergeNQuery的path一致
// invariant(skipIndex > 0, 'err path:', pathArr)
if (skipIndex > 0) {
// console.log('path arr:', context.qid,pathArr,mergeNQueryBulk[context.qid])
let path = pathArr[0]
for (let i = 1; i < pathArr.length; ++i) {
if (i === skipIndex) { continue }
path = helper.contactPath(path, pathArr[i])
}
const queryContext = mergeNQueryBulk[context.qid] && mergeNQueryBulk[context.qid][path]
// console.log('addRemoteResolver', context.qid,id, path, queryContext)
if (queryContext && queryContext.fn) {
const res = queryContext.fn(target, id, queryContext)
// if (_.isEmpty(Object.keys(queryContext))) {
// delete mergeNQueryBulk[context.qid][path]
// if (_.isEmpty(Object.keys(mergeNQueryBulk[context.qid]))) {
// delete mergeNQueryBulk[context.qid]
// }
// }
if (res) {
return res
}
}
}
}
if (root && id && (
typeof id === 'number' ||
typeof id === 'string'
)) {
if (self.remoteLoader && self.options.remoteLoader !== false) {
return self.remoteLoader.load({id, info, target, context})
}
return info.mergeInfo.delegateToSchema({
schema: targetSchema,
operation: 'query',
fieldName: StringHelper.toInitialLowerCase(target),
args: {
id: id
},
context,
info
})
// console.log('context.addResolver:',res)
} else {
// throw new Error('Must provide linkId',linkId,schema.name)
}
return root[fieldName]
}
})
return fn(root, args, context, info)
}
}
}
addSchema (schema) {
if (this.schemas[schema.name]) {
throw new Error('Schema ' + schema.name + ' already define.')
}
if (this.services[schema.name]) {
throw new Error('Schema ' + schema.name + ' conflict with Service ' + schema.name)
}
if (schema.name.length >= 1 && (schema.name[0] === '_' || schema.name.endsWith('Id'))) {
throw new Error(`Schema "${schema.name}" must not begin with "_" or end with "Id", which is reserved by MGS`)
}
if (!schema.config.description && schema.config.description.startsWith('__')) {
throw new Error(`Schema "${schema.name}" description must not begin with "__" which is reserved by MGS`)
}
this.schemas[schema.name] = schema
this.dbContext.applyPlugin(schema)
schema.fields({
createdAt: {
$type: Date,
initializable: false,
mutable: false
},
updatedAt: {
$type: Date,
initializable: false,
mutable: false
}
})
if (schema.config.options && schema.config.options.table && schema.config.options.table.paranoid) {
schema.fields({
deletedAt: {
$type: Date,
initializable: false
}
})
}
_.forOwn(schema.config.queries, (value, key) => {
if (!value['name']) {
value['name'] = key
}
this.addQuery(value)
})
_.forOwn(schema.config.mutations, (value, key) => {
if (!value['name']) {
value['name'] = key
}
this.addMutation(value)
})
_.forOwn(schema.config.subscriptions, (value, key) => {
if (!value['name']) {
value['name'] = key
}
this.addSubscription(value)
})
this.dbModel(schema.name)
// 添加loader
if (this.options.dataLoader !== false) this.loaders[schema.name] = this.initLoader(schema.name)
}
addService (service) {
const self = this
if (self.services[service.name]) {
throw new Error('Service ' + service.name + ' already define.')
}
if (self.schemas[service.name]) {
throw new Error('Service ' + service.name + ' conflict with Schema ' + service.name)
}
service.statics({
getSGContext: () => self.getSGContext()
})
self.services[service.name] = service
_.forOwn(service.config.queries, (value, key) => {
if (!value['name']) {
value['name'] = key
}
self.addQuery(value)
})
_.forOwn(service.config.mutations, (value, key) => {
if (!value['name']) {
value['name'] = key
}
self.addMutation(value)
})
}
addQuery (config) {
if (this.queries[config.name]) {
throw new Error('Query ' + config.name + ' already define.')
}
this.queries[config.name] = config
}
addMutation (config) {
if (this.mutations[config.name]) {
throw new Error('Mutation ' + config.name + ' already define.')
}
this.mutations[config.name] = config
}
addSubscription (config) {
if (this.subscriptions[config.name]) {
throw new Error('Subscription ' + config.name + ' already define.')
}
this.subscriptions[config.name] = config
}
remoteGraphQLObjectType (name) {
// console.log('Context.remoteGraphQLObjectType',name)
const typeName = this.remotePrefix + name
if (!this.graphQLObjectTypes[typeName]) {
const objectType = new GraphQLObjectType({
name: typeName,
fields: {
'id': {
type: GraphQLString,
resolve: () => {
return 'MGS only fake ,not supported'
}
}
}, // TODO support arguments
description: JSON.stringify({
target: name
})
})
this.graphQLObjectTypes[typeName] = objectType
}
return this.graphQLObjectTypes[typeName]
}
getFieldsAndLinks (model, name) {
const schemaFieldsAndLinks = this.schemasFieldsAndLinks[name]
if (schemaFieldsAndLinks) {
return schemaFieldsAndLinks
}
const obj = {}
Object.assign(obj, model.config.fields, model.config.links)
obj.id = {
$type: new GraphQLNonNull(GraphQLID),
resolve: async function (root) {
return relay.toGlobalId(StringHelper.toInitialUpperCase(model.name), root.id)
}
}
this.schemasFieldsAndLinks[name] = obj
return obj
}
graphQLObjectType (name) {
const model = this.schemas[name]
if (!model) {
throw new Error('Schema ' + name + ' not define.')
} else {
invariant(model.name === name, `${model.name}与${name}不一致`)
}
const typeName = name
if (!this.graphQLObjectTypes[typeName]) {
const interfaces = [this.nodeInterface]
const objectType = Transformer.toGraphQLFieldConfig(typeName, '', this.getFieldsAndLinks(model, name), this, interfaces, true).type
if (objectType instanceof GraphQLObjectType) {
objectType.description = model.config.options.description
this.graphQLObjectTypes[typeName] = objectType
} else {
invariant(false, `wrong model format:${name}`)
}
}
return this.graphQLObjectTypes[typeName]
}
dbModel (name) {
const model = this.schemas[name]
if (!model) {
throw new Error('Schema ' + name + ' not define.')
}
const typeName = model.name
const self = this
if (!self.dbModels[typeName]) {
self.dbModels[typeName] = self.dbContext.define(model)
Object.assign(self.dbModels[typeName], model.config.statics)
Object.assign(self.dbModels[typeName].prototype, model.config.methods)
}
return self.dbModels[typeName]
}
initLoader (name) {
const model = this.dbModels[name]
return new DataLoader(async(ids) => {
const lists = await model.findAll({
where: {
id: {
[Sequelize.Op.in]: ids
}
}
})
const temp = {}
lists.map(item => {
temp[item.id] = item
})
return ids.map(id => temp[id])
}, {
cache: false
})
}
/**
* 当一个请求有多个remote时,分别组织参数
* @param options
*/
parseRemoteTarget (options) {
const targets = {}
const self = this
if (options) {
options.map(({id, info, context}) => {
const target = info.fieldName
const aliasMap = {}
self.findAliasField(info, aliasMap, '')
if (_.keys(targets).indexOf(target) === -1) {
_.assign(targets, {
[target]: {
ids: [],
info,
// 默认传递id,防止前端不传id导致下面匹配不上
parsedInfo: {id: true},
aliasMap,
context
}
})
}
if (_.keys(targets).indexOf(target) >= 0) {
targets[target].ids.push(id)
targets[target].parsedInfo = self.analysisInfo(targets[target].parsedInfo, info)
targets[target].aliasMap = _.assign(targets[target].aliasMap, aliasMap)
}
})
}
return targets
}
// 将所有同类型的字段合并,防止一个请求取同类型不同字段出现报错。如{id, name} {id, code}
analysisInfo (parsed, newInfo) {
const newParsed = parseFields(newInfo)
return _.merge(parsed, newParsed)
}
// 对id做简单处理,防止一个请求中clinic{id}和getClinic{id, name}匹配错乱问题
encryptId (id, target) {
return `${id}-${target}`
}
recursionSetAlias(obj, aliasFields, fields, index = 0) {
if (obj === null || obj === undefined) {
return true
}
if (Array.isArray(obj)) {
for (let o of obj) {
this.recursionSetAlias(o, aliasFields, fields, index)
}
return true
}
if (fields.length - 1 === index) {
obj[aliasFields[index]] = obj[fields[index]]
return true
}
const subObj = obj[fields[index]]
this.recursionSetAlias(subObj, aliasFields, fields, index + 1)
}
joinKey(root, key) {
if (root) {
return root + '.' + key
}
return key
}
findAliasField (info, aliasMap, key = '', isFieldNode = false) {
if (info.fieldNodes) {
for (let fieldNode of info.fieldNodes) {
this.findAliasField(fieldNode, aliasMap, key, true)
}
} else {
const newKey = this.joinKey(key, info.name.value)
if (info.alias) {
aliasMap[this.joinKey(key, info.alias.value)] = newKey
}
if (info.selectionSet && info.selectionSet.selections) {
for (let selection of info.selectionSet.selections) {
this.findAliasField(selection, aliasMap, isFieldNode ? '' : newKey)
}
}
}
}
// 把有别名的字段设置(还原)回去
setAliasFieldValue (aliasMap, node) {
for (let [aliasName, fieldName] of Object.entries(aliasMap)) {
const fields = fieldName.split('.')
const aliasFields = aliasName.split('.')
this.recursionSetAlias(node, aliasFields, fields)
}
}
initRemoteLoader () {
const self = this
return new DataLoader(async(options) => {
const targets = self.parseRemoteTarget(options)
const encryptIds = options.map(o => self.encryptId(o.id, o.info.fieldName))
const temp = {}
for (let target in targets) {
const {ids, info, parsedInfo, aliasMap, context} = targets[target]
let strInfo = JSON.stringify(parsedInfo).replace(/"/g, '').replace(/:true/g, '').replace(/:{/g, '{')
const type = info.returnType.name
const binding = this.getSGContext().getTargetBinding(type)
if (!binding) return []
// 优先使用get${modelName}sByIds
const distinctIds = [...new Set(ids)]
if (binding.query[`get${type}sByIds`]) {
const res = await binding.query[`get${type}sByIds`]({
ids: distinctIds
}, strInfo, { context })
for (let node of res) {
self.setAliasFieldValue(aliasMap, node)
temp[self.encryptId(node.id, target)] = node
}
} else {
const res = await binding.query[StringHelper.toInitialLowerCase(type) + 's'](
{
first: distinctIds.length,
options: {
where: {
id: {
in: distinctIds
}
}
}
},
`{edges{node${strInfo}}}`,
{ context }
)
res.edges.map(({node}) => {
self.setAliasFieldValue(aliasMap, node)
temp[self.encryptId(node.id, target)] = node
})
}
}
return encryptIds.map(encryptId => temp[encryptId])
}, {
cache: false
})
}
wrapQueryResolve (config) {
const self = this
const {handleError} = this.options
let hookFun = async (action, invokeInfo, next) => {
try {
return await next()
} catch (e) {
if (handleError) {
handleError(e)
} else {
throw e
}
}
}
if (this.options.hooks != null) {
this.options.hooks.reverse().forEach(hook => {
if (!hook.filter || hook.filter({type: 'query', config})) {
const preHook = hookFun
hookFun = (action, invokeInfo, next) => hook.hook(action, invokeInfo, preHook.bind(null, action, invokeInfo, next))
}
})
}
return (source, args, context, info) => hookFun({
type: 'query',
config: config
}, {
source: source,
args: args,
context: context,
info: info,
sgContext: self.getSGContext()
},
() => {
return config.resolve(args, context, info, self.getSGContext())
}
)
}
wrapSubscriptionResolve (config) {
const self = this
const {handleError} = this.options
let hookFun = async (action, invokeInfo, next) => {
try {
return await next()
} catch (e) {
if (handleError) {
handleError(e)
} else {
throw e
}
}
}
if (this.options.hooks != null) {
this.options.hooks.reverse().forEach(hook => {
if (!hook.filter || hook.filter({type: 'subscription', config})) {
const preHook = hookFun
hookFun = (action, invokeInfo, next) => hook.hook(action, invokeInfo, preHook.bind(null, action, invokeInfo, next))
}
})
}
return (source, args, context, info) => hookFun({
type: 'subscription',
config: config
}, {
source: source,
args: args,
context: context,
info: info,
sgContext: self.getSGContext()
},
() => {
return config.resolve(source, args, context, info, self.getSGContext())
}
)
}
wrapFieldResolve (config) {
const self = this
let hookFun = (action, invokeInfo, next) => next()
if (this.options.hooks != null) {
this.options.hooks.reverse().forEach(hook => {
if (!hook.filter || hook.filter({type: 'field', config})) {
const preHook = hookFun
hookFun = (action, invokeInfo, next) => hook.hook(action, invokeInfo, preHook.bind(null, action, invokeInfo, next))
}
})
}
return (source, args, context, info) => hookFun({
type: 'field',
config: config
}, {
source: source,
args: args,
context: context,
info: info,
sgContext: self.getSGContext()
},
() => config.resolve(source, args, context, info, self.getSGContext())
)
}
wrapMutateAndGetPayload (config) {
const self = this
const {handleError} = this.options
let hookFun = async (action, invokeInfo, next) => {
try {
return await next()
} catch (e) {
if (handleError) {
handleError(e)
} else {
throw e
}
}
}
if (this.options.hooks != null) {
this.options.hooks.reverse().forEach(hook => {
if (!hook.filter || hook.filter({type: 'mutation', config})) {
const preHook = hookFun
hookFun = (action, invokeInfo, next) => hook.hook(action, invokeInfo, preHook.bind(null, action, invokeInfo, next))
}
})
}
return (args, context, info) => hookFun({
type: 'mutation',
config: config
}, {
args: args,
context: context,
info: info,
sgContext: self.getSGContext()
},
() => config.mutateAndGetPayload(args, context, info, self.getSGContext())
)
}
connectionDefinition (schemaName) {
if (!this.connectionDefinitions[schemaName]) {
this.connectionDefinitions[schemaName] = relay.connectionDefinitions({
name: StringHelper.toInitialUpperCase(schemaName),
nodeType: this.graphQLObjectType(schemaName),
connectionFields: {
count: {
type: GraphQLFloat
}
}
})
}
return this.connectionDefinitions[schemaName]
}
connectionType (schemaName) {
return this.connectionDefinition(schemaName).connectionType
}
edgeType (schemaName) {
return this.connectionDefinition(schemaName).edgeType
}
buildModelAssociations () {
const self = this
_.forOwn(self.schemas, (schema) => {
_.forOwn(schema.config.associations.hasMany, (config, key) => {
config.foreignKey = {
name: camelcase(config.foreignKey || config.foreignField + 'Id'),
field: config.foreignKey || config.foreignField + 'Id'
}
config.as = key
self.dbModel(schema.name).hasMany(self.dbModel(config.target), config)
})
_.forOwn(schema.config.associations.belongsToMany, (config, key) => {
config.through && (config.through.model = self.dbModel(config.through.model))
config.as = key
config.foreignKey = config.foreignKey || config.foreignField + 'Id'
self.dbModel(schema.name).belongsToMany(self.dbModel(config.target), config)
})
_.forOwn(schema.config.associations.hasOne, (config, key) => {
config.as = key
config.foreignKey = {
name: camelcase(config.foreignKey || config.foreignField + 'Id'),
field: config.foreignKey || config.foreignField + 'Id'
}
self.dbModel(schema.name).hasOne(self.dbModel(config.target), config)
})
_.forOwn(schema.config.associations.belongsTo, (config, key) => {
config.as = key
config.foreignKey = config.foreignKey || config.foreignField + 'Id'
self.dbModel(schema.name).belongsTo(self.dbModel(config.target), config)
})
})
}
}