diff --git a/package-lock.json b/package-lock.json index cc77b956..94e9f851 100644 --- a/package-lock.json +++ b/package-lock.json @@ -103,6 +103,41 @@ "fastq": "^1.6.0" } }, + "@sinonjs/commons": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.3.tgz", + "integrity": "sha512-xkNcLAn/wZaX14RPlwizcKicDk9G3F8m2nU3L7Ukm5zBgTwiT0wsoFAHx9Jq56fJA1z/7uKGtCRu16sOUCLIHQ==", + "dev": true, + "requires": { + "type-detect": "4.0.8" + } + }, + "@sinonjs/fake-timers": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-6.0.1.tgz", + "integrity": "sha512-MZPUxrmFubI36XS1DI3qmI0YdN1gks62JtFZvxR67ljjSNCeK6U08Zx4msEWOXuofgqUt6zPHSi1H9fbjR/NRA==", + "dev": true, + "requires": { + "@sinonjs/commons": "^1.7.0" + } + }, + "@sinonjs/samsam": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-5.3.1.tgz", + "integrity": "sha512-1Hc0b1TtyfBu8ixF/tpfSHTVWKwCBLY4QJbkgnE7HcwyvT2xArDxb4K7dMgqRm3szI+LJbzmW/s4xxEhv6hwDg==", + "dev": true, + "requires": { + "@sinonjs/commons": "^1.6.0", + "lodash.get": "^4.4.2", + "type-detect": "^4.0.8" + } + }, + "@sinonjs/text-encoding": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.1.tgz", + "integrity": "sha512-+iTbntw2IZPb/anVDbypzfQa+ay64MW0Zo8aJ8gZPWMMK6/OubMVb6lUPMagqjOPnmtauXnFCACVl3O7ogjeqQ==", + "dev": true + }, "@types/color-name": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz", @@ -171,6 +206,11 @@ "integrity": "sha512-L8ogeT6vDzT1vxlW3KITTCt+BVXXVkLXfZ/XNm6UqbcJgxf+KPO7yjWx7dQQE8RW07KopL10x2gNMs41+IkMGQ==", "dev": true }, + "@types/prettier": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.2.3.tgz", + "integrity": "sha512-PijRCG/K3s3w1We6ynUKdxEc5AcuuH3NBmMDP8uvKVp6X43UY7NQlTzczakXP3DJR0F4dfNQIGjU2cUeRYs2AA==" + }, "@types/strip-bom": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/@types/strip-bom/-/strip-bom-3.0.0.tgz", @@ -2303,6 +2343,12 @@ "integrity": "sha512-pBxcB3LFc8QVgdggvZWyeys+hnrNWg4OcZIU/1X59k5jQdLBlCsYGRQaz234SqoRLTCgMH00fY0xRJH+F9METQ==", "dev": true }, + "just-extend": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-4.2.1.tgz", + "integrity": "sha512-g3UB796vUFIY90VIv/WX3L2c8CS2MdWUww3CNrYmqza1Fg0DURc2K/O4YrnklBdQarSJ/y8JnJYDGc+1iumQjg==", + "dev": true + }, "kind-of": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", @@ -2385,6 +2431,12 @@ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "dev": true }, + "lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=", + "dev": true + }, "log-symbols": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-3.0.0.tgz", @@ -2813,6 +2865,19 @@ "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", "dev": true }, + "nise": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/nise/-/nise-4.1.0.tgz", + "integrity": "sha512-eQMEmGN/8arp0xsvGoQ+B1qvSkR73B1nWSCh7nOt5neMCtwcQVYQGdzQMhcNscktTsWB54xnlSQFzOAPJD8nXA==", + "dev": true, + "requires": { + "@sinonjs/commons": "^1.7.0", + "@sinonjs/fake-timers": "^6.0.0", + "@sinonjs/text-encoding": "^0.7.1", + "just-extend": "^4.0.2", + "path-to-regexp": "^1.7.0" + } + }, "node-environment-flags": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/node-environment-flags/-/node-environment-flags-1.0.6.tgz", @@ -3159,6 +3224,23 @@ "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==", "dev": true }, + "path-to-regexp": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz", + "integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==", + "dev": true, + "requires": { + "isarray": "0.0.1" + }, + "dependencies": { + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", + "dev": true + } + } + }, "path-type": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", @@ -3559,8 +3641,7 @@ "prettier": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.0.5.tgz", - "integrity": "sha512-7PtVymN48hGcO4fGjybyBSIWDsLU4H4XlvOHfq91pz9kkGlonzwTfYkaIEwiRg/dAJF9YlbsduBAgtYLi+8cFg==", - "dev": true + "integrity": "sha512-7PtVymN48hGcO4fGjybyBSIWDsLU4H4XlvOHfq91pz9kkGlonzwTfYkaIEwiRg/dAJF9YlbsduBAgtYLi+8cFg==" }, "process-nextick-args": { "version": "2.0.1", @@ -4012,6 +4093,43 @@ "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==", "dev": true }, + "sinon": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-10.0.0.tgz", + "integrity": "sha512-XAn5DxtGVJBlBWYrcYKEhWCz7FLwZGdyvANRyK06419hyEpdT0dMc5A8Vcxg5SCGHc40CsqoKsc1bt1CbJPfNw==", + "dev": true, + "requires": { + "@sinonjs/commons": "^1.8.1", + "@sinonjs/fake-timers": "^6.0.1", + "@sinonjs/samsam": "^5.3.1", + "diff": "^4.0.2", + "nise": "^4.1.0", + "supports-color": "^7.1.0" + }, + "dependencies": { + "diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, "slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", @@ -4768,6 +4886,12 @@ "prelude-ls": "~1.1.2" } }, + "type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true + }, "type-fest": { "version": "0.13.1", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz", diff --git a/package.json b/package.json index f825a2ae..bc637086 100755 --- a/package.json +++ b/package.json @@ -22,11 +22,14 @@ "start": "ts-node-dev src/server/app.ts | pino-pretty --colorize", "dev": "run-s clean format start", "pkg": "run-s clean format build:server && pkg --out-path bin .pkg.config.json", - "test": "run-s build:server && node -r esm ./node_modules/.bin/mocha 'test/**/*.js' --recursive" + "test": "run-s build:server && node -r esm ./node_modules/.bin/mocha 'test/**/*.js' --recursive", + "unit-test": "run-s build:server && node -r esm ./node_modules/.bin/mocha 'test/lib/*.js' --recursive" }, "dependencies": { + "@types/prettier": "^2.2.3", "pg": "^7.0.0", - "pg-format": "^1.0.4" + "pg-format": "^1.0.4", + "prettier": "^2.0.5" }, "devDependencies": { "@types/crypto-js": "^3.1.47", @@ -43,8 +46,8 @@ "npm-run-all": "^4.1.5", "pino-pretty": "^4.7.1", "pkg": "^4.4.8", - "prettier": "^2.0.5", "rimraf": "^3.0.2", + "sinon": "^10.0.0", "ts-node-dev": "^1.1.6", "typescript": "^3.9.3" } diff --git a/src/lib/TypeScriptInterfaces.ts b/src/lib/TypeScriptInterfaces.ts new file mode 100644 index 00000000..a662a1f3 --- /dev/null +++ b/src/lib/TypeScriptInterfaces.ts @@ -0,0 +1,93 @@ +import prettier from "prettier" +import parserTypescript from "prettier/parser-typescript" + +import { PostgresMeta } from "." +import { PostgresColumn } from "./types" + +const parseColumns = (columns: PostgresColumn[]) => { + const tableGroupings = columns.reduce((prev, current) => { + if (current.table in prev) { + prev[current.table].push(current) + } else { + prev[current.table] = [current] + } + + return prev + }, {} as { [key: string]: PostgresColumn[] }) + + return Object + .entries(tableGroupings) + .map(([table, columns]: [string, PostgresColumn[]]) => { + return `${table}: { ${columns.map(parseColumn).join(';')} };` + }).join('') +} + +const parseColumn = (column: PostgresColumn) => { + const dataType = postgresToTypescriptType(column.format) + const nullableSuffix = column.is_nullable ? '?' : '' + + return `${column.name}${nullableSuffix}: ${dataType}` +} + +const postgresToTypescriptType = (format: string) => { + switch (format) { + // adapted from https://github.com/jawj/zapatos/blob/master/src/generate/pgTypes.ts + case 'int8': + case 'int2': + case 'int4': + case 'float4': + case 'float8': + case 'numeric': + case 'money': + case 'oid': + return 'number' + case 'date': + case 'timestamp': + case 'timestamptz': + return 'Date' + case 'bpchar': + case 'char': + case 'varchar': + case 'text': + case 'citext': + case 'uuid': + case 'bytea': + case 'inet': + case 'time': + case 'timetz': + case 'interval': + case 'name': + case 'json': + case 'jsonb': + return 'string' + case 'bool': + return 'boolean' + default: + return 'any' + } +} + +export default class TypeScriptInterfaces { + pgMeta: PostgresMeta + + constructor({ pgMeta }: { pgMeta: PostgresMeta }) { + this.pgMeta = pgMeta + } + + async dump(): Promise { + const { data, error } = await this.pgMeta.columns.list() + // TODO: handle error + + if (data) { + const tableDefString = parseColumns(data) + let output = `export interface definitions { ${tableDefString} };` + + // Prettify output + let prettierOptions: prettier.Options = { + parser: "typescript", + plugins: [parserTypescript], + }; + return prettier.format(output, prettierOptions); + } + } +} diff --git a/test/lib/TypeScriptInterfaces.spec.js b/test/lib/TypeScriptInterfaces.spec.js new file mode 100644 index 00000000..5169fdd4 --- /dev/null +++ b/test/lib/TypeScriptInterfaces.spec.js @@ -0,0 +1,72 @@ +var assert = require('assert') +var sinon = require('sinon') + +import TypeScriptInterfaces from '../../bin/src/lib/TypeScriptInterfaces' +import { PostgresMeta } from '../../bin/src/lib' + + +describe('.dump()', () => { + it('handles nullable columns', async () => { + const pgMeta = new PostgresMeta({ connectionString: '', max: 1 }) + const columnsData = [ + { + table: 'todos', + name: 'name', + format: 'text', + is_nullable: true + } + ] + sinon + .stub(pgMeta.columns, "list") + .returns(Promise.resolve({ data: columnsData })) + + const example = new TypeScriptInterfaces({ pgMeta: pgMeta }); + + const expected = `export interface definitions { + todos: { name?: string }; +} +` + + assert.equal(await example.dump(), expected) + }) + + it('returns a string of TypeScript types', async () => { + const pgMeta = new PostgresMeta({ connectionString: '', max: 1 }) + const columnsData = [ + { + table: 'todos', + name: 'id', + format: 'int8' + }, + { + table: 'todos', + name: 'done', + format: 'bool' + }, + { + table: 'todos', + name: 'done_at', + is_nullable: true, + format: 'date', + }, + { + table: 'memes', + name: 'example', + format: 'some-invalid-format', + } + ] + sinon + .stub(pgMeta.columns, "list") + .returns(Promise.resolve({ data: columnsData })) + + const example = new TypeScriptInterfaces({ pgMeta: pgMeta }); + + const expected = `export interface definitions { + todos: { id: number; done: boolean; done_at?: Date }; + memes: { example: any }; +} +` + + assert.equal(await example.dump(), expected) + }) +})