Skip to content

Commit d939d45

Browse files
committed
feat: Add graphql schema generator
1 parent f5e7b9e commit d939d45

File tree

9 files changed

+268
-60
lines changed

9 files changed

+268
-60
lines changed

package.json

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -66,58 +66,58 @@
6666
]
6767
},
6868
"dependencies": {
69-
"acorn": "^5.4.1",
69+
"acorn": "^5.5.3",
7070
"app-root-path": "^2.0.1",
7171
"babel-polyfill": "^6.23.0",
72-
"cloud-config-client": "^1.0.0",
72+
"cloud-config-client": "^1.1.0",
7373
"connect-redis": "^3.3.3",
7474
"constitute": "^1.6.2",
7575
"continuation-local-storage": "^3.2.1",
7676
"doctrine": "^2.1.0",
77-
"express": "^4.15.3",
77+
"express": "^4.16.3",
7878
"express-session": "^1.15.3",
7979
"glob": "^7.1.2",
8080
"ioredis": "^3.1.1",
8181
"joi": "^12.0.0",
82-
"js-yaml": "^3.8.4",
82+
"js-yaml": "^3.11.0",
8383
"jwt-simple": "^0.5.1",
8484
"later": "^1.2.0",
8585
"lodash": "^4.17.5",
8686
"mkdirp": "^0.5.1",
87-
"moment": "^2.20.1",
87+
"moment": "^2.22.0",
8888
"moment-timezone": "^0.5.13",
8989
"morgan": "^1.8.2",
9090
"mysql": "^2.13.0",
9191
"node-mocks-http": "^1.6.6",
9292
"on-headers": "^1.0.1",
93-
"pug": "^2.0.0-beta6",
94-
"request": "^2.81.0",
93+
"pug": "^2.0.3",
94+
"request": "^2.85.0",
9595
"request-promise-native": "^1.0.4",
9696
"sequelize": "^3.31.0",
97-
"swagger-ui-dist": "^3.10.0",
98-
"winston": "^2.3.1",
97+
"swagger-ui-dist": "^3.13.3",
98+
"winston": "^2.4.1",
9999
"yargs": "^11.0.0"
100100
},
101101
"devDependencies": {
102102
"ava": "^0.25.0",
103103
"babel-cli": "^6.24.1",
104104
"babel-core": "^6.25.0",
105-
"babel-eslint": "^8.2.1",
105+
"babel-eslint": "^8.2.2",
106106
"babel-plugin-transform-builtin-extend": "^1.1.2",
107107
"babel-plugin-transform-decorators-legacy": "^1.3.4",
108108
"babel-plugin-transform-object-rest-spread": "^6.26.0",
109109
"babel-preset-env": "^1.6.1",
110-
"cross-env": "^5.1.3",
111-
"eslint": "^4.17.0",
110+
"cross-env": "^5.1.4",
111+
"eslint": "^4.19.1",
112112
"eslint-config-airbnb": "^16.1.0",
113-
"eslint-plugin-import": "^2.2.0",
113+
"eslint-plugin-import": "^2.10.0",
114114
"eslint-plugin-jsx-a11y": "^6.0.2",
115-
"eslint-plugin-react": "^7.6.1",
115+
"eslint-plugin-react": "^7.7.0",
116116
"is-type-of": "^1.0.0",
117-
"nock": "^9.1.6",
118-
"nyc": "^11.4.1",
117+
"nock": "^9.2.3",
118+
"nyc": "^11.6.0",
119119
"pre-commit": "^1.2.2",
120-
"semantic-release": "^12.4.1",
121-
"travis-deploy-once": "^4.3.4"
120+
"semantic-release": "^15.1.5",
121+
"travis-deploy-once": "^4.4.1"
122122
}
123123
}

src/commands/index.js

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import Command from './interface';
2-
import { MakeEntityCommand, MakeDbViewCommand } from './make_entity';
2+
import { MakeEntity, MakeDbView, MakeGraphql } from './make_entity';
33

44
export default Command;
55

66
export {
7-
MakeEntityCommand,
8-
MakeDbViewCommand
7+
MakeEntity,
8+
MakeDbView,
9+
MakeGraphql
910
};

src/commands/make_entity.js

Lines changed: 202 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import DI from '../di';
77
import Entities from '../entities';
88

99

10-
export class MakeDbViewCommand extends Command {
10+
export class MakeDbView extends Command {
1111
static getName() {
1212
return 'make:dbview';
1313
}
@@ -68,7 +68,7 @@ CREATE ALGORITHM=UNDEFINED SQL SECURITY DEFINER VIEW view_${tableName}
6868
}
6969
}
7070

