@bungres/kit 0.3.0 → 0.5.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.
@@ -1,339 +0,0 @@
1
- import { resolve, join } from "node:path";
2
- import type { ResolvedConfig } from "../config.js";
3
-
4
- // ---------------------------------------------------------------------------
5
- // pull — introspect a live Postgres database and generate schema TypeScript
6
- // Reads information_schema + pg_indexes to reconstruct table definitions
7
- // ---------------------------------------------------------------------------
8
-
9
- interface PgColumn {
10
- table_name: string;
11
- column_name: string;
12
- data_type: string;
13
- udt_name: string;
14
- is_nullable: string;
15
- column_default: string | null;
16
- character_maximum_length: number | null;
17
- }
18
-
19
- interface PgConstraint {
20
- table_name: string;
21
- column_name: string;
22
- constraint_type: string;
23
- foreign_table: string | null;
24
- foreign_column: string | null;
25
- delete_rule: string | null;
26
- update_rule: string | null;
27
- }
28
-
29
- interface PgIndex {
30
- tablename: string;
31
- indexname: string;
32
- indexdef: string;
33
- }
34
-
35
- export async function introspectDb(
36
- sql: any, // Bun.SQL
37
- dbSchema: string
38
- ): Promise<Map<string, TableInfo>> {
39
- // Fetch columns
40
- const columns = await sql.unsafe(
41
- `SELECT
42
- c.table_name,
43
- c.column_name,
44
- c.data_type,
45
- c.udt_name,
46
- c.is_nullable,
47
- c.column_default,
48
- c.character_maximum_length
49
- FROM information_schema.columns c
50
- WHERE c.table_schema = $1
51
- AND c.table_name NOT IN ('__bungres_migrations', '__bungres_push')
52
- ORDER BY c.table_name, c.ordinal_position`,
53
- [dbSchema]
54
- ) as unknown as PgColumn[];
55
-
56
- // Fetch constraints (PK, FK, UNIQUE)
57
- const constraints = await sql.unsafe(
58
- `SELECT
59
- tc.table_name,
60
- kcu.column_name,
61
- tc.constraint_type,
62
- ccu.table_name AS foreign_table,
63
- ccu.column_name AS foreign_column,
64
- rc.delete_rule,
65
- rc.update_rule
66
- FROM information_schema.table_constraints tc
67
- JOIN information_schema.key_column_usage kcu
68
- ON tc.constraint_name = kcu.constraint_name
69
- AND tc.table_schema = kcu.table_schema
70
- LEFT JOIN information_schema.constraint_column_usage ccu
71
- ON tc.constraint_name = ccu.constraint_name
72
- AND tc.table_schema = ccu.table_schema
73
- LEFT JOIN information_schema.referential_constraints rc
74
- ON tc.constraint_name = rc.constraint_name
75
- AND tc.table_schema = rc.constraint_schema
76
- WHERE tc.table_schema = $1`,
77
- [dbSchema]
78
- ) as unknown as PgConstraint[];
79
-
80
- // Fetch indexes
81
- const indexes = await sql.unsafe(
82
- `SELECT tablename, indexname, indexdef
83
- FROM pg_indexes
84
- WHERE schemaname = $1`,
85
- [dbSchema]
86
- ) as unknown as PgIndex[];
87
-
88
- // Group by table
89
- return groupByTable(columns, constraints, indexes);
90
- }
91
-
92
- export async function runPull(config: ResolvedConfig): Promise<void> {
93
- console.log("@bungres/kit pull: introspecting database...");
94
-
95
- const sql = new Bun.SQL(config.dbUrl);
96
-
97
- try {
98
- const dbSchema = config.dbSchema;
99
- const tableMap = await introspectDb(sql, dbSchema);
100
-
101
- if (tableMap.size === 0) {
102
- console.log("No tables found in schema:", dbSchema);
103
- return;
104
- }
105
-
106
- // Generate TypeScript
107
- const outDir = resolve(config.outDir);
108
- await Bun.$`mkdir -p ${outDir}`.quiet();
109
-
110
- const outPath = join(outDir, "schema.ts");
111
- const code = generateSchemaTS(tableMap, dbSchema);
112
-
113
- await Bun.write(outPath, code);
114
- console.log(`Generated schema: ${outPath}`);
115
- console.log(` Tables: ${[...tableMap.keys()].join(", ")}`);
116
- } finally {
117
- await sql.end();
118
- }
119
- }
120
-
121
- // ---------------------------------------------------------------------------
122
- // Internals
123
- // ---------------------------------------------------------------------------
124
-
125
- export interface TableInfo {
126
- tableName: string;
127
- columns: Array<{
128
- name: string;
129
- dataType: string;
130
- udtName: string;
131
- isNullable: boolean;
132
- columnDefault: string | null;
133
- maxLength: number | null;
134
- isPrimary: boolean;
135
- isUnique: boolean;
136
- foreignTable: string | undefined;
137
- foreignColumn: string | undefined;
138
- deleteRule: string | undefined;
139
- updateRule: string | undefined;
140
- }>;
141
- indexes: PgIndex[];
142
- }
143
-
144
- function groupByTable(
145
- columns: PgColumn[],
146
- constraints: PgConstraint[],
147
- indexes: PgIndex[]
148
- ): Map<string, TableInfo> {
149
- const map = new Map<string, TableInfo>();
150
-
151
- for (const col of columns) {
152
- if (!map.has(col.table_name)) {
153
- map.set(col.table_name, {
154
- tableName: col.table_name,
155
- columns: [],
156
- indexes: indexes.filter((i) => i.tablename === col.table_name),
157
- });
158
- }
159
-
160
- const tableConstraints = constraints.filter(
161
- (c) => c.table_name === col.table_name && c.column_name === col.column_name
162
- );
163
-
164
- const pkConstraint = tableConstraints.find((c) => c.constraint_type === "PRIMARY KEY");
165
- const uniqueConstraint = tableConstraints.find((c) => c.constraint_type === "UNIQUE");
166
- const fkConstraint = tableConstraints.find((c) => c.constraint_type === "FOREIGN KEY");
167
-
168
- map.get(col.table_name)!.columns.push({
169
- name: col.column_name,
170
- dataType: col.data_type,
171
- udtName: col.udt_name,
172
- isNullable: col.is_nullable === "YES",
173
- columnDefault: col.column_default,
174
- maxLength: col.character_maximum_length,
175
- isPrimary: !!pkConstraint,
176
- isUnique: !!uniqueConstraint,
177
- foreignTable: fkConstraint?.foreign_table ?? undefined,
178
- foreignColumn: fkConstraint?.foreign_column ?? undefined,
179
- deleteRule: fkConstraint?.delete_rule ?? undefined,
180
- updateRule: fkConstraint?.update_rule ?? undefined,
181
- });
182
- }
183
-
184
- return map;
185
- }
186
-
187
- function generateSchemaTS(
188
- tableMap: Map<string, TableInfo>,
189
- dbSchema: string
190
- ): string {
191
- const lines: string[] = [
192
- `// Generated by @bungres/kit pull`,
193
- `// Do not edit manually — re-run \`bungres pull\` to regenerate`,
194
- `// Generated at: ${new Date().toISOString()}`,
195
- ``,
196
- `import {`,
197
- ` snakeCase,`,
198
- ` text, varchar, char, integer, bigint, smallint,`,
199
- ` serial, bigserial, boolean, real, doublePrecision,`,
200
- ` numeric, decimal, json, jsonb,`,
201
- ` timestamp, timestamptz, date, time, uuid,`,
202
- ` bytea, inet,`,
203
- ` textArray, integerArray, varcharArray, uuidArray,`,
204
- `} from "@bungres/orm";`,
205
- ``,
206
- ];
207
-
208
- for (const [, table] of tableMap) {
209
- const varName = toCamelCase(table.tableName);
210
- lines.push(`export const ${varName} = snakeCase.table("${table.tableName}", {`);
211
-
212
- for (const col of table.columns) {
213
- const colExpr = buildColumnExpression(col);
214
- lines.push(` ${toCamelCase(col.name)}: ${colExpr},`);
215
- }
216
-
217
- // Third argument: Table options (schema, indexes)
218
- const options: string[] = [];
219
- if (dbSchema !== "public") {
220
- options.push(`schema: "${dbSchema}"`);
221
- }
222
-
223
- if (table.indexes.length > 0) {
224
- const idxLines: string[] = [];
225
- for (const idx of table.indexes) {
226
- // Simple regex to parse `indexdef`
227
- // e.g. CREATE UNIQUE INDEX my_idx ON users USING btree (col1, col2) WHERE col3 = 1
228
- const m = idx.indexdef.match(/CREATE (UNIQUE )?INDEX (.+) ON (.+) USING (\w+) \((.+)\)(?: WHERE (.+))?/i);
229
- if (m) {
230
- const isUnique = !!m[1];
231
- const name = m[2]!.trim().replace(/^"|"$/g, "");
232
- // Skip auto-generated PK and UNIQUE constraint indexes which are already attached to columns
233
- if (name.endsWith("_pkey") || name.endsWith("_key")) continue;
234
-
235
- const using = m[4]!.toLowerCase();
236
- const cols = m[5]!.split(",").map(c => `"${c.trim().replace(/^"|"$/g, "")}"`);
237
- let idxStr = `{ name: "${name}", columns: [${cols.join(", ")}], using: "${using}"`;
238
- if (isUnique) idxStr += `, unique: true`;
239
- if (m[6]) idxStr += `, where: \`${m[6].trim()}\``;
240
- idxStr += ` }`;
241
- idxLines.push(idxStr);
242
- }
243
- }
244
- if (idxLines.length > 0) {
245
- options.push(`indexes: [\n ${idxLines.join(",\n ")}\n ]`);
246
- }
247
- }
248
-
249
- if (options.length > 0) {
250
- lines.push(`}, {`);
251
- lines.push(` ${options.join(",\n ")}`);
252
- lines.push(`});`);
253
- } else {
254
- lines.push(`});`);
255
- }
256
-
257
- lines.push(``);
258
- }
259
-
260
- return lines.join("\n");
261
- }
262
-
263
- function buildColumnExpression(col: TableInfo["columns"][number]): string {
264
- const opts: string[] = [];
265
-
266
- if (col.dataType === "character varying" || col.dataType === "character") {
267
- if (col.maxLength) opts.push(`length: ${col.maxLength}`);
268
- }
269
-
270
- if (col.isPrimary) opts.push(`primaryKey: true`);
271
- else if (!col.isNullable) opts.push(`notNull: true`);
272
-
273
- if (col.isUnique && !col.isPrimary) opts.push(`unique: true`);
274
-
275
- if (col.columnDefault !== null && !col.isPrimary) {
276
- if (col.columnDefault.includes("(")) {
277
- opts.push(`defaultRaw: "${col.columnDefault}"`);
278
- } else if (col.dataType === "boolean" || col.dataType.includes("int") || col.dataType.includes("numeric") || col.dataType.includes("real") || col.dataType.includes("double")) {
279
- opts.push(`default: ${col.columnDefault}`);
280
- } else {
281
- const cleaned = col.columnDefault.replace(/^'(.*)'::.*$/, "$1");
282
- opts.push(`default: "${cleaned}"`);
283
- }
284
- }
285
-
286
- if (col.foreignTable && col.foreignColumn) {
287
- const deleteRule = col.deleteRule?.toLowerCase().replace(" ", " ");
288
- const updateRule = col.updateRule?.toLowerCase().replace(" ", " ");
289
- let refOpts = `table: "${col.foreignTable}", column: "${col.foreignColumn}"`;
290
- if (deleteRule && deleteRule !== "no action") refOpts += `, onDelete: "${deleteRule}"`;
291
- if (updateRule && updateRule !== "no action") refOpts += `, onUpdate: "${updateRule}"`;
292
- opts.push(`references: { ${refOpts} }`);
293
- }
294
-
295
- let builderName = pgTypeToBungresBuilderName(col);
296
-
297
- if (opts.length > 0) {
298
- return `${builderName}({ ${opts.join(", ")} })`;
299
- }
300
- return `${builderName}()`;
301
- }
302
-
303
- function pgTypeToBungresBuilderName(col: TableInfo["columns"][number]): string {
304
- const dt = col.dataType;
305
-
306
- if (dt === "uuid") return "uuid";
307
- if (dt === "text") return "text";
308
- if (dt === "character varying") return "varchar";
309
- if (dt === "character") return "char";
310
- if (dt === "integer") return "integer";
311
- if (dt === "bigint") return "bigint";
312
- if (dt === "smallint") return "smallint";
313
- if (dt === "boolean") return "boolean";
314
- if (dt === "real") return "real";
315
- if (dt === "double precision") return "doublePrecision";
316
- if (dt === "numeric" || dt === "decimal") return "numeric";
317
- if (dt === "json") return "json";
318
- if (dt === "jsonb") return "jsonb";
319
- if (dt === "timestamp without time zone") return "timestamp";
320
- if (dt === "timestamp with time zone") return "timestamptz";
321
- if (dt === "date") return "date";
322
- if (dt === "time without time zone") return "time";
323
- if (dt === "bytea") return "bytea";
324
- if (dt === "inet") return "inet";
325
- if (dt === "USER-DEFINED" && col.udtName === "citext") return "text";
326
- if (dt === "ARRAY") {
327
- if (col.udtName === "_text") return "textArray";
328
- if (col.udtName === "_int4" || col.udtName === "_int8") return "integerArray";
329
- if (col.udtName === "_varchar") return "varcharArray";
330
- if (col.udtName === "_uuid") return "uuidArray";
331
- return "textArray";
332
- }
333
- // Fallback
334
- return "text";
335
- }
336
-
337
- function toCamelCase(str: string): string {
338
- return str.replace(/_([a-z])/g, (_, c) => c.toUpperCase());
339
- }
@@ -1,105 +0,0 @@
1
- import * as fs from "node:fs";
2
- import type { ResolvedConfig } from "../config.js";
3
- import { diffSchemas, type SchemaSnapshot } from "../differ.js";
4
- import { loadSchemas } from "../schema-loader.js";
5
-
6
- // ---------------------------------------------------------------------------
7
- // push — apply schema directly to the database (no migration files)
8
- // Uses a hidden table __bungres_push to track the previous schema state
9
- // and perform an exact diff.
10
- // ---------------------------------------------------------------------------
11
-
12
- export async function runPush(
13
- config: ResolvedConfig,
14
- opts: { force?: boolean } = {}
15
- ): Promise<void> {
16
- console.log("@bungres/kit push: loading schemas...");
17
-
18
- const schemas = await loadSchemas(config.schema);
19
-
20
- if (schemas.length === 0) {
21
- console.warn("No table definitions found. Check your schema glob pattern.");
22
- return;
23
- }
24
-
25
- const sql = new Bun.SQL(config.dbUrl);
26
-
27
- try {
28
- // Ensure our tracking table exists
29
- await sql.unsafe(`
30
- CREATE TABLE IF NOT EXISTS public.__bungres_push (
31
- id SERIAL PRIMARY KEY,
32
- snapshot JSONB NOT NULL
33
- );
34
- `);
35
-
36
- // Load previous snapshot from DB
37
- const rows = await sql.unsafe(`SELECT snapshot FROM public.__bungres_push ORDER BY id DESC LIMIT 1;`) as any[];
38
- let prevSnapshot: SchemaSnapshot = {};
39
- if (rows.length > 0) {
40
- prevSnapshot = typeof rows[0].snapshot === "string"
41
- ? JSON.parse(rows[0].snapshot)
42
- : rows[0].snapshot;
43
- }
44
-
45
- // Current snapshot from TypeScript
46
- const currentSnapshot: SchemaSnapshot = Object.fromEntries(
47
- schemas.map((s) => [s.config.name, s.config])
48
- );
49
-
50
- // Diff
51
- const diff = diffSchemas(prevSnapshot, currentSnapshot);
52
-
53
- if (diff.statements.length === 0) {
54
- console.log("\nNo schema changes detected. Database is up to date.");
55
- return;
56
- }
57
-
58
- console.log(`\nChanges to apply:`);
59
- for (const s of diff.summary) console.log(` + ${s}`);
60
-
61
- if (diff.warnings && diff.warnings.length > 0) {
62
- console.warn(`\n ⚠️ WARNING: Data Loss Detected!`);
63
- for (const w of diff.warnings) console.warn(` ! ${w}`);
64
- console.warn(`\n These changes will be immediately executed against the database!`);
65
- }
66
-
67
- if (!opts.force) {
68
- process.stdout.write("\nAre you sure you want to push these changes? Type YES to continue: ");
69
- const answer = await readLine();
70
- if (answer.trim().toLowerCase() !== "yes") {
71
- console.log("Aborted.");
72
- return;
73
- }
74
- }
75
-
76
- console.log(`\nPushing changes...`);
77
-
78
- // We must run extension first if needed
79
- await sql.unsafe(`CREATE EXTENSION IF NOT EXISTS "pgcrypto";`);
80
-
81
- // Execute diff statements
82
- for (const stmt of diff.statements) {
83
- if (config.verbose) {
84
- console.log(`-- ${stmt}`);
85
- }
86
- await sql.unsafe(stmt);
87
- }
88
-
89
- // Save new snapshot
90
- await sql.unsafe(
91
- `INSERT INTO public.__bungres_push (snapshot) VALUES ($1);`,
92
- [JSON.stringify(currentSnapshot)]
93
- );
94
-
95
- console.log("\nPush complete.");
96
- } finally {
97
- await sql.end();
98
- }
99
- }
100
-
101
- async function readLine(): Promise<string> {
102
- const buf = Buffer.alloc(256);
103
- const n = fs.readSync(0, buf, 0, 256, null);
104
- return buf.subarray(0, n).toString().trim();
105
- }
@@ -1,37 +0,0 @@
1
- import type { ResolvedConfig } from "../config.js";
2
- import { loadSchemas } from "../schema-loader.js";
3
- import { colorize } from "../utils/colors.js";
4
-
5
- // ---------------------------------------------------------------------------
6
- // refresh — truncate all tables to reset data without dropping schema
7
- // ---------------------------------------------------------------------------
8
-
9
- export async function runRefresh(config: ResolvedConfig): Promise<void> {
10
- const schemas = await loadSchemas(config.schema);
11
-
12
- if (schemas.length === 0) {
13
- console.warn("No table definitions found in schema files.");
14
- return;
15
- }
16
-
17
- const sql = new Bun.SQL(config.dbUrl);
18
-
19
- try {
20
- const tableNames = schemas.map(
21
- (s) => `"${s.config.schema ? s.config.schema + '"."' : ""}${s.config.name}"`
22
- );
23
-
24
- console.log(`Truncating ${tableNames.length} tables...`);
25
-
26
- // We use CASCADE to handle foreign key constraints automatically
27
- for (const entry of schemas) {
28
- const ddl = `TRUNCATE TABLE "${entry.config.name}" CASCADE;`;
29
- await sql.unsafe(ddl);
30
- console.log(colorize(` ✓ truncated ${entry.config.name}`, "green"));
31
- }
32
-
33
- console.log(colorize("\nRefresh complete. All tables are now empty.", "green"));
34
- } finally {
35
- await sql.end();
36
- }
37
- }
@@ -1,41 +0,0 @@
1
- import { resolve } from "node:path";
2
- import type { ResolvedConfig } from "../config.js";
3
- import { colorize } from "../utils/colors.js";
4
-
5
- // ---------------------------------------------------------------------------
6
- // seed — execute a seed script to populate the database
7
- // ---------------------------------------------------------------------------
8
-
9
- export async function runSeed(config: ResolvedConfig): Promise<void> {
10
- if (!config.seed) {
11
- console.log(colorize(`No seed file configured in bungres.config.ts`, "yellow"));
12
- return;
13
- }
14
-
15
- const seedPath = resolve(process.cwd(), config.seed);
16
-
17
- const file = Bun.file(seedPath);
18
- if (!(await file.exists())) {
19
- console.error(colorize(`Seed file not found at ${seedPath}`, "red"));
20
- process.exit(1);
21
- }
22
-
23
- console.log(colorize(`\nRunning seeder: ${config.seed}...`, "cyan"));
24
-
25
- // Run the seed script in a new process to ensure a clean state
26
- const proc = Bun.spawn(["bun", "run", seedPath], {
27
- cwd: process.cwd(),
28
- stdout: "inherit",
29
- stderr: "inherit",
30
- env: { ...process.env, DATABASE_URL: config.dbUrl } // Pass dbUrl implicitly just in case
31
- });
32
-
33
- const exitCode = await proc.exited;
34
-
35
- if (exitCode === 0) {
36
- console.log("\nSeed complete.");
37
- } else {
38
- console.error(`\nSeed failed with exit code ${exitCode}.`);
39
- process.exit(exitCode);
40
- }
41
- }
@@ -1,64 +0,0 @@
1
- import { resolve, join } from "node:path";
2
- import type { ResolvedConfig } from "../config.js";
3
- import { colorize } from "../utils/colors.js";
4
-
5
- // ---------------------------------------------------------------------------
6
- // status — show which migrations have been applied vs. pending
7
- // ---------------------------------------------------------------------------
8
-
9
- const MIGRATIONS_TABLE = "__bungres_migrations";
10
-
11
- export async function runStatus(config: ResolvedConfig): Promise<void> {
12
- const migrationsDir = resolve(config.migrationsDir);
13
- const sql = new Bun.SQL(config.dbUrl);
14
-
15
- try {
16
- // Check if migrations table exists
17
- const tableCheck = await sql.unsafe(
18
- `SELECT EXISTS (
19
- SELECT 1 FROM information_schema.tables
20
- WHERE table_name = $1
21
- ) AS exists`,
22
- [MIGRATIONS_TABLE]
23
- ) as Array<{ exists: boolean }>;
24
-
25
- const trackingExists = tableCheck[0]?.exists ?? false;
26
-
27
- // Discover migration files
28
- const glob = new Bun.Glob("*.sql");
29
- const files: string[] = [];
30
- for await (const file of glob.scan({ cwd: migrationsDir, absolute: false })) {
31
- files.push(file);
32
- }
33
- files.sort();
34
-
35
- if (files.length === 0) {
36
- console.log(colorize("No migration files found. Run `bungres generate` first.", "yellow"));
37
- return;
38
- }
39
-
40
- let appliedSet = new Set<string>();
41
- if (trackingExists) {
42
- const applied = await sql.unsafe(
43
- `SELECT name FROM "${MIGRATIONS_TABLE}" ORDER BY applied_at`
44
- ) as Array<{ name: string }>;
45
- appliedSet = new Set(applied.map((r) => r.name));
46
- }
47
-
48
- console.log(colorize("\nMigration status:\n", "cyan"));
49
- let pendingCount = 0;
50
-
51
- for (const file of files) {
52
- const isApplied = appliedSet.has(file);
53
- const status = isApplied ? colorize("✓ applied ", "green") : colorize("✗ pending ", "yellow");
54
- if (!isApplied) pendingCount++;
55
- console.log(` ${status} ${file}`);
56
- }
57
-
58
- console.log(
59
- `\n${colorize(appliedSet.size.toString(), "green")} applied, ${colorize(pendingCount.toString(), "yellow")} pending.\n`
60
- );
61
- } finally {
62
- await sql.end();
63
- }
64
- }