From d4aebb7787ec21f066b959ad0dbb48dbbe0e39a4 Mon Sep 17 00:00:00 2001 From: Marshall Cottrell Date: Fri, 25 Jan 2019 14:25:29 -0600 Subject: [PATCH] Add option for consistently sorting generated output --- bin/schemats.ts | 5 +- src/options.ts | 22 ++++-- src/schemaPostgres.ts | 7 +- src/typescript.ts | 33 +++++---- test/integration/cli.test.ts | 4 +- test/testUtility.ts | 17 +++-- test/unit/schemaPostgres.test.ts | 2 +- test/unit/typescript.test.ts | 120 ++++++++++++------------------- 8 files changed, 95 insertions(+), 115 deletions(-) diff --git a/bin/schemats.ts b/bin/schemats.ts index d46ae74..e039a7b 100755 --- a/bin/schemats.ts +++ b/bin/schemats.ts @@ -7,13 +7,13 @@ import * as yargs from 'yargs' import * as fs from 'fs' import { typescriptOfSchema, getDatabase } from '../src/index' -import Options from '../src/options' interface SchematsConfig { conn: string, table: string[] | string, schema: string, output: string, + order: boolean, camelCase: boolean, noHeader: boolean, } @@ -41,6 +41,7 @@ let argv: SchematsConfig = yargs .alias('C', 'camelCase') .describe('C', 'Camel-case columns') .describe('noHeader', 'Do not write header') + .describe('order', 'Sort type and interface properties') .demand('o') .nargs('o', 1) .alias('o', 'output') @@ -61,7 +62,7 @@ let argv: SchematsConfig = yargs } let formattedOutput = await typescriptOfSchema( - argv.conn, argv.table, argv.schema, { camelCase: argv.camelCase, writeHeader: !argv.noHeader }) + argv.conn, argv.table, argv.schema, { camelCase: argv.camelCase, order: argv.order, writeHeader: !argv.noHeader }) fs.writeFileSync(argv.output, formattedOutput) } catch (e) { diff --git a/src/options.ts b/src/options.ts index 139aeaa..47c28c2 100644 --- a/src/options.ts +++ b/src/options.ts @@ -1,22 +1,32 @@ -import { camelCase, upperFirst } from 'lodash' +import { camelCase, upperFirst, sortBy, keys } from 'lodash' -const DEFAULT_OPTIONS: OptionValues = { - writeHeader: true, - camelCase: false +const DEFAULT_OPTIONS: Required = { + camelCase: false, + order: false, + writeHeader: true } export type OptionValues = { camelCase?: boolean + order?: boolean writeHeader?: boolean // write schemats description header } export default class Options { - public options: OptionValues + public options: Required - constructor (options: OptionValues = {}) { + constructor (options?: OptionValues) { this.options = {...DEFAULT_OPTIONS, ...options} } + getKeys (obj: any): string[] { + return this.getMaybeSorted(keys(obj)) + } + + getMaybeSorted (arr: string[]): string[] { + return this.options.order ? sortBy(arr) : arr + } + transformTypeName (typename: string) { return this.options.camelCase ? upperFirst(camelCase(typename)) : typename } diff --git a/src/schemaPostgres.ts b/src/schemaPostgres.ts index 2acceb9..d4a86bc 100644 --- a/src/schemaPostgres.ts +++ b/src/schemaPostgres.ts @@ -1,6 +1,5 @@ import * as PgPromise from 'pg-promise' -import { mapValues } from 'lodash' -import { keys } from 'lodash' +import { mapValues, keys } from 'lodash' import Options from './options' import { TableDefinition, Database } from './schemaInterfaces' @@ -67,7 +66,7 @@ export class PostgresDatabase implements Database { return column case '_varchar': case '_text': - case '_citext': + case '_citext': case '_uuid': case '_bytea': column.tsType = 'Array' @@ -81,7 +80,7 @@ export class PostgresDatabase implements Database { return column default: if (customTypes.indexOf(column.udtName) !== -1) { - column.tsType = options.transformTypeName(column.udtName) + column.tsType = `customTypes.${options.transformTypeName(column.udtName)}` return column } else { console.log(`Type [${column.udtName} has been mapped to [any] because no specific type has been found.`) diff --git a/src/typescript.ts b/src/typescript.ts index 0add9e0..f4289cb 100644 --- a/src/typescript.ts +++ b/src/typescript.ts @@ -3,8 +3,6 @@ * Created by xiamx on 2016-08-10. */ -import * as _ from 'lodash' - import { TableDefinition } from './schemaInterfaces' import Options from './options' @@ -27,42 +25,43 @@ function normalizeName (name: string, options: Options): string { export function generateTableInterface (tableNameRaw: string, tableDefinition: TableDefinition, options: Options) { const tableName = options.transformTypeName(tableNameRaw) - let members = '' - Object.keys(tableDefinition).map(c => options.transformColumnName(c)).forEach((columnName) => { - members += `${columnName}: ${tableName}Fields.${normalizeName(columnName, options)};\n` + const members = options.getKeys(tableDefinition).map((columnNameRaw) => { + const columnName = options.transformColumnName(columnNameRaw) + return `${columnName}: ${tableName}Fields.${normalizeName(columnName, options)};` }) return ` export interface ${normalizeName(tableName, options)} { - ${members} + ${members.join('\n')} } ` } export function generateEnumType (enumObject: any, options: Options) { - let enumString = '' - for (let enumNameRaw in enumObject) { + const enumNamespace = options.getKeys(enumObject).map((enumNameRaw) => { const enumName = options.transformTypeName(enumNameRaw) - enumString += `export type ${enumName} = ` - enumString += enumObject[enumNameRaw].map((v: string) => `'${v}'`).join(' | ') - enumString += ';\n' - } - return enumString + return `export type ${enumName} = '${options.getMaybeSorted(enumObject[enumNameRaw]).join(`' | '`)}';` + }) + + return ` + export namespace customTypes { + ${enumNamespace.join('\n')} + } + ` } export function generateTableTypes (tableNameRaw: string, tableDefinition: TableDefinition, options: Options) { const tableName = options.transformTypeName(tableNameRaw) - let fields = '' - Object.keys(tableDefinition).forEach((columnNameRaw) => { + const tableNamespace = options.getKeys(tableDefinition).map((columnNameRaw) => { let type = tableDefinition[columnNameRaw].tsType let nullable = tableDefinition[columnNameRaw].nullable ? '| null' : '' const columnName = options.transformColumnName(columnNameRaw) - fields += `export type ${normalizeName(columnName, options)} = ${type}${nullable};\n` + return `export type ${normalizeName(columnName, options)} = ${type}${nullable};` }) return ` export namespace ${tableName}Fields { - ${fields} + ${tableNamespace.join('\n')} } ` } diff --git a/test/integration/cli.test.ts b/test/integration/cli.test.ts index 298c14e..32dc347 100644 --- a/test/integration/cli.test.ts +++ b/test/integration/cli.test.ts @@ -11,7 +11,7 @@ describe('schemats cli tool integration testing', () => { it('should run without error', () => { let {status, stdout, stderr} = spawnSync('node', [ 'bin/schemats', 'generate', - '-c', process.env.POSTGRES_URL, + '-c', process.env.POSTGRES_URL as string, '-o', '/tmp/schemats_cli_postgres.ts' ], { encoding: 'utf-8' }) console.log('opopopopop', stdout, stderr) @@ -27,7 +27,7 @@ describe('schemats cli tool integration testing', () => { it('should run without error', () => { let {status} = spawnSync('node', [ 'bin/schemats', 'generate', - '-c', process.env.MYSQL_URL, + '-c', process.env.MYSQL_URL as string, '-s', 'test', '-o', '/tmp/schemats_cli_postgres.ts' ]) diff --git a/test/testUtility.ts b/test/testUtility.ts index 713d168..dfc5935 100644 --- a/test/testUtility.ts +++ b/test/testUtility.ts @@ -1,7 +1,7 @@ +import * as assert from 'assert' import * as fs from 'mz/fs' import { typescriptOfSchema, Database } from '../src/index' -import Options from '../src/options' -import * as ts from 'typescript'; +import * as ts from 'typescript' const diff = require('diff') interface IDiffResult { @@ -11,13 +11,13 @@ interface IDiffResult { removed?: boolean } -export function compile(fileNames: string[], options: ts.CompilerOptions): boolean { +export function compile (fileNames: string[], options: ts.CompilerOptions): boolean { let program = ts.createProgram(fileNames, options) let emitResult = program.emit() let exitCode = emitResult.emitSkipped ? 1 : 0 return exitCode === 0 } -export async function compare(goldStandardFile: string, outputFile: string): Promise { +export async function compare (goldStandardFile: string, outputFile: string): Promise { let gold = await fs.readFile(goldStandardFile, {encoding: 'utf8'}) let actual = await fs.readFile(outputFile, {encoding: 'utf8'}) @@ -38,15 +38,14 @@ export async function compare(goldStandardFile: string, outputFile: string): Pro } } - -export async function loadSchema(db: Database, file: string) { +export async function loadSchema (db: Database, file: string) { let query = await fs.readFile(file, { encoding: 'utf8' }) return await db.query(query) } -export async function writeTsFile(inputSQLFile: string, inputConfigFile: string, outputFile: string, db: Database) { +export async function writeTsFile (inputSQLFile: string, inputConfigFile: string, outputFile: string, db: Database) { await loadSchema(db, inputSQLFile) const config: any = require(inputConfigFile) let formattedOutput = await typescriptOfSchema( @@ -57,3 +56,7 @@ export async function writeTsFile(inputSQLFile: string, inputConfigFile: string, ) await fs.writeFile(outputFile, formattedOutput) } + +export function assertEqualCode (expected: string, actual: string, message?: string) { + return assert.equal(actual.replace(/\s+/g, ' ').trim(), expected.replace(/\s+/g, ' ').trim(), message) +} diff --git a/test/unit/schemaPostgres.test.ts b/test/unit/schemaPostgres.test.ts index f8faa16..96c918f 100644 --- a/test/unit/schemaPostgres.test.ts +++ b/test/unit/schemaPostgres.test.ts @@ -556,7 +556,7 @@ describe('PostgresDatabase', () => { nullable: false } } - assert.equal(PostgresDBReflection.mapTableDefinitionToType(td, ['CustomType'], options).column.tsType, 'CustomType') + assert.equal(PostgresDBReflection.mapTableDefinitionToType(td, ['CustomType'], options).column.tsType, 'customTypes.CustomType') }) }) describe('maps to any', () => { diff --git a/test/unit/typescript.test.ts b/test/unit/typescript.test.ts index 2be0bdb..678f8e3 100644 --- a/test/unit/typescript.test.ts +++ b/test/unit/typescript.test.ts @@ -1,42 +1,31 @@ import * as assert from 'assert' import * as Typescript from '../../src/typescript' import Options from '../../src/options' +import { assertEqualCode } from '../testUtility' -const options = new Options({}) +const options = new Options() describe('Typescript', () => { describe('generateTableInterface', () => { it('empty table definition object', () => { const tableInterface = Typescript.generateTableInterface('tableName', {}, options) - assert.equal(tableInterface, - '\n' + - ' export interface tableName {\n' + - ' \n' + - ' }\n' + - ' ') + assertEqualCode(tableInterface, `export interface tableName { }`) }) it('table name is reserved', () => { const tableInterface = Typescript.generateTableInterface('package', {}, options) - assert.equal(tableInterface, - '\n' + - ' export interface package_ {\n' + - ' \n' + - ' }\n' + - ' ') + assertEqualCode(tableInterface, `export interface package_ { }`) }) it('table with columns', () => { const tableInterface = Typescript.generateTableInterface('tableName', { col1: {udtName: 'name1', nullable: false}, col2: {udtName: 'name2', nullable: false} }, options) - assert.equal(tableInterface, - '\n' + - ' export interface tableName {\n' + - ' col1: tableNameFields.col1;\n' + - 'col2: tableNameFields.col2;\n' + - '\n' + - ' }\n' + - ' ') + assertEqualCode(tableInterface, ` + export interface tableName { + col1: tableNameFields.col1; + col2: tableNameFields.col2; + } + `) }) it('table with reserved columns', () => { const tableInterface = Typescript.generateTableInterface('tableName', { @@ -44,85 +33,64 @@ describe('Typescript', () => { number: {udtName: 'name2', nullable: false}, package: {udtName: 'name3', nullable: false} }, options) - assert.equal(tableInterface, - '\n' + - ' export interface tableName {\n' + - ' string: tableNameFields.string_;\n' + - 'number: tableNameFields.number_;\n' + - 'package: tableNameFields.package_;\n' + - '\n' + - ' }\n' + - ' ') + assertEqualCode(tableInterface, ` + export interface tableName { + string: tableNameFields.string_; + number: tableNameFields.number_; + package: tableNameFields.package_; + } + `) }) }) describe('generateEnumType', () => { it('empty object', () => { const enumType = Typescript.generateEnumType({}, options) - assert.equal(enumType,'') + assertEqualCode(enumType, `export namespace customTypes { }`) }) it('with enumerations', () => { - const enumType = Typescript.generateEnumType({ - enum1: ['val1','val2','val3','val4'], - enum2: ['val5','val6','val7','val8'] - }, options) - assert.equal(enumType, - 'export type enum1 = \'val1\' | \'val2\' | \'val3\' | \'val4\';\n' + - 'export type enum2 = \'val5\' | \'val6\' | \'val7\' | \'val8\';\n') - }) - }) - describe('generateEnumType', () => { - it('empty object', () => { - const enumType = Typescript.generateEnumType({}, options) - assert.equal(enumType,'') - }) - it('with enumerations', () => { - const enumType = Typescript.generateEnumType({ - enum1: ['val1','val2','val3','val4'], - enum2: ['val5','val6','val7','val8'] - }, options) - assert.equal(enumType, - 'export type enum1 = \'val1\' | \'val2\' | \'val3\' | \'val4\';\n' + - 'export type enum2 = \'val5\' | \'val6\' | \'val7\' | \'val8\';\n') + const type = { + enum2: ['val5','val6','val7','val8'], + enum1: ['val3','val4','val1','val2'] + } + assertEqualCode(Typescript.generateEnumType(type, options), ` + export namespace customTypes { + export type enum2 = 'val5' | 'val6' | 'val7' | 'val8'; + export type enum1 = 'val3' | 'val4' | 'val1' | 'val2'; + } + `) + + assertEqualCode(Typescript.generateEnumType(type, new Options({ order: true })), ` + export namespace customTypes { + export type enum1 = 'val1' | 'val2' | 'val3' | 'val4'; + export type enum2 = 'val5' | 'val6' | 'val7' | 'val8'; + } + `) }) }) describe('generateTableTypes', () => { it('empty table definition object', () => { const tableTypes = Typescript.generateTableTypes('tableName',{}, options) - assert.equal(tableTypes, - '\n' + - ' export namespace tableNameFields {' + - '\n ' + - '\n ' + - '}' + - '\n ') + assertEqualCode(tableTypes, `export namespace tableNameFields { }`) }) it('with table definitions', () => { const tableTypes = Typescript.generateTableTypes('tableName', { col1: {udtName: 'name1', nullable: false, tsType: 'string'}, col2: {udtName: 'name2', nullable: false, tsType: 'number'} }, options) - assert.equal(tableTypes, - '\n' + - ' export namespace tableNameFields {' + - '\n export type col1 = string;' + - '\nexport type col2 = number;' + - '\n' + - '\n }' + - '\n ') + assertEqualCode(tableTypes, `export namespace tableNameFields { + export type col1 = string; + export type col2 = number; + }`) }) it('with nullable column definitions', () => { const tableTypes = Typescript.generateTableTypes('tableName', { col1: {udtName: 'name1', nullable: true, tsType: 'string'}, col2: {udtName: 'name2', nullable: true, tsType: 'number'} }, options) - assert.equal(tableTypes, - '\n' + - ' export namespace tableNameFields {' + - '\n export type col1 = string| null;' + - '\nexport type col2 = number| null;' + - '\n' + - '\n }' + - '\n ') + assertEqualCode(tableTypes, `export namespace tableNameFields { + export type col1 = string| null; + export type col2 = number| null; + }`) }) }) })