Skip to content
This repository was archived by the owner on Jun 26, 2021. It is now read-only.

Commit

Permalink
Cli wip
Browse files Browse the repository at this point in the history
  • Loading branch information
typeofweb committed Apr 13, 2020
1 parent 330b894 commit 05d08c7
Show file tree
Hide file tree
Showing 6 changed files with 101 additions and 53 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
"gostek-generate": "./lib/cli.js"
},
"scripts": {
"build": "tsc && chmod +x ./lib/cli.js",
"build": "tsc -p tsconfig.build.json && chmod +x ./lib/cli.js",
"test": "jest",
"lint": "eslint src --ext .js,.jsx,.ts,.tsx"
},
Expand Down
16 changes: 10 additions & 6 deletions src/__tests__/test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import 'jest-extended';
import { db, sql, pgp } from '../generator/db';
import { sql, pgp, getDbConnection } from '../generator/db';
import {
getTablesSchemas,
schemaToTableObj,
Expand Down Expand Up @@ -107,27 +107,31 @@ export const User = {
});

describe('integration tests', () => {
const db = getDbConnection({
user: 'test',
database: 'test',
password: 'test',
});

const drop = async () => {
await db.none(sql(Path.join(__dirname, 'drop_user.sql')));
await db.none(sql(Path.join(__dirname, 'drop_invoice.sql')));
};
const create = async () => {
await db.none(sql(Path.join(__dirname, 'create_user.sql')));
await db.none(sql(Path.join(__dirname, 'create_invoice.sql')));
// const x = await db.one('SELECT * FROM "user";');
// console.log(x, typeof x.editTime);
};
beforeAll(drop);
beforeEach(create);
afterEach(drop);
afterAll(() => pgp.end());

it('reads postgres version', async () => {
expect(await getPostgresVersion()).toBeGreaterThanOrEqual(120000);
expect(await getPostgresVersion(db)).toBeGreaterThanOrEqual(120000);
});

it('correctly reads schema for table user', async () => {
const result = await getTablesSchemas();
const result = await getTablesSchemas(db);

const userSchema = result.find((i) => i.tableName === 'user');
expect(userSchema).toBeDefined();
Expand All @@ -153,7 +157,7 @@ describe('integration tests', () => {
});

it('generates valid TS code for all schemas', async () => {
const code = await generateTSCodeForAllSchemas();
const code = await generateTSCodeForAllSchemas(db);

expect(code).toEqual(
`
Expand Down
32 changes: 30 additions & 2 deletions src/cli.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
#!/usr/bin/env node
import Path from 'path';
import Fs from 'fs';
import { getDbConnection, pgp } from './generator/db';
import { generateTSCodeForAllSchemas } from './generator';

const pkgPath = Path.resolve(process.cwd(), 'package.json');
if (!Fs.existsSync(pkgPath)) {
Expand All @@ -9,6 +11,32 @@ if (!Fs.existsSync(pkgPath)) {
);
}

// const pkg = require(pkgPath);
const connectionOptions = {
user: 'test',
database: 'test',
password: 'test',
};

// const config = {};
const outputFilename = 'generated/models.ts';

function ensureDirectoryExistence(filePath: string) {
const dirname = Path.dirname(filePath);
if (Fs.existsSync(dirname)) {
return;
}
ensureDirectoryExistence(dirname);
Fs.mkdirSync(dirname);
}

(async () => {
const db = getDbConnection(connectionOptions);
const schemas = await generateTSCodeForAllSchemas(db);
pgp.end();
console.log(schemas);

ensureDirectoryExistence(outputFilename);
Fs.writeFileSync(outputFilename, schemas, 'utf8');
})().catch((err) => {
console.error(err);
process.exitCode = 1;
});
20 changes: 15 additions & 5 deletions src/generator/db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,21 @@ import PgPromise, { QueryFile } from 'pg-promise';

export const pgp = PgPromise();

export const db = pgp({
user: 'test',
database: 'test',
password: 'test',
});
type ConnectionOptions =
| {
connectionString: string;
}
| {
host?: string;
database: string;
user: string;
password: string;
port?: number;
};

export const getDbConnection = (options: ConnectionOptions) => {
return pgp(options);
};

const sqlCache = new Map<string, QueryFile>();

Expand Down
80 changes: 41 additions & 39 deletions src/generator/index.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { db } from './db';
import { TableSchema, ColumnType } from './types';
import { Table } from '..';
import Prettier from 'prettier';
import { defaults, flatMap } from 'lodash';
import { makeIntrospectionQuery } from './introspectionQuery';
import { xByY, xByYAndZ, parseTags } from './utils';
import { IDatabase } from 'pg-promise';

// @ts-ignore
// eslint-disable-next-line @typescript-eslint/no-unused-vars
Expand Down Expand Up @@ -215,13 +215,44 @@ type PgIntrospectionResultsByKind = PgIntrospectionOriginalResultsByKind & {
typeById: { [typeId: string]: PgType };
};

export async function getPostgresVersion(): Promise<number> {
export function schemaToTableObj(schema: TableSchema): Table {
return {
name: schema.tableName,
columns: Object.fromEntries(
schema.schema.map((s) => {
return [s.column_name, { type: s.udt_name, notNull: s.is_nullable === 'NO' }];
}),
),
};
}

export function tableObjToTSCode(table: Table, opts?: Prettier.Options): string {
const typeName = table.name.slice(0, 1).toLocaleUpperCase() + table.name.slice(1);
const code = `export const ${typeName} = ${JSON.stringify(table)} as const;`;
const defaultOptions: Prettier.Options = {
semi: true,
singleQuote: true,
trailingComma: 'all',
printWidth: 100,
tabWidth: 2,
useTabs: false,
} as const;
const options = defaults({}, opts, defaultOptions);
return Prettier.format(code, {
...options,
parser: 'typescript',
});
}

export async function getPostgresVersion(db: IDatabase<any>): Promise<number> {
const versionResult = await db.one('show server_version_num;');
return Number.parseInt(versionResult.server_version_num, 10);
}

export async function runIntrospectionQuery(): Promise<PgIntrospectionResultsByKind> {
const version = await getPostgresVersion();
export async function runIntrospectionQuery(
db: IDatabase<any>,
): Promise<PgIntrospectionResultsByKind> {
const version = await getPostgresVersion(db);
const sql = makeIntrospectionQuery(version);
const kinds = [
'namespace',
Expand Down Expand Up @@ -293,15 +324,15 @@ export async function runIntrospectionQuery(): Promise<PgIntrospectionResultsByK
return Object.freeze(result);
}

export async function introspectSchemas() {
const intro = await runIntrospectionQuery();
export async function introspectSchemas(db: IDatabase<any>) {
const intro = await runIntrospectionQuery(db);
const namespaceIds = intro.namespace.map((n) => n.id);
const classes = intro.class.filter((c) => namespaceIds.includes(c.namespaceId));
return { classes, intro };
}

export async function getTablesSchemas(): Promise<Array<TableSchema>> {
const { classes, intro } = await introspectSchemas();
export async function getTablesSchemas(db: IDatabase<any>): Promise<Array<TableSchema>> {
const { classes, intro } = await introspectSchemas(db);

return classes.map((klass) => {
return {
Expand All @@ -317,39 +348,10 @@ export async function getTablesSchemas(): Promise<Array<TableSchema>> {
});
}

export function schemaToTableObj(schema: TableSchema): Table {
return {
name: schema.tableName,
columns: Object.fromEntries(
schema.schema.map((s) => {
return [s.column_name, { type: s.udt_name, notNull: s.is_nullable === 'NO' }];
}),
),
};
}

export function tableObjToTSCode(table: Table, opts?: Prettier.Options): string {
const typeName = table.name.slice(0, 1).toLocaleUpperCase() + table.name.slice(1);
const code = `export const ${typeName} = ${JSON.stringify(table)} as const;`;
const defaultOptions: Prettier.Options = {
semi: true,
singleQuote: true,
trailingComma: 'all',
printWidth: 100,
tabWidth: 2,
useTabs: false,
} as const;
const options = defaults({}, opts, defaultOptions);
return Prettier.format(code, {
...options,
parser: 'typescript',
});
}

export async function generateTSCodeForAllSchemas() {
export async function generateTSCodeForAllSchemas(db: IDatabase<any>) {
// @ts-ignore
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const schemas = await getTablesSchemas();
const schemas = await getTablesSchemas(db);
const allModelsCode = schemas
.map(schemaToTableObj)
.map((obj) => tableObjToTSCode(obj))
Expand Down
4 changes: 4 additions & 0 deletions tsconfig.build.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"extends": "./tsconfig.json",
"exclude": ["src/__tests__", "src/index.test-d.ts"]
}

0 comments on commit 05d08c7

Please sign in to comment.