Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Extend overrides #13

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 86 additions & 3 deletions main.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,15 @@
/* SPDX-License-Identifier: MIT */

import { knex } from "knex";
import { camelCase, snakeCase, upperFirst } from "lodash";
import { PassThrough } from "stream";
import { updateTypes } from "./main";

const db = knex({ client: "pg", connection: { database: "update_types" } });
let db: ReturnType<typeof knex>;

beforeEach(async function setup() {
db = knex({ client: "pg", connection: { database: "update_types" } });

beforeAll(async function setup() {
await createDatabase();

await db.raw(`CREATE DOMAIN short_id AS TEXT CHECK(VALUE ~ '^[0-9a-z]{6}$')`);
Expand Down Expand Up @@ -74,7 +77,7 @@ beforeAll(async function setup() {
});
});

afterAll(async function teardown() {
afterEach(async function teardown() {
await db.destroy();
});

Expand Down Expand Up @@ -165,6 +168,86 @@ test("updateTypes", async function () {
`);
});

test("updateTypes with formatters", async function () {
const output = new PassThrough();

await updateTypes(db, {
output,
formatters: {
column: (name) => upperFirst(camelCase(name)),
enum: (name) => name, // disable default
enumEl: (name) => snakeCase(name).toUpperCase(),
table: (name) => `prefix${upperFirst(name)}`,
},
});

expect(await toString(output)).toMatchInlineSnapshot(`
"// The TypeScript definitions below are automatically generated.
// Do not touch them, or risk, your modifications being lost.

export enum identity_provider {
GOOGLE = \\"google\\",
FACEBOOK = \\"facebook\\",
LINKEDIN = \\"linkedin\\",
}

export enum Table {
prefixLogin = \\"login\\",
prefixUser = \\"user\\",
}

export type prefixLogin = {
Secret: number;
};

export type prefixUser = {
Int: number;
Provider: identity_provider;
ProviderNull: identity_provider | null;
ProviderArray: identity_provider[];
IntArray: number[];
ShortId: string;
Decimal: string;
DecimalArray: string[];
Double: number;
DoubleArray: number[];
Float: number;
FloatArray: number[];
Money: string;
Bigint: string;
Binary: Buffer;
BinaryNull: Buffer | null;
BinaryArray: Buffer[];
Uuid: string;
UuidNull: string | null;
UuidArray: string[];
Text: string;
TextNull: string | null;
TextArray: string[];
Citext: string;
CitextNull: string | null;
CitextArray: string[];
Char: string;
Varchar: string;
Bool: boolean;
BoolNull: boolean | null;
BoolArray: boolean[];
JsonbObject: Record<string, unknown>;
JsonbObjectNull: Record<string, unknown> | null;
JsonbArray: unknown[];
JsonbArrayNull: unknown[] | null;
Timestamp: Date;
TimestampNull: Date | null;
Time: string;
TimeNull: string | null;
TimeArray: string[];
Interval: PostgresInterval;
};

"
`);
});

async function createDatabase(): Promise<void> {
try {
await db.select(db.raw("version()")).first();
Expand Down
46 changes: 34 additions & 12 deletions main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,12 +51,39 @@ export type Options = {
* exclude: ["migration", "migration_lock"]
*/
exclude?: string[] | string;

/**
* Custom formatters for specific type of data - enum, enumEl, table and column.
*
* If any type is null/not specified default formatter for this type will be used.
*
* @example
* formatters: {
* column: (name) => kebabCase(name),
* table: (name) => upperFirst(name),
* }
*/
formatters?: {
column?: (name: string) => string;
enum?: (name: string) => string;
enumEl?: (name: string) => string;
table?: (name: string) => string;
};
};

/**
* Generates TypeScript definitions (types) from a PostgreSQL database schema.
*/
export async function updateTypes(db: Knex, options: Options): Promise<void> {
// const formatter: Record<string, (name: string) => string> = {
const formatter: Required<Options["formatters"]> = {
column: options.formatters?.column ?? ((name) => name),
enum: options.formatters?.enum ?? ((name) => upperFirst(camelCase(name))),
enumEl:
options.formatters?.enumEl ??
((name) => upperFirst(camelCase(name.replace(/[.-]/g, "_")))),
table: options.formatters?.table ?? ((name) => upperFirst(camelCase(name))),
};
const overrides: Record<string, string> = options.overrides ?? {};
const output: Writable =
typeof options.output === "string"
Expand Down Expand Up @@ -103,14 +130,12 @@ export async function updateTypes(db: Knex, options: Options): Promise<void> {
enums.forEach((x, i) => {
// The first line of enum declaration
if (!(enums[i - 1] && enums[i - 1].key === x.key)) {
const enumName = overrides[x.key] ?? upperFirst(camelCase(x.key));
const enumName = overrides[x.key] ?? formatter.enum(x.key);
output.write(`export enum ${enumName} {\n`);
}

// Enum body
const key =
overrides[`${x.key}.${x.value}`] ??
upperFirst(camelCase(x.value.replace(/[.-]/g, "_")));
const key = overrides[`${x.key}.${x.value}`] ?? formatter.enumEl(x.value);
output.write(` ${key} = "${x.value}",\n`);

// The closing line
Expand All @@ -120,10 +145,7 @@ export async function updateTypes(db: Knex, options: Options): Promise<void> {
});

const enumsMap = new Map(
enums.map((x) => [
x.key,
overrides[x.key] ?? upperFirst(camelCase(x.key)),
])
enums.map((x) => [x.key, overrides[x.key] ?? formatter.enum(x.key)])
);

// Fetch the list of tables/columns
Expand Down Expand Up @@ -155,17 +177,17 @@ export async function updateTypes(db: Knex, options: Options): Promise<void> {
})
);
Array.from(tableSet).forEach((value) => {
const key = overrides[value] ?? upperFirst(camelCase(value));
const key = overrides[value] ?? formatter.table(value);
output.write(` ${key} = "${value}",\n`);
});
output.write("}\n\n");

// Construct TypeScript db record types
columns.forEach((x, i) => {
if (!(columns[i - 1] && columns[i - 1].table === x.table)) {
const tableName = overrides[x.table] ?? upperFirst(camelCase(x.table));
const tableName = overrides[x.table] ?? formatter.table(x.table);
const schemaName =
x.schema !== "public" ? upperFirst(camelCase(x.schema)) : "";
x.schema !== "public" ? formatter.table(x.schema) : "";
output.write(`export type ${schemaName}${tableName} = {\n`);
}

Expand All @@ -178,7 +200,7 @@ export async function updateTypes(db: Knex, options: Options): Promise<void> {
type += " | null";
}

output.write(` ${x.column}: ${type};\n`);
output.write(` ${formatter.column(x.column)}: ${type};\n`);

if (!(columns[i + 1] && columns[i + 1].table === x.table)) {
output.write("};\n\n");
Expand Down