Skip to content

Commit

Permalink
Add options.prefix; support more types
Browse files Browse the repository at this point in the history
  • Loading branch information
koistya committed Apr 19, 2021
1 parent e89e64e commit 6ad3931
Show file tree
Hide file tree
Showing 5 changed files with 247 additions and 132 deletions.
2 changes: 2 additions & 0 deletions main.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,10 @@ export declare type Options = {
* }
*/
overrides?: Record<string, string>;
prefix?: string;
};
/**
* Generates TypeScript definitions (types) from a PostgreSQL database schema.
*/
export declare function updateTypes(db: Knex, options: Options): Promise<void>;
export declare function getType(udt: string, customTypes: Map<string, string>, defaultValue: string | null): string;
109 changes: 76 additions & 33 deletions main.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ Object.defineProperty(exports, "__esModule", {
value: true
});
exports.updateTypes = updateTypes;
exports.getType = getType;

var _camelCase2 = _interopRequireDefault(require("lodash/camelCase"));

Expand All @@ -27,7 +28,12 @@ async function updateTypes(db, options) {
const output = typeof options.output === "string" ? _fs.default.createWriteStream(options.output, {
encoding: "utf-8"
}) : options.output;
["// The TypeScript definitions below are automatically generated.\n", "// Do not touch them, or risk, your modifications being lost.\n\n", 'import { Knex } from "knex";\n\n'].forEach(line => output.write(line));
["// The TypeScript definitions below are automatically generated.\n", "// Do not touch them, or risk, your modifications being lost.\n\n"].forEach(line => output.write(line));

if (options.prefix) {
output.write(options.prefix);
output.write("\n\n");
}

try {
// Fetch the list of custom enum types
Expand All @@ -51,7 +57,12 @@ async function updateTypes(db, options) {
if (!(enums[i + 1] && enums[i + 1].key === x.key)) {
output.write("}\n\n");
}
}); // Fetch the list of tables/columns
});
const enumsMap = new Map(enums.map(x => {
var _overrides$x$key2;

return [x.key, (_overrides$x$key2 = overrides[x.key]) !== null && _overrides$x$key2 !== void 0 ? _overrides$x$key2 : (0, _upperFirst2.default)((0, _camelCase2.default)(x.key))];
})); // Fetch the list of tables/columns

const columns = await db.withSchema("information_schema").table("columns").where("table_schema", "public").orderBy("table_name").orderBy("ordinal_position").select("table_name as table", "column_name as column", db.raw("(is_nullable = 'YES') as nullable"), "column_default as default", "data_type as type", "udt_name as udt"); // The list of database tables as enum

Expand All @@ -72,23 +83,13 @@ async function updateTypes(db, options) {
output.write(`export type ${tableName} = {\n`);
}

output.write(` ${x.column}: ${toType(x, enums, overrides)};\n`);

if (!(columns[i + 1] && columns[i + 1].table === x.table)) {
output.write("};\n\n");
}
}); // Construct TypeScript db record types

