@andymic/pigeon 1.3.0 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/pigeon.js CHANGED
@@ -1,8 +1,4 @@
1
1
  #!/usr/bin/env node
2
- /*
3
- * Copyright (c) 2025 Andreas Michael
4
- * This software is under the Apache 2.0 License
5
- */
6
2
  import { cli, run } from "../src/cli.js";
7
3
  import { PigeonError } from "../src/index.js";
8
4
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@andymic/pigeon",
3
- "version": "1.3.0",
3
+ "version": "1.4.0",
4
4
  "author": "Andreas Michael <ateasm03@gmail.com>",
5
5
  "description": "Pigeon is a TypeScript-based tool for generating TypeScript classes and methods from PostgreSQL database schemas.",
6
6
  "keywords": [
@@ -35,5 +35,5 @@
35
35
  "pg": "^8.15.6",
36
36
  "prompt-sync": "^4.2.0"
37
37
  },
38
- "gitHead": "40d793e0267631471bde05ffddc53ba908e07512"
38
+ "gitHead": "ec33791a735792a340bfa41c1f66e9a08534ffa6"
39
39
  }
package/src/cli.js CHANGED
@@ -1,12 +1,9 @@
1
- /*
2
- * Copyright (c) 2025 Andreas Michael
3
- * This software is under the Apache 2.0 License
4
- */
5
1
  import meow from "meow";
6
2
  import { createConfig } from "./config.js";
7
- import { Database, deleteDir, guided, PigeonError, queryDB, runGeneration } from "./index.js";
3
+ import { Database, deleteDir, enumsQuery, guided, PigeonError, queryDB, runGeneration } from "./index.js";
8
4
  import * as path from "node:path";
9
5
  import fs from "node:fs";
