Graphql combat series

Posted by bird_man11 on Fri, 10 May 2019 09:00:20 +0200

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.

Topics: MySQL Database github git