71-
export class MakeEntityCommand extends Command {
71+
export class MakeEntity extends Command {
7272
static getName() {
7373
return 'make:entity';
7474
}
@@ -175,7 +175,6 @@ export class MakeEntityCommand extends Command {
175175

176176
static typeAdditional(_type, sequlizeType, rawColumn) {
177177
const type = _type.toLowerCase();
178-
// console.log(_type, sequlizeType)
179178
let finalType = sequlizeType;
180179

181180
if (type.match(/unsigned/)) {
@@ -253,17 +252,17 @@ export class MakeEntityCommand extends Command {
253252
});
254253
Object.values(rawColumns).forEach((rawColumn) => {
255254
const columnName = rawColumn.Field;
256-
columns[columnName].type = MakeEntityCommand.typeAdditional(
255+
columns[columnName].type = MakeEntity.typeAdditional(
257256
columns[columnName].type,
258-
MakeEntityCommand.typeMapping(columns[columnName].type),
257+
MakeEntity.typeMapping(columns[columnName].type),
259258
rawColumn
260259
);
261260
columns[columnName].unique = rawColumn.Key === 'UNI';
262261
columns[columnName].comment = rawColumn.Comment;
263262
columns[columnName].autoIncrement = rawColumn.Extra.startsWith('auto_increment') === true;
264263
});
265264

266-
const indexes = await MakeEntityCommand.getIndexes(table, sequelize);
265+
const indexes = await MakeEntity.getIndexes(table, sequelize);
267266
const entityFile = `${path}/${table}.js`;
268267
const schemaFile = `${schemaPath}/${table}.js`;
269268
try {
@@ -289,7 +288,203 @@ export class MakeEntityCommand extends Command {
289288
};
290289

291290
//Skip sequelize migrate table
292-
await Promise.all(Object.values(tables).filter(t => t !== 'sequelizemeta').map(tableHandler));
291+
await Promise.all(Object.values(tables).filter(t => !['sequelizemeta', 'tramp_migrations'].includes(t)).map(tableHandler));
292+
logger.info('All DB schemas generated');
293+
}
294+
}
295+
296+
export class MakeGraphql extends Command {
297+
static getName() {
298+
return 'make:graphql';
299+
}
300+
301+
static getDescription() {
302+
return 'Generate graphql schema';
303+
}
304+
305+
static getSpec() {
306+
return {
307+
dir: {
308+
required: false,
309+
description: 'Where entity files to be generated'
310+
},
311+
prefix: {
312+
required: false,
313+
description: 'Table prefix'
314+
},
315+
mapping: {
316+
required: false,
317+
description: 'Mapping file path'
318+
}
319+
};
320+
}
321+
322+
static typeMapping(_type) {
323+
const type = _type.toLowerCase();
324+
if (type === 'tinyint(1)' || type === 'boolean' || type === 'bit(1)') {
325+
return 'Boolean';
326+
}
327+
328+
if (type.match(/^(smallint|mediumint|tinyint|int)/)) {
329+
return 'Int';
330+
}
331+
332+
if (type.startsWith('bigint')) {
333+
return 'Int';
334+
}
335+
336+
if (type.startsWith('enum')) {
337+
return 'Enum';
338+
}
339+
340+
if (type.match(/^string|varchar|varying|nvarchar/)) {
341+
return 'String';
342+
}
343+
344+
if (type.startsWith('char')) {
345+
return 'String';
346+
}
347+
348+
if (type.match(/text|ntext$/)) {
349+
return 'String';
350+
}
351+
352+
if (type.startsWith('year')) {
353+
return 'Int';
354+
}
355+
356+
if (type.startsWith('datetime')) {
357+
return 'String';
358+
}
359+
360+
if (type.startsWith('date')) {
361+
return 'String';
362+
}
363+
364+
if (type.startsWith('time')) {
365+
return 'String';
366+
}
367+
368+
if (type.match(/^(float8|double precision)/)) {
369+
return 'Float';
370+
}
371+
372+
if (type.match(/^(float|float4)/)) {
373+
return 'Float';
374+
}
375+
376+
if (type.startsWith('decimal')) {
377+
return 'Float';
378+
}
379+
380+
if (type.match(/^uuid|uniqueidentifier/)) {
381+
return 'String';
382+
}
383+
384+
if (type.startsWith('jsonb')) {
385+
return 'JSON';
386+
}
387+
if (type.startsWith('json')) {
388+
return 'JSON';
389+
}
390+
391+
if (type.startsWith('geometry')) {
392+
return 'String';
393+
}
394+
395+
return type;
396+
}
397+
398+
getEnums(_type, columnName, tableName) {
399+
const type = _type.toLowerCase();
400+
const values = type.slice(5, -1).split(',').map(v => v.slice(1, -1));
401+
return {
402+
name: `ENUM_${tableName}_${columnName}`,
403+
values
404+
};
405+
}
406+
407+
async run() {
408+
const config = DI.get('config').get();
409+
const logger = DI.get('logger');
410+
const sequelize = new Sequelize(
411+
config.db.database,
412+
null,
413+
null,
414+
Object.assign({}, config.sequelize, config.db, { logging: logger.getInstance().verbose })
415+
);
416+
const query = sequelize.getQueryInterface();
417+
418+
let tables = await query.showAllTables();
419+
const views = await sequelize.query(`SHOW FULL TABLES IN ${config.db.database} WHERE TABLE_TYPE LIKE 'VIEW'`, {
420+
type: sequelize.QueryTypes.SELECT,
421+
raw: true
422+
});
423+
if (views) {
424+
const viewNames = views.map(v => Object.values(v)[0]);
425+
tables = tables.filter(t => !viewNames.includes(t));
426+
}
427+
const {
428+
dir, prefix, mapping
429+
} = this.getArgv();
430+
if (prefix) {
431+
tables = tables.filter(t => t.startsWith(prefix));
432+
}
433+
434+
const path = dir ? `${process.cwd()}/${dir}` : `${process.cwd()}/src/graphql`;
435+
const schemaPath = `${path}/entities`;
436+
const schemaTemplate = fs.readFileSync(`${__dirname}/../../template/graphql.ejs`, 'utf8');
437+
const mappingFile = mapping ? `${process.cwd()}/${mapping}` : `${process.cwd()}/src/graphql/mapping.json`;
438+
const mappingContent = JSON.parse(fs.readFileSync(mappingFile, 'utf8'));
439+
const getMappedTableName = tableName =>
440+
(mappingContent[tableName] ? mappingContent[tableName] : tableName);
441+
mkdirp.sync(path);
442+
mkdirp.sync(schemaPath);
443+
444+
logger.info('Start generate GraphQL schemas to dir %s', path);
445+
446+
const tableHandler = async (tableName) => {
447+
const enums = [];
448+
const columns = await query.describeTable(tableName);
449+
const rawColumns = await sequelize.query(`SHOW FULL COLUMNS FROM ${tableName}`, {
450+
type: sequelize.QueryTypes.SELECT,
451+
raw: true
452+
});
453+
const mappedTableName = getMappedTableName(tableName);
454+
Object.values(rawColumns).forEach((rawColumn) => {
455+
const columnName = rawColumn.Field;
456+
let type = MakeGraphql.typeMapping(columns[columnName].type, mappedTableName);
457+
if (type === 'Enum') {
458+
const enumObj = this.getEnums(columns[columnName].type, columnName, mappedTableName);
459+
type = enumObj.name;
460+
enums.push(enumObj);
461+
}
462+
463+
columns[columnName].type = type;
464+
columns[columnName].unique = rawColumn.Key === 'UNI';
465+
columns[columnName].comment = rawColumn.Comment;
466+
columns[columnName].autoIncrement = rawColumn.Extra.startsWith('auto_increment') === true;
467+
});
468+
469+
const indexes = await MakeEntity.getIndexes(tableName, sequelize);
470+
const schemaFile = `${schemaPath}/${tableName}.graphqls`;
471+
try {
472+
fs.accessSync(schemaFile);
473+
logger.info('Graphql schema file %s generate override, already exists by %s', tableName, schemaFile);
474+
} catch (e) {
475+
logger.info('Graphql schema file %s generated as %s', tableName, schemaFile);
476+
}
477+
fs.writeFileSync(schemaFile, _.template(schemaTemplate)({
478+
tableName,
479+
mappedTableName,
480+
columns,
481+
indexes,
482+
enums
483+
}));
484+
};
485+
486+
//Skip sequelize migrate table
487+
await Promise.all(Object.values(tables).filter(t => !['sequelizemeta', 'tramp_migrations'].includes(t)).map(tableHandler));
293488
logger.info('All DB schemas generated');
294489
}
295490
}

src/di.js

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
import constitute from 'constitute';
2-
import {
3-
RuntimeException
4-
} from './exceptions';
2+
import { RuntimeException } from './exceptions';
53
import { ServiceProvider } from './services/providers';
64

75
let container = new constitute.Container();

src/engine.js

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,7 @@ import moment from 'moment-timezone';
88
import DI from './di';
99
import * as ServiceProviders from './services/providers';
1010
import * as MiddlewareProviders from './middlewares/providers';
11-
import {
12-
StandardException, RuntimeException
13-
} from './exceptions';
11+
import { StandardException, RuntimeException } from './exceptions';
1412

1513
moment.tz.setDefault(process.env.TZ ? process.env.TZ : 'Asia/Shanghai');
1614

src/middlewares/auth.js

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
import { Dependencies } from 'constitute';
22
import wrapper from '../utils/wrapper';
3-
import {
4-
UnauthorizedException
5-
} from '../exceptions';
3+
import { UnauthorizedException } from '../exceptions';
64
import Config from '../services/config';
75
import Now from '../services/now';
86
import JsonWebToken from '../services/jwt_token';

src/services/logger.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { Dependencies } from 'constitute';
22
import moment from 'moment-timezone';
33
import winston from 'winston';
4+
import { inspect } from 'util';
45
import Env from './env';
56
import Config from './config';
67
import Namespace from './namespace';
@@ -138,4 +139,8 @@ export default class Logger extends ServiceInterface {
138139
error(...args) {
139140
return this.getInstance().error(...this.populateTraceId(args));
140141
}
142+
143+
dump(obj) {
144+
return this.debug(inspect(obj, { depth: null, colors: true }));
145+
}
141146
}

0 commit comments

Comments
 (0)