@akshythere/pg-sync 0.0.0-stage → 1.0.1

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 pg-sync contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,4 +1,152 @@
1
- # Temporary Holding Version
1
+ # pg-sync
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
4
- If no other versions are published within 30 days, this package and version will be deleted.
3
+ > PostgreSQL schema introspection that turns your database into useful code and documentation.
4
+
5
+ <p align="left">
6
+ <a href="https://www.npmjs.com/package/@akshythere/pg-sync"><img src="https://img.shields.io/npm/v/@akshythere/pg-sync?color=cb3837&logo=npm" alt="npm version"></a>
7
+ <a href="https://www.typescriptlang.org/"><img src="https://img.shields.io/badge/TypeScript-5.0%2B-3178C6?logo=typescript&logoColor=white" alt="TypeScript 5.0+"></a>
8
+ <a href="https://www.postgresql.org/"><img src="https://img.shields.io/badge/PostgreSQL-12%2B-336791?logo=postgresql&logoColor=white" alt="PostgreSQL 12+"></a>
9
+ <a href="https://opensource.org/licenses/MIT"><img src="https://img.shields.io/badge/license-MIT-yellow.svg" alt="MIT license"></a>
10
+ </p>
11
+
12
+ `pg-sync` connects directly to PostgreSQL, reads its catalog metadata, and generates artifacts from a normalized schema model. Use it as `pg-sync` or the shorter `pgs` command.
13
+
14
+ ## Why pg-sync?
15
+
16
+ Keep generated types, framework schemas, and architecture documentation aligned with the database that actually runs your application.
17
+
18
+ - No ORM runtime dependency
19
+ - Direct queries against `pg_catalog` and `information_schema`
20
+ - Foreign keys and partitioned tables included in the schema model
21
+ - Prettier-formatted TypeScript output
22
+ - Interactive setup with a connection check
23
+
24
+ ## Outputs
25
+
26
+ | Target | Output | Best for |
27
+ | ------------ | -------------------------- | --------------------------------------------------- |
28
+ | `typescript` | `schema.ts` | Typed application code with zero runtime dependency |
29
+ | `frappe` | One JSON DocType per table | Frappe custom DocTypes and field configuration |
30
+ | `erd` | `ERD.md` with Mermaid | Schema reviews, technical docs, and onboarding |
31
+
32
+ The generators understand nullable columns, defaults, arrays, primary keys, foreign keys, and common PostgreSQL types. Frappe output also maps relationships to `Link` fields and common numeric, date, and boolean types to suitable field types.
33
+
34
+ ## How it works
35
+
36
+ ```mermaid
37
+ flowchart LR
38
+ A[(PostgreSQL)] --> B[Catalog introspection]
39
+ B --> C[Normalized schema IR]
40
+ C --> D[TypeScript]
41
+ C --> E[Frappe DocTypes]
42
+ C --> F[Mermaid ERD]
43
+ D --> G[(Local files)]
44
+ E --> G
45
+ F --> G
46
+ ```
47
+
48
+ ## Install
49
+
50
+ ### Use globally
51
+
52
+ ```bash
53
+ npm install -g @akshythere/pg-sync
54
+ pgs --help
55
+ ```
56
+
57
+ ### Build from source
58
+
59
+ ```bash
60
+ git clone https://github.com/akshitbhardwajhere/pg-sync.git
61
+ cd pg-sync
62
+ npm install
63
+ npm run build
64
+ npm link
65
+ ```
66
+
67
+ ## Quick start
68
+
69
+ ### 1. Create a configuration
70
+
71
+ The wizard asks for your connection details, verifies the database connection, and writes `pg-sync.config.json`.
72
+
73
+ ```bash
74
+ pgs init
75
+ ```
76
+
77
+ Choose TypeScript interfaces, Frappe DocTypes, or Mermaid ERD documentation. The wizard defaults to `./src/types`, `./doctypes`, or `./docs` respectively.
78
+
79
+ Example configuration:
80
+
81
+ ```json
82
+ {
83
+ "schema": "public",
84
+ "target": "typescript",
85
+ "outputDir": "./src/types"
86
+ }
87
+ ```
88
+
89
+ Set the connection string in `.env` as `DATABASE_URL=postgresql://user:password@localhost:5432/mydb`.
90
+
91
+ ### 2. Generate files
92
+
93
+ ```bash
94
+ pgs generate
95
+ ```
96
+
97
+ Command-line options override values from the config file:
98
+
99
+ ```bash
100
+ # TypeScript definitions
101
+ pgs generate -t typescript -o ./src/types
102
+
103
+ # Frappe DocTypes
104
+ pgs generate -t frappe -o ./doctypes
105
+
106
+ # Mermaid ERD documentation
107
+ pgs generate -t erd -o ./docs
108
+ ```
109
+
110
+ `DATABASE_URL` is also supported when no connection string is supplied through a flag or configuration file.
111
+
112
+ ## Configuration
113
+
114
+ Create `pg-sync.config.json` in the project root, or pass a custom file with `-c`.
115
+
116
+ | Property | Type | Default | Description |
117
+ | ------------------ | -------- | ---------------- | -------------------------------- |
118
+ | `connectionString` | `string` | `DATABASE_URL` | Optional config override |
119
+ | `schema` | `string` | `public` | PostgreSQL schema to inspect |
120
+ | `target` | `string` | `typescript` | `typescript`, `frappe`, or `erd` |
121
+ | `outputDir` | `string` | Target-dependent | Directory for generated files |
122
+
123
+ Useful flags:
124
+
125
+ ```text
126
+ -u, --url <url> PostgreSQL connection string
127
+ -s, --schema <name> Database schema to inspect
128
+ -o, --output <dir> Output directory
129
+ -t, --target <type> typescript | frappe | erd
130
+ -c, --config <file> Custom configuration path
131
+ ```
132
+
133
+ ## Development
134
+
135
+ ```bash
136
+ npm run build # Compile TypeScript
137
+ npm test # Run the test suite
138
+ npm run test:coverage # Generate coverage metrics
139
+ npm run test:watch # Watch tests during development
140
+ ```
141
+
142
+ Integration tests expect a reachable PostgreSQL instance. The CI workflow runs them against a PostgreSQL service container.
143
+
144
+ ## Requirements
145
+
146
+ - Node.js 18+
147
+ - PostgreSQL 12+
148
+ - A database user with permission to read the target schema metadata
149
+
150
+ ## License
151
+
152
+ Distributed under the [MIT License](https://opensource.org/licenses/MIT).
@@ -0,0 +1,74 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import ora from "ora";
4
+ import pc from "picocolors";
5
+ import { createDatabasePool } from "../engine/db.js";
6
+ import { introspectDatabase } from "../engine/introspector.js";
7
+ import { generateTypeScriptDefinitions } from "../generators/typescript.js";
8
+ import { generateFrappeDocTypes } from "../generators/frappe.js";
9
+ import { loadConfig } from "../utils/config.js";
10
+ import { generateMermaidERD } from "../generators/erd.js";
11
+ export async function runGenerate(options) {
12
+ const fileConfig = await loadConfig(options.configFile);
13
+ const connectionString = options.connectionString ||
14
+ process.env.DATABASE_URL ||
15
+ fileConfig?.connectionString;
16
+ if (!connectionString) {
17
+ console.error(pc.red("Error: No connection string provided."));
18
+ console.log(`Run ${pc.cyan("pg-sync init")} or supply ${pc.yellow("-u <connectionString>")}`);
19
+ process.exit(1);
20
+ }
21
+ const schema = options.schema || fileConfig?.schema || "public";
22
+ const target = options.target || fileConfig?.target || "typescript";
23
+ const rawOutputDir = options.outputDir ||
24
+ fileConfig?.outputDir ||
25
+ (target === "typescript" ? "./generated" : "./doctypes");
26
+ const outputDir = path.resolve(process.cwd(), rawOutputDir);
27
+ const spinner = ora("Connecting to PostgreSQL database...").start();
28
+ const pool = createDatabasePool(connectionString);
29
+ try {
30
+ spinner.text = "Introspecting database catalog and schema tables...";
31
+ const schemaIR = await introspectDatabase(pool, schema);
32
+ spinner.succeed(pc.green(`Introspection complete! Found ${schemaIR.tables.length} tables.`));
33
+ for (const table of schemaIR.tables) {
34
+ const partitionInfo = table.isPartitioned
35
+ ? pc.magenta(` [Partitioned: ${table.partitionStrategy}]`)
36
+ : "";
37
+ console.log(` • ${pc.bold(table.tableName)}${partitionInfo} (${table.columns.length} columns)`);
38
+ }
39
+ await fs.mkdir(outputDir, { recursive: true });
40
+ if (target === "typescript") {
41
+ const genSpinner = ora("Generating TypeScript definitions...").start();
42
+ const formattedCode = await generateTypeScriptDefinitions(schemaIR);
43
+ const outputPath = path.join(outputDir, "schema.ts");
44
+ await fs.writeFile(outputPath, formattedCode, "utf-8");
45
+ genSpinner.succeed(pc.green(`TypeScript types saved to: ${pc.bold(outputPath)}`));
46
+ }
47
+ else if (target === "frappe") {
48
+ const genSpinner = ora("Generating Frappe DocType schemas...").start();
49
+ const docTypes = await generateFrappeDocTypes(schemaIR);
50
+ for (const dt of docTypes) {
51
+ const docTypeDir = path.join(outputDir, dt.docTypeName.toLowerCase());
52
+ await fs.mkdir(docTypeDir, { recursive: true });
53
+ const filePath = path.join(docTypeDir, `${dt.docTypeName.toLowerCase()}.json`);
54
+ await fs.writeFile(filePath, dt.content, "utf-8");
55
+ }
56
+ genSpinner.succeed(pc.green(`Generated ${docTypes.length} Frappe DocType schemas in: ${pc.bold(outputDir)}`));
57
+ }
58
+ else if (target === "erd") {
59
+ const genSpinner = ora("Generating Mermaid ERD documentation...").start();
60
+ const erdMarkdown = generateMermaidERD(schemaIR);
61
+ const outputPath = path.join(outputDir, "ERD.md");
62
+ await fs.writeFile(outputPath, erdMarkdown, "utf-8");
63
+ genSpinner.succeed(pc.green(`ERD documentation saved to: ${pc.bold(outputPath)}`));
64
+ }
65
+ }
66
+ catch (err) {
67
+ spinner.fail(pc.red("Generation failed."));
68
+ console.error(pc.red(err.message));
69
+ process.exit(1);
70
+ }
71
+ finally {
72
+ await pool.end();
73
+ }
74
+ }
@@ -0,0 +1,62 @@
1
+ import { input, select } from "@inquirer/prompts";
2
+ import ora from "ora";
3
+ import pc from "picocolors";
4
+ import { createDatabasePool } from "../engine/db.js";
5
+ import { saveConfig } from "../utils/config.js";
6
+ export async function runInit() {
7
+ console.log(pc.bold(pc.cyan("\n⚙️ pg-sync Initialization Wizard\n")));
8
+ const connectionString = await input({
9
+ message: "Enter PostgreSQL connection string:",
10
+ default: process.env.DATABASE_URL,
11
+ validate: (val) => val.startsWith("postgresql://") || val.startsWith("postgres://")
12
+ ? true
13
+ : "Invalid PostgreSQL URI format",
14
+ });
15
+ const schema = await input({
16
+ message: "Schema to introspect:",
17
+ default: "public",
18
+ });
19
+ const target = await select({
20
+ message: "Select output generator target:",
21
+ choices: [
22
+ {
23
+ name: "TypeScript Interfaces (Universal / Next.js / Node)",
24
+ value: "typescript",
25
+ },
26
+ { name: "Frappe Custom DocType JSONs", value: "frappe" },
27
+ { name: "Mermaid ERD Documentation", value: "erd" },
28
+ ],
29
+ });
30
+ const outputDir = await input({
31
+ message: "Target output directory:",
32
+ default: target === "typescript"
33
+ ? "./src/types"
34
+ : target === "frappe"
35
+ ? "./doctypes"
36
+ : "./docs",
37
+ });
38
+ // Verify connection before saving
39
+ const spinner = ora("Testing database connection...").start();
40
+ const pool = createDatabasePool(connectionString);
41
+ try {
42
+ const client = await pool.connect();
43
+ client.release();
44
+ spinner.succeed(pc.green("Database connection verified successfully!"));
45
+ }
46
+ catch (err) {
47
+ spinner.fail(pc.red(`Failed to connect: ${err.message}`));
48
+ console.log(pc.yellow("Writing config anyway, but double-check your credentials.\n"));
49
+ }
50
+ finally {
51
+ await pool.end();
52
+ }
53
+ const config = {
54
+ connectionString,
55
+ schema,
56
+ outputDir,
57
+ target, // Ensure this says target, not format
58
+ };
59
+ const savedPath = await saveConfig(config);
60
+ console.log(pc.green(`\n✔ Configuration written to ${pc.bold(savedPath)}`));
61
+ console.log(`You can now run ${pc.cyan("pg-sync generate")} or ${pc.cyan("pgs generate")} to create your files.\n`);
62
+ }
@@ -0,0 +1,12 @@
1
+ import pg from "pg";
2
+ const { Pool } = pg;
3
+ export function createDatabasePool(connectionString) {
4
+ return new Pool({
5
+ connectionString,
6
+ ssl: connectionString.includes("localhost") ||
7
+ connectionString.includes("127.0.0.1")
8
+ ? false
9
+ : { rejectUnauthorized: false },
10
+ connectionTimeoutMillis: 5000,
11
+ });
12
+ }
@@ -0,0 +1,111 @@
1
+ export async function introspectDatabase(pool, targetSchema = "public") {
2
+ const client = await pool.connect();
3
+ try {
4
+ // 1. Fetch tables and partition info
5
+ const tablesQuery = `
6
+ SELECT
7
+ c.relname AS table_name,
8
+ c.relkind,
9
+ pt.partstrat AS partition_strategy
10
+ FROM pg_catalog.pg_class c
11
+ JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
12
+ LEFT JOIN pg_catalog.pg_partitioned_table pt ON pt.partrelid = c.oid
13
+ WHERE n.nspname = $1
14
+ AND c.relkind IN ('r', 'p')
15
+ ORDER BY c.relname;
16
+ `;
17
+ const { rows: tableRows } = await client.query(tablesQuery, [targetSchema]);
18
+ // 2. Fetch columns
19
+ const columnsQuery = `
20
+ SELECT
21
+ table_name,
22
+ column_name,
23
+ data_type,
24
+ udt_name,
25
+ is_nullable,
26
+ column_default
27
+ FROM information_schema.columns
28
+ WHERE table_schema = $1
29
+ ORDER BY table_name, ordinal_position;
30
+ `;
31
+ const { rows: columnRows } = await client.query(columnsQuery, [
32
+ targetSchema,
33
+ ]);
34
+ // 3. Fetch primary keys
35
+ const pkQuery = `
36
+ SELECT
37
+ kcu.table_name,
38
+ kcu.column_name
39
+ FROM information_schema.table_constraints tc
40
+ JOIN information_schema.key_column_usage kcu
41
+ ON tc.constraint_name = kcu.constraint_name
42
+ AND tc.table_schema = kcu.table_schema
43
+ WHERE tc.constraint_type = 'PRIMARY KEY'
44
+ AND tc.table_schema = $1;
45
+ `;
46
+ const { rows: pkRows } = await client.query(pkQuery, [targetSchema]);
47
+ // 4. Fetch foreign keys
48
+ const fkQuery = `
49
+ SELECT
50
+ kcu.table_name,
51
+ kcu.column_name,
52
+ ccu.table_name AS foreign_table_name,
53
+ ccu.column_name AS foreign_column_name
54
+ FROM information_schema.table_constraints tc
55
+ JOIN information_schema.key_column_usage kcu
56
+ ON tc.constraint_name = kcu.constraint_name
57
+ AND tc.table_schema = kcu.table_schema
58
+ JOIN information_schema.constraint_column_usage ccu
59
+ ON ccu.constraint_name = tc.constraint_name
60
+ AND ccu.table_schema = tc.table_schema
61
+ WHERE tc.constraint_type = 'FOREIGN KEY'
62
+ AND tc.table_schema = $1;
63
+ `;
64
+ const { rows: fkRows } = await client.query(fkQuery, [targetSchema]);
65
+ // Construct the Schema Intermediate Representation
66
+ const tables = tableRows.map((t) => {
67
+ const currentTableColumns = columnRows.filter((c) => c.table_name === t.table_name);
68
+ const currentTablePKs = pkRows
69
+ .filter((pk) => pk.table_name === t.table_name)
70
+ .map((pk) => pk.column_name);
71
+ const currentTableFKs = fkRows
72
+ .filter((fk) => fk.table_name === t.table_name)
73
+ .map((fk) => ({
74
+ columnName: fk.column_name,
75
+ foreignTableName: fk.foreign_table_name,
76
+ foreignColumnName: fk.foreign_column_name,
77
+ }));
78
+ const columns = currentTableColumns.map((col) => ({
79
+ name: col.column_name,
80
+ dbType: col.data_type,
81
+ udtName: col.udt_name,
82
+ isNullable: col.is_nullable === "YES",
83
+ isPrimaryKey: currentTablePKs.includes(col.column_name),
84
+ defaultValue: col.column_default,
85
+ }));
86
+ const partitionStrategyMap = {
87
+ r: "RANGE",
88
+ l: "LIST",
89
+ h: "HASH",
90
+ };
91
+ return {
92
+ tableName: t.table_name,
93
+ schema: targetSchema,
94
+ isPartitioned: t.relkind === "p",
95
+ partitionStrategy: t.partition_strategy
96
+ ? partitionStrategyMap[t.partition_strategy]
97
+ : undefined,
98
+ columns,
99
+ primaryKeys: currentTablePKs,
100
+ foreignKeys: currentTableFKs,
101
+ };
102
+ });
103
+ return {
104
+ extractedAt: new Date().toISOString(),
105
+ tables,
106
+ };
107
+ }
108
+ finally {
109
+ client.release();
110
+ }
111
+ }
@@ -0,0 +1,55 @@
1
+ export function mapPgTypeToTs(dbType, udtName) {
2
+ const normalizedType = dbType.toLowerCase();
3
+ const normalizedUdt = udtName.toLowerCase();
4
+ // Check for PostgreSQL array types (udt_name typically starts with an underscore, e.g., _text)
5
+ const isArray = normalizedType === "array" || normalizedUdt.startsWith("_");
6
+ const baseUdt = isArray && normalizedUdt.startsWith("_")
7
+ ? normalizedUdt.slice(1)
8
+ : normalizedUdt;
9
+ let tsType;
10
+ switch (baseUdt) {
11
+ case "int2":
12
+ case "int4":
13
+ case "int8":
14
+ case "float4":
15
+ case "float8":
16
+ case "numeric":
17
+ case "money":
18
+ tsType = "number";
19
+ break;
20
+ case "bool":
21
+ tsType = "boolean";
22
+ break;
23
+ case "varchar":
24
+ case "char":
25
+ case "text":
26
+ case "uuid":
27
+ case "citext":
28
+ tsType = "string";
29
+ break;
30
+ case "timestamp":
31
+ case "timestamptz":
32
+ case "date":
33
+ case "time":
34
+ case "timetz":
35
+ tsType = "Date | string";
36
+ break;
37
+ case "json":
38
+ case "jsonb":
39
+ tsType = "Record<string, unknown>";
40
+ break;
41
+ case "bytea":
42
+ tsType = "Buffer";
43
+ break;
44
+ default:
45
+ tsType = "unknown";
46
+ break;
47
+ }
48
+ return isArray ? `${tsType}[]` : tsType;
49
+ }
50
+ export function toPascalCase(str) {
51
+ return str
52
+ .split(/[-_]/)
53
+ .map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
54
+ .join("");
55
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,35 @@
1
+ export function generateMermaidERD(schemaIR) {
2
+ const lines = [];
3
+ lines.push("# Database Entity-Relationship Diagram");
4
+ lines.push(`> Extracted at: ${schemaIR.extractedAt}\n`);
5
+ lines.push("```mermaid");
6
+ lines.push("erDiagram");
7
+ // 1. Define entities and their attributes
8
+ for (const table of schemaIR.tables) {
9
+ // Skip partitioned child tables to keep the diagram clean
10
+ if (table.tableName.includes("_y20") || table.tableName.includes("_p20")) {
11
+ continue;
12
+ }
13
+ const tableNameUpper = table.tableName.toUpperCase();
14
+ lines.push(` ${tableNameUpper} {`);
15
+ for (const col of table.columns) {
16
+ const isFK = table.foreignKeys.some((fk) => fk.columnName === col.name);
17
+ const keyAnnotation = col.isPrimaryKey ? "PK" : isFK ? "FK" : "";
18
+ // Mermaid requires sanitized type identifiers (no spaces or special chars)
19
+ const cleanType = col.udtName.replace(/[^a-zA-Z0-9]/g, "");
20
+ lines.push(` ${cleanType} ${col.name} ${keyAnnotation}`.trimEnd());
21
+ }
22
+ lines.push(" }");
23
+ }
24
+ lines.push("");
25
+ // 2. Define relationships from foreign keys
26
+ for (const table of schemaIR.tables) {
27
+ for (const fk of table.foreignKeys) {
28
+ const source = fk.foreignTableName.toUpperCase();
29
+ const target = table.tableName.toUpperCase();
30
+ lines.push(` ${source} ||--o{ ${target} : "${fk.columnName}"`);
31
+ }
32
+ }
33
+ lines.push("```\n");
34
+ return lines.join("\n");
35
+ }
@@ -0,0 +1,101 @@
1
+ import prettier from "prettier";
2
+ import { toPascalCase } from "../engine/mapper.js";
3
+ function mapPgToFrappeFieldType(col, table) {
4
+ // Check if this column is a Foreign Key -> Map to Link fieldtype
5
+ const foreignKey = table.foreignKeys.find((fk) => fk.columnName === col.name);
6
+ if (foreignKey) {
7
+ return {
8
+ fieldtype: "Link",
9
+ options: toPascalCase(foreignKey.foreignTableName),
10
+ };
11
+ }
12
+ const baseUdt = col.udtName.toLowerCase();
13
+ switch (baseUdt) {
14
+ case "int2":
15
+ case "int4":
16
+ case "int8":
17
+ return { fieldtype: "Int" };
18
+ case "numeric":
19
+ case "money":
20
+ return { fieldtype: "Currency" };
21
+ case "float4":
22
+ case "float8":
23
+ return { fieldtype: "Float" };
24
+ case "bool":
25
+ return { fieldtype: "Check" };
26
+ case "text":
27
+ return { fieldtype: "Text" };
28
+ case "date":
29
+ return { fieldtype: "Date" };
30
+ case "time":
31
+ case "timetz":
32
+ return { fieldtype: "Time" };
33
+ case "timestamp":
34
+ case "timestamptz":
35
+ return { fieldtype: "Datetime" };
36
+ case "json":
37
+ case "jsonb":
38
+ return { fieldtype: "Code", options: "JSON" };
39
+ default:
40
+ return { fieldtype: "Data" };
41
+ }
42
+ }
43
+ function toLabel(name) {
44
+ return name
45
+ .split("_")
46
+ .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
47
+ .join(" ");
48
+ }
49
+ export async function generateFrappeDocTypes(schemaIR, moduleName = "Core") {
50
+ const generatedFiles = [];
51
+ for (const table of schemaIR.tables) {
52
+ // Skip internal partition child tables if partitioned
53
+ if (table.tableName.includes("_y20") || table.tableName.includes("_p20")) {
54
+ continue;
55
+ }
56
+ const docTypeName = toPascalCase(table.tableName);
57
+ const fields = [];
58
+ for (const col of table.columns) {
59
+ // Frappe provides its own primary key 'name' field
60
+ if (col.isPrimaryKey && (col.name === "id" || col.name === "name")) {
61
+ continue;
62
+ }
63
+ const { fieldtype, options } = mapPgToFrappeFieldType(col, table);
64
+ const fieldDef = {
65
+ fieldname: col.name,
66
+ label: toLabel(col.name),
67
+ fieldtype,
68
+ reqd: !col.isNullable && col.defaultValue === null ? 1 : 0,
69
+ };
70
+ if (options) {
71
+ fieldDef.options = options;
72
+ }
73
+ fields.push(fieldDef);
74
+ }
75
+ const docTypePayload = {
76
+ name: docTypeName,
77
+ doctype: "DocType",
78
+ module: moduleName,
79
+ custom: 1,
80
+ is_submittable: 0,
81
+ fields,
82
+ permissions: [
83
+ {
84
+ role: "System Manager",
85
+ read: 1,
86
+ write: 1,
87
+ create: 1,
88
+ },
89
+ ],
90
+ };
91
+ const formattedJson = await prettier.format(JSON.stringify(docTypePayload), {
92
+ parser: "json",
93
+ tabWidth: 2,
94
+ });
95
+ generatedFiles.push({
96
+ docTypeName,
97
+ content: formattedJson,
98
+ });
99
+ }
100
+ return generatedFiles;
101
+ }
@@ -0,0 +1,47 @@
1
+ import prettier from "prettier";
2
+ import { mapPgTypeToTs, toPascalCase } from "../engine/mapper.js";
3
+ export async function generateTypeScriptDefinitions(schemaIR) {
4
+ const buffer = [];
5
+ buffer.push("/**");
6
+ buffer.push(" * Auto-generated by pg-sync");
7
+ buffer.push(` * Extracted at: ${schemaIR.extractedAt}`);
8
+ buffer.push(" * Do not edit this file directly.");
9
+ buffer.push(" */\n");
10
+ for (const table of schemaIR.tables) {
11
+ const interfaceName = toPascalCase(table.tableName);
12
+ buffer.push("/**");
13
+ buffer.push(` * Table: ${table.schema}.${table.tableName}`);
14
+ if (table.isPartitioned) {
15
+ buffer.push(` * Partition Strategy: ${table.partitionStrategy}`);
16
+ }
17
+ if (table.foreignKeys.length > 0) {
18
+ buffer.push(" * Relations:");
19
+ for (const fk of table.foreignKeys) {
20
+ buffer.push(` * - ${fk.columnName} -> ${fk.foreignTableName}(${fk.foreignColumnName})`);
21
+ }
22
+ }
23
+ buffer.push(" */");
24
+ buffer.push(`export interface ${interfaceName} {`);
25
+ for (const col of table.columns) {
26
+ const tsType = mapPgTypeToTs(col.dbType, col.udtName);
27
+ const nullableFlag = col.isNullable ? " | null" : "";
28
+ const optionalFlag = col.isNullable || col.defaultValue !== null ? "?" : "";
29
+ buffer.push(` ${col.name}${optionalFlag}: ${tsType}${nullableFlag};`);
30
+ }
31
+ buffer.push("}\n");
32
+ }
33
+ // Generate a master schema lookup map
34
+ buffer.push("export interface DatabaseSchema {");
35
+ for (const table of schemaIR.tables) {
36
+ const interfaceName = toPascalCase(table.tableName);
37
+ buffer.push(` ${table.tableName}: ${interfaceName};`);
38
+ }
39
+ buffer.push("}\n");
40
+ const rawCode = buffer.join("\n");
41
+ return await prettier.format(rawCode, {
42
+ parser: "typescript",
43
+ singleQuote: true,
44
+ semi: true,
45
+ tabWidth: 2,
46
+ });
47
+ }
package/dist/index.js ADDED
@@ -0,0 +1,35 @@
1
+ #!/usr/bin/env node
2
+ import "dotenv/config";
3
+ import { Command } from "commander";
4
+ import pc from "picocolors";
5
+ import { runGenerate } from "./commands/generate.js";
6
+ import { runInit } from "./commands/init.js";
7
+ const program = new Command();
8
+ program
9
+ .name("pg-sync")
10
+ .description(pc.cyan("PostgreSQL Schema Introspector & Code Generator"))
11
+ .version("1.0.0");
12
+ program
13
+ .command("init")
14
+ .description("Launch interactive setup wizard and generate configuration")
15
+ .action(async () => {
16
+ await runInit();
17
+ });
18
+ program
19
+ .command("generate")
20
+ .description("Introspect database and generate output files")
21
+ .option("-u, --url <url>", "PostgreSQL connection string")
22
+ .option("-s, --schema <schema>", "Database schema to inspect")
23
+ .option("-o, --output <dir>", "Output directory for generated files")
24
+ .option("-t, --target <type>", "Output generator target (typescript | frappe)")
25
+ .option("-c, --config <file>", "Custom configuration file path")
26
+ .action(async (options) => {
27
+ await runGenerate({
28
+ connectionString: options.url,
29
+ schema: options.schema,
30
+ outputDir: options.output,
31
+ target: options.target,
32
+ configFile: options.config,
33
+ });
34
+ });
35
+ program.parse(process.argv);
@@ -0,0 +1,18 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ const CONFIG_FILENAME = "pg-sync.config.json";
4
+ export async function loadConfig(customPath) {
5
+ const targetPath = path.resolve(process.cwd(), customPath || CONFIG_FILENAME);
6
+ try {
7
+ const raw = await fs.readFile(targetPath, "utf-8");
8
+ return JSON.parse(raw);
9
+ }
10
+ catch {
11
+ return null;
12
+ }
13
+ }
14
+ export async function saveConfig(config, customPath) {
15
+ const targetPath = path.resolve(process.cwd(), customPath || CONFIG_FILENAME);
16
+ await fs.writeFile(targetPath, JSON.stringify(config, null, 2), "utf-8");
17
+ return targetPath;
18
+ }
package/package.json CHANGED
@@ -1,6 +1,48 @@
1
1
  {
2
2
  "name": "@akshythere/pg-sync",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "1.0.1",
4
+ "description": "PostgreSQL Schema Introspector",
5
+ "main": "dist/index.js",
6
+ "bin": {
7
+ "pg-sync": "./dist/index.js",
8
+ "pgs": "./dist/index.js"
9
+ },
10
+ "files": [
11
+ "dist",
12
+ "README.md"
13
+ ],
14
+ "publishConfig": {
15
+ "access": "public"
16
+ },
17
+ "scripts": {
18
+ "build": "tsc && chmod +x dist/index.js",
19
+ "start": "node dist/index.js",
20
+ "dev": "ts-node src/index.ts",
21
+ "test": "vitest run",
22
+ "test:watch": "vitest",
23
+ "test:coverage": "vitest run --coverage"
24
+ },
25
+ "keywords": [],
26
+ "author": "",
27
+ "license": "ISC",
28
+ "type": "module",
29
+ "dependencies": {
30
+ "@inquirer/prompts": "^8.7.3",
31
+ "commander": "^15.0.0",
32
+ "dotenv": "^18.0.6",
33
+ "ejs": "^7.0.1",
34
+ "ora": "^9.4.1",
35
+ "pg": "^8.23.1",
36
+ "picocolors": "^1.1.1",
37
+ "prettier": "^3.9.9"
38
+ },
39
+ "devDependencies": {
40
+ "@types/ejs": "^3.1.5",
41
+ "@types/node": "^26.6.4",
42
+ "@types/pg": "^8.23.1",
43
+ "@vitest/coverage-v8": "^5.0.3",
44
+ "ts-node": "^10.9.2",
45
+ "typescript": "^7.0.2",
46
+ "vitest": "^5.0.3"
47
+ }
48
+ }