columns.forEach((x, i) => {
if (!(columns[i - 1] && columns[i - 1].table === x.table)) {
var _overrides$x$table2;
let type = x.type === "ARRAY" ? `${getType(x.udt.substring(1), enumsMap, x.default)}[]` : getType(x.udt, enumsMap, x.default);

const tableName = (_overrides$x$table2 = overrides[x.table]) !== null && _overrides$x$table2 !== void 0 ? _overrides$x$table2 : (0, _upperFirst2.default)((0, _camelCase2.default)(x.table));
output.write(`export type ${tableName}Record = {\n`);
if (x.nullable) {
type += " | null";
}

const optional = x.nullable || x.default !== null ? "?" : "";
output.write(` ${x.column}${optional}: ${toType(x, enums, overrides, true)};\n`);
output.write(` ${x.column}: ${type};\n`);

if (!(columns[i + 1] && columns[i + 1].table === x.table)) {
output.write("};\n\n");
Expand All @@ -100,26 +101,68 @@ async function updateTypes(db, options) {
}
}

function toType(c, enums, overrides, isRecord = false) {
var _c$default, _c$default2;

let type = ["integer", "numeric", "decimal", "bigint"].includes(c.type) ? "number" : c.type === "boolean" ? "boolean" : c.type === "jsonb" ? isRecord ? "string" : (_c$default = c.default) !== null && _c$default !== void 0 && _c$default.startsWith("'{") ? "Record<string, unknown>" : (_c$default2 = c.default) !== null && _c$default2 !== void 0 && _c$default2.startsWith("'[") ? "unknown[]" : "unknown" : c.type === "ARRAY" && (c.udt === "_text" || c.udt === "_citext") ? "string[]" : c.type.startsWith("timestamp") || c.type === "date" ? "Date" : "string";

if (c.type === "USER-DEFINED") {
var _enums$find;
function getType(udt, customTypes, defaultValue) {
var _customTypes$get;

switch (udt) {
case "bool":
return "boolean";

case "text":
case "citext":
case "money":
case "numeric":
case "int8":
case "char":
case "character":
case "bpchar":
case "varchar":
case "time":
case "tsquery":
case "tsvector":
case "uuid":
case "xml":
case "cidr":
case "inet":
case "macaddr":
return "string";

case "smallint":
case "integer":
case "int":
case "int4":
case "real":
case "float":
case "float4":
case "float8":
return "number";

case "date":
case "timestamp":
case "timestamptz":
return "Date";

case "json":
case "jsonb":
if (defaultValue) {
if (defaultValue.startsWith("'{")) {
return "Record<string, unknown>";
}

if (defaultValue.startsWith("'[")) {
return "unknown[]";
}
}

const key = (_enums$find = enums.find(x => x.key === c.udt)) === null || _enums$find === void 0 ? void 0 : _enums$find.key;
return "unknown";

if (key) {
var _overrides$key;
case "bytea":
return "Buffer";

type = (_overrides$key = overrides[key]) !== null && _overrides$key !== void 0 ? _overrides$key : (0, _upperFirst2.default)((0, _camelCase2.default)(key));
}
}
case "interval":
return "PostgresInterval";

if (type === "Date" && isRecord) {
type = "Date | string";
default:
return (_customTypes$get = customTypes.get(udt)) !== null && _customTypes$get !== void 0 ? _customTypes$get : "unknown";
}

return `${isRecord ? "Knex.Raw | " : ""}${type}${c.nullable ? " | null" : ""}`;
}
132 changes: 87 additions & 45 deletions main.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,23 +10,51 @@ const db = knex({ client: "pg", connection: { database: "update_types" } });
beforeAll(async function setup() {
await createDatabase();

await db.raw(`CREATE DOMAIN user_id AS TEXT CHECK(VALUE ~ '^[0-9a-z]{6}$')`);
await db.raw(`CREATE DOMAIN short_id AS TEXT CHECK(VALUE ~ '^[0-9a-z]{6}$')`);
await db.raw(`CREATE TYPE identity_provider AS ENUM ('google', 'facebook', 'linkedin')`); // prettier-ignore

await db.schema.createTable("user", (table) => {
table.specificType("id", "user_id").notNullable().primary();
table.text("name").notNullable();
table.text("name_null");
table.specificType("roles", "text[]").notNullable();
table.specificType("roles_null", "text[]");
table.specificType("roles_citext", "citext[]").notNullable();
table.jsonb("credentials").notNullable().defaultTo("{}");
table.jsonb("credentials_null");
table.jsonb("events").notNullable().defaultTo("[]");
table.integer("followers").notNullable();
table.integer("followers_null");
table.timestamp("created_at").notNullable();
table.timestamp("deleted_at");
table.increments("int").notNullable().primary();
table.specificType("provider", "identity_provider").notNullable();
table.specificType("provider_null", "identity_provider");
table.specificType("provider_array", "identity_provider[]").notNullable();
table.specificType("int_array", "integer[]").notNullable();
table.specificType("short_id", "short_id").notNullable();
table.decimal("decimal").notNullable();
table.specificType("decimal_array", "decimal[]").notNullable();
table.double("double").notNullable();
table.specificType("double_array", "float8[]").notNullable();
table.float("float").notNullable();
table.specificType("float_array", "float4[]").notNullable();
table.specificType("money", "money").notNullable();
table.bigInteger("bigint").notNullable();
table.binary("binary").notNullable();
table.binary("binary_null");
table.specificType("binary_array", "bytea[]").notNullable();
table.uuid("uuid").notNullable();
table.uuid("uuid_null");
table.specificType("uuid_array", "uuid[]").notNullable();
table.text("text").notNullable();
table.text("text_null");
table.specificType("text_array", "text[]").notNullable();
table.specificType("citext", "citext").notNullable();
table.specificType("citext_null", "citext");
table.specificType("citext_array", "citext[]").notNullable();
table.specificType("char", "char(2)").notNullable();
table.string("varchar", 10).notNullable();
table.boolean("bool").notNullable();
table.boolean("bool_null");
table.specificType("bool_array", "bool[]").notNullable();
table.jsonb("jsonb_object").notNullable().defaultTo("{}");
table.jsonb("jsonb_object_null").defaultTo("{}");
table.jsonb("jsonb_array").notNullable().defaultTo("[]");
table.jsonb("jsonb_array_null").defaultTo("[]");
table.timestamp("timestamp").notNullable();
table.timestamp("timestamp_null");
table.time("time").notNullable();
table.time("time_null");
table.specificType("time_array", "time[]").notNullable();
table.specificType("interval", "interval").notNullable();
});
});

Expand All @@ -40,13 +68,15 @@ test("updateTypes", async function () {
"identity_provider.linkedin": "LinkedIn",
};

await updateTypes(db, { output, overrides });
const prefix = 'import { PostgresInterval} from "postgres-interval";';

await updateTypes(db, { output, overrides, prefix });

expect(await toString(output)).toMatchInlineSnapshot(`
"// The TypeScript definitions below are automatically generated.
// Do not touch them, or risk, your modifications being lost.
import { Knex } from \\"knex\\";
import { PostgresInterval} from \\"postgres-interval\\";
export enum IdentityProvider {
Google = \\"google\\",
Expand All @@ -59,35 +89,47 @@ test("updateTypes", async function () {
}
export type User = {
id: string;
name: string;
name_null: string | null;
roles: string[];
roles_null: string[] | null;
roles_citext: string[];
credentials: Record<string, unknown>;
credentials_null: unknown | null;
events: unknown[];
followers: number;
followers_null: number | null;
created_at: Date;
deleted_at: Date | null;
};
export type UserRecord = {
id: Knex.Raw | string;
name: Knex.Raw | string;
name_null?: Knex.Raw | string | null;
roles: Knex.Raw | string[];
roles_null?: Knex.Raw | string[] | null;
roles_citext: Knex.Raw | string[];
credentials?: Knex.Raw | string;
credentials_null?: Knex.Raw | string | null;
events?: Knex.Raw | string;
followers: Knex.Raw | number;
followers_null?: Knex.Raw | number | null;
created_at: Knex.Raw | Date | string;
deleted_at?: Knex.Raw | Date | string | null;
int: number;
provider: IdentityProvider;
provider_null: IdentityProvider | null;
provider_array: IdentityProvider[];
int_array: number[];
short_id: string;
decimal: string;
decimal_array: string[];
double: number;
double_array: number[];
float: number;
float_array: number[];
money: string;
bigint: string;
binary: Buffer;
binary_null: Buffer | null;
binary_array: Buffer[];
uuid: string;
uuid_null: string | null;
uuid_array: string[];
text: string;
text_null: string | null;
text_array: string[];
citext: string;
citext_null: string | null;
citext_array: string[];
char: string;
varchar: string;
bool: boolean;
bool_null: boolean | null;
bool_array: boolean[];
jsonb_object: Record<string, unknown>;
jsonb_object_null: Record<string, unknown> | null;
jsonb_array: unknown[];
jsonb_array_null: unknown[] | null;
timestamp: Date;
timestamp_null: Date | null;
time: string;
time_null: string | null;
time_array: string[];
interval: PostgresInterval;
};
"
Expand Down
Loading

0 comments on commit 6ad3931

Please sign in to comment.