From 2bba0bb2592af129c6b4e766bb431fcef133ddbf Mon Sep 17 00:00:00 2001 From: Rasmus Syberg Date: Thu, 29 Apr 2021 14:49:48 +0200 Subject: [PATCH 1/2] chore: Add switches for more configs --- __tests__/index.test.ts | 6 +++++- src/column-map.ts | 6 +++++- src/index.ts | 20 +++++++++++++++++--- 3 files changed, 27 insertions(+), 5 deletions(-) diff --git a/__tests__/index.test.ts b/__tests__/index.test.ts index 88e8274..f2664ad 100644 --- a/__tests__/index.test.ts +++ b/__tests__/index.test.ts @@ -13,7 +13,8 @@ const agreements = sql` id varbinary(24) NOT NULL, billing_plan_id varbinary(24) NOT NULL, category varbinary(24) NOT NULL, - name varbinary(64) NOT NULL, + name varbinary(64) NOT NULL, + count tinyint NOT NULL, PRIMARY KEY (id) )` @@ -52,6 +53,9 @@ beforeAll(async () => { await query(conn, withJSON) }) +process.env.BINARY_AS_BUFFER = 'true' +process.env.TINYINT_AS_BOOLEAN = 'false' + describe('inferTable', () => { it('infers a table', async () => { const code = await inferTable(connectionString, 'agreements') diff --git a/src/column-map.ts b/src/column-map.ts index bde3662..b08bb87 100644 --- a/src/column-map.ts +++ b/src/column-map.ts @@ -5,10 +5,13 @@ import { Enums } from './mysql-client' interface MapColumnOptions { /** Treats binary fields as strings */ BinaryAsBuffer: boolean + /** Treats tinyint fields as booleans */ + TinyInAsBoolean: boolean } const options: MapColumnOptions = { BinaryAsBuffer: Boolean(process.env.BINARY_AS_BUFFER), + TinyInAsBoolean: Boolean(process.env.TINYINT_AS_BOOLEAN), } export function mapColumn(Table: TableNonTsType, enumTypes: Enums): Table { @@ -45,7 +48,6 @@ function findTsType(udtName: string): string | null { return 'string' case 'integer': case 'int': - case 'tinyint': case 'smallint': case 'mediumint': case 'bigint': @@ -69,6 +71,8 @@ function findTsType(udtName: string): string | null { case 'varbinary': case 'bit': return options.BinaryAsBuffer ? 'Buffer' : 'string' + case 'tinyint': + return options.TinyInAsBoolean ? 'boolean' : 'number' default: return null } diff --git a/src/index.ts b/src/index.ts index f0977eb..966841d 100755 --- a/src/index.ts +++ b/src/index.ts @@ -8,8 +8,10 @@ const cli = meow( $ mysql-schema-ts Options - --table, -t Table name - --prefix, -p Prefix to add to table names + --table, -t Table name + --prefix, -p Prefix to add to table names + --tinyIntAsBoolean -tb Treat TinyInt as Boolean + --binaryAsBuffer -bb Treat Binary as Buffer Examples $ mysql-schema-ts --prefix SQL @@ -25,18 +27,30 @@ const cli = meow( alias: 'p', default: '', }, + tinyIntAsBoolean: { + type: 'boolean', + alias: 'tb', + default: false, + }, + binaryAsBuffer: { + type: 'boolean', + alias: 'bb', + default: false, + }, }, } ) const db = cli.input[0] -const { table, prefix } = cli.flags +const { table, prefix, tinyIntAsBoolean, binaryAsBuffer } = cli.flags async function main(): Promise { if (!db) { cli.showHelp() } console.error('bool', cli.flags) + process.env.BINARY_AS_BUFFER = binaryAsBuffer.toString() + process.env.TINYINT_AS_BOOLEAN = tinyIntAsBoolean.toString() if (table) { return inferTable(db, table, prefix) } From 3a4663e54fec07f1bd4e43393c00155b1069edc8 Mon Sep 17 00:00:00 2001 From: Rasmus Syberg Date: Thu, 29 Apr 2021 17:19:07 +0200 Subject: [PATCH 2/2] chore: added new flags --tinyIntAsBoolean, -tb Treat TinyInt as Boolean --binaryAsBuffer, -bb Treat Binary as Buffer --nullAsUndefined, -nu Treat null as undefined --- __tests__/index.test.ts | 242 ++++++++++++++++++++++++++++++++++++++-- package-lock.json | 85 ++++++++++++++ package.json | 7 +- src/column-map.ts | 17 +-- src/config.ts | 5 + src/generator.ts | 7 +- src/index.ts | 28 +++-- src/types.ts | 9 ++ 8 files changed, 365 insertions(+), 35 deletions(-) create mode 100644 src/config.ts create mode 100644 src/types.ts diff --git a/__tests__/index.test.ts b/__tests__/index.test.ts index f2664ad..67fd2a8 100644 --- a/__tests__/index.test.ts +++ b/__tests__/index.test.ts @@ -4,29 +4,41 @@ import { inferTable, inferSchema } from '../src/table' import { SQL as sql } from 'sql-template-strings' import moment from 'moment' import pkg from '../src/pkg.json' +import config from '../src/config' const connectionString = 'mysql://root@localhost:33306/test' const conn = createConnection(connectionString) -const agreements = sql` - CREATE TABLE IF NOT EXISTS agreements ( +const agreements = sql` + CREATE TABLE agreements ( id varbinary(24) NOT NULL, billing_plan_id varbinary(24) NOT NULL, category varbinary(24) NOT NULL, name varbinary(64) NOT NULL, - count tinyint NOT NULL, + PRIMARY KEY (id) +)` + +const withNumbers = sql` + CREATE TABLE with_numbers ( + id int unsigned NOT NULL, + tiny tinyint NOT NULL, + small smallint NOT NULL, + medium mediumint NOT NULL, + big bigint NOT NULL, + flo float NOT NULL, + dou double NOT NULL, PRIMARY KEY (id) )` const withJSON = sql` - CREATE TABLE IF NOT EXISTS table_with_json ( + CREATE TABLE table_with_json ( id varbinary(24) NOT NULL, data json DEFAULT NULL, PRIMARY KEY (id) )` const requests = sql` - CREATE TABLE IF NOT EXISTS requests ( + CREATE TABLE requests ( id int(11) NOT NULL, name varchar(255) NOT NULL, url varchar(255) NOT NULL, @@ -35,7 +47,7 @@ const requests = sql` ` const complex = sql` - CREATE TABLE IF NOT EXISTS complex ( + CREATE TABLE complex ( id varbinary(255) NOT NULL, name varchar(255) NOT NULL, nullable varchar(255), @@ -47,14 +59,23 @@ const complex = sql` const time = moment().format('YYYY-MM-DD') beforeAll(async () => { + await query(conn, sql`DROP TABLE IF EXISTS agreements`) + await query(conn, sql`DROP TABLE IF EXISTS table_with_json`) + await query(conn, sql`DROP TABLE IF EXISTS requests`) + await query(conn, sql`DROP TABLE IF EXISTS complex`) + await query(conn, sql`DROP TABLE IF EXISTS with_numbers`) await query(conn, agreements) await query(conn, requests) await query(conn, complex) await query(conn, withJSON) + await query(conn, withNumbers) }) -process.env.BINARY_AS_BUFFER = 'true' -process.env.TINYINT_AS_BOOLEAN = 'false' +beforeEach(async () => { + config.binaryAsBuffer = false + config.tinyIntAsBoolean = false + config.nullAsUndefined = false +}) describe('inferTable', () => { it('infers a table', async () => { @@ -216,6 +237,179 @@ describe('inferTable', () => { " `) }) + + it('works with binaryAsBuffer = true', async () => { + config.binaryAsBuffer = true + const code = await inferTable(connectionString, 'agreements') + expect(code).toMatchInlineSnapshot(` + "/** + * AUTO-GENERATED FILE @ ${time} - DO NOT EDIT! + * + * This file was automatically generated by ${pkg.name} ${pkg.version} + */ + + /** + * Exposes all fields present in agreements as a typescript + * interface. + * This is especially useful for SELECT * FROM + */ + export interface Agreements { + id: Buffer + billing_plan_id: Buffer + category: Buffer + name: Buffer + } + + /** + * Exposes the same fields as Agreements, + * but makes every field containing a DEFAULT value optional. + * + * This is especially useful when generating inserts, as you + * should be able to ommit these fields if you'd like + */ + export interface AgreementsWithDefaults { + id: Buffer + billing_plan_id: Buffer + category: Buffer + name: Buffer + } + " + `) + }) + + it('works with tinyIntAsBoolean = false', async () => { + const code = await inferTable(connectionString, 'with_numbers') + expect(code).toMatchInlineSnapshot(` + "/** + * AUTO-GENERATED FILE @ ${time} - DO NOT EDIT! + * + * This file was automatically generated by ${pkg.name} ${pkg.version} + */ + + /** + * Exposes all fields present in with_numbers as a typescript + * interface. + * This is especially useful for SELECT * FROM + */ + export interface WithNumbers { + id: number + tiny: number + small: number + medium: number + big: number + flo: number + dou: number + } + + /** + * Exposes the same fields as WithNumbers, + * but makes every field containing a DEFAULT value optional. + * + * This is especially useful when generating inserts, as you + * should be able to ommit these fields if you'd like + */ + export interface WithNumbersWithDefaults { + id: number + tiny: number + small: number + medium: number + big: number + flo: number + dou: number + } + " + `) + }) + + it('works with tinyIntAsBoolean = true', async () => { + config.tinyIntAsBoolean = true + const code = await inferTable(connectionString, 'with_numbers') + expect(code).toMatchInlineSnapshot(` + "/** + * AUTO-GENERATED FILE @ ${time} - DO NOT EDIT! + * + * This file was automatically generated by ${pkg.name} ${pkg.version} + */ + + /** + * Exposes all fields present in with_numbers as a typescript + * interface. + * This is especially useful for SELECT * FROM + */ + export interface WithNumbers { + id: number + tiny: boolean + small: number + medium: number + big: number + flo: number + dou: number + } + + /** + * Exposes the same fields as WithNumbers, + * but makes every field containing a DEFAULT value optional. + * + * This is especially useful when generating inserts, as you + * should be able to ommit these fields if you'd like + */ + export interface WithNumbersWithDefaults { + id: number + tiny: boolean + small: number + medium: number + big: number + flo: number + dou: number + } + " + `) + }) + it('works with nullAsUndefined = true', async () => { + config.nullAsUndefined = true + const code = await inferTable(connectionString, 'complex') + expect(code).toMatchInlineSnapshot(` + "/** + * AUTO-GENERATED FILE @ ${time} - DO NOT EDIT! + * + * This file was automatically generated by ${pkg.name} ${pkg.version} + */ + + /** + * Exposes all fields present in complex as a typescript + * interface. + * This is especially useful for SELECT * FROM + */ + export interface Complex { + id: string + name: string + nullable?: string + created_at?: Date + created_on: Date + /** This is an awesome field */ + documented_field?: string + } + + /** + * Exposes the same fields as Complex, + * but makes every field containing a DEFAULT value optional. + * + * This is especially useful when generating inserts, as you + * should be able to ommit these fields if you'd like + */ + export interface ComplexWithDefaults { + id: string + name: string + nullable?: string | null + /** Defaults to: CURRENT_TIMESTAMP */ + created_at?: Date | null + created_on: Date + /** This is an awesome field */ + documented_field?: string | null + } + " + `) + }) }) describe('inferSchema', () => { @@ -339,6 +533,38 @@ describe('inferSchema', () => { id: string data?: JSONValue | null } + + /** + * Exposes all fields present in with_numbers as a typescript + * interface. + * This is especially useful for SELECT * FROM + */ + export interface WithNumbers { + id: number + tiny: number + small: number + medium: number + big: number + flo: number + dou: number + } + + /** + * Exposes the same fields as WithNumbers, + * but makes every field containing a DEFAULT value optional. + * + * This is especially useful when generating inserts, as you + * should be able to ommit these fields if you'd like + */ + export interface WithNumbersWithDefaults { + id: number + tiny: number + small: number + medium: number + big: number + flo: number + dou: number + } " `) }) diff --git a/package-lock.json b/package-lock.json index 9665026..c487119 100644 --- a/package-lock.json +++ b/package-lock.json @@ -366,6 +366,21 @@ "minimist": "^1.2.0" } }, + "@hapi/hoek": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.2.0.tgz", + "integrity": "sha512-sqKVVVOe5ivCaXDWivIJYVSaEgdQK9ul7a4Kity5Iw7u9+wBAPbX1RMSnLLmp7O4Vzj0WOWwMAJsTL00xwaNug==", + "dev": true + }, + "@hapi/topo": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-5.0.0.tgz", + "integrity": "sha512-tFJlT47db0kMqVm3H4nQYgn6Pwg10GTZHb1pwmSiv1K4ks6drQOtfEF5ZnPjkvC+y4/bUPHK+bc87QvLcL+WMw==", + "dev": true, + "requires": { + "@hapi/hoek": "^9.0.0" + } + }, "@istanbuljs/load-nyc-config": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", @@ -913,6 +928,27 @@ } } }, + "@sideway/address": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.1.tgz", + "integrity": "sha512-+I5aaQr3m0OAmMr7RQ3fR9zx55sejEYR2BFJaxL+zT3VM2611X0SHvPWIbAUBZVTn/YzYKbV8gJ2oT/QELknfQ==", + "dev": true, + "requires": { + "@hapi/hoek": "^9.0.0" + } + }, + "@sideway/formula": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.0.tgz", + "integrity": "sha512-vHe7wZ4NOXVfkoRb8T5otiENVlT7a3IAiw7H5M2+GO+9CDgcVUUsX1zalAztCmwyOr2RUTGJdgB+ZvSVqmdHmg==", + "dev": true + }, + "@sideway/pinpoint": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz", + "integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==", + "dev": true + }, "@sindresorhus/is": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-2.1.1.tgz", @@ -1508,6 +1544,15 @@ "integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==", "dev": true }, + "axios": { + "version": "0.21.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.1.tgz", + "integrity": "sha512-dKQiRHxGD9PPRIUNIWvZhPTPpl1rf/OxTYKsqKUDjBwYylTvV7SjSHJb9ratfyzM6wCdLCOYLzs73qpg5c4iGA==", + "dev": true, + "requires": { + "follow-redirects": "^1.10.0" + } + }, "babel-jest": { "version": "26.6.3", "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-26.6.3.tgz", @@ -3159,6 +3204,12 @@ "integrity": "sha512-r5wGx7YeOwNWNlCA0wQ86zKyDLMQr+/RB8xy74M4hTphfmjlijTSSXGuH8rnvKZnfT9i+75zmd8jcKdMR4O6jA==", "dev": true }, + "follow-redirects": { + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.0.tgz", + "integrity": "sha512-0vRwd7RKQBTt+mgu87mtYeofLFZpTas2S9zY+jIeuLJMNvudIgF52nr19q40HOwH5RrhWIPuj9puybzSJiRrVg==", + "dev": true + }, "for-in": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", @@ -5612,6 +5663,19 @@ } } }, + "joi": { + "version": "17.4.0", + "resolved": "https://registry.npmjs.org/joi/-/joi-17.4.0.tgz", + "integrity": "sha512-F4WiW2xaV6wc1jxete70Rw4V/VuMd6IN+a5ilZsxG4uYtUXWu2kq9W5P2dz30e7Gmw8RCbY/u/uk+dMPma9tAg==", + "dev": true, + "requires": { + "@hapi/hoek": "^9.0.0", + "@hapi/topo": "^5.0.0", + "@sideway/address": "^4.1.0", + "@sideway/formula": "^3.0.0", + "@sideway/pinpoint": "^2.0.0" + } + }, "js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -9665,6 +9729,27 @@ "xml-name-validator": "^3.0.0" } }, + "wait-on": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/wait-on/-/wait-on-5.3.0.tgz", + "integrity": "sha512-DwrHrnTK+/0QFaB9a8Ol5Lna3k7WvUR4jzSKmz0YaPBpuN2sACyiPVKVfj6ejnjcajAcvn3wlbTyMIn9AZouOg==", + "dev": true, + "requires": { + "axios": "^0.21.1", + "joi": "^17.3.0", + "lodash": "^4.17.21", + "minimist": "^1.2.5", + "rxjs": "^6.6.3" + }, + "dependencies": { + "lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true + } + } + }, "walker": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.7.tgz", diff --git a/package.json b/package.json index 7a4e1dc..c4c3d8c 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "author": "Netto Farah ", "license": "MIT", "scripts": { - "test": "docker-compose up -d && jest --runInBand --forceExit", + "test": "docker-compose up -d && wait-on tcp:localhost:33306 && jest --runInBand --forceExit", "clean": "rm -rf dist && mkdir dist", "compile": "tsc -p ./tsconfig.build.json", "build": "npm run clean && npm run compile ", @@ -68,13 +68,14 @@ "@typescript-eslint/parser": "^4.9.0", "eslint": "^6.7.2", "eslint-config-prettier": "^7.0.0", - "prettier": "^2.2.1", "husky": "^4.3.5", "jest": "^26.6.3", "lint-staged": "^10.5.3", "np": "^7.5.0", + "prettier": "^2.2.1", "ts-jest": "^26.4.4", "ts-node": "^9.1.0", - "typescript": "^4.1.2" + "typescript": "^4.1.2", + "wait-on": "^5.3.0" } } diff --git a/src/column-map.ts b/src/column-map.ts index b08bb87..b6af672 100644 --- a/src/column-map.ts +++ b/src/column-map.ts @@ -1,18 +1,7 @@ import { Table, TableNonTsType } from '@/../src/generator' import { mapValues } from 'lodash' import { Enums } from './mysql-client' - -interface MapColumnOptions { - /** Treats binary fields as strings */ - BinaryAsBuffer: boolean - /** Treats tinyint fields as booleans */ - TinyInAsBoolean: boolean -} - -const options: MapColumnOptions = { - BinaryAsBuffer: Boolean(process.env.BINARY_AS_BUFFER), - TinyInAsBoolean: Boolean(process.env.TINYINT_AS_BOOLEAN), -} +import config from './config' export function mapColumn(Table: TableNonTsType, enumTypes: Enums): Table { return mapValues(Table, (column) => { @@ -70,9 +59,9 @@ function findTsType(udtName: string): string | null { case 'binary': case 'varbinary': case 'bit': - return options.BinaryAsBuffer ? 'Buffer' : 'string' + return config.binaryAsBuffer ? 'Buffer' : 'string' case 'tinyint': - return options.TinyInAsBoolean ? 'boolean' : 'number' + return config.tinyIntAsBoolean ? 'boolean' : 'number' default: return null } diff --git a/src/config.ts b/src/config.ts new file mode 100644 index 0000000..9345004 --- /dev/null +++ b/src/config.ts @@ -0,0 +1,5 @@ +export default { + tinyIntAsBoolean: false, + binaryAsBuffer: false, + nullAsUndefined: false, +} diff --git a/src/generator.ts b/src/generator.ts index ada6b40..c3bf60d 100644 --- a/src/generator.ts +++ b/src/generator.ts @@ -1,4 +1,5 @@ import camelcase from 'camelcase' +import config from './config' export interface Column { udtName: string @@ -36,7 +37,7 @@ export function tableToTS(name: string, prefix: string, table: Table): string { Object.keys(table).map((column) => { const type = table[column].tsType const nullable = table[column].nullable - const nullablestr = nullable ? '| null' : '' + const nullablestr = !nullable || (!withDefaults && config.nullAsUndefined) ? '' : '| null' const hasDefault = table[column].hasDefault const defaultValue = table[column].defaultValue ?? '' const defaultComment = withDefaults && hasDefault ? `Defaults to: ${defaultValue}` : '' @@ -45,7 +46,9 @@ export function tableToTS(name: string, prefix: string, table: Table): string { const isOptional = withDefaults ? nullable || hasDefault : nullable - return `${tsComment}${normalize(column)}${withDefaults && isOptional ? '?' : ''}: ${type}${nullablestr}\n` + return `${tsComment}${normalize(column)}${ + (withDefaults && isOptional) || (isOptional && config.nullAsUndefined) ? '?' : '' + }: ${type}${nullablestr}\n` }) const tableName = (prefix || '') + camelize(normalize(name)) diff --git a/src/index.ts b/src/index.ts index 966841d..dd0bb96 100755 --- a/src/index.ts +++ b/src/index.ts @@ -1,26 +1,29 @@ #!/usr/bin/env node import { inferSchema, inferTable } from './table' import meow from 'meow' +import config from './config' const cli = meow( ` Usage - $ mysql-schema-ts + $ mysql8-schema-ts Options --table, -t Table name --prefix, -p Prefix to add to table names - --tinyIntAsBoolean -tb Treat TinyInt as Boolean - --binaryAsBuffer -bb Treat Binary as Buffer + --tinyIntAsBoolean, -tb Treat TinyInt as Boolean + --binaryAsBuffer, -bb Treat Binary as Buffer + --nullAsUndefined, -nu Treat null as undefined Examples - $ mysql-schema-ts --prefix SQL + $ mysql8-schema-ts --prefix SQL `, { flags: { table: { type: 'string', alias: 't', + default: '', }, prefix: { type: 'string', @@ -37,21 +40,30 @@ const cli = meow( alias: 'bb', default: false, }, + nullAsUndefined: { + type: 'boolean', + alias: 'nu', + default: false, + }, }, } ) const db = cli.input[0] -const { table, prefix, tinyIntAsBoolean, binaryAsBuffer } = cli.flags +const { table, prefix, tinyIntAsBoolean, binaryAsBuffer, nullAsUndefined } = cli.flags async function main(): Promise { if (!db) { cli.showHelp() } console.error('bool', cli.flags) - process.env.BINARY_AS_BUFFER = binaryAsBuffer.toString() - process.env.TINYINT_AS_BOOLEAN = tinyIntAsBoolean.toString() - if (table) { + + // Set the config from flags + config.binaryAsBuffer = binaryAsBuffer + config.tinyIntAsBoolean = tinyIntAsBoolean + config.nullAsUndefined = nullAsUndefined + + if (cli.flags.table) { return inferTable(db, table, prefix) } diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..7281374 --- /dev/null +++ b/src/types.ts @@ -0,0 +1,9 @@ +export type Options = { + table: string + prefix: string +} & MapColumnOptions + +export type MapColumnOptions = { + tinyIntAsBoolean: boolean + binaryAsBuffer: boolean +}