Former love introduction
stay Graphql Series of Actual Warfare (Part I) We have completed the technical selection and bridged the graphql Gel gels In the project, and dynamic handwritten schema, through http://localhost 5000/graphql view effect. In this section, we automatically generate the basic query and update schema according to the database tables, and can easily extend the schema to realize the business logic we want.
Design thinking
Object definitions are made by strings in apollo-server, while Query and Mutation can only have one, and our definitions are scattered in multiple files, so they can only be stored in an array in a certain form and then combined at the moment before schema is generated.
Business Logic Module Template Design:
const customDefs = { textDefs: ` type ReviseResult { id: Int affectedRows: Int status: Int message: String }, queryDefs: [], mutationDefs: [] } const customResolvers = { Query: { }, Mutation: { } } export { customDefs, customResolvers }
schema merge algorithm
let typeDefs = [] let dirGraphql = requireDir('../../graphql') //Read files from the handwritten schema business module directory G.L.each(dirGraphql, (item, name) => { if (item && item.customDefs && item.customResolvers) { typeDefs.push(item.customDefs.textDefs || '') //Merging Text Object Definitions typeDefObj.query = typeDefObj.query.concat(item.customDefs.queryDefs || []) //Merge Query typeDefObj.mutation = typeDefObj.mutation.concat(item.customDefs.mutationDefs || []) //Merge Matation let { Query, Mutation, ...Other } = item.customResolvers Object.assign(resolvers.Query, Query) //Merge resolvers.Query Object.assign(resolvers.Mutation, Mutation) //Merge resolvers.Mutation Object.assign(resolvers, Other) //Merge other resolvers } }) //Converting query and matation query update objects from custom arrays to text typeDefs.push(Object.entries(typeDefObj).reduce((total, cur) => { return total += ` type ${G.tools.bigCamelCase(cur[0])} { ${cur[1].join('')} } ` }, ''))
Generating schema dynamically from database tables
Automatically generate content:
- One table, one object;
- Each table has two Queries, one is a single Query, and the other is a list Query.
- Three Mutation s are added, updated and deleted.
- For example, the Book and Author in the previous article in the association table, the author_id in the Book generates an Author object, while the Author table generates a list of objects [Book]
mysql type => graphql type transformation constant definition
Define a class of type conversions, which are not defined by default as String.
const TYPEFROMMYSQLTOGRAPHQL = { int: 'Int', smallint: 'Int', tinyint: 'Int', bigint: 'Int', double: 'Float', float: 'Float', decimal: 'Float', }
Read data table information from database
let dao = new BaseDao() let tables = await dao.querySql('select TABLE_NAME,TABLE_COMMENT from information_schema.`TABLES` ' + ' where TABLE_SCHEMA = ? and TABLE_TYPE = ? and substr(TABLE_NAME,1,2) <> ? order by ?', [G.CONFIGS.dbconfig.db_name, 'BASE TABLE', 't_', 'TABLE_NAME'])
Read table field information from database
tables.data.forEach((table) => { columnRs.push(dao.querySql('SELECT `COLUMNS`.COLUMN_NAME,`COLUMNS`.COLUMN_TYPE,`COLUMNS`.IS_NULLABLE,' + '`COLUMNS`.CHARACTER_SET_NAME,`COLUMNS`.COLUMN_DEFAULT,`COLUMNS`.EXTRA,' + '`COLUMNS`.COLUMN_KEY,`COLUMNS`.COLUMN_COMMENT,`STATISTICS`.TABLE_NAME,' + '`STATISTICS`.INDEX_NAME,`STATISTICS`.SEQ_IN_INDEX,`STATISTICS`.NON_UNIQUE,' + '`COLUMNS`.COLLATION_NAME ' + 'FROM information_schema.`COLUMNS` ' + 'LEFT JOIN information_schema.`STATISTICS` ON ' + 'information_schema.`COLUMNS`.TABLE_NAME = `STATISTICS`.TABLE_NAME ' + 'AND information_schema.`COLUMNS`.COLUMN_NAME = information_schema.`STATISTICS`.COLUMN_NAME ' + 'AND information_schema.`STATISTICS`.table_schema = ? ' + 'where information_schema.`COLUMNS`.TABLE_NAME = ? and `COLUMNS`.table_schema = ?', [G.CONFIGS.dbconfig.db_name, table.TABLE_NAME, G.CONFIGS.dbconfig.db_name])) })
Several Tool Functions
Remove parentheses and length information by taking field types of database tables
getStartTillBracket(str: string) { return str.indexOf('(') > -1 ? str.substr(0, str.indexOf('(')) : str }
Underline-separated table fields are converted to big camel-case
bigCamelCase(str: string) { return str.split('_').map((al) => { if (al.length > 0) { return al.substr(0, 1).toUpperCase() + al.substr(1).toLowerCase() } return al }).join('') }
Underline-separated table fields are converted to small camel-case
smallCamelCase(str: string) { let strs = str.split('_') if (strs.length < 2) { return str } else { let tail = strs.slice(1).map((al) => { if (al.length > 0) { return al.substr(0, 1).toUpperCase() + al.substr(1).toLowerCase() } return al }).join('') return strs[0] + tail } }
Does the field end with _id, which is the flag associated with the table?
It does not end with _id. It is a normal field. If it is null, it must be filled in.
typeDefObj[table].unshift(`${col['COLUMN_NAME']}: ${typeStr}${col['IS_NULLABLE'] === 'NO' ? '!' : ''}\n`)
Ending with _id, you need to deal with the Association
//Book table associates a single Author entity with author_id typeDefObj[table].unshift(`"""Associated entities""" ${G.L.trimEnd(col['COLUMN_NAME'], '_id')}: ${G.tools.bigCamelCase(G.L.trimEnd(col['COLUMN_NAME'], '_id'))}`) resolvers[G.tools.bigCamelCase(table)] = { [G.L.trimEnd(col['COLUMN_NAME'], '_id')]: async (element) => { let rs = await new BaseDao(G.L.trimEnd(col['COLUMN_NAME'], '_id')).retrieve({ id: element[col['COLUMN_NAME']] }) return rs.data[0] } } //Author table associated Book list let fTable = G.L.trimEnd(col['COLUMN_NAME'], '_id') if (!typeDefObj[fTable]) { typeDefObj[fTable] = [] } if (typeDefObj[fTable].length >= 2) typeDefObj[fTable].splice(typeDefObj[fTable].length - 2, 0, `"""Association entity set"""${table}s: [${G.tools.bigCamelCase(table)}]\n`) else typeDefObj[fTable].push(`${table}s: [${G.tools.bigCamelCase(table)}]\n`) resolvers[G.tools.bigCamelCase(fTable)] = { [`${table}s`]: async (element) => { let rs = await new BaseDao(table).retrieve({ [col['COLUMN_NAME']]: element.id}) return rs.data } }
Generating Query Queries
single query
if (paramId.length > 0) { typeDefObj['query'].push(`${G.tools.smallCamelCase(table)}(${paramId}!): ${G.tools.bigCamelCase(table)}\n`) resolvers.Query[`${G.tools.smallCamelCase(table)}`] = async (_, { id }) => { let rs = await new BaseDao(table).retrieve({ id }) return rs.data[0] } } else { G.logger.error(`Table [${table}] must have id field.`) }
List query
let complex = table.endsWith('s') ? (table.substr(0, table.length - 1) + 'z') : (table + 's') typeDefObj['query'].push(`${G.tools.smallCamelCase(complex)}(${paramStr.join(', ')}): [${G.tools.bigCamelCase(table)}]\n`) resolvers.Query[`${G.tools.smallCamelCase(complex)}`] = async (_, args) => { let rs = await new BaseDao(table).retrieve(args) return rs.data }
Generating Mutation Queries
typeDefObj['mutation'].push(` create${G.tools.bigCamelCase(table)}(${paramForMutation.slice(1).join(', ')}):ReviseResult update${G.tools.bigCamelCase(table)}(${paramForMutation.join(', ')}):ReviseResult delete${G.tools.bigCamelCase(table)}(${paramId}!):ReviseResult `) resolvers.Mutation[`create${G.tools.bigCamelCase(table)}`] = async (_, args) => { let rs = await new BaseDao(table).create(args) return rs } resolvers.Mutation[`update${G.tools.bigCamelCase(table)}`] = async (_, args) => { let rs = await new BaseDao(table).update(args) return rs } resolvers.Mutation[`delete${G.tools.bigCamelCase(table)}`] = async (_, { id }) => { let rs = await new BaseDao(table).delete({ id }) return rs }
Project address
https://github.com/zhoutk/gels
Usage method
git clone https://github.com/zhoutk/gels cd gels yarn tsc -w nodemon dist/index.js
Then you can open the link with a browser: http://localhost 5000/graphql to see the effect.
Summary
I can only write out the general idea, so that you have a whole concept, if you want to have a good understanding, you have to run the project, according to the ideas I provide, slowly understand. Because I still encounter many difficulties in the process of writing, this block is not only automated, but also easy to accept the schema module written manually, it is indeed a little difficult.