6
+ import { getRelationshipsAndTables, tableProcessing } from "./pgAdmin.js";
10
7
  export const cli = meow(`
11
8
  Usage
12
9
  $ pigeon [options]
@@ -17,12 +14,15 @@ export const cli = meow(`
17
14
  --force overwrites already existing files
18
15
  --output [path:String] output directory for the generated files.
19
16
  --config [path:String] path to .pigeon.json config file.
17
+ --pgAdmin [path:String] path to the pgAdmin ERD file.
18
+ --offline (only with pgAdmin) does not contact the database
20
19
 
21
20
  Examples
22
21
  $ pigeon --init
23
22
  $ pigeon --output C:/Users/User/Documents/Project
24
23
  $ pigeon --output ./generatedFiles --force
25
24
  $ pigeon --config ./customPigeonConfig.json
25
+ $ pigeon --pgAdmin C:/Users/User/Documents/Project/ERD.json --offline
26
26
 
27
27
  Exit Status
28
28
  Pigeon returns the following codes:
@@ -57,6 +57,13 @@ export const cli = meow(`
57
57
  type: "string",
58
58
  default: process.cwd(),
59
59
  },
60
+ pgAdmin: {
61
+ type: "string",
62
+ },
63
+ offline: {
64
+ type: "boolean",
65
+ default: false,
66
+ }
60
67
  },
61
68
  autoHelp: true,
62
69
  autoVersion: true,
@@ -67,6 +74,36 @@ export async function run(flags) {
67
74
  return createConfig(flags.cwd);
68
75
  if (flags.force)
69
76
  deleteDir(flags.output);
77
+ if (flags.pgAdmin) {
78
+ let enums;
79
+ let database;
80
+ if (!flags.offline) {
81
+ if (!fs.existsSync(path.join(process.cwd(), ".pigeon.json")))
82
+ return new PigeonError(1, "", new Error("The configuration file does not exist. Generate one using the \"pigeon --init\" command"));
83
+ const params = JSON.parse(fs.readFileSync(flags.config).toString());
84
+ database = new Database(params.host, params.port, params.database, params.username, params.password);
85
+ enums = await enumsQuery(database);
86
+ }
87
+ else {
88
+ enums = [];
89
+ database = new Database("localhost", "5432", "database", "username", "password");
90
+ }
91
+ if (enums instanceof PigeonError)
92
+ return enums;
93
+ if (!fs.existsSync(flags.pgAdmin))
94
+ return new PigeonError(1, "", new Error("The pgAdmin ERD file specified does not exist."));
95
+ const file = fs.readFileSync(flags.pgAdmin).toString();
96
+ let tables;
97
+ try {
98
+ tables = getRelationshipsAndTables(file).tables;
99
+ }
100
+ catch (e) {
101
+ return e;
102
+ }
103
+ const generationResult = runGeneration(flags.output, database, tableProcessing(tables), enums);
104
+ if (generationResult instanceof PigeonError)
105
+ return generationResult;
106
+ }
70
107
  if (flags.guided) {
71
108
  const params = guided();
72
109
  const database = new Database(params.host, String(params.port), params.db, params.user, params.pass);
package/src/config.js CHANGED
@@ -1,7 +1,3 @@
1
- /*
2
- * Copyright (c) 2025 Andreas Michael
3
- * This software is under the Apache 2.0 License
4
- */
5
1
  import { PigeonError } from "./index.js";
6
2
  import fs from "node:fs";
7
3
  import * as path from "node:path";
package/src/index.js CHANGED
@@ -1,7 +1,3 @@
1
- /*
2
- * Copyright (c) 2025 Andreas Michael
3
- * This software is under the Apache 2.0 License
4
- */
5
1
  import { arrayMaker, consoleMessage, getCombinations, getType, nameBeautifier, queryMaker, runQuery, singularize, sleep, tabsInserter } from "./utils.js";
6
2
  import prompt from "prompt-sync";
7
3
  import fs from "node:fs";
@@ -30,7 +26,7 @@ export class Database {
30
26
  this.pass = pass;
31
27
  }
32
28
  }
33
- class Table {
29
+ export class Table {
34
30
  table_schema;
35
31
  table_name;
36
32
  columns = [];
@@ -46,7 +42,7 @@ class Table {
46
42
  this.unique = unique;
47
43
  }
48
44
  }
49
- class Enum {
45
+ export class Enum {
50
46
  name;
51
47
  labels;
52
48
  constructor(name, labels) {
@@ -54,7 +50,7 @@ class Enum {
54
50
  this.labels = labels;
55
51
  }
56
52
  }
57
- class ColumnQueryRow {
53
+ export class ColumnQueryRow {
58
54
  column_name;
59
55
  ordinal_position;
60
56
  column_default;
@@ -74,19 +70,21 @@ class ColumnQueryRow {
74
70
  this.identity_generation = identity_generation;
75
71
  }
76
72
  }
77
- class PrimaryKeyQueryRow {
73
+ export class PrimaryKeyQueryRow {
78
74
  column_name;
79
75
  constructor(column_name) {
80
76
  this.column_name = column_name;
81
77
  }
82
78
  }
83
- class ForeignKeyQueryRow {
79
+ export class ForeignKeyQueryRow {
80
+ local_schema;
84
81
  local_table;
85
82
  local_column;
86
83
  foreign_schema;
87
84
  foreign_table;
88
85
  foreign_column;
89
- constructor(local_table, local_column, foreign_schema, foreign_table, foreign_column) {
86
+ constructor(local_schema, local_table, local_column, foreign_schema, foreign_table, foreign_column) {
87
+ this.local_schema = local_schema;
90
88
  this.local_table = local_table;
91
89
  this.local_column = local_column;
92
90
  this.foreign_schema = foreign_schema;
@@ -94,7 +92,7 @@ class ForeignKeyQueryRow {
94
92
  this.foreign_column = foreign_column;
95
93
  }
96
94
  }
97
- class UniqueQueryRow {
95
+ export class UniqueQueryRow {
98
96
  columns;
99
97
  constructor(columns) {
100
98
  this.columns = columns;
@@ -152,14 +150,7 @@ export function guided() {
152
150
  const pass = input("Database Password: ");
153
151
  return { host, port, db, user, pass };
154
152
  }
155
- export async function queryDB(db) {
156
- const tableQuery = await runQuery(`SELECT table_schema, table_name
157
- FROM information_schema.tables
158
- WHERE table_type = 'BASE TABLE'
159
- AND table_schema NOT IN
160
- ('pg_catalog', 'information_schema');`, [], db);
161
- if (typeof tableQuery === "undefined")
162
- return new PigeonError(1, "", new Error("An SQL error has occurred."));
153
+ export async function enumsQuery(db) {
163
154
  const customTypeQuery = await runQuery(`SELECT t.oid, t.typname
164
155
  FROM pg_type t
165
156
  WHERE (t.typrelid = 0 OR t.typrelid IN (SELECT oid FROM pg_class WHERE relkind = 'c'))
@@ -181,6 +172,19 @@ export async function queryDB(db) {
181
172
  labels.push(enumLabel.enumlabel);
182
173
  enums.push(new Enum(type.typname, labels));
183
174
  }
175
+ return enums;
176
+ }
177
+ export async function queryDB(db) {
178
+ const tableQuery = await runQuery(`SELECT table_schema, table_name
179
+ FROM information_schema.tables
180
+ WHERE table_type = 'BASE TABLE'
181
+ AND table_schema NOT IN
182
+ ('pg_catalog', 'information_schema');`, [], db);
183
+ if (typeof tableQuery === "undefined")
184
+ return new PigeonError(1, "", new Error("An SQL error has occurred."));
185
+ const enums = await enumsQuery(db);
186
+ if (enums instanceof PigeonError)
187
+ return enums;
184
188
  const tables = [];
185
189
  for (const table of tableQuery.rows) {
186
190
  const columnQuery = await runQuery(`SELECT column_name,
@@ -383,7 +387,7 @@ function createClass(tableName, columns, primaryKey, foreignKeys) {
383
387
  text += "\t * A primary key representing the " + nameBeautifier(column.column_name) + " for the " + nameBeautifier(tableName) + " table.\n";
384
388
  else if (foreignKeys && foreignKeyIndex)
385
389
  text += "\t * A foreign key representing the " + nameBeautifier(column.column_name) + " for the " + nameBeautifier(tableName) + " table and referencing the " + nameBeautifier(foreignKeys[foreignKeyIndex].foreign_column) + " in the " + nameBeautifier(foreignKeys[foreignKeyIndex].foreign_table) + " table in the " + nameBeautifier(foreignKeys[foreignKeyIndex].foreign_schema) + " schema.\n";
386
- else if (column.column_name.toLowerCase().startsWith('is_'))
390
+ else if (column.column_name.toLowerCase().startsWith("is_"))
387
391
  text += "\t * Indicates whether this record in the table " + nameBeautifier(tableName) + " is currently " + nameBeautifier(column.column_name.slice(3)).toLowerCase() + ".\n";
388
392
  else
389
393
  text += "\t * The " + nameBeautifier(column.column_name) + " for the " + nameBeautifier(tableName) + " table.\n";
@@ -396,7 +400,7 @@ function createClass(tableName, columns, primaryKey, foreignKeys) {
396
400
  if (column.column_default)
397
401
  text += " = new Date()";
398
402
  else
399
- text += " = new Date(" + column.column_default.replace(' ', 'T') + ")";
403
+ text += " = new Date(" + column.column_default.replace(" ", "T") + ")";
400
404
  }
401
405
  else if (dataType === "number" || dataType === "boolean")
402
406
  text += " = " + column.column_default;
@@ -417,7 +421,7 @@ function createClass(tableName, columns, primaryKey, foreignKeys) {
417
421
  if (column.is_nullable === "YES")
418
422
  text += " | undefined";
419
423
  text += "} " + column.column_name;
420
- if (!column.column_name.toLowerCase().startsWith('is_'))
424
+ if (!column.column_name.toLowerCase().startsWith("is_"))
421
425
  text += " - The " + nameBeautifier(column.column_name) + " of the " + nameBeautifier(tableName) + " table. \n";
422
426
  else
423
427
  text += " - Indicates whether this record in the table " + nameBeautifier(tableName) + " is currently " + nameBeautifier(column.column_name.slice(3)).toLowerCase() + ".\n";
package/src/maps.js CHANGED
@@ -1,8 +1,4 @@
1
- /*
2
- * Copyright (c) 2024 Andreas Michael
3
- * This software is under the Apache 2.0 License
4
- */
5
- export const types = new Map([
1
+ export const jsTypes = new Map([
6
2
  ["bigint", "number"],
7
3
  ["int8", "number"],
8
4
  ["bigserial", "number"],
@@ -65,3 +61,62 @@ export const types = new Map([
65
61
  ["uuid", "string"],
66
62
  ["xml", "string"],
67
63
  ]);
64
+ export const udtTypes = new Map([
65
+ ["smallint", "int2"],
66
+ ["int2", "int2"],
67
+ ["integer", "int4"],
68
+ ["int", "int4"],
69
+ ["bigint", "int8"],
70
+ ["int8", "int8"],
71
+ ["real", "float4"],
72
+ ["float4", "float4"],
73
+ ["double precision", "float8"],
74
+ ["float8", "float8"],
75
+ ["numeric", "numeric"],
76
+ ["decimal", "numeric"],
77
+ ["money", "money"],
78
+ ["smallserial", "int2"],
79
+ ["serial", "int4"],
80
+ ["bigserial", "int8"],
81
+ ["character", "bpchar"],
82
+ ["char", "bpchar"],
83
+ ["character varying", "varchar"],
84
+ ["varchar", "varchar"],
85
+ ["text", "text"],
86
+ ["bytea", "bytea"],
87
+ ["bit", "bit"],
88
+ ["bit varying", "varbit"],
89
+ ["varbit", "varbit"],
90
+ ["boolean", "bool"],
91
+ ["bool", "bool"],
92
+ ["date", "date"],
93
+ ["time without time zone", "time"],
94
+ ["time", "time"],
95
+ ["time with time zone", "timetz"],
96
+ ["timetz", "timetz"],
97
+ ["timestamp without time zone", "timestamp"],
98
+ ["timestamp", "timestamp"],
99
+ ["timestamp with time zone", "timestamptz"],
100
+ ["timestamptz", "timestamptz"],
101
+ ["interval", "interval"],
102
+ ["box", "box"],
103
+ ["circle", "circle"],
104
+ ["line", "line"],
105
+ ["lseg", "lseg"],
106
+ ["path", "path"],
107
+ ["point", "point"],
108
+ ["polygon", "polygon"],
109
+ ["cidr", "cidr"],
110
+ ["inet", "inet"],
111
+ ["macaddr", "macaddr"],
112
+ ["macaddr8", "macaddr8"],
113
+ ["json", "json"],
114
+ ["jsonb", "jsonb"],
115
+ ["uuid", "uuid"],
116
+ ["tsquery", "tsquery"],
117
+ ["tsvector", "tsvector"],
118
+ ["pg_lsn", "pg_lsn"],
119
+ ["pg_snapshot", "pg_snapshot"],
120
+ ["txid_snapshot", "txid_snapshot"],
121
+ ["xml", "xml"],
122
+ ]);
package/src/pgAdmin.js ADDED
@@ -0,0 +1,117 @@
1
+ import { ColumnQueryRow, ForeignKeyQueryRow, PigeonError, PrimaryKeyQueryRow, Table, UniqueQueryRow } from "./index.js";
2
+ import { udtTypes } from "./maps.js";
3
+ function objectToArray(json) {
4
+ let arrayJSON = "";
5
+ let enteredObject = false;
6
+ let brackets = 0;
7
+ for (let i = 0; i < json.length; i++) {
8
+ if (json[i] === "{") {
9
+ if (!enteredObject) {
10
+ enteredObject = true;
11
+ arrayJSON += "[";
12
+ brackets++;
13
+ continue;
14
+ }
15
+ brackets++;
16
+ arrayJSON += json[i];
17
+ continue;
18
+ }
19
+ else if (json[i] === "}") {
20
+ brackets--;
21
+ if (brackets === 0)
22
+ return JSON.parse(arrayJSON.slice(0, -1) + "]");
23
+ arrayJSON += json[i];
24
+ if (brackets === 1)
25
+ arrayJSON += ",";
26
+ continue;
27
+ }
28
+ if (enteredObject && brackets !== 1)
29
+ arrayJSON += json[i];
30
+ }
31
+ }
32
+ export function getRelationshipsAndTables(json) {
33
+ if (!json.startsWith("{\"version\":"))
34
+ throw new PigeonError(1, "", new Error("The file specified is not an pgAdmin ERD file."));
35
+ let relationships = [];
36
+ let tables = [];
37
+ const relationshipsIndex = json.match(/"type":"diagram-links"/)?.index;
38
+ if (relationshipsIndex)
39
+ relationships = objectToArray(json.slice(relationshipsIndex));
40
+ const tablesIndex = json.match(/"type":"diagram-nodes"/)?.index;
41
+ if (tablesIndex)
42
+ tables = objectToArray(json.slice(tablesIndex));
43
+ return {
44
+ relationships: relationships,
45
+ tables: tables,
46
+ };
47
+ }
48
+ export function tableProcessing(tables) {
49
+ const pigeonTables = [];
50
+ for (const table of tables) {
51
+ const data = table.otherInfo.data;
52
+ const columns = [];
53
+ let ordinalPossition = 1;
54
+ for (const column of data.columns) {
55
+ let dataType = column.cltype;
56
+ let udtType;
57
+ udtType = udtTypes.get(dataType);
58
+ if (!udtType) {
59
+ if (dataType.endsWith("[]")) {
60
+ udtType = "_" + udtTypes.get(dataType.slice(0, -2));
61
+ dataType = "ARRAY";
62
+ }
63
+ else {
64
+ udtType = dataType;
65
+ dataType = "USER-DEFINED";
66
+ }
67
+ }
68
+ if (dataType === "smallserial" || dataType === "serial" || dataType === "bigserial") {
69
+ dataType = dataType.replace("serial", "int");
70
+ if (dataType === "int")
71
+ dataType = "integer";
72
+ column.defval = "nextval('" + data.name + "_" + column.name + "_seq'::regclass)";
73
+ }
74
+ let isNullable;
75
+ if (column.attnotnull)
76
+ isNullable = "NO";
77
+ else
78
+ isNullable = "YES";
79
+ let columnDefault;
80
+ if (column.defval !== "" && column.defval !== undefined)
81
+ columnDefault = column.defval;
82
+ else
83
+ columnDefault = null;
84
+ let identity;
85
+ if (column.colconstype === "i")
86
+ identity = "YES";
87
+ else
88
+ identity = "NO";
89
+ let identityGeneration = null;
90
+ if (identity === "YES") {
91
+ if (column.attidentity === "a")
92
+ identityGeneration = "ALWAYS";
93
+ if (column.attidentity === "b")
94
+ identityGeneration = "BY DEFAULT";
95
+ }
96
+ columns.push(new ColumnQueryRow(column.name, ordinalPossition, columnDefault, isNullable, dataType, udtType, identity, identityGeneration));
97
+ ordinalPossition++;
98
+ }
99
+ let primaryKey = undefined;
100
+ if (data.primary_key[0]?.columns[0]?.column)
101
+ primaryKey = new PrimaryKeyQueryRow(data.primary_key[0].columns[0].column);
102
+ const foreignKeys = [];
103
+ for (const foreignKey of data.foreign_key) {
104
+ for (const column of foreignKey.columns) {
105
+ const match = column.references_table_name.match(/(?:\((.*?)\))? ?(.*)/);
106
+ foreignKeys.push(new ForeignKeyQueryRow(data.schema, data.name, column.local_column, match[1] || data.schema, match[2], column.referenced));
107
+ }
108
+ }
109
+ const uniqueConstraints = [];
110
+ if (data.unique_constraint)
111
+ for (const uniqueConstraint of data.unique_constraint)
112
+ for (const column of uniqueConstraint.columns)
113
+ uniqueConstraints.push(column.column);
114
+ pigeonTables.push(new Table(data.schema, data.name, columns, primaryKey, foreignKeys, new UniqueQueryRow(uniqueConstraints)));
115
+ }
116
+ return pigeonTables;
117
+ }
package/src/utils.js CHANGED
@@ -1,9 +1,5 @@
1
- /*
2
- * Copyright (c) 2025 Andreas Michael
3
- * This software is under the Apache 2.0 License
4
- */
5
1
  import pg from "pg";
6
- import { types } from "./maps.js";
2
+ import { jsTypes } from "./maps.js";
7
3
  const { Client } = pg;
8
4
  export async function runQuery(command, parameters, db) {
9
5
  const client = new Client({
@@ -156,7 +152,7 @@ export function getType(dataType, udtName) {
156
152
  dataType = udtName.slice(1);
157
153
  isArray = true;
158
154
  }
159
- let foundDataType = types.get(dataType);
155
+ let foundDataType = jsTypes.get(dataType);
160
156
  if (foundDataType === undefined)
161
157
  foundDataType = nameBeautifier(udtName).replaceAll(" ", "");
162
158
  if (isArray)