├── .babelrc ├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .flowconfig ├── .gitignore ├── .npmignore ├── .travis.yml ├── LICENSE ├── README.md ├── package.json ├── src ├── bin │ ├── commands │ │ ├── .eslintrc │ │ └── generate-types.js │ └── index.js ├── factories │ ├── createDebug.js │ └── index.js ├── queries │ ├── getDatabaseMaterializedViewColumns.js │ ├── getDatabaseTableColumns.js │ └── index.js ├── types.js └── utilities │ ├── generateFlowTypeDocument.js │ ├── index.js │ ├── mapFlowType.js │ └── normalizeColumns.js └── test ├── .eslintrc └── utilities └── mapFlowTypes.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "test": { 4 | "plugins": [ 5 | "istanbul" 6 | ] 7 | } 8 | }, 9 | "plugins": [ 10 | "transform-object-rest-spread", 11 | "transform-flow-strip-types" 12 | ], 13 | "presets": [ 14 | [ 15 | "env", 16 | { 17 | "targets": { 18 | "node": 6 19 | }, 20 | "useBuiltIns": true 21 | } 22 | ] 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_size = 2 7 | indent_style = space 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gajus/database-types/417f51e4c625667ffc70158b8f26d5072cabfcf5/.eslintignore -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "canonical", 4 | "canonical/flowtype" 5 | ], 6 | "root": true, 7 | "rules": { 8 | "flowtype/no-weak-types": 0, 9 | "no-duplicate-imports": 0, 10 | "no-restricted-syntax": 0 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | .*/node_modules/.*/test/.* 3 | .*/node_modules/babel-plugin-flow-runtime/.* 4 | .*/node_modules/config-chain/.* 5 | .*/node_modules/conventional-changelog-core/.* 6 | .*/node_modules/flow-runtime/.* 7 | .*/node_modules/npmconf/.* 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | coverage 2 | dist 3 | node_modules 4 | *.log 5 | .* 6 | !.babelrc 7 | !.editorconfig 8 | !.eslintignore 9 | !.eslintrc 10 | !.flowconfig 11 | !.gitignore 12 | !.npmignore 13 | !.travis.yml 14 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src 2 | test 3 | coverage 4 | .* 5 | *.log 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - node 4 | - 6 5 | script: 6 | - npm run build 7 | - npm run test 8 | - npm run lint 9 | # - nyc --silent npm run test 10 | # - nyc report --reporter=text-lcov | coveralls 11 | # - nyc check-coverage --lines 80 12 | after_success: 13 | - npm run build 14 | - semantic-release pre 15 | - npm publish 16 | - semantic-release post 17 | notifications: 18 | email: false 19 | sudo: false 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017, Gajus Kuizinas (http://gajus.com/) 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | * Redistributions of source code must retain the above copyright 7 | notice, this list of conditions and the following disclaimer. 8 | * Redistributions in binary form must reproduce the above copyright 9 | notice, this list of conditions and the following disclaimer in the 10 | documentation and/or other materials provided with the distribution. 11 | * Neither the name of the Gajus Kuizinas (http://gajus.com/) nor the 12 | names of its contributors may be used to endorse or promote products 13 | derived from this software without specific prior written permission. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 16 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 17 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | DISCLAIMED. IN NO EVENT SHALL ANUARY BE LIABLE FOR ANY 19 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 20 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 21 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 22 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 24 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # database-types 2 | 3 | [![Travis build status](http://img.shields.io/travis/gajus/database-types/master.svg?style=flat-square)](https://travis-ci.org/gajus/database-types) 4 | [![Coveralls](https://img.shields.io/coveralls/gajus/database-types.svg?style=flat-square)](https://coveralls.io/github/gajus/database-types) 5 | [![NPM version](http://img.shields.io/npm/v/database-types.svg?style=flat-square)](https://www.npmjs.org/package/database-types) 6 | [![Canonical Code Style](https://img.shields.io/badge/code%20style-canonical-blue.svg?style=flat-square)](https://github.com/gajus/canonical) 7 | [![Twitter Follow](https://img.shields.io/twitter/follow/kuizinas.svg?style=social&label=Follow)](https://twitter.com/kuizinas) 8 | 9 | A generic type generator for various databases. 10 | 11 | The current supported database backend is Postgres. Track [#1 issue](https://github.com/gajus/database-types/issues/1) for MySQL support. 12 | 13 | ## Use case 14 | 15 | If you are developing applications in JavaScript and using either of the strict type systems, then you can use `database-types` to generate types describing the database. 16 | 17 | ## Example usage 18 | 19 | ### Generating Flow types 20 | 21 | ```bash 22 | export DATABASE_TYPES_DATABASE_CONNECTION_URI=postgres://postgres:password@127.0.0.1/test 23 | export DATABASE_TYPES_COLUMN_FILTER="return !['raster_overviews', 'raster_columns', 'geometry_columns', 'geography_columns', 'spatial_ref_sys'].includes(tableName)" 24 | export DATABASE_TYPES_DIALECT=flow 25 | 26 | database-types generate-types > ./types.js 27 | 28 | ``` 29 | 30 | This generates file containing Flow type declarations in the following format: 31 | 32 | ```js 33 | export type ReservationSeatRecordType = {| 34 | +createdAt: string, 35 | +id: number, 36 | +reservationId: number, 37 | +seatId: number 38 | |}; 39 | 40 | export type TicketTypeRecordType = {| 41 | +cinemaId: number, 42 | +id: number, 43 | +name: string, 44 | +nid: string, 45 | +policy: string | null 46 | |}; 47 | 48 | // ... 49 | 50 | ``` 51 | 52 | ## CLI 53 | 54 | ```bash 55 | $ npm install database-types -g 56 | $ database-types --help 57 | 58 | ``` 59 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": { 3 | "email": "gajus@gajus.com", 4 | "name": "Gajus Kuizinas", 5 | "url": "http://gajus.com" 6 | }, 7 | "ava": { 8 | "babel": "inherit", 9 | "require": [ 10 | "babel-register" 11 | ] 12 | }, 13 | "bin": "./dist/bin/index.js", 14 | "dependencies": { 15 | "debug": "^3.1.0", 16 | "es6-error": "^4.0.2", 17 | "lodash": "^4.17.4", 18 | "mightyql": "^4.6.1", 19 | "yargs": "^10.0.3" 20 | }, 21 | "description": "A generic type generator for various databases.", 22 | "devDependencies": { 23 | "ava": "^0.23.0", 24 | "babel-cli": "^6.26.0", 25 | "babel-plugin-istanbul": "^4.1.5", 26 | "babel-plugin-transform-flow-strip-types": "^6.22.0", 27 | "babel-plugin-transform-object-rest-spread": "^6.26.0", 28 | "babel-preset-env": "^1.6.1", 29 | "babel-register": "^6.26.0", 30 | "coveralls": "^3.0.0", 31 | "eslint": "^4.11.0", 32 | "eslint-config-canonical": "^9.3.1", 33 | "flow-bin": "^0.59.0", 34 | "husky": "^0.14.3", 35 | "nyc": "^11.3.0", 36 | "semantic-release": "^8.2.0" 37 | }, 38 | "engines": { 39 | "node": ">=6" 40 | }, 41 | "keywords": [ 42 | "flowtype", 43 | "postgres" 44 | ], 45 | "license": "BSD-3-Clause", 46 | "main": "./dist/index.js", 47 | "name": "database-types", 48 | "nyc": { 49 | "include": [ 50 | "src/**/*.js" 51 | ], 52 | "instrument": false, 53 | "reporter": [ 54 | "text-lcov" 55 | ], 56 | "require": [ 57 | "babel-register" 58 | ], 59 | "sourceMap": false 60 | }, 61 | "repository": { 62 | "type": "git", 63 | "url": "https://github.com/gajus/database-types" 64 | }, 65 | "scripts": { 66 | "build": "rm -fr ./dist && NODE_ENV=production babel ./src --out-dir ./dist --copy-files --source-maps", 67 | "lint": "eslint ./src ./test && flow", 68 | "test": "ava --verbose" 69 | }, 70 | "version": "1.0.2" 71 | } 72 | -------------------------------------------------------------------------------- /src/bin/commands/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "filenames/match-regex": 0 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /src/bin/commands/generate-types.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | /* eslint-disable no-new-func */ 4 | 5 | import { 6 | camelCase, 7 | upperFirst 8 | } from 'lodash'; 9 | import { 10 | createConnection 11 | } from 'mightyql'; 12 | import type { 13 | ColumnType, 14 | TypePropertyType 15 | } from '../../types'; 16 | import { 17 | getDatabaseTableColumns, 18 | getDatabaseMaterializedViewColumns 19 | } from '../../queries'; 20 | import { 21 | generateFlowTypeDocument, 22 | mapFlowType, 23 | normalizeColumns 24 | } from '../../utilities'; 25 | 26 | export const command = 'generate-types'; 27 | export const desc = 'Generate types for a Postgres database.'; 28 | 29 | type ConfigurationType = {| 30 | +columnFilter: string, 31 | +databaseConnectionUri: string, 32 | +dialect: 'flow', 33 | +includeMaterializedViews: boolean, 34 | +propertyNameFormatter: string | null, 35 | +typeNameFormatter: string | null 36 | |}; 37 | 38 | export const builder = (yargs: *): void => { 39 | yargs 40 | .options({ 41 | 'column-filter': { 42 | description: 'Function used to filter columns. Function is constructed using `new Function`. Function receives table name as the first parameter and column name as the second parameter (parameter names are "tableName" and "columnName").', 43 | type: 'string' 44 | }, 45 | 'database-connection-uri': { 46 | demand: true 47 | }, 48 | dialect: { 49 | choices: [ 50 | 'flow' 51 | ], 52 | demand: true 53 | }, 54 | 'include-materialized-views': { 55 | default: true, 56 | type: 'boolean' 57 | }, 58 | 'property-name-formatter': { 59 | default: null, 60 | description: 'Function used to format property name. Function is constructed using `new Function`. Function receives column name as the first parameter (parameter name is "columnName"). The default behaviour is to (lower) camelCase the column name.', 61 | type: 'string' 62 | }, 63 | 'type-name-formatter': { 64 | default: null, 65 | description: 'Function used to format type name. Function is constructed using `new Function`. Function receives table name as the first parameter (parameter name is "tableName"). The default behaviour is to (upper) CamelCase the table name and suffix it with "RecordType".', 66 | type: 'string' 67 | } 68 | }); 69 | }; 70 | 71 | type ColumnFilterType = (tableName: string, columnName: string) => boolean; 72 | type FormatterType = (name: string) => string; 73 | 74 | export const handler = async (argv: ConfigurationType): Promise => { 75 | const defaultFormatTypeName = (tableName: string): string => { 76 | return upperFirst(camelCase(tableName)) + 'RecordType'; 77 | }; 78 | 79 | const defaultFormatPropertyName = (columnName: string): string => { 80 | return camelCase(columnName); 81 | }; 82 | 83 | // eslint-disable-next-line no-extra-parens 84 | const filterColumns: ColumnFilterType = (argv.columnFilter ? new Function('tableName', 'columnName', argv.columnFilter) : null: any); 85 | 86 | // eslint-disable-next-line no-extra-parens 87 | const formatTypeName: FormatterType = (argv.typeNameFormatter ? new Function('columnName', argv.typeNameFormatter) : defaultFormatTypeName: any); 88 | // eslint-disable-next-line no-extra-parens 89 | const formatPropertyName: FormatterType = (argv.propertyNameFormatter ? new Function('tableName', argv.propertyNameFormatter) : defaultFormatPropertyName: any); 90 | 91 | const createProperties = (columns: $ReadOnlyArray): $ReadOnlyArray => { 92 | let filteredColumns = columns; 93 | 94 | if (filterColumns) { 95 | filteredColumns = filteredColumns.filter((column) => { 96 | // $FlowFixMe 97 | return filterColumns(column.tableName, column.columnName); 98 | }); 99 | } 100 | 101 | return filteredColumns.map((column) => { 102 | return { 103 | name: formatPropertyName(column.columnName), 104 | type: mapFlowType(column.databaseType) + (column.nullable ? ' | null' : ''), 105 | typeName: formatTypeName(column.tableName) 106 | }; 107 | }); 108 | }; 109 | 110 | const connection = await createConnection(argv.databaseConnectionUri); 111 | 112 | let unnormalizedColumns; 113 | 114 | unnormalizedColumns = await getDatabaseTableColumns(connection); 115 | 116 | if (argv.includeMaterializedViews) { 117 | unnormalizedColumns = unnormalizedColumns.concat(await getDatabaseMaterializedViewColumns(connection)); 118 | } 119 | 120 | const normalizedColumns = normalizeColumns(unnormalizedColumns); 121 | 122 | const properties = createProperties(normalizedColumns); 123 | 124 | // eslint-disable-next-line no-console 125 | console.log(generateFlowTypeDocument(properties)); 126 | 127 | await connection.end(); 128 | }; 129 | -------------------------------------------------------------------------------- /src/bin/index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import yargs from 'yargs'; 4 | 5 | process.on('unhandledRejection', (reason) => { 6 | throw reason; 7 | }); 8 | 9 | process.on('uncaughtException', (error) => { 10 | throw error; 11 | }); 12 | 13 | // eslint-disable-next-line no-unused-expressions 14 | yargs 15 | .env('DATABASE_TYPES') 16 | .commandDir('commands') 17 | .help() 18 | .wrap(80) 19 | .argv; 20 | -------------------------------------------------------------------------------- /src/factories/createDebug.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import debug from 'debug'; 4 | 5 | export default (namespace: string) => { 6 | return debug('database-types:' + namespace); 7 | }; 8 | -------------------------------------------------------------------------------- /src/factories/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | export {default as createDebug} from './createDebug'; 4 | -------------------------------------------------------------------------------- /src/queries/getDatabaseMaterializedViewColumns.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { 4 | sql 5 | } from 'mightyql'; 6 | import type { 7 | DatabaseConnectionType, 8 | UnnormalizedColumnType 9 | } from '../types'; 10 | 11 | export default async (connection: DatabaseConnectionType): Promise<$ReadOnlyArray> => { 12 | return connection.any(sql` 13 | SELECT 14 | mv.relname AS "tableName", 15 | atr.attname AS "columnName", 16 | pg_catalog.format_type (atr.atttypid, NULL) "dataType", 17 | (case when atr.attnotnull then 'YES' else 'NO' end) "isNullable" 18 | FROM 19 | pg_class mv 20 | JOIN pg_namespace ns ON mv.relnamespace = ns.oid 21 | JOIN pg_attribute atr ON atr.attrelid = mv.oid 22 | AND atr.attnum > 0 23 | AND NOT atr.attisdropped 24 | WHERE 25 | ns.nspname = 'public' AND 26 | mv.relkind = 'm' 27 | `); 28 | }; 29 | -------------------------------------------------------------------------------- /src/queries/getDatabaseTableColumns.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { 4 | sql 5 | } from 'mightyql'; 6 | import type { 7 | DatabaseConnectionType, 8 | UnnormalizedColumnType 9 | } from '../types'; 10 | 11 | export default async (connection: DatabaseConnectionType): Promise<$ReadOnlyArray> => { 12 | return connection.any(sql` 13 | SELECT 14 | table_name "tableName", 15 | column_name "columnName", 16 | is_nullable "isNullable", 17 | data_type "dataType" 18 | FROM information_schema.columns 19 | WHERE table_schema = 'public' 20 | `); 21 | }; 22 | -------------------------------------------------------------------------------- /src/queries/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | export {default as getDatabaseMaterializedViewColumns} from './getDatabaseMaterializedViewColumns'; 4 | export {default as getDatabaseTableColumns} from './getDatabaseTableColumns'; 5 | -------------------------------------------------------------------------------- /src/types.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | export type { 4 | DatabaseConnectionType 5 | } from 'mightyql'; 6 | 7 | export type UnnormalizedColumnType = {| 8 | +columnName: string, 9 | +dataType: string, 10 | +isNullable: 'YES' | 'NO', 11 | +tableName: string 12 | |}; 13 | 14 | export type ColumnType = {| 15 | +columnName: string, 16 | +databaseType: string, 17 | +nullable: boolean, 18 | +tableName: string 19 | |}; 20 | 21 | export type TypePropertyType = {| 22 | +name: string, 23 | +type: string, 24 | +typeName: string 25 | |}; 26 | -------------------------------------------------------------------------------- /src/utilities/generateFlowTypeDocument.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { 4 | groupBy, 5 | sortBy 6 | } from 'lodash'; 7 | import type { 8 | TypePropertyType 9 | } from '../types'; 10 | 11 | const generateFlowTypeDeclarationBody = (properties: $ReadOnlyArray): string => { 12 | const sortedProperties = sortBy(properties, 'name'); 13 | 14 | const propertyDeclarations = []; 15 | 16 | for (const column of sortedProperties) { 17 | propertyDeclarations.push('+' + column.name + ': ' + column.type); 18 | } 19 | 20 | return propertyDeclarations.join('\n'); 21 | }; 22 | 23 | export default ( 24 | columns: $ReadOnlyArray 25 | ): string => { 26 | const groupedProperties = groupBy(columns, 'typeName'); 27 | 28 | const typeDeclarations = []; 29 | 30 | const typeNames = Object.keys(groupedProperties); 31 | 32 | for (const typeName of typeNames) { 33 | const typeProperties = groupedProperties[typeName]; 34 | 35 | const typeDeclaration = ` 36 | export type ${typeName} = {| 37 | ${generateFlowTypeDeclarationBody(typeProperties).split('\n').join(',\n ')} 38 | |};`; 39 | 40 | typeDeclarations.push(typeDeclaration); 41 | } 42 | 43 | return typeDeclarations.join('\n'); 44 | }; 45 | -------------------------------------------------------------------------------- /src/utilities/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | export {default as generateFlowTypeDocument} from './generateFlowTypeDocument'; 4 | export {default as mapFlowType} from './mapFlowType'; 5 | export {default as normalizeColumns} from './normalizeColumns'; 6 | -------------------------------------------------------------------------------- /src/utilities/mapFlowType.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { 4 | createDebug 5 | } from '../factories'; 6 | 7 | const debug = createDebug('mapFlowType'); 8 | 9 | export default (databaseTypeName: string): string => { 10 | if (databaseTypeName === 'json') { 11 | return 'Object'; 12 | } 13 | 14 | if (/^(?:text|character|timestamp|coordinates)(\s|$)/.test(databaseTypeName)) { 15 | return 'string'; 16 | } 17 | 18 | if (databaseTypeName === 'boolean') { 19 | return 'boolean'; 20 | } 21 | 22 | if (databaseTypeName === 'bigint' || databaseTypeName === 'integer') { 23 | return 'number'; 24 | } 25 | 26 | debug('unknown type', databaseTypeName); 27 | 28 | return 'any'; 29 | }; 30 | -------------------------------------------------------------------------------- /src/utilities/normalizeColumns.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import type { 4 | ColumnType, 5 | UnnormalizedColumnType 6 | } from '../types'; 7 | 8 | export default (unnormalizedColumns: $ReadOnlyArray): $ReadOnlyArray => { 9 | const normalizedColumns = unnormalizedColumns.map((column) => { 10 | const valueIsNullable = column.isNullable === 'YES'; 11 | 12 | return { 13 | columnName: column.columnName, 14 | databaseType: column.dataType, 15 | nullable: valueIsNullable, 16 | tableName: column.tableName 17 | }; 18 | }); 19 | 20 | return normalizedColumns; 21 | }; 22 | -------------------------------------------------------------------------------- /test/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "canonical/ava", 3 | "rules": { 4 | "filenames/match-regex": 0, 5 | "id-length": 0 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /test/utilities/mapFlowTypes.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import test from 'ava'; 4 | import mapFlowType from '../../src/utilities/mapFlowType'; 5 | 6 | const knownTypes = { 7 | bigint: 'number', 8 | boolean: 'boolean', 9 | character: 'string', 10 | coordinates: 'string', 11 | integer: 'number', 12 | json: 'Object', 13 | text: 'string', 14 | timestamp: 'string' 15 | }; 16 | 17 | test('correctly maps known types', (t) => { 18 | const databaseTypeNames = Object.keys(knownTypes); 19 | 20 | for (const databaseTypeName of databaseTypeNames) { 21 | const flowType = knownTypes[databaseTypeName]; 22 | 23 | if (typeof flowType !== 'string') { 24 | throw new TypeError(); 25 | } 26 | 27 | t.true(mapFlowType(databaseTypeName) === flowType, flowType); 28 | } 29 | }); 30 | --------------------------------------------------------------------------------