preface

In the development of NestJs + Mysql project, you may want to enable the synchronous generation of data tables from entity classes. This method is ok, and typeOrm is good at, but there is a risk that data will be deleted by mistake.

The target

No matter what we want to develop, the first thing we need to be clear is, what do we want this thing to do, what are the functions, and what are the approximate implementation methods of these functions? It would be nice to draw an architecture diagram or flow chart in advance 😁 Here we summarize a few things that we want the tool to do for us:

Data table structure through a database

  • Generate the corresponding entity class in reverse
  • Generate the corresponding controller in reverse order
  • Generate the corresponding service layer in reverse

Thinking about

  1. I want to likenest-cliorvue-cliThese are some excellent onescliSimilarly, a variety of commands can be generated by typing a single command on the command lineFile (Entity, Controller, Services)Because it’s very, very convenient.
  2. I want to take the data table structure specified by the user and convert it to the structure I need.
  3. User inputv type srcThe tool will be based on the data tabletypegenerateEntity class, controller, service layerAnd mount the generated folder tosrcDirectory.

start

First of all, let’s look at how to doThe code is run by command

  1. Let’s start by executing the following command to create a projectnpm init -y && mkdir bin && touch ./bin/code-gen && mkdir src && touch ./src/index.ts
  2. We will bepackage.jsonAdd an attribute tobinIts value is an object, of an objectkeyis“Instruction”The name of the objectvalueis“Command execution file”For example:

Two questions can be seen here:

  1. I specified multiple keys (V, code-gen, nest-code-generate). I want to be clear that these are just aliases. I can use as many names as I want.
  2. Code-gen seems to be a file with no suffix, neither.js nor.ts, yes this file does have no suffix
  1. We are inbin/code-genWrite something in it

If the computer is using vscode, code-gen is usually not prompted, we use the following figure to solve the problem

#! /usr/bin/env node

console.log('code-gen');
Copy the code

Let’s take a look at the code above:

  • The first line#! /usr/bin/env nodeThe interpreter used to specify the script file, which we specify herenode“And then we simply printed something.
  • So how do we execute this program? This is done on the command linevWill we see code-gen printed on the command line?
  • Obviously not, because we’re missing the last very, very important step, which is executionnpm linkThis command.
  • The effect of this command is to create a locally and quick operation, let you can be on the premise of not publish a package Can experience the function of the package, the information on this command, friends can take a look at the official documentation of NPM, or check the information online Is very simple, if the conflicts in the execution of this order, We can add one--forceSuffix, force update.
  • At this point we are executing on the command linevYou will see the command line print out the code-gen message.

How do I receive user input parameters on the command line?

  1. We’re going to use a large part herecliIt’s called a package calledcommander, the implementation ofnpm i commanderThe method of use is also very simple, we will simply use the knowledge here, because I found that the official website said not clear (anyway I can not understand).
  2. Let’s look at the following code:
  • The first paragraph:program.version().usage()It’s actually very simple when we typev -vWill print out this number,usage()Is to performv -hTime to prompt.
  • The second paragraph:program.argument().argument().action()This code accepts two arguments, one of which istable-nameOne is thedirAmong themdirIt’s optional, in the backactionThat’s the callback method, which accepts2The two parameters that we acceptargument.
#! /usr/bin/env node
const program = require('commander');

program
  .version(`nest-code-generate@The ${require(".. /package.json").version}`)
  .usage(`
      
        [dir]`
      ,table_name2...>);


program
  .argument('<table-name>'."Data table name")
  .argument('[dir]'.'Folder path')
  .action((tableName, dir) = > {
    console.log(tableName, dir);
   });

program.parse(process.argv);
Copy the code

Gets the structure of the data table

With commander we can easily interact with the user from the command line. What should we do if the user enters the v type SRC command? The first thing we need to do is to read the user’s type table. Here we also need to borrow a database connection tool called ali-rds-async, which can be installed by executing NPM I ali-rds-async.

Ali-rds-async use method:

  • We first insrcCreate one in the directoryclientFolder and create oneindex.ts
  • Connecting to the database (isn’t that easy)
    import AsyncAliRds from "ali-rds-async";
    
    export const db = new AsyncAliRds({
        host: 'localhost'.port: 3306.user: 'root'.password: 'password'.database: 'nest-code-generate'
    });
    Copy the code
  • Read the table structure: Here we can do this by executing an SQL, but here we need to modify the file to facilitate our testing and coding.
  1. newtsconfig.json;
  2. bin/code-genParser is ParserLib (package path)The introduction of

  1. src/index.ts: Here is the packaged entry file, exposedParser

  1. package.json: Add a few herescript

  • File modification done, we run firstnpm run serveornpm run buildPackage the file tolibFolder, and then executev typeInstructions to see what happens? We got it.typeTable structure.

Generate entity class

There is also a command line question and answer mode that is involved in generating entity classes. In this case, we will implement this feature, which allows users to create files selectively like vue-CLI.

  • Use the inquirer tool to implement command line question and answer mode, this tool is relatively simple to use, will be briefly described later

  • Let’s start with a piece of code

    import { prompt } from 'inquirer';
    import { findPath } from "./utils";
    
    export class Parser {
      tableName   : string; // Table name
      dir         : string; // Generate a pathtype ! : string;// Generate entity class or control layer or service layertargetPath ! : string;// Generate a path
    
      constructor(tableName: string, dir: string) {
        this.tableName = tableName;
        this.dir = dir;
        this.prompt();
      }
    
      // Initiate an inquiry
      async prompt() { 
        const { type } = await prompt([
          {
            name: 'type'.type: 'list'.message: 'What content is generated? : '.choices: [{name: 'Entity class '.value: 'entity' },
              { name: 'Tier (Entity class + Controller and service layer methods)'.value: 'tier' },
              { name: 'CURD (Entity class + simple add, Delete, change, check)'.value: 'curd' },
              { name: 'All (All generated: Entity class + Controller and service layer methods + simple add, delete, change and query)'.value: 'all'}}]]);this.type = type;
    
        // Get the build path
        const targetPath = findPath(this.dir);
        this.targetPath = targetPath;
    
        this.parseOption(); }}Copy the code

    Let’s look at what the prompt method does in this code:

    1. callinquirerthepromptMethod, which initiates a query on the command line, like this;

    So let’s look at the transfer topromptSo far we’ve only used the first parameter, which is an array, and each item in the array represents a problem, so we’ve only passed one problem, so let’s look at the problemkeyWhat do they all mean?

    • name: is the name of the field that this method will return to you later;
    • type: Indicates the type of the current problemlistinputOne is the list and one is the input.
    • message: This is an easy question to ask.
    • choices: This property is an array, onlytypeforlistIs available in each of the items in the arraynameThat’s what’s displayed to the user, and the other onevalueThat’s what the user returns to you when they select it.

    Let’s look at the actual application. What do we get?

    1. willtypeSave it, because that’s what the user is going to generate;
    2. Obtain the build path, this method has only one function, is to obtain the user’s final generation path, the default issrc;
    3. callparseOptionMethods;
  • The parseOption method lets see what this method does:

    async parseOption() {
      const typeMap: { [k in Options]: () = > any } = {
        'entity': () = > this.generateEntity(),
        'tier': () = > this.generateTier(),
        'curd': () = > this.generateCURD(),
        'all': () = > this.generateAll()
      };
    
      if (this.type && Reflect.has(typeMap, this.type)) {
        await typeMap[this.type]();
      } else {
        await typeMap.entity();
      }
      this.exit();
    }
    Copy the code
    • Create a PolicymapthroughtypeTo call the corresponding method, if notypeortypeNot inmap, the method that generates the instance is directly called;
    • callexit()Method exits.
  • GenerateEntity method

We’ll focus on the generateEntity method here, because we’ll only talk about generating the entity class, not the control or service layer. Look at the code:

// Generate entity classes separately
async generateEntity() {
  // Get all the table names
  const tableNames: string[] = this.tableName.split(",");
  await hasTableName(tableNames, async() = > {// Get table structure (source)
    const structure = await getTableStructure(tableNames);
    
    // Determine whether the instance has a base class
    const { collect, base_name } = baseEntity();
    
    // Convert the source structure to the desired structure
    const columnStructure = transformStructure(structure, collect);
    
    // Generate the entity class
    generateEntity(columnStructure, this.targetPath, base_name);
  });
}
Copy the code
  1. Get all table names, because table names are likely to be multiple and separated by commastableNames;
  2. callhasTableNameMethod to determine whether the table name was passed in, if not terminated directly;
  3. Perform the callback if there is a table name;

What does the callback method do?

  1. performgetTableStructureMethod to obtain the structure of all data tables;
  2. callbaseEntityMethod to determine whether the instance has a base class;
  3. calltransformStructureMethod to transform the source data into the desired structure;
  4. callgenearteEntityMethod to generate an entity class;

getTableStructuremethods

// Get the table structure
export const getTableStructure = async (tableNames: string[]) :Promise<RowMap> => {
  // @ts-ignore
  const structure: Promise<RowMap> = tableNames.reduce(async (map: Promise<RowMap>, name: string) = > {const newMap = (await map);
    try {
      newMap[name] = await db.query(`SHOW FULL FIELDS FROM ${name}`);
    } catch (error) {
      throw error;
    }
    return map;
  }, {});

  return structure;
}
Copy the code

This method is very simple. It is to obtain the structure of each table through the reduce method of array, which is the basic use of Reduce and promise, which will not be described here. The obtained structure is:

{ 
  type: [{Field: 't_binary'.Type: 'binary(10)'.Collation: null.Null: 'NO'.Key: ' '.Default: null.Extra: ' '.Privileges: 'select,insert,update,references'.Comment: ' '}, {... }].// If there are multiple table names, such as v type,sys_file, there will be one more
  sys_file: [{...}, {...}]}Copy the code

baseEntitymethods

export const baseEntity = (): { base_name: string, collect: string[] } => {
  let { base_name = ' ', collect = ' ' } = readYMLConfig('data_config') | | {};if(collect ! = =' '&& collect ! =null) {
    collect = collect.split(', ').map((field: string) = > field.trim()).filter((field: string) = >field ! = =' ');
  }

  return { base_name, collect: collect === ' ' ? [] : collect };
}
Copy the code

This method is used to obtain the data_config field in the code-gen.yml configuration file.

transformStructureMethod (code more, want to see the code partners can see the source code)

The function of this method is to convert the source data structure obtained by the getTableStructure method into the option data required by the @column method, for example: source data

{
  type: [{Field: 't_dec'.Type: 'a decimal (20, 8)'.Collation: null.Null: 'NO'.Key: ' '.Default: null.Extra: ' '.Privileges: 'select,insert,update,references'.Comment: ' '}}]Copy the code

Converted data

{
  type: [{type: 'decimal'.length: undefined.precision: 20.scale: 8.primaryGeneratedColumn: false.enum: undefined.name: 't_dec'.collation: undefined.nullable: undefined.default: undefined.comment: undefined.update: undefined.jsType: 'number'.isIndex: false}}]Copy the code

genearteEntityMethod (code more, want to see the code partners can see the source code)

The method generates files according to the data generated by transformStructure method.

The effect

Let’s take a look at the result. Suppose we have an empty SRC directory;

├ ─ ─ the SRCCopy the code

We also have a Type data table;

When we execute v type SRC /demo or v type demo, we automatically create a demo directory containing the Entity file;

├ ─ ─ the SRC │ ├ ─ ─ demo │ │ └ ─ ─ entities │ │ └ ─ ─ the entity. The tsCopy the code

Is it so cool? Hey hey 😁

conclusion

The tool itself is simple to implement, but it can help ease a lot of development burdens and solve a lot of development problems. In fact, a lot of things look very gorgeous and complicated on the surface, but all changes are the same. Data processing is very important. I hope you can make progress together and make progress every day!

contact

Making: github.com/Veloma-Time… NPM: www.npmjs.com/package/nes… Have not understood the place can also add my wechat: __veloma__