@bungres/kit 0.3.0 → 0.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/dist/cli.js CHANGED
@@ -1,47 +1,10 @@
1
1
  #!/usr/bin/env bun
2
2
  // @bun
3
3
 
4
- // src/config.ts
4
+ // src/schema-loader.ts
5
5
  import { resolve, join } from "path";
6
- var CONFIG_FILES = [
7
- "bungres.config.ts",
8
- "bungres.config.js",
9
- "bungres.config.mts",
10
- "bungres.config.mjs"
11
- ];
12
- async function loadConfig(cwd = process.cwd()) {
13
- let userConfig = {};
14
- for (const file of CONFIG_FILES) {
15
- const configPath = join(cwd, file);
16
- if (await Bun.file(configPath).exists()) {
17
- const mod = await import(resolve(configPath));
18
- userConfig = mod.default ?? mod;
19
- break;
20
- }
21
- }
22
- const dbUrl = userConfig.dbCredentials?.url ?? process.env["DATABASE_URL"] ?? process.env["POSTGRES_URL"] ?? "";
23
- if (!dbUrl) {
24
- console.error(`bungres: No database URL found.
25
- Add dbCredentials.url to bungres.config.ts or set DATABASE_URL.`);
26
- process.exit(1);
27
- }
28
- const out = userConfig.out ?? "./migrations";
29
- return {
30
- schema: userConfig.schema ?? "src/db/schema/**/*.ts",
31
- seed: userConfig.seed ?? "src/db/seed.ts",
32
- out,
33
- dbUrl,
34
- dbSchema: userConfig.dbSchema ?? "public",
35
- migrationsDir: out,
36
- outDir: "./src/db/generated",
37
- verbose: userConfig.verbose ?? false
38
- };
39
- }
40
-
41
- // src/commands/push.ts
42
- import * as fs from "fs";
43
6
 
44
- // ../../node_modules/.bun/@bungres+orm@0.2.0/node_modules/@bungres/orm/src/table.ts
7
+ // ../bungres-orm/src/schema/table.ts
45
8
  var TableConfigSymbol = Symbol.for("BungresTableConfig");
46
9
  function getTableConfig(table) {
47
10
  return table[TableConfigSymbol];
@@ -51,8 +14,13 @@ function camelToSnakeCase(str) {
51
14
  }
52
15
  function createTableFactory(casing) {
53
16
  return function(name, columns, extra) {
17
+ let schema;
18
+ if (extra && typeof extra !== "function") {
19
+ schema = extra.schema;
20
+ }
21
+ const qualifiedName = schema ? `"${schema}"."${name}"` : `"${name}"`;
54
22
  const columnConfigs = Object.fromEntries(Object.entries(columns).map(([key, config2]) => {
55
- const c = { ...config2 };
23
+ const c = { ...config2, tableName: qualifiedName };
56
24
  if (!c.name) {
57
25
  if (casing === "snake") {
58
26
  c.name = camelToSnakeCase(key);
@@ -62,16 +30,40 @@ function createTableFactory(casing) {
62
30
  }
63
31
  return [key, c];
64
32
  }));
65
- const schema = extra?.schema;
66
- const qualifiedName = schema ? `"${schema}"."${name}"` : `"${name}"`;
33
+ const indexes = [];
34
+ const checks = [];
35
+ const primaryKeys = [];
36
+ const foreignKeys = [];
37
+ if (extra) {
38
+ if (typeof extra === "function") {
39
+ const builders = extra(columnConfigs);
40
+ for (const builder of builders) {
41
+ const config2 = builder.build();
42
+ if (config2.type === "index")
43
+ indexes.push(config2);
44
+ else if (config2.type === "check")
45
+ checks.push(config2.condition);
46
+ else if (config2.type === "primaryKey")
47
+ primaryKeys.push(...config2.columns);
48
+ else if (config2.type === "foreignKey")
49
+ foreignKeys.push(config2);
50
+ }
51
+ } else {
52
+ if (extra.indexes)
53
+ indexes.push(...extra.indexes);
54
+ if (extra.checks)
55
+ checks.push(...extra.checks);
56
+ }
57
+ }
67
58
  const tableObj = {
68
59
  [TableConfigSymbol]: {
69
60
  name,
70
61
  schema,
71
62
  columns: columnConfigs,
72
- indexes: extra?.indexes ?? [],
73
- checks: extra?.checks ?? [],
74
- primaryKeys: extra?.primaryKeys ?? [],
63
+ indexes,
64
+ checks,
65
+ primaryKeys,
66
+ foreignKeys,
75
67
  qualifiedName
76
68
  }
77
69
  };
@@ -81,7 +73,7 @@ function createTableFactory(casing) {
81
73
  var table = createTableFactory("none");
82
74
  var snakeCase = { table: createTableFactory("snake") };
83
75
  var camelCase = { table: createTableFactory("camel") };
84
- // ../../node_modules/.bun/@bungres+orm@0.2.0/node_modules/@bungres/orm/src/column.ts
76
+ // ../bungres-orm/src/schema/columns.ts
85
77
  function buildColumn(dataType, nameOrOpts, opts) {
86
78
  let name = "";
87
79
  let options = opts;
@@ -135,7 +127,33 @@ var interval = col("interval");
135
127
  var inet = col("inet");
136
128
  var cidr = col("cidr");
137
129
  var macaddr = col("macaddr");
138
- // ../../node_modules/.bun/@bungres+orm@0.2.0/node_modules/@bungres/orm/src/sql.ts
130
+ var textArray = col("text[]");
131
+ var integerArray = col("integer[]");
132
+ var varcharArray = col("varchar[]");
133
+ var uuidArray = col("uuid[]");
134
+ // ../bungres-orm/src/core/sql.ts
135
+ function sql(strings, ...values) {
136
+ let query = "";
137
+ const params = [];
138
+ for (let i = 0;i < strings.length; i++) {
139
+ query += strings[i];
140
+ if (i < values.length) {
141
+ const val = values[i];
142
+ if (isSQLChunk(val)) {
143
+ const offset = params.length;
144
+ query += val.sql.replace(/\$(\d+)/g, (_, n) => `$${parseInt(n) + offset}`);
145
+ params.push(...val.params);
146
+ } else {
147
+ params.push(val);
148
+ query += `$${params.length}`;
149
+ }
150
+ }
151
+ }
152
+ return { sql: query, params };
153
+ }
154
+ function isSQLChunk(value) {
155
+ return typeof value === "object" && value !== null && "sql" in value && "params" in value && typeof value.sql === "string" && Array.isArray(value.params);
156
+ }
139
157
  function sqlJoin(chunks, separator = ", ") {
140
158
  const params = [];
141
159
  const parts = [];
@@ -146,12 +164,326 @@ function sqlJoin(chunks, separator = ", ") {
146
164
  }
147
165
  return { sql: parts.join(separator), params };
148
166
  }
149
- // ../../node_modules/.bun/@bungres+orm@0.2.0/node_modules/@bungres/orm/src/query.ts
167
+ function rawSql(query) {
168
+ return { sql: query, params: [] };
169
+ }
170
+ // ../bungres-orm/src/core/conditions.ts
171
+ var colName = (c) => {
172
+ if (typeof c === "string")
173
+ return `"${c}"`;
174
+ if (isSQLChunk(c))
175
+ return c.sql;
176
+ return c.tableName ? `${c.tableName}."${c.name}"` : `"${c.name}"`;
177
+ };
178
+ function isColumnConfig(val) {
179
+ return val !== null && typeof val === "object" && "name" in val && "dataType" in val;
180
+ }
181
+ var eq = (column, value) => {
182
+ if (isColumnConfig(value))
183
+ return sql`${rawSql(colName(column))} = ${rawSql(colName(value))}`;
184
+ if (isSQLChunk(value))
185
+ return sql`${rawSql(colName(column))} = ${value}`;
186
+ return sql`${rawSql(colName(column))} = ${value}`;
187
+ };
188
+ var ne = (column, value) => {
189
+ if (isColumnConfig(value))
190
+ return sql`${rawSql(colName(column))} != ${rawSql(colName(value))}`;
191
+ if (isSQLChunk(value))
192
+ return sql`${rawSql(colName(column))} != ${value}`;
193
+ return sql`${rawSql(colName(column))} != ${value}`;
194
+ };
195
+ var gt = (column, value) => {
196
+ if (isColumnConfig(value))
197
+ return sql`${rawSql(colName(column))} > ${rawSql(colName(value))}`;
198
+ if (isSQLChunk(value))
199
+ return sql`${rawSql(colName(column))} > ${value}`;
200
+ return sql`${rawSql(colName(column))} > ${value}`;
201
+ };
202
+ var gte = (column, value) => {
203
+ if (isColumnConfig(value))
204
+ return sql`${rawSql(colName(column))} >= ${rawSql(colName(value))}`;
205
+ if (isSQLChunk(value))
206
+ return sql`${rawSql(colName(column))} >= ${value}`;
207
+ return sql`${rawSql(colName(column))} >= ${value}`;
208
+ };
209
+ var lt = (column, value) => {
210
+ if (isColumnConfig(value))
211
+ return sql`${rawSql(colName(column))} < ${rawSql(colName(value))}`;
212
+ if (isSQLChunk(value))
213
+ return sql`${rawSql(colName(column))} < ${value}`;
214
+ return sql`${rawSql(colName(column))} < ${value}`;
215
+ };
216
+ var lte = (column, value) => {
217
+ if (isColumnConfig(value))
218
+ return sql`${rawSql(colName(column))} <= ${rawSql(colName(value))}`;
219
+ if (isSQLChunk(value))
220
+ return sql`${rawSql(colName(column))} <= ${value}`;
221
+ return sql`${rawSql(colName(column))} <= ${value}`;
222
+ };
223
+ var like = (column, pattern) => sql`${rawSql(colName(column))} LIKE ${pattern}`;
224
+ var ilike = (column, pattern) => sql`${rawSql(colName(column))} ILIKE ${pattern}`;
225
+ var isNull = (column) => rawSql(`${colName(column)} IS NULL`);
226
+ var isNotNull = (column) => rawSql(`${colName(column)} IS NOT NULL`);
227
+ var inArray = (column, values) => {
228
+ if (values.length === 0)
229
+ return rawSql("FALSE");
230
+ const params = values;
231
+ const placeholders = params.map((_, i) => `$${i + 1}`).join(", ");
232
+ return { sql: `${colName(column)} = ANY(ARRAY[${placeholders}])`, params };
233
+ };
234
+ var and = (...conditions) => sqlJoin(conditions, " AND ");
235
+ var or = (...conditions) => {
236
+ const joined = sqlJoin(conditions, " OR ");
237
+ return { sql: `(${joined.sql})`, params: joined.params };
238
+ };
239
+ var not = (condition) => ({
240
+ sql: `NOT (${condition.sql})`,
241
+ params: condition.params
242
+ });
243
+ var asc = (column) => sql`${rawSql(colName(column))} ASC`;
244
+ var desc = (column) => sql`${rawSql(colName(column))} DESC`;
245
+ function parseWhereObject(tableConfig, whereObj) {
246
+ const conditions = [];
247
+ for (const [key, val] of Object.entries(whereObj)) {
248
+ if (val === undefined)
249
+ continue;
250
+ if (key === "OR") {
251
+ const orConditions = val.map((o) => parseWhereObject(tableConfig, o));
252
+ if (orConditions.length > 0)
253
+ conditions.push(or(...orConditions));
254
+ continue;
255
+ }
256
+ if (key === "AND") {
257
+ const andConditions = val.map((o) => parseWhereObject(tableConfig, o));
258
+ if (andConditions.length > 0)
259
+ conditions.push(and(...andConditions));
260
+ continue;
261
+ }
262
+ if (key === "NOT") {
263
+ conditions.push(not(parseWhereObject(tableConfig, val)));
264
+ continue;
265
+ }
266
+ const colConfig = tableConfig.columns[key];
267
+ const columnArg = colConfig ?? key;
268
+ if (val !== null && typeof val === "object" && !Array.isArray(val) && !(val instanceof Date) && !(val instanceof Uint8Array)) {
269
+ const opVal = val;
270
+ if (opVal.eq !== undefined)
271
+ conditions.push(eq(columnArg, opVal.eq));
272
+ if (opVal.ne !== undefined)
273
+ conditions.push(ne(columnArg, opVal.ne));
274
+ if (opVal.gt !== undefined)
275
+ conditions.push(gt(columnArg, opVal.gt));
276
+ if (opVal.gte !== undefined)
277
+ conditions.push(gte(columnArg, opVal.gte));
278
+ if (opVal.lt !== undefined)
279
+ conditions.push(lt(columnArg, opVal.lt));
280
+ if (opVal.lte !== undefined)
281
+ conditions.push(lte(columnArg, opVal.lte));
282
+ if (opVal.in !== undefined)
283
+ conditions.push(inArray(columnArg, opVal.in));
284
+ if (opVal.like !== undefined)
285
+ conditions.push(like(columnArg, opVal.like));
286
+ if (opVal.ilike !== undefined)
287
+ conditions.push(ilike(columnArg, opVal.ilike));
288
+ if (opVal.isNull)
289
+ conditions.push(isNull(columnArg));
290
+ if (opVal.isNotNull)
291
+ conditions.push(isNotNull(columnArg));
292
+ } else {
293
+ if (val === null) {
294
+ conditions.push(isNull(columnArg));
295
+ } else {
296
+ conditions.push(eq(columnArg, val));
297
+ }
298
+ }
299
+ }
300
+ if (conditions.length === 0) {
301
+ return rawSql("TRUE");
302
+ }
303
+ return and(...conditions);
304
+ }
305
+ function parseOrderByObject(tableConfig, orderByObj) {
306
+ const parts = [];
307
+ for (const [key, dir] of Object.entries(orderByObj)) {
308
+ if (dir === undefined)
309
+ continue;
310
+ const colConfig = tableConfig.columns[key];
311
+ const columnArg = colConfig ?? key;
312
+ if (dir === "asc")
313
+ parts.push(asc(columnArg));
314
+ else if (dir === "desc")
315
+ parts.push(desc(columnArg));
316
+ }
317
+ return parts;
318
+ }
319
+
320
+ // ../bungres-orm/src/builders/delete.ts
321
+ class DeleteBuilder {
322
+ _table;
323
+ _executor;
324
+ _where = [];
325
+ _returning;
326
+ _comment;
327
+ constructor(table2, executor) {
328
+ this._table = table2;
329
+ this._executor = executor;
330
+ }
331
+ then(onfulfilled, onrejected) {
332
+ return this._executor.execute(this).then(onfulfilled, onrejected);
333
+ }
334
+ async single() {
335
+ return this._executor.executeSingle(this);
336
+ }
337
+ where(condition) {
338
+ if (condition && typeof condition === "object" && !("sql" in condition)) {
339
+ this._where.push(parseWhereObject(getTableConfig(this._table), condition));
340
+ } else {
341
+ this._where.push(condition);
342
+ }
343
+ return this;
344
+ }
345
+ returning(...columns) {
346
+ this._returning = columns.length > 0 ? columns : ["*"];
347
+ return this;
348
+ }
349
+ comment(tag) {
350
+ this._comment = tag;
351
+ return this;
352
+ }
353
+ toSQL() {
354
+ let query = `DELETE FROM ${getTableConfig(this._table).qualifiedName}`;
355
+ const params = [];
356
+ if (this._where.length > 0) {
357
+ const combined = sqlJoin(this._where, " AND ");
358
+ query += " WHERE " + combined.sql;
359
+ params.push(...combined.params);
360
+ }
361
+ if (this._returning) {
362
+ query += " RETURNING " + (this._returning[0] === "*" ? Object.keys(getTableConfig(this._table).columns).map((c) => `"${getTableConfig(this._table).columns[c].name}" AS "${c}"`).join(", ") : this._returning.map((c) => `"${getTableConfig(this._table).columns[c]?.name ?? c}" AS "${c}"`).join(", "));
363
+ }
364
+ return { sql: query, params };
365
+ }
366
+ }
367
+ // ../bungres-orm/src/builders/insert.ts
368
+ class InsertBuilder {
369
+ _table;
370
+ _executor;
371
+ _values = [];
372
+ _onConflict;
373
+ _returning;
374
+ _comment;
375
+ constructor(table2, executor) {
376
+ this._table = table2;
377
+ this._executor = executor;
378
+ }
379
+ then(onfulfilled, onrejected) {
380
+ return this._executor.execute(this).then(onfulfilled, onrejected);
381
+ }
382
+ async single() {
383
+ return this._executor.executeSingle(this);
384
+ }
385
+ values(data) {
386
+ if (Array.isArray(data)) {
387
+ this._values.push(...data);
388
+ } else {
389
+ this._values.push(data);
390
+ }
391
+ return this;
392
+ }
393
+ onConflictDoNothing() {
394
+ this._onConflict = "do nothing";
395
+ return this;
396
+ }
397
+ onConflict(clause) {
398
+ this._onConflict = clause;
399
+ return this;
400
+ }
401
+ returning(...columns) {
402
+ this._returning = columns.length > 0 ? columns : ["*"];
403
+ return this;
404
+ }
405
+ comment(tag) {
406
+ this._comment = tag;
407
+ return this;
408
+ }
409
+ toSQL() {
410
+ if (this._values.length === 0) {
411
+ throw new Error("InsertBuilder: no values provided");
412
+ }
413
+ const tConfig = getTableConfig(this._table);
414
+ const keySet = new Set;
415
+ for (const v of this._values) {
416
+ for (const k of Object.keys(v)) {
417
+ keySet.add(k);
418
+ }
419
+ }
420
+ const keys = Array.from(keySet);
421
+ if (keys.length === 0) {
422
+ return { sql: `INSERT INTO ${tConfig.qualifiedName} DEFAULT VALUES`, params: [] };
423
+ }
424
+ const columnsStr = keys.map((k) => `"${tConfig.columns[k]?.name ?? k}"`).join(", ");
425
+ const params = [];
426
+ const valuesStrs = this._values.map((v) => {
427
+ const vals = keys.map((k) => {
428
+ const val = v[k];
429
+ if (val && typeof val === "object" && "sql" in val && "params" in val) {
430
+ const chunk = val;
431
+ const offset = params.length;
432
+ params.push(...chunk.params);
433
+ return chunk.sql.replace(/\$(\d+)/g, (_, n) => `$${parseInt(n) + offset}`);
434
+ }
435
+ if (val && typeof val === "object" && !(val instanceof Date)) {
436
+ const colType = tConfig.columns[k]?.dataType;
437
+ if (colType === "json" || colType === "jsonb") {
438
+ params.push(val);
439
+ } else if (Array.isArray(val)) {
440
+ const pgArray = "{" + val.map((item) => {
441
+ if (item === null || item === undefined)
442
+ return "NULL";
443
+ if (typeof item === "string")
444
+ return '"' + item.replace(/"/g, "\\\"") + '"';
445
+ return typeof item === "object" ? '"' + JSON.stringify(item).replace(/"/g, "\\\"") + '"' : String(item);
446
+ }).join(",") + "}";
447
+ params.push(pgArray);
448
+ } else {
449
+ params.push(JSON.stringify(val));
450
+ }
451
+ return `$${params.length}`;
452
+ }
453
+ if (val === undefined)
454
+ return "DEFAULT";
455
+ params.push(val);
456
+ return `$${params.length}`;
457
+ });
458
+ return `(${vals.join(", ")})`;
459
+ });
460
+ let query = `INSERT INTO ${tConfig.qualifiedName} (${columnsStr}) VALUES ${valuesStrs.join(", ")}`;
461
+ if (this._onConflict) {
462
+ if (this._onConflict === "do nothing") {
463
+ query += " ON CONFLICT DO NOTHING";
464
+ } else {
465
+ const offset = params.length;
466
+ query += " " + this._onConflict.sql.replace(/\$(\d+)/g, (_, n) => `$${parseInt(n) + offset}`);
467
+ params.push(...this._onConflict.params);
468
+ }
469
+ }
470
+ if (this._returning) {
471
+ query += " RETURNING " + (this._returning[0] === "*" ? Object.keys(tConfig.columns).map((c) => `"${tConfig.columns[c].name}" AS "${c}"`).join(", ") : this._returning.map((c) => `"${tConfig.columns[c]?.name ?? c}" AS "${c}"`).join(", "));
472
+ }
473
+ if (this._comment) {
474
+ query += ` /* ${this._comment} */`;
475
+ }
476
+ return { sql: query, params };
477
+ }
478
+ }
479
+ // ../bungres-orm/src/builders/select.ts
150
480
  class SelectBuilder {
151
481
  _table;
152
482
  _executor;
153
483
  _where = [];
154
484
  _orderBy = [];
485
+ _groupBy = [];
486
+ _having = [];
155
487
  _limit;
156
488
  _offset;
157
489
  _select;
@@ -178,11 +510,27 @@ class SelectBuilder {
178
510
  return this;
179
511
  }
180
512
  where(condition) {
181
- this._where.push(condition);
513
+ if (condition && typeof condition === "object" && !("sql" in condition)) {
514
+ this._where.push(parseWhereObject(getTableConfig(this._table), condition));
515
+ } else {
516
+ this._where.push(condition);
517
+ }
182
518
  return this;
183
519
  }
184
520
  orderBy(column, dir = "asc") {
185
- this._orderBy.push({ column: typeof column === "string" ? column : column.name, dir });
521
+ this._orderBy.push({ column, dir });
522
+ return this;
523
+ }
524
+ groupBy(...columns) {
525
+ this._groupBy.push(...columns);
526
+ return this;
527
+ }
528
+ having(condition) {
529
+ if (condition && typeof condition === "object" && !("sql" in condition)) {
530
+ this._having.push(parseWhereObject(getTableConfig(this._table), condition));
531
+ } else {
532
+ this._having.push(condition);
533
+ }
186
534
  return this;
187
535
  }
188
536
  limit(n) {
@@ -197,35 +545,118 @@ class SelectBuilder {
197
545
  this._joins.push(rawClause);
198
546
  return this;
199
547
  }
548
+ innerJoin(table2, condition) {
549
+ this._joins.push(sql`INNER JOIN ${rawSql(getTableConfig(table2).qualifiedName)} ON ${condition}`);
550
+ return this;
551
+ }
552
+ leftJoin(table2, condition) {
553
+ this._joins.push(sql`LEFT JOIN ${rawSql(getTableConfig(table2).qualifiedName)} ON ${condition}`);
554
+ return this;
555
+ }
556
+ rightJoin(table2, condition) {
557
+ this._joins.push(sql`RIGHT JOIN ${rawSql(getTableConfig(table2).qualifiedName)} ON ${condition}`);
558
+ return this;
559
+ }
560
+ fullJoin(table2, condition) {
561
+ this._joins.push(sql`FULL JOIN ${rawSql(getTableConfig(table2).qualifiedName)} ON ${condition}`);
562
+ return this;
563
+ }
564
+ _buildSelection(selection, params) {
565
+ const parts = [];
566
+ for (const [alias, col2] of Object.entries(selection)) {
567
+ if (typeof col2 === "object" && col2 !== null) {
568
+ if ("sql" in col2 && "params" in col2) {
569
+ const chunk = col2;
570
+ const offset = params.length;
571
+ params.push(...chunk.params);
572
+ parts.push(`'${alias}', ${chunk.sql.replace(/\$(\d+)/g, (_, n) => `$${parseInt(n) + offset}`)}`);
573
+ } else if ("name" in col2 && "dataType" in col2) {
574
+ const c = col2;
575
+ parts.push(`'${alias}', ${c.tableName ? c.tableName + "." : ""}"${c.name}"`);
576
+ } else {
577
+ parts.push(`'${alias}', json_build_object(${this._buildSelection(col2, params)})`);
578
+ }
579
+ }
580
+ }
581
+ return parts.join(", ");
582
+ }
200
583
  toSQL() {
201
584
  let cols = "";
585
+ const params = [];
202
586
  if (this._selection) {
203
- cols = Object.entries(this._selection).map(([alias, col2]) => `"${col2.name}" AS "${alias}"`).join(", ");
587
+ cols = Object.entries(this._selection).map(([alias, col2]) => {
588
+ if (typeof col2 === "object" && col2 !== null && "sql" in col2 && "params" in col2) {
589
+ const chunk = col2;
590
+ const offset = params.length;
591
+ params.push(...chunk.params);
592
+ return `${chunk.sql.replace(/\$(\d+)/g, (_, n) => `$${parseInt(n) + offset}`)} AS "${alias}"`;
593
+ } else if (typeof col2 === "object" && col2 !== null && "name" in col2 && "dataType" in col2) {
594
+ const c = col2;
595
+ return `${c.tableName ? c.tableName + "." : ""}"${c.name}" AS "${alias}"`;
596
+ } else {
597
+ return `json_build_object(${this._buildSelection(col2, params)}) AS "${alias}"`;
598
+ }
599
+ }).join(", ");
204
600
  } else if (this._select && this._select.length > 0) {
601
+ const qName = getTableConfig(this._table).qualifiedName;
205
602
  cols = this._select.map((c) => {
206
603
  if (typeof c === "string") {
207
- return `"${getTableConfig(this._table).columns[c]?.name ?? c}" AS "${c}"`;
604
+ return `${qName}."${getTableConfig(this._table).columns[c]?.name ?? c}" AS "${c}"`;
208
605
  }
209
- return `"${c.name}" AS "${c.alias || c.name}"`;
606
+ return `${c.tableName ? c.tableName + "." : ""}"${c.name}" AS "${c.alias || c.name}"`;
210
607
  }).join(", ");
211
608
  } else {
212
- cols = Object.keys(getTableConfig(this._table).columns).map((c) => `"${getTableConfig(this._table).columns[c].name}" AS "${c}"`).join(", ");
609
+ const qName = getTableConfig(this._table).qualifiedName;
610
+ cols = Object.keys(getTableConfig(this._table).columns).map((c) => `${qName}."${getTableConfig(this._table).columns[c].name}" AS "${c}"`).join(", ");
213
611
  }
214
612
  let query = `SELECT ${cols} FROM ${getTableConfig(this._table).qualifiedName}`;
215
613
  if (this._joins.length > 0) {
216
- query += " " + this._joins.join(" ");
614
+ const joinChunks = this._joins.map((j) => typeof j === "string" ? rawSql(j) : j);
615
+ const combinedJoins = sqlJoin(joinChunks, " ");
616
+ const offset = params.length;
617
+ query += " " + combinedJoins.sql.replace(/\$(\d+)/g, (_, n) => `$${parseInt(n) + offset}`);
618
+ params.push(...combinedJoins.params);
217
619
  }
218
- const params = [];
219
620
  if (this._where.length > 0) {
220
621
  const combined = sqlJoin(this._where, " AND ");
221
- const offset = 0;
622
+ const offset = params.length;
222
623
  query += " WHERE " + combined.sql.replace(/\$(\d+)/g, (_, n) => `$${parseInt(n) + offset}`);
223
624
  params.push(...combined.params);
224
625
  }
626
+ if (this._groupBy.length > 0) {
627
+ const qName = getTableConfig(this._table).qualifiedName;
628
+ query += " GROUP BY " + this._groupBy.map((c) => {
629
+ if (typeof c === "string") {
630
+ const dbCol = getTableConfig(this._table).columns[c]?.name ?? c;
631
+ return `${qName}."${dbCol}"`;
632
+ } else if ("sql" in c && "params" in c) {
633
+ const offset = params.length;
634
+ params.push(...c.params);
635
+ return c.sql.replace(/\$(\d+)/g, (_, n) => `$${parseInt(n) + offset}`);
636
+ } else {
637
+ return `${c.tableName ? c.tableName + "." : ""}"${c.name}"`;
638
+ }
639
+ }).join(", ");
640
+ }
641
+ if (this._having.length > 0) {
642
+ const combined = sqlJoin(this._having, " AND ");
643
+ const offset = params.length;
644
+ query += " HAVING " + combined.sql.replace(/\$(\d+)/g, (_, n) => `$${parseInt(n) + offset}`);
645
+ params.push(...combined.params);
646
+ }
225
647
  if (this._orderBy.length > 0) {
648
+ const qName = getTableConfig(this._table).qualifiedName;
226
649
  query += " ORDER BY " + this._orderBy.map((o) => {
227
- const dbCol = getTableConfig(this._table).columns[o.column]?.name ?? o.column;
228
- return `"${dbCol}" ${o.dir.toUpperCase()}`;
650
+ if (typeof o.column === "string") {
651
+ const dbCol = getTableConfig(this._table).columns[o.column]?.name ?? o.column;
652
+ return `${qName}."${dbCol}" ${o.dir.toUpperCase()}`;
653
+ } else if ("sql" in o.column && "params" in o.column) {
654
+ const offset = params.length;
655
+ params.push(...o.column.params);
656
+ return `${o.column.sql.replace(/\$(\d+)/g, (_, n) => `$${parseInt(n) + offset}`)} ${o.dir.toUpperCase()}`;
657
+ } else {
658
+ return `${o.column.tableName ? o.column.tableName + "." : ""}"${o.column.name}" ${o.dir.toUpperCase()}`;
659
+ }
229
660
  }).join(", ");
230
661
  }
231
662
  if (this._limit !== undefined) {
@@ -254,12 +685,12 @@ class SelectBuilderIntermediate {
254
685
  return new SelectBuilder(table2, this._executor, this._selection);
255
686
  }
256
687
  }
257
-
258
- class InsertBuilder {
688
+ // ../bungres-orm/src/builders/update.ts
689
+ class UpdateBuilder {
259
690
  _table;
260
691
  _executor;
261
- _values = [];
262
- _onConflict;
692
+ _set = {};
693
+ _where = [];
263
694
  _returning;
264
695
  _comment;
265
696
  constructor(table2, executor) {
@@ -272,17 +703,16 @@ class InsertBuilder {
272
703
  async single() {
273
704
  return this._executor.executeSingle(this);
274
705
  }
275
- values(data) {
276
- const rows = Array.isArray(data) ? data : [data];
277
- this._values.push(...rows);
278
- return this;
279
- }
280
- onConflictDoNothing() {
281
- this._onConflict = "do nothing";
706
+ set(data) {
707
+ this._set = { ...this._set, ...data };
282
708
  return this;
283
709
  }
284
- onConflictDoUpdate(clause) {
285
- this._onConflict = clause;
710
+ where(condition) {
711
+ if (condition && typeof condition === "object" && !("sql" in condition)) {
712
+ this._where.push(parseWhereObject(getTableConfig(this._table), condition));
713
+ } else {
714
+ this._where.push(condition);
715
+ }
286
716
  return this;
287
717
  }
288
718
  returning(...columns) {
@@ -294,28 +724,46 @@ class InsertBuilder {
294
724
  return this;
295
725
  }
296
726
  toSQL() {
297
- if (this._values.length === 0) {
298
- throw new Error("InsertBuilder: no values provided");
727
+ const entries = Object.entries(this._set);
728
+ if (entries.length === 0) {
729
+ throw new Error("UpdateBuilder: no fields to set");
299
730
  }
300
- const keys = Array.from(new Set(this._values.flatMap((v) => Object.keys(v))));
301
731
  const params = [];
302
- const rowPlaceholders = [];
303
- for (const row of this._values) {
304
- const placeholders = [];
305
- for (const key of keys) {
306
- params.push(row[key] ?? null);
307
- placeholders.push(`$${params.length}`);
732
+ const setClauses = entries.map(([key, value]) => {
733
+ const dbCol = getTableConfig(this._table).columns[key]?.name ?? key;
734
+ if (value && typeof value === "object" && "sql" in value && "params" in value) {
735
+ const chunk = value;
736
+ const offset = params.length;
737
+ params.push(...chunk.params);
738
+ return `"${dbCol}" = ${chunk.sql.replace(/\$(\d+)/g, (_, n) => `$${parseInt(n) + offset}`)}`;
308
739
  }
309
- rowPlaceholders.push(`(${placeholders.join(", ")})`);
310
- }
311
- const colList = keys.map((k) => `"${getTableConfig(this._table).columns[k]?.name ?? k}"`).join(", ");
312
- let query = `INSERT INTO ${getTableConfig(this._table).qualifiedName} (${colList}) VALUES ${rowPlaceholders.join(", ")}`;
313
- if (this._onConflict === "do nothing") {
314
- query += " ON CONFLICT DO NOTHING";
315
- } else if (this._onConflict && typeof this._onConflict === "object") {
740
+ if (value && typeof value === "object" && !(value instanceof Date)) {
741
+ const colType = getTableConfig(this._table).columns[key]?.dataType;
742
+ if (colType === "json" || colType === "jsonb") {
743
+ params.push(value);
744
+ } else if (Array.isArray(value)) {
745
+ const pgArray = "{" + value.map((item) => {
746
+ if (item === null || item === undefined)
747
+ return "NULL";
748
+ if (typeof item === "string")
749
+ return '"' + item.replace(/"/g, "\\\"") + '"';
750
+ return typeof item === "object" ? '"' + JSON.stringify(item).replace(/"/g, "\\\"") + '"' : String(item);
751
+ }).join(",") + "}";
752
+ params.push(pgArray);
753
+ } else {
754
+ params.push(JSON.stringify(value));
755
+ }
756
+ return `"${dbCol}" = $${params.length}`;
757
+ }
758
+ params.push(value);
759
+ return `"${dbCol}" = $${params.length}`;
760
+ });
761
+ let query = `UPDATE ${getTableConfig(this._table).qualifiedName} SET ${setClauses.join(", ")}`;
762
+ if (this._where.length > 0) {
763
+ const combined = sqlJoin(this._where, " AND ");
316
764
  const offset = params.length;
317
- query += " ON CONFLICT " + this._onConflict.sql.replace(/\$(\d+)/g, (_, n) => `$${parseInt(n) + offset}`);
318
- params.push(...this._onConflict.params);
765
+ query += " WHERE " + combined.sql.replace(/\$(\d+)/g, (_, n) => `$${parseInt(n) + offset}`);
766
+ params.push(...combined.params);
319
767
  }
320
768
  if (this._returning) {
321
769
  query += " RETURNING " + (this._returning[0] === "*" ? Object.keys(getTableConfig(this._table).columns).map((c) => `"${getTableConfig(this._table).columns[c].name}" AS "${c}"`).join(", ") : this._returning.map((c) => `"${getTableConfig(this._table).columns[c]?.name ?? c}" AS "${c}"`).join(", "));
@@ -323,109 +771,8 @@ class InsertBuilder {
323
771
  return { sql: query, params };
324
772
  }
325
773
  }
326
-
327
- class UpdateBuilder {
328
- _table;
329
- _executor;
330
- _set = {};
331
- _where = [];
332
- _returning;
333
- _comment;
334
- constructor(table2, executor) {
335
- this._table = table2;
336
- this._executor = executor;
337
- }
338
- then(onfulfilled, onrejected) {
339
- return this._executor.execute(this).then(onfulfilled, onrejected);
340
- }
341
- async single() {
342
- return this._executor.executeSingle(this);
343
- }
344
- set(data) {
345
- this._set = { ...this._set, ...data };
346
- return this;
347
- }
348
- where(condition) {
349
- this._where.push(condition);
350
- return this;
351
- }
352
- returning(...columns) {
353
- this._returning = columns.length > 0 ? columns : ["*"];
354
- return this;
355
- }
356
- comment(tag) {
357
- this._comment = tag;
358
- return this;
359
- }
360
- toSQL() {
361
- const entries = Object.entries(this._set);
362
- if (entries.length === 0) {
363
- throw new Error("UpdateBuilder: no fields to set");
364
- }
365
- const params = [];
366
- const setClauses = entries.map(([key, value]) => {
367
- params.push(value);
368
- const dbCol = getTableConfig(this._table).columns[key]?.name ?? key;
369
- return `"${dbCol}" = $${params.length}`;
370
- });
371
- let query = `UPDATE ${getTableConfig(this._table).qualifiedName} SET ${setClauses.join(", ")}`;
372
- if (this._where.length > 0) {
373
- const combined = sqlJoin(this._where, " AND ");
374
- const offset = params.length;
375
- query += " WHERE " + combined.sql.replace(/\$(\d+)/g, (_, n) => `$${parseInt(n) + offset}`);
376
- params.push(...combined.params);
377
- }
378
- if (this._returning) {
379
- query += " RETURNING " + (this._returning[0] === "*" ? Object.keys(getTableConfig(this._table).columns).map((c) => `"${getTableConfig(this._table).columns[c].name}" AS "${c}"`).join(", ") : this._returning.map((c) => `"${getTableConfig(this._table).columns[c]?.name ?? c}" AS "${c}"`).join(", "));
380
- }
381
- return { sql: query, params };
382
- }
383
- }
384
-
385
- class DeleteBuilder {
386
- _table;
387
- _executor;
388
- _where = [];
389
- _returning;
390
- _comment;
391
- constructor(table2, executor) {
392
- this._table = table2;
393
- this._executor = executor;
394
- }
395
- then(onfulfilled, onrejected) {
396
- return this._executor.execute(this).then(onfulfilled, onrejected);
397
- }
398
- async single() {
399
- return this._executor.executeSingle(this);
400
- }
401
- where(condition) {
402
- this._where.push(condition);
403
- return this;
404
- }
405
- returning(...columns) {
406
- this._returning = columns.length > 0 ? columns : ["*"];
407
- return this;
408
- }
409
- comment(tag) {
410
- this._comment = tag;
411
- return this;
412
- }
413
- toSQL() {
414
- let query = `DELETE FROM ${getTableConfig(this._table).qualifiedName}`;
415
- const params = [];
416
- if (this._where.length > 0) {
417
- const combined = sqlJoin(this._where, " AND ");
418
- query += " WHERE " + combined.sql;
419
- params.push(...combined.params);
420
- }
421
- if (this._returning) {
422
- query += " RETURNING " + (this._returning[0] === "*" ? Object.keys(getTableConfig(this._table).columns).map((c) => `"${getTableConfig(this._table).columns[c].name}" AS "${c}"`).join(", ") : this._returning.map((c) => `"${getTableConfig(this._table).columns[c]?.name ?? c}" AS "${c}"`).join(", "));
423
- }
424
- return { sql: query, params };
425
- }
426
- }
427
- // ../../node_modules/.bun/@bungres+orm@0.2.0/node_modules/@bungres/orm/src/relational.ts
428
- var _relationsCache = new WeakMap;
774
+ // ../bungres-orm/src/builders/relational.ts
775
+ var _relationsCache = new WeakMap;
429
776
 
430
777
  class RelationalQueryBuilder {
431
778
  _executor;
@@ -508,7 +855,7 @@ class RelationalQueryBuilder {
508
855
  const ones = {};
509
856
  const manys = {};
510
857
  const manyToManys = {};
511
- for (const [colName, col2] of Object.entries(tConfig.columns)) {
858
+ for (const [colName2, col2] of Object.entries(tConfig.columns)) {
512
859
  if (col2.references) {
513
860
  const ref = col2.references;
514
861
  const relName = ref.relationName || ref.table;
@@ -517,7 +864,7 @@ class RelationalQueryBuilder {
517
864
  }
518
865
  for (const [otherName, otherTable] of Object.entries(this._schema)) {
519
866
  const otherConfig = otherTable[TableConfigSymbol];
520
- for (const [colName, col2] of Object.entries(otherConfig.columns)) {
867
+ for (const [colName2, col2] of Object.entries(otherConfig.columns)) {
521
868
  if (col2.references && col2.references.table === tableName) {
522
869
  const ref = col2.references;
523
870
  const backRelName = ref.backRelationName || otherName;
@@ -556,10 +903,15 @@ class RelationalQueryBuilder {
556
903
  const jsonFields = [];
557
904
  const lateralJoins = [];
558
905
  const columnsConfig = args.columns;
906
+ const hasTrue = columnsConfig ? Object.values(columnsConfig).some((v) => v === true) : false;
559
907
  for (const [colKey, colConfig] of Object.entries(tableConfig.columns)) {
560
908
  if (columnsConfig) {
561
- if (columnsConfig[colKey] !== true) {
562
- continue;
909
+ if (hasTrue) {
910
+ if (columnsConfig[colKey] !== true)
911
+ continue;
912
+ } else {
913
+ if (columnsConfig[colKey] === false)
914
+ continue;
563
915
  }
564
916
  }
565
917
  jsonFields.push(`'${colKey}', "${alias}"."${colConfig.name}"`);
@@ -618,10 +970,16 @@ class RelationalQueryBuilder {
618
970
  if (joinCondition) {
619
971
  fromSql += ` WHERE ${joinCondition}`;
620
972
  }
621
- if (args.where && args.where.sql) {
973
+ if (args.where) {
622
974
  const offset = params.length;
623
- fromSql += (joinCondition ? " AND " : " WHERE ") + args.where.sql.replace(/\$(\d+)/g, (_, n) => `$${parseInt(n) + offset}`);
624
- params.push(...args.where.params);
975
+ let whereChunk = args.where;
976
+ if (args.where && !args.where.sql) {
977
+ whereChunk = parseWhereObject(tableConfig, args.where);
978
+ }
979
+ if (whereChunk && whereChunk.sql) {
980
+ fromSql += (joinCondition ? " AND " : " WHERE ") + whereChunk.sql.replace(/\$(\d+)/g, (_, n) => `$${parseInt(n) + offset}`);
981
+ params.push(...whereChunk.params);
982
+ }
625
983
  }
626
984
  if (args.orderBy) {
627
985
  if (typeof args.orderBy === "string") {
@@ -630,6 +988,15 @@ class RelationalQueryBuilder {
630
988
  const offset = params.length;
631
989
  fromSql += ` ORDER BY ` + args.orderBy.sql.replace(/\$(\d+)/g, (_, n) => `$${parseInt(n) + offset}`);
632
990
  params.push(...args.orderBy.params);
991
+ } else {
992
+ const chunks = parseOrderByObject(tableConfig, args.orderBy);
993
+ if (chunks.length > 0) {
994
+ fromSql += ` ORDER BY ` + chunks.map((c) => {
995
+ const offset = params.length;
996
+ params.push(...c.params);
997
+ return c.sql.replace(/\$(\d+)/g, (_, n) => `$${parseInt(n) + offset}`);
998
+ }).join(", ");
999
+ }
633
1000
  }
634
1001
  }
635
1002
  if (args.limit !== undefined) {
@@ -651,7 +1018,7 @@ class RelationalQueryBuilder {
651
1018
  }
652
1019
  }
653
1020
 
654
- // ../../node_modules/.bun/@bungres+orm@0.2.0/node_modules/@bungres/orm/src/db.ts
1021
+ // ../bungres-orm/src/core/db.ts
655
1022
  function parseDBName(url) {
656
1023
  try {
657
1024
  return new URL(url).pathname.slice(1);
@@ -817,7 +1184,7 @@ function createDB(config2) {
817
1184
  }
818
1185
  return db2;
819
1186
  }
820
- // ../../node_modules/.bun/@bungres+orm@0.2.0/node_modules/@bungres/orm/src/ddl.ts
1187
+ // ../bungres-orm/src/ddl.ts
821
1188
  function generateCreateTable(config2, ifNotExists = true) {
822
1189
  const tableName = config2.schema ? `"${config2.schema}"."${config2.name}"` : `"${config2.name}"`;
823
1190
  const exists = ifNotExists ? " IF NOT EXISTS" : "";
@@ -936,6 +1303,177 @@ function generateDropColumn(tableName, schema, columnName) {
936
1303
  const tbl = schema ? `"${schema}"."${tableName}"` : `"${tableName}"`;
937
1304
  return `ALTER TABLE ${tbl} DROP COLUMN IF EXISTS "${columnName}";`;
938
1305
  }
1306
+ // src/schema-loader.ts
1307
+ async function loadSchemas(patterns, cwd = process.cwd()) {
1308
+ const globs = Array.isArray(patterns) ? patterns : [patterns];
1309
+ const entries = [];
1310
+ for (const pattern of globs) {
1311
+ const glob = new Bun.Glob(pattern);
1312
+ for await (const file of glob.scan({ cwd, absolute: false })) {
1313
+ const absPath = resolve(join(cwd, file));
1314
+ const mod = await import(absPath);
1315
+ for (const [exportName, value] of Object.entries(mod)) {
1316
+ if (isTable(value)) {
1317
+ entries.push({
1318
+ exportName,
1319
+ config: value[TableConfigSymbol],
1320
+ table: value,
1321
+ filePath: absPath
1322
+ });
1323
+ }
1324
+ }
1325
+ }
1326
+ }
1327
+ return entries;
1328
+ }
1329
+ function isTable(value) {
1330
+ return typeof value === "object" && value !== null && TableConfigSymbol in value;
1331
+ }
1332
+
1333
+ // src/utils/colors.ts
1334
+ function colorize(text2, color) {
1335
+ const code2 = Bun.color(color, "ansi");
1336
+ return code2 ? `${code2}${text2}\x1B[0m` : text2;
1337
+ }
1338
+
1339
+ // src/commands/drop.ts
1340
+ async function runDrop(config2, opts = {}) {
1341
+ const schemas2 = await loadSchemas(config2.schema);
1342
+ if (schemas2.length === 0) {
1343
+ console.warn("No table definitions found in schema files.");
1344
+ return;
1345
+ }
1346
+ const sql2 = new Bun.SQL(config2.dbUrl);
1347
+ try {
1348
+ const existingTablesResult = await sql2`
1349
+ SELECT table_name
1350
+ FROM information_schema.tables
1351
+ WHERE table_schema = 'public' OR table_schema = current_schema()
1352
+ `;
1353
+ const existingTableNames = new Set(existingTablesResult.map((row) => row.table_name));
1354
+ const migrationTableExists = existingTableNames.has("__bungres_migrations");
1355
+ const pushTableExists = existingTableNames.has("__bungres_push");
1356
+ const tablesToDrop = schemas2.filter((s) => existingTableNames.has(s.config.name));
1357
+ if (tablesToDrop.length === 0 && !migrationTableExists && !pushTableExists) {
1358
+ console.log("No tables to drop (they either don't exist or were already dropped).");
1359
+ return;
1360
+ }
1361
+ const tableNamesToPrint = tablesToDrop.map((s) => (s.config.schema ? s.config.schema + "." : "") + s.config.name);
1362
+ if (migrationTableExists) {
1363
+ tableNamesToPrint.push("__bungres_migrations");
1364
+ }
1365
+ if (pushTableExists) {
1366
+ tableNamesToPrint.push("__bungres_push");
1367
+ }
1368
+ if (!opts.force) {
1369
+ console.warn(colorize(`
1370
+ \u26A0\uFE0F WARNING: This will drop the following tables and ALL their data:
1371
+ `, "red"));
1372
+ for (const name of tableNamesToPrint)
1373
+ console.log(colorize(` - ${name}`, "yellow"));
1374
+ process.stdout.write(colorize(`
1375
+ Are you sure? Type YES to continue: `, "cyan"));
1376
+ for await (const line2 of console) {
1377
+ if (line2.trim().toLowerCase() !== "yes") {
1378
+ console.log("Aborted.");
1379
+ return;
1380
+ }
1381
+ break;
1382
+ }
1383
+ }
1384
+ for (const entry of tablesToDrop) {
1385
+ const ddl = `DROP TABLE IF EXISTS "${entry.config.name}" CASCADE;`;
1386
+ await sql2.unsafe(ddl);
1387
+ console.log(colorize(` \u2713 dropped ${entry.config.name}`, "green"));
1388
+ }
1389
+ if (migrationTableExists) {
1390
+ await sql2.unsafe("DROP TABLE IF EXISTS public.__bungres_migrations CASCADE");
1391
+ console.log(colorize(` \u2713 dropped __bungres_migrations`, "green"));
1392
+ }
1393
+ if (pushTableExists) {
1394
+ await sql2.unsafe("DROP TABLE IF EXISTS public.__bungres_push CASCADE");
1395
+ console.log(colorize(` \u2713 dropped __bungres_push`, "green"));
1396
+ }
1397
+ console.log(colorize(`
1398
+ Drop complete.`, "green"));
1399
+ } finally {
1400
+ await sql2.end();
1401
+ }
1402
+ }
1403
+
1404
+ // src/commands/migrate.ts
1405
+ import { join as join2, resolve as resolve2 } from "path";
1406
+ var MIGRATIONS_TABLE = "__bungres_migrations";
1407
+ var CREATE_MIGRATIONS_TABLE = `
1408
+ CREATE TABLE IF NOT EXISTS "${MIGRATIONS_TABLE}" (
1409
+ id SERIAL PRIMARY KEY,
1410
+ name TEXT NOT NULL UNIQUE,
1411
+ applied_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
1412
+ );
1413
+ `.trim();
1414
+ async function runMigrate(config2) {
1415
+ const migrationsDir = resolve2(config2.migrationsDir);
1416
+ const sql2 = new Bun.SQL(config2.dbUrl);
1417
+ try {
1418
+ await sql2.unsafe(CREATE_MIGRATIONS_TABLE);
1419
+ const glob = new Bun.Glob("*.sql");
1420
+ const files = [];
1421
+ for await (const file of glob.scan({ cwd: migrationsDir, absolute: false })) {
1422
+ files.push(file);
1423
+ }
1424
+ files.sort();
1425
+ if (files.length === 0) {
1426
+ console.log(colorize("No migration files found in " + migrationsDir, "yellow"));
1427
+ console.log(colorize("Run `bungres generate` first.", "yellow"));
1428
+ return;
1429
+ }
1430
+ const applied = await sql2.unsafe(`SELECT name FROM "${MIGRATIONS_TABLE}"`);
1431
+ const appliedSet = new Set(applied.map((r) => r.name));
1432
+ const pending = files.filter((f) => !appliedSet.has(f));
1433
+ if (pending.length === 0) {
1434
+ console.log(colorize("Everything is up to date.", "green"));
1435
+ return;
1436
+ }
1437
+ console.log(colorize(`
1438
+ Running ${pending.length} pending migration(s)...
1439
+ `, "cyan"));
1440
+ for (const file of pending) {
1441
+ const content = await Bun.file(join2(migrationsDir, file)).text();
1442
+ if (config2.verbose) {
1443
+ console.log(`-- ${file} --
1444
+ ${content}
1445
+ `);
1446
+ }
1447
+ await sql2.transaction(async (txSql) => {
1448
+ const statements = content.split(";").map((s) => s.trim()).filter(Boolean);
1449
+ for (const stmt of statements) {
1450
+ await txSql.unsafe(stmt + ";");
1451
+ }
1452
+ await txSql.unsafe(`INSERT INTO "${MIGRATIONS_TABLE}" (name) VALUES ($1)`, [file]);
1453
+ });
1454
+ console.log(colorize(` \u2713 ${file}`, "green"));
1455
+ }
1456
+ console.log(colorize(`
1457
+ Done.`, "green"));
1458
+ } finally {
1459
+ await sql2.end();
1460
+ }
1461
+ }
1462
+
1463
+ // src/commands/fresh.ts
1464
+ async function runFresh(config2) {
1465
+ console.log("Dropping all tables...");
1466
+ await runDrop(config2, { force: true });
1467
+ console.log(`
1468
+ Re-running migrations...`);
1469
+ await runMigrate(config2);
1470
+ console.log(`
1471
+ Fresh complete.`);
1472
+ }
1473
+
1474
+ // src/commands/generate.ts
1475
+ import { join as join3, resolve as resolve3 } from "path";
1476
+
939
1477
  // src/differ.ts
940
1478
  function diffSchemas(prev, next) {
941
1479
  const statements = [];
@@ -1021,11 +1559,11 @@ function diffSchemas(prev, next) {
1021
1559
  const idxName = idx.name ?? `idx_${tableName}_${idx.columns.join("_")}`;
1022
1560
  if (!prevIdxNames.has(idxName)) {
1023
1561
  const tbl = nextConfig.schema ? `"${nextConfig.schema}"."${tableName}"` : `"${tableName}"`;
1024
- const unique = idx.unique ? "UNIQUE " : "";
1562
+ const unique2 = idx.unique ? "UNIQUE " : "";
1025
1563
  const using = idx.using ? ` USING ${idx.using.toUpperCase()}` : "";
1026
1564
  const cols = idx.columns.map((c) => `"${c}"`).join(", ");
1027
1565
  const where = idx.where ? ` WHERE ${idx.where}` : "";
1028
- statements.push(`CREATE ${unique}INDEX IF NOT EXISTS "${idxName}" ON ${tbl}${using} (${cols})${where};`);
1566
+ statements.push(`CREATE ${unique2}INDEX IF NOT EXISTS "${idxName}" ON ${tbl}${using} (${cols})${where};`);
1029
1567
  summary.push(`CREATE INDEX ${idxName} ON ${tableName}`);
1030
1568
  }
1031
1569
  }
@@ -1080,133 +1618,25 @@ function diffColumn(prev, next) {
1080
1618
  if (nextDef !== undefined) {
1081
1619
  const val = next.defaultFn ?? formatDefault(next.defaultValue, next.dataType);
1082
1620
  changes.push(`ALTER COLUMN ${col2} SET DEFAULT ${val}`);
1083
- } else {
1084
- changes.push(`ALTER COLUMN ${col2} DROP DEFAULT`);
1085
- }
1086
- }
1087
- return changes;
1088
- }
1089
- function formatDefault(value, dataType) {
1090
- if (value === null || value === undefined)
1091
- return "NULL";
1092
- if (typeof value === "boolean")
1093
- return value ? "TRUE" : "FALSE";
1094
- if (typeof value === "number")
1095
- return String(value);
1096
- if (typeof value === "string") {
1097
- if (dataType === "boolean")
1098
- return value;
1099
- return `'${value.replace(/'/g, "''")}'`;
1100
- }
1101
- return `'${JSON.stringify(value)}'`;
1102
- }
1103
-
1104
- // src/schema-loader.ts
1105
- import { resolve as resolve2, join as join2 } from "path";
1106
- async function loadSchemas(patterns, cwd = process.cwd()) {
1107
- const globs = Array.isArray(patterns) ? patterns : [patterns];
1108
- const entries = [];
1109
- for (const pattern of globs) {
1110
- const glob = new Bun.Glob(pattern);
1111
- for await (const file of glob.scan({ cwd, absolute: false })) {
1112
- const absPath = resolve2(join2(cwd, file));
1113
- const mod = await import(absPath);
1114
- for (const [exportName, value] of Object.entries(mod)) {
1115
- if (isTable(value)) {
1116
- entries.push({
1117
- exportName,
1118
- config: value[TableConfigSymbol],
1119
- table: value,
1120
- filePath: absPath
1121
- });
1122
- }
1123
- }
1124
- }
1125
- }
1126
- return entries;
1127
- }
1128
- function isTable(value) {
1129
- return typeof value === "object" && value !== null && TableConfigSymbol in value;
1130
- }
1131
-
1132
- // src/commands/push.ts
1133
- async function runPush(config2, opts = {}) {
1134
- console.log("@bungres/kit push: loading schemas...");
1135
- const schemas2 = await loadSchemas(config2.schema);
1136
- if (schemas2.length === 0) {
1137
- console.warn("No table definitions found. Check your schema glob pattern.");
1138
- return;
1139
- }
1140
- const sql2 = new Bun.SQL(config2.dbUrl);
1141
- try {
1142
- await sql2.unsafe(`
1143
- CREATE TABLE IF NOT EXISTS public.__bungres_push (
1144
- id SERIAL PRIMARY KEY,
1145
- snapshot JSONB NOT NULL
1146
- );
1147
- `);
1148
- const rows = await sql2.unsafe(`SELECT snapshot FROM public.__bungres_push ORDER BY id DESC LIMIT 1;`);
1149
- let prevSnapshot = {};
1150
- if (rows.length > 0) {
1151
- prevSnapshot = typeof rows[0].snapshot === "string" ? JSON.parse(rows[0].snapshot) : rows[0].snapshot;
1152
- }
1153
- const currentSnapshot = Object.fromEntries(schemas2.map((s) => [s.config.name, s.config]));
1154
- const diff = diffSchemas(prevSnapshot, currentSnapshot);
1155
- if (diff.statements.length === 0) {
1156
- console.log(`
1157
- No schema changes detected. Database is up to date.`);
1158
- return;
1159
- }
1160
- console.log(`
1161
- Changes to apply:`);
1162
- for (const s of diff.summary)
1163
- console.log(` + ${s}`);
1164
- if (diff.warnings && diff.warnings.length > 0) {
1165
- console.warn(`
1166
- \u26A0\uFE0F WARNING: Data Loss Detected!`);
1167
- for (const w of diff.warnings)
1168
- console.warn(` ! ${w}`);
1169
- console.warn(`
1170
- These changes will be immediately executed against the database!`);
1171
- }
1172
- if (!opts.force) {
1173
- process.stdout.write(`
1174
- Are you sure you want to push these changes? Type YES to continue: `);
1175
- const answer = await readLine();
1176
- if (answer.trim().toLowerCase() !== "yes") {
1177
- console.log("Aborted.");
1178
- return;
1179
- }
1180
- }
1181
- console.log(`
1182
- Pushing changes...`);
1183
- await sql2.unsafe(`CREATE EXTENSION IF NOT EXISTS "pgcrypto";`);
1184
- for (const stmt of diff.statements) {
1185
- if (config2.verbose) {
1186
- console.log(`-- ${stmt}`);
1187
- }
1188
- await sql2.unsafe(stmt);
1189
- }
1190
- await sql2.unsafe(`INSERT INTO public.__bungres_push (snapshot) VALUES ($1);`, [JSON.stringify(currentSnapshot)]);
1191
- console.log(`
1192
- Push complete.`);
1193
- } finally {
1194
- await sql2.end();
1621
+ } else {
1622
+ changes.push(`ALTER COLUMN ${col2} DROP DEFAULT`);
1623
+ }
1195
1624
  }
1625
+ return changes;
1196
1626
  }
1197
- async function readLine() {
1198
- const buf = Buffer.alloc(256);
1199
- const n = fs.readSync(0, buf, 0, 256, null);
1200
- return buf.subarray(0, n).toString().trim();
1201
- }
1202
-
1203
- // src/commands/generate.ts
1204
- import { join as join3, resolve as resolve3 } from "path";
1205
-
1206
- // src/utils/colors.ts
1207
- function colorize(text2, color) {
1208
- const code2 = Bun.color(color, "ansi");
1209
- return code2 ? `${code2}${text2}\x1B[0m` : text2;
1627
+ function formatDefault(value, dataType) {
1628
+ if (value === null || value === undefined)
1629
+ return "NULL";
1630
+ if (typeof value === "boolean")
1631
+ return value ? "TRUE" : "FALSE";
1632
+ if (typeof value === "number")
1633
+ return String(value);
1634
+ if (typeof value === "string") {
1635
+ if (dataType === "boolean")
1636
+ return value;
1637
+ return `'${value.replace(/'/g, "''")}'`;
1638
+ }
1639
+ return `'${JSON.stringify(value)}'`;
1210
1640
  }
1211
1641
 
1212
1642
  // src/commands/generate.ts
@@ -1310,69 +1740,93 @@ function topoSort(schemas2) {
1310
1740
  return result2;
1311
1741
  }
1312
1742
 
1313
- // src/commands/migrate.ts
1314
- import { join as join4, resolve as resolve4 } from "path";
1315
- var MIGRATIONS_TABLE = "__bungres_migrations";
1316
- var CREATE_MIGRATIONS_TABLE = `
1317
- CREATE TABLE IF NOT EXISTS "${MIGRATIONS_TABLE}" (
1318
- id SERIAL PRIMARY KEY,
1319
- name TEXT NOT NULL UNIQUE,
1320
- applied_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
1321
- );
1322
- `.trim();
1323
- async function runMigrate(config2) {
1324
- const migrationsDir = resolve4(config2.migrationsDir);
1325
- const sql2 = new Bun.SQL(config2.dbUrl);
1743
+ // src/commands/init.ts
1744
+ import { existsSync } from "fs";
1745
+ import { mkdir } from "fs/promises";
1746
+ import { join as join4 } from "path";
1747
+ async function runInit(cwd = process.cwd()) {
1748
+ const configPath = join4(cwd, "bungres.config.ts");
1749
+ const dbDir = join4(cwd, "src", "db");
1750
+ if (existsSync(configPath)) {
1751
+ console.log("Config file already exists at bungres.config.ts");
1752
+ return;
1753
+ }
1326
1754
  try {
1327
- await sql2.unsafe(CREATE_MIGRATIONS_TABLE);
1328
- const glob = new Bun.Glob("*.sql");
1329
- const files = [];
1330
- for await (const file of glob.scan({ cwd: migrationsDir, absolute: false })) {
1331
- files.push(file);
1332
- }
1333
- files.sort();
1334
- if (files.length === 0) {
1335
- console.log(colorize("No migration files found in " + migrationsDir, "yellow"));
1336
- console.log(colorize("Run `bungres generate` first.", "yellow"));
1337
- return;
1338
- }
1339
- const applied = await sql2.unsafe(`SELECT name FROM "${MIGRATIONS_TABLE}"`);
1340
- const appliedSet = new Set(applied.map((r) => r.name));
1341
- const pending = files.filter((f) => !appliedSet.has(f));
1342
- if (pending.length === 0) {
1343
- console.log(colorize("Everything is up to date.", "green"));
1344
- return;
1345
- }
1346
- console.log(colorize(`
1347
- Running ${pending.length} pending migration(s)...
1348
- `, "cyan"));
1349
- for (const file of pending) {
1350
- const content = await Bun.file(join4(migrationsDir, file)).text();
1351
- if (config2.verbose) {
1352
- console.log(`-- ${file} --
1353
- ${content}
1354
- `);
1355
- }
1356
- await sql2.transaction(async (txSql) => {
1357
- const statements = content.split(";").map((s) => s.trim()).filter(Boolean);
1358
- for (const stmt of statements) {
1359
- await txSql.unsafe(stmt + ";");
1360
- }
1361
- await txSql.unsafe(`INSERT INTO "${MIGRATIONS_TABLE}" (name) VALUES ($1)`, [file]);
1362
- });
1363
- console.log(colorize(` \u2713 ${file}`, "green"));
1364
- }
1365
- console.log(colorize(`
1366
- Done.`, "green"));
1367
- } finally {
1368
- await sql2.end();
1755
+ await mkdir(dbDir, { recursive: true });
1756
+ console.log("Created src/db directory");
1757
+ } catch (e) {
1758
+ console.error("Failed to create src/db directory:", e);
1759
+ return;
1760
+ }
1761
+ const configContent = `import { defineConfig } from "@bungres/kit";
1762
+
1763
+ export default defineConfig({
1764
+ schema: "./src/db/schema.ts",
1765
+ out: "./bungres",
1766
+ dbCredentials: {
1767
+ url: Bun.env.DATABASE_URL!,
1768
+ },
1769
+ });
1770
+ `;
1771
+ try {
1772
+ await Bun.write(configPath, configContent);
1773
+ console.log("Created bungres.config.ts");
1774
+ } catch (e) {
1775
+ console.error("Failed to create config file:", e);
1776
+ return;
1777
+ }
1778
+ const schemaContent = `import {
1779
+ uuid,
1780
+ varchar,
1781
+ timestamptz,
1782
+ snakeCase,
1783
+ unique,
1784
+ index
1785
+ } from "@bungres/orm";
1786
+
1787
+ export const users = snakeCase.table("users", {
1788
+ id: uuid({ primaryKey: true }),
1789
+ name: varchar({ length: 255, notNull: true }),
1790
+ email: varchar({ length: 255, notNull: true }),
1791
+ createdAt: timestamptz({ notNull: true, defaultRaw: "NOW()" }),
1792
+ }, (t) => [
1793
+ unique().on(t.email),
1794
+ index().on(t.email),
1795
+ ]);
1796
+ `;
1797
+ try {
1798
+ await Bun.write(join4(dbDir, "schema.ts"), schemaContent);
1799
+ console.log("Created src/db/schema.ts with example table");
1800
+ } catch (e) {
1801
+ console.error("Failed to create schema file:", e);
1802
+ }
1803
+ const clientContent = `import { createDB } from "@bungres/orm";
1804
+ import * as schema from "./schema";
1805
+
1806
+ const url = Bun.env.DATABASE_URL || "postgres://postgres:postgres@localhost:5432/postgres";
1807
+
1808
+ export const db = createDB({ url, schema });
1809
+ `;
1810
+ try {
1811
+ await Bun.write(join4(dbDir, "client.ts"), clientContent);
1812
+ console.log("Created src/db/client.ts");
1813
+ } catch (e) {
1814
+ console.error("Failed to create client file:", e);
1369
1815
  }
1816
+ console.log(`
1817
+ \u2728 Bungres project initialized!`);
1818
+ console.log(`
1819
+ Next steps:`);
1820
+ console.log(" 1. Set DATABASE_URL in your .env file");
1821
+ console.log(" 2. Edit src/db/schema.ts to define your tables");
1822
+ console.log(" 3. Run 'bungres generate' to create migrations");
1823
+ console.log(" 4. Run 'bungres migrate' to apply migrations");
1370
1824
  }
1371
1825
 
1372
1826
  // src/commands/pull.ts
1373
- import { resolve as resolve5, join as join5 } from "path";
1827
+ import { resolve as resolve4, join as join5 } from "path";
1374
1828
  async function introspectDb(sql2, dbSchema) {
1375
- const columns = await sql2.unsafe(`SELECT
1829
+ const columns2 = await sql2.unsafe(`SELECT
1376
1830
  c.table_name,
1377
1831
  c.column_name,
1378
1832
  c.data_type,
@@ -1406,7 +1860,7 @@ async function introspectDb(sql2, dbSchema) {
1406
1860
  const indexes = await sql2.unsafe(`SELECT tablename, indexname, indexdef
1407
1861
  FROM pg_indexes
1408
1862
  WHERE schemaname = $1`, [dbSchema]);
1409
- return groupByTable(columns, constraints, indexes);
1863
+ return groupByTable(columns2, constraints, indexes);
1410
1864
  }
1411
1865
  async function runPull(config2) {
1412
1866
  console.log("@bungres/kit pull: introspecting database...");
@@ -1418,7 +1872,7 @@ async function runPull(config2) {
1418
1872
  console.log("No tables found in schema:", dbSchema);
1419
1873
  return;
1420
1874
  }
1421
- const outDir = resolve5(config2.outDir);
1875
+ const outDir = resolve4(config2.outDir);
1422
1876
  await Bun.$`mkdir -p ${outDir}`.quiet();
1423
1877
  const outPath = join5(outDir, "schema.ts");
1424
1878
  const code2 = generateSchemaTS(tableMap, dbSchema);
@@ -1429,9 +1883,9 @@ async function runPull(config2) {
1429
1883
  await sql2.end();
1430
1884
  }
1431
1885
  }
1432
- function groupByTable(columns, constraints, indexes) {
1886
+ function groupByTable(columns2, constraints, indexes) {
1433
1887
  const map = new Map;
1434
- for (const col2 of columns) {
1888
+ for (const col2 of columns2) {
1435
1889
  if (!map.has(col2.table_name)) {
1436
1890
  map.set(col2.table_name, {
1437
1891
  tableName: col2.table_name,
@@ -1625,126 +2079,76 @@ function toCamelCase(str) {
1625
2079
  return str.replace(/_([a-z])/g, (_, c) => c.toUpperCase());
1626
2080
  }
1627
2081
 
1628
- // src/commands/status.ts
1629
- import { resolve as resolve6 } from "path";
1630
- var MIGRATIONS_TABLE2 = "__bungres_migrations";
1631
- async function runStatus(config2) {
1632
- const migrationsDir = resolve6(config2.migrationsDir);
1633
- const sql2 = new Bun.SQL(config2.dbUrl);
1634
- try {
1635
- const tableCheck = await sql2.unsafe(`SELECT EXISTS (
1636
- SELECT 1 FROM information_schema.tables
1637
- WHERE table_name = $1
1638
- ) AS exists`, [MIGRATIONS_TABLE2]);
1639
- const trackingExists = tableCheck[0]?.exists ?? false;
1640
- const glob = new Bun.Glob("*.sql");
1641
- const files = [];
1642
- for await (const file of glob.scan({ cwd: migrationsDir, absolute: false })) {
1643
- files.push(file);
1644
- }
1645
- files.sort();
1646
- if (files.length === 0) {
1647
- console.log(colorize("No migration files found. Run `bungres generate` first.", "yellow"));
1648
- return;
1649
- }
1650
- let appliedSet = new Set;
1651
- if (trackingExists) {
1652
- const applied = await sql2.unsafe(`SELECT name FROM "${MIGRATIONS_TABLE2}" ORDER BY applied_at`);
1653
- appliedSet = new Set(applied.map((r) => r.name));
1654
- }
1655
- console.log(colorize(`
1656
- Migration status:
1657
- `, "cyan"));
1658
- let pendingCount = 0;
1659
- for (const file of files) {
1660
- const isApplied = appliedSet.has(file);
1661
- const status = isApplied ? colorize("\u2713 applied ", "green") : colorize("\u2717 pending ", "yellow");
1662
- if (!isApplied)
1663
- pendingCount++;
1664
- console.log(` ${status} ${file}`);
1665
- }
1666
- console.log(`
1667
- ${colorize(appliedSet.size.toString(), "green")} applied, ${colorize(pendingCount.toString(), "yellow")} pending.
1668
- `);
1669
- } finally {
1670
- await sql2.end();
1671
- }
1672
- }
1673
-
1674
- // src/commands/drop.ts
1675
- async function runDrop(config2, opts = {}) {
2082
+ // src/commands/push.ts
2083
+ import * as fs from "fs";
2084
+ async function runPush(config2, opts = {}) {
2085
+ console.log("@bungres/kit push: loading schemas...");
1676
2086
  const schemas2 = await loadSchemas(config2.schema);
1677
2087
  if (schemas2.length === 0) {
1678
- console.warn("No table definitions found in schema files.");
2088
+ console.warn("No table definitions found. Check your schema glob pattern.");
1679
2089
  return;
1680
2090
  }
1681
2091
  const sql2 = new Bun.SQL(config2.dbUrl);
1682
2092
  try {
1683
- const existingTablesResult = await sql2`
1684
- SELECT table_name
1685
- FROM information_schema.tables
1686
- WHERE table_schema = 'public' OR table_schema = current_schema()
1687
- `;
1688
- const existingTableNames = new Set(existingTablesResult.map((row) => row.table_name));
1689
- const migrationTableExists = existingTableNames.has("__bungres_migrations");
1690
- const pushTableExists = existingTableNames.has("__bungres_push");
1691
- const tablesToDrop = schemas2.filter((s) => existingTableNames.has(s.config.name));
1692
- if (tablesToDrop.length === 0 && !migrationTableExists && !pushTableExists) {
1693
- console.log("No tables to drop (they either don't exist or were already dropped).");
1694
- return;
2093
+ await sql2.unsafe(`
2094
+ CREATE TABLE IF NOT EXISTS public.__bungres_push (
2095
+ id SERIAL PRIMARY KEY,
2096
+ snapshot JSONB NOT NULL
2097
+ );
2098
+ `);
2099
+ const rows = await sql2.unsafe(`SELECT snapshot FROM public.__bungres_push ORDER BY id DESC LIMIT 1;`);
2100
+ let prevSnapshot = {};
2101
+ if (rows.length > 0) {
2102
+ prevSnapshot = typeof rows[0].snapshot === "string" ? JSON.parse(rows[0].snapshot) : rows[0].snapshot;
1695
2103
  }
1696
- const tableNamesToPrint = tablesToDrop.map((s) => (s.config.schema ? s.config.schema + "." : "") + s.config.name);
1697
- if (migrationTableExists) {
1698
- tableNamesToPrint.push("__bungres_migrations");
2104
+ const currentSnapshot = Object.fromEntries(schemas2.map((s) => [s.config.name, s.config]));
2105
+ const diff = diffSchemas(prevSnapshot, currentSnapshot);
2106
+ if (diff.statements.length === 0) {
2107
+ console.log(`
2108
+ No schema changes detected. Database is up to date.`);
2109
+ return;
1699
2110
  }
1700
- if (pushTableExists) {
1701
- tableNamesToPrint.push("__bungres_push");
2111
+ console.log(`
2112
+ Changes to apply:`);
2113
+ for (const s of diff.summary)
2114
+ console.log(` + ${s}`);
2115
+ if (diff.warnings && diff.warnings.length > 0) {
2116
+ console.warn(`
2117
+ \u26A0\uFE0F WARNING: Data Loss Detected!`);
2118
+ for (const w of diff.warnings)
2119
+ console.warn(` ! ${w}`);
2120
+ console.warn(`
2121
+ These changes will be immediately executed against the database!`);
1702
2122
  }
1703
2123
  if (!opts.force) {
1704
- console.warn(colorize(`
1705
- \u26A0\uFE0F WARNING: This will drop the following tables and ALL their data:
1706
- `, "red"));
1707
- for (const name of tableNamesToPrint)
1708
- console.log(colorize(` - ${name}`, "yellow"));
1709
- process.stdout.write(colorize(`
1710
- Are you sure? Type YES to continue: `, "cyan"));
1711
- for await (const line2 of console) {
1712
- if (line2.trim().toLowerCase() !== "yes") {
1713
- console.log("Aborted.");
1714
- return;
1715
- }
1716
- break;
2124
+ process.stdout.write(`
2125
+ Are you sure you want to push these changes? Type YES to continue: `);
2126
+ const answer = await readLine();
2127
+ if (answer.trim().toLowerCase() !== "yes") {
2128
+ console.log("Aborted.");
2129
+ return;
1717
2130
  }
1718
2131
  }
1719
- for (const entry of tablesToDrop) {
1720
- const ddl = `DROP TABLE IF EXISTS "${entry.config.name}" CASCADE;`;
1721
- await sql2.unsafe(ddl);
1722
- console.log(colorize(` \u2713 dropped ${entry.config.name}`, "green"));
1723
- }
1724
- if (migrationTableExists) {
1725
- await sql2.unsafe("DROP TABLE IF EXISTS public.__bungres_migrations CASCADE");
1726
- console.log(colorize(` \u2713 dropped __bungres_migrations`, "green"));
1727
- }
1728
- if (pushTableExists) {
1729
- await sql2.unsafe("DROP TABLE IF EXISTS public.__bungres_push CASCADE");
1730
- console.log(colorize(` \u2713 dropped __bungres_push`, "green"));
2132
+ console.log(`
2133
+ Pushing changes...`);
2134
+ await sql2.unsafe(`CREATE EXTENSION IF NOT EXISTS "pgcrypto";`);
2135
+ for (const stmt of diff.statements) {
2136
+ if (config2.verbose) {
2137
+ console.log(`-- ${stmt}`);
2138
+ }
2139
+ await sql2.unsafe(stmt);
1731
2140
  }
1732
- console.log(colorize(`
1733
- Drop complete.`, "green"));
1734
- } finally {
1735
- await sql2.end();
1736
- }
1737
- }
1738
-
1739
- // src/commands/fresh.ts
1740
- async function runFresh(config2) {
1741
- console.log("Dropping all tables...");
1742
- await runDrop(config2, { force: true });
1743
- console.log(`
1744
- Re-running migrations...`);
1745
- await runMigrate(config2);
1746
- console.log(`
1747
- Fresh complete.`);
2141
+ await sql2.unsafe(`INSERT INTO public.__bungres_push (snapshot) VALUES ($1);`, [JSON.stringify(currentSnapshot)]);
2142
+ console.log(`
2143
+ Push complete.`);
2144
+ } finally {
2145
+ await sql2.end();
2146
+ }
2147
+ }
2148
+ async function readLine() {
2149
+ const buf = Buffer.alloc(256);
2150
+ const n = fs.readSync(0, buf, 0, 256, null);
2151
+ return buf.subarray(0, n).toString().trim();
1748
2152
  }
1749
2153
 
1750
2154
  // src/commands/refresh.ts
@@ -1771,13 +2175,13 @@ Refresh complete. All tables are now empty.`, "green"));
1771
2175
  }
1772
2176
 
1773
2177
  // src/commands/seed.ts
1774
- import { resolve as resolve7 } from "path";
2178
+ import { resolve as resolve5 } from "path";
1775
2179
  async function runSeed(config2) {
1776
2180
  if (!config2.seed) {
1777
2181
  console.log(colorize(`No seed file configured in bungres.config.ts`, "yellow"));
1778
2182
  return;
1779
2183
  }
1780
- const seedPath = resolve7(process.cwd(), config2.seed);
2184
+ const seedPath = resolve5(process.cwd(), config2.seed);
1781
2185
  const file = Bun.file(seedPath);
1782
2186
  if (!await file.exists()) {
1783
2187
  console.error(colorize(`Seed file not found at ${seedPath}`, "red"));
@@ -1802,6 +2206,52 @@ Seed failed with exit code ${exitCode}.`);
1802
2206
  }
1803
2207
  }
1804
2208
 
2209
+ // src/commands/status.ts
2210
+ import { resolve as resolve6 } from "path";
2211
+ var MIGRATIONS_TABLE2 = "__bungres_migrations";
2212
+ async function runStatus(config2) {
2213
+ const migrationsDir = resolve6(config2.migrationsDir);
2214
+ const sql2 = new Bun.SQL(config2.dbUrl);
2215
+ try {
2216
+ const tableCheck = await sql2.unsafe(`SELECT EXISTS (
2217
+ SELECT 1 FROM information_schema.tables
2218
+ WHERE table_name = $1
2219
+ ) AS exists`, [MIGRATIONS_TABLE2]);
2220
+ const trackingExists = tableCheck[0]?.exists ?? false;
2221
+ const glob = new Bun.Glob("*.sql");
2222
+ const files = [];
2223
+ for await (const file of glob.scan({ cwd: migrationsDir, absolute: false })) {
2224
+ files.push(file);
2225
+ }
2226
+ files.sort();
2227
+ if (files.length === 0) {
2228
+ console.log(colorize("No migration files found. Run `bungres generate` first.", "yellow"));
2229
+ return;
2230
+ }
2231
+ let appliedSet = new Set;
2232
+ if (trackingExists) {
2233
+ const applied = await sql2.unsafe(`SELECT name FROM "${MIGRATIONS_TABLE2}" ORDER BY applied_at`);
2234
+ appliedSet = new Set(applied.map((r) => r.name));
2235
+ }
2236
+ console.log(colorize(`
2237
+ Migration status:
2238
+ `, "cyan"));
2239
+ let pendingCount = 0;
2240
+ for (const file of files) {
2241
+ const isApplied = appliedSet.has(file);
2242
+ const status = isApplied ? colorize("\u2713 applied ", "green") : colorize("\u2717 pending ", "yellow");
2243
+ if (!isApplied)
2244
+ pendingCount++;
2245
+ console.log(` ${status} ${file}`);
2246
+ }
2247
+ console.log(`
2248
+ ${colorize(appliedSet.size.toString(), "green")} applied, ${colorize(pendingCount.toString(), "yellow")} pending.
2249
+ `);
2250
+ } finally {
2251
+ await sql2.end();
2252
+ }
2253
+ }
2254
+
1805
2255
  // src/commands/studio.ts
1806
2256
  async function runStudio(config2) {
1807
2257
  const schemas2 = await loadSchemas(config2.schema);
@@ -1840,8 +2290,19 @@ async function runStudio(config2) {
1840
2290
  return new Response("Table not found", { status: 404 });
1841
2291
  }
1842
2292
  try {
1843
- const data = await db2.select().from(schema.table).limit(100);
1844
- return new Response(JSON.stringify(data), {
2293
+ const page = parseInt(url.searchParams.get("page") || "1", 10);
2294
+ const limit = parseInt(url.searchParams.get("limit") || "50", 10);
2295
+ const offset = (page - 1) * limit;
2296
+ const countResult = await db2.select({ count: schema.table }).from(schema.table);
2297
+ const total = Array.isArray(countResult) ? countResult.length : 0;
2298
+ const data = await db2.select().from(schema.table).limit(limit).offset(offset);
2299
+ return new Response(JSON.stringify({
2300
+ data,
2301
+ total,
2302
+ page,
2303
+ limit,
2304
+ totalPages: Math.ceil(total / limit)
2305
+ }), {
1845
2306
  headers: { "Content-Type": "application/json" }
1846
2307
  });
1847
2308
  } catch (e) {
@@ -1891,18 +2352,26 @@ var htmlTemplate = `
1891
2352
  <meta charset="UTF-8">
1892
2353
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
1893
2354
  <title>Bungres Studio</title>
1894
- <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
2355
+ <link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700&display=swap" rel="stylesheet">
1895
2356
  <style>
1896
2357
  :root {
1897
- --bg-color: #0d1117;
1898
- --panel-bg: #161b22;
1899
- --border-color: #30363d;
1900
- --text-main: #c9d1d9;
1901
- --text-muted: #8b949e;
1902
- --accent-color: #58a6ff;
1903
- --accent-hover: #1f6feb;
1904
- --header-bg: #161b22;
1905
- --row-hover: #1f2428;
2358
+ --bg-color: #09090b;
2359
+ --panel-bg: rgba(24, 24, 27, 0.7);
2360
+ --border-color: #27272a;
2361
+ --text-main: #f4f4f5;
2362
+ --text-muted: #a1a1aa;
2363
+ --accent-color: #8b5cf6;
2364
+ --accent-hover: #7c3aed;
2365
+ --header-bg: rgba(9, 9, 11, 0.85);
2366
+ --row-hover: rgba(39, 39, 42, 0.8);
2367
+ --glass-border: rgba(255, 255, 255, 0.08);
2368
+
2369
+ --type-number: #f472b6;
2370
+ --type-string: #60a5fa;
2371
+ --type-boolean: #34d399;
2372
+ --type-date: #c084fc;
2373
+ --type-json: #fbbf24;
2374
+ --type-null: #71717a;
1906
2375
  }
1907
2376
 
1908
2377
  * { box-sizing: border-box; }
@@ -1910,7 +2379,7 @@ var htmlTemplate = `
1910
2379
  body {
1911
2380
  margin: 0;
1912
2381
  padding: 0;
1913
- font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
2382
+ font-family: 'Outfit', -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
1914
2383
  background-color: var(--bg-color);
1915
2384
  color: var(--text-main);
1916
2385
  display: flex;
@@ -1920,56 +2389,91 @@ var htmlTemplate = `
1920
2389
 
1921
2390
  /* Sidebar */
1922
2391
  .sidebar {
1923
- width: 250px;
2392
+ width: 280px;
1924
2393
  background-color: var(--panel-bg);
2394
+ backdrop-filter: blur(12px);
2395
+ -webkit-backdrop-filter: blur(12px);
1925
2396
  border-right: 1px solid var(--border-color);
1926
2397
  display: flex;
1927
2398
  flex-direction: column;
2399
+ box-shadow: 4px 0 24px rgba(0,0,0,0.2);
2400
+ z-index: 20;
1928
2401
  }
1929
2402
 
1930
2403
  .sidebar-header {
1931
- padding: 20px;
1932
- border-bottom: 1px solid var(--border-color);
2404
+ padding: 24px;
2405
+ border-bottom: 1px solid var(--glass-border);
1933
2406
  display: flex;
1934
2407
  align-items: center;
1935
- gap: 10px;
2408
+ gap: 12px;
2409
+ background: linear-gradient(to bottom, rgba(255,255,255,0.03), transparent);
1936
2410
  }
1937
2411
 
1938
2412
  .sidebar-header h1 {
1939
2413
  margin: 0;
1940
- font-size: 16px;
2414
+ font-size: 18px;
1941
2415
  font-weight: 600;
1942
- color: white;
2416
+ background: linear-gradient(to right, #fff, #a1a1aa);
2417
+ -webkit-background-clip: text;
2418
+ -webkit-text-fill-color: transparent;
2419
+ letter-spacing: -0.5px;
2420
+ }
2421
+
2422
+ .sidebar-header svg {
2423
+ color: var(--accent-color);
2424
+ filter: drop-shadow(0 0 8px rgba(139, 92, 246, 0.5));
1943
2425
  }
1944
2426
 
1945
2427
  .table-list {
1946
2428
  flex: 1;
1947
2429
  overflow-y: auto;
1948
- padding: 10px 0;
2430
+ padding: 16px 12px;
1949
2431
  list-style: none;
1950
2432
  margin: 0;
1951
2433
  }
1952
2434
 
2435
+ .table-list::-webkit-scrollbar {
2436
+ width: 6px;
2437
+ }
2438
+ .table-list::-webkit-scrollbar-thumb {
2439
+ background: #3f3f46;
2440
+ border-radius: 4px;
2441
+ }
2442
+
1953
2443
  .table-item {
1954
- padding: 8px 20px;
2444
+ padding: 12px 16px;
1955
2445
  cursor: pointer;
1956
- font-size: 14px;
2446
+ font-size: 15px;
2447
+ font-weight: 500;
1957
2448
  color: var(--text-muted);
1958
- transition: all 0.2s ease;
2449
+ border-radius: 8px;
2450
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
1959
2451
  display: flex;
1960
2452
  align-items: center;
1961
- gap: 8px;
2453
+ gap: 10px;
2454
+ margin-bottom: 4px;
2455
+ border: 1px solid transparent;
1962
2456
  }
1963
2457
 
1964
2458
  .table-item:hover {
1965
- background-color: var(--row-hover);
2459
+ background-color: rgba(255, 255, 255, 0.05);
1966
2460
  color: var(--text-main);
2461
+ transform: translateX(4px);
1967
2462
  }
1968
2463
 
1969
2464
  .table-item.active {
1970
- background-color: rgba(88, 166, 255, 0.1);
2465
+ background-color: rgba(139, 92, 246, 0.15);
2466
+ color: #fff;
2467
+ border: 1px solid rgba(139, 92, 246, 0.3);
2468
+ box-shadow: 0 4px 12px rgba(139, 92, 246, 0.1);
2469
+ }
2470
+
2471
+ .table-item svg {
2472
+ transition: transform 0.3s ease;
2473
+ }
2474
+ .table-item.active svg {
1971
2475
  color: var(--accent-color);
1972
- border-right: 3px solid var(--accent-color);
2476
+ transform: scale(1.1);
1973
2477
  }
1974
2478
 
1975
2479
  /* Main Content */
@@ -1977,92 +2481,168 @@ var htmlTemplate = `
1977
2481
  flex: 1;
1978
2482
  display: flex;
1979
2483
  flex-direction: column;
1980
- background-color: var(--bg-color);
2484
+ position: relative;
1981
2485
  }
1982
2486
 
1983
2487
  .main-header {
1984
- height: 60px;
1985
- padding: 0 20px;
2488
+ height: 70px;
2489
+ padding: 0 24px;
1986
2490
  border-bottom: 1px solid var(--border-color);
1987
2491
  display: flex;
1988
2492
  align-items: center;
1989
2493
  justify-content: space-between;
1990
2494
  background-color: var(--header-bg);
2495
+ backdrop-filter: blur(12px);
2496
+ -webkit-backdrop-filter: blur(12px);
2497
+ z-index: 10;
1991
2498
  }
1992
2499
 
1993
2500
  .main-header h2 {
1994
2501
  margin: 0;
1995
- font-size: 16px;
1996
- font-weight: 500;
2502
+ font-size: 20px;
2503
+ font-weight: 600;
1997
2504
  color: white;
2505
+ letter-spacing: -0.5px;
2506
+ display: flex;
2507
+ align-items: center;
2508
+ gap: 10px;
1998
2509
  }
1999
2510
 
2000
2511
  .btn {
2001
- background-color: var(--accent-color);
2002
- color: white;
2003
- border: none;
2512
+ background-color: rgba(255, 255, 255, 0.05);
2513
+ border: 1px solid var(--border-color);
2514
+ color: var(--text-main);
2004
2515
  padding: 6px 12px;
2005
2516
  border-radius: 6px;
2006
- font-size: 12px;
2517
+ font-size: 13px;
2007
2518
  font-weight: 500;
2519
+ font-family: 'Outfit', sans-serif;
2008
2520
  cursor: pointer;
2009
- transition: background-color 0.2s ease;
2521
+ transition: all 0.2s ease;
2010
2522
  display: flex;
2011
2523
  align-items: center;
2012
- gap: 6px;
2524
+ gap: 8px;
2013
2525
  }
2014
2526
 
2015
- .btn:hover {
2016
- background-color: var(--accent-hover);
2527
+ .btn:hover:not(:disabled) {
2528
+ background-color: rgba(255, 255, 255, 0.1);
2529
+ }
2530
+
2531
+ .btn:active:not(:disabled) {
2532
+ transform: translateY(1px);
2017
2533
  }
2018
2534
 
2019
2535
  .btn:disabled {
2020
- opacity: 0.5;
2536
+ opacity: 0.4;
2021
2537
  cursor: not-allowed;
2022
2538
  }
2023
2539
 
2024
2540
  .content-area {
2025
2541
  flex: 1;
2026
2542
  overflow: auto;
2027
- padding: 20px;
2543
+ padding: 0;
2544
+ position: relative;
2545
+ z-index: 1;
2546
+ text-align: left;
2028
2547
  }
2029
2548
 
2030
2549
  /* Data Table */
2031
2550
  .data-grid-container {
2032
- background-color: var(--panel-bg);
2551
+ background-color: rgba(24, 24, 27, 0.5);
2033
2552
  border: 1px solid var(--border-color);
2034
- border-radius: 8px;
2035
- overflow: hidden;
2553
+ border-radius: 0;
2554
+ overflow: auto;
2555
+ box-shadow: 0 8px 32px rgba(0,0,0,0.2);
2556
+ backdrop-filter: blur(8px);
2557
+ max-height: 100%;
2558
+ width: 100%;
2559
+ }
2560
+
2561
+ .data-grid-container::-webkit-scrollbar {
2562
+ width: 8px;
2563
+ height: 8px;
2564
+ }
2565
+ .data-grid-container::-webkit-scrollbar-corner {
2566
+ background: transparent;
2567
+ }
2568
+ .data-grid-container::-webkit-scrollbar-thumb {
2569
+ background: #3f3f46;
2570
+ border-radius: 4px;
2036
2571
  }
2037
2572
 
2038
2573
  table {
2039
- width: 100%;
2040
- border-collapse: collapse;
2574
+ width: auto;
2575
+ border-collapse: separate;
2576
+ border-spacing: 0;
2041
2577
  text-align: left;
2042
- font-size: 13px;
2578
+ font-size: 14px;
2579
+ table-layout: auto;
2043
2580
  }
2044
2581
 
2045
2582
  th {
2046
- background-color: rgba(255,255,255,0.02);
2047
- color: var(--text-muted);
2583
+ background-color: rgba(24, 24, 27, 0.95);
2584
+ backdrop-filter: blur(4px);
2585
+ color: var(--text-main);
2048
2586
  font-weight: 500;
2049
- padding: 10px 16px;
2587
+ padding: 12px 20px;
2050
2588
  border-bottom: 1px solid var(--border-color);
2589
+ border-right: 1px solid var(--border-color);
2051
2590
  white-space: nowrap;
2052
2591
  position: sticky;
2053
2592
  top: 0;
2054
2593
  z-index: 10;
2055
2594
  }
2595
+
2596
+
2597
+ th::after {
2598
+ content: '';
2599
+ position: absolute;
2600
+ bottom: -1px; left: 0; right: 0;
2601
+ height: 1px;
2602
+ background: var(--border-color);
2603
+ }
2604
+
2605
+ .col-header {
2606
+ display: flex;
2607
+ flex-direction: column;
2608
+ gap: 4px;
2609
+ }
2610
+
2611
+ .col-name {
2612
+ font-weight: 600;
2613
+ display: flex;
2614
+ align-items: center;
2615
+ gap: 6px;
2616
+ }
2617
+
2618
+ .pk-badge {
2619
+ font-size: 10px;
2620
+ background: rgba(251, 191, 36, 0.2);
2621
+ color: #fbbf24;
2622
+ padding: 2px 6px;
2623
+ border-radius: 4px;
2624
+ font-weight: 600;
2625
+ letter-spacing: 0.5px;
2626
+ }
2627
+
2628
+ .col-type {
2629
+ font-size: 12px;
2630
+ color: var(--text-muted);
2631
+ font-family: monospace;
2632
+ }
2056
2633
 
2057
2634
  td {
2058
- padding: 10px 16px;
2635
+ padding: 12px 20px;
2059
2636
  border-bottom: 1px solid var(--border-color);
2637
+ border-right: 1px solid var(--border-color);
2060
2638
  color: var(--text-main);
2061
2639
  max-width: 300px;
2062
2640
  overflow: hidden;
2063
2641
  text-overflow: ellipsis;
2064
2642
  white-space: nowrap;
2643
+ transition: background-color 0.2s ease;
2065
2644
  }
2645
+
2066
2646
 
2067
2647
  tr:last-child td {
2068
2648
  border-bottom: none;
@@ -2072,11 +2652,17 @@ var htmlTemplate = `
2072
2652
  background-color: var(--row-hover);
2073
2653
  }
2074
2654
 
2075
- .type-number { color: #79c0ff; }
2076
- .type-string { color: #a5d6ff; }
2077
- .type-boolean { color: #ff7b72; }
2078
- .type-date { color: #d2a8ff; }
2079
- .type-null { color: var(--text-muted); font-style: italic; }
2655
+ .type-number { color: var(--type-number); }
2656
+ .type-string { color: var(--type-string); }
2657
+ .type-boolean { color: var(--type-boolean); }
2658
+ .type-date { color: var(--type-date); }
2659
+ .type-json { color: var(--type-json); font-family: monospace; }
2660
+ .type-null { color: var(--type-null); font-style: italic; }
2661
+
2662
+ .cell-value {
2663
+ font-family: 'JetBrains Mono', 'Fira Code', monospace;
2664
+ font-size: 13px;
2665
+ }
2080
2666
 
2081
2667
  .empty-state {
2082
2668
  display: flex;
@@ -2084,7 +2670,98 @@ var htmlTemplate = `
2084
2670
  align-items: center;
2085
2671
  justify-content: center;
2086
2672
  height: 100%;
2673
+ width: 100%;
2674
+ color: var(--text-muted);
2675
+ animation: fadeIn 0.5s ease-out;
2676
+ }
2677
+
2678
+ .empty-state svg {
2679
+ margin-bottom: 16px;
2680
+ color: #3f3f46;
2681
+ width: 64px;
2682
+ height: 64px;
2683
+ }
2684
+
2685
+ .empty-state h3 {
2686
+ color: var(--text-main);
2687
+ margin: 0 0 8px 0;
2688
+ font-size: 18px;
2689
+ }
2690
+
2691
+ @keyframes fadeIn {
2692
+ from { opacity: 0; transform: translateY(10px); }
2693
+ to { opacity: 1; transform: translateY(0); }
2694
+ }
2695
+
2696
+ @keyframes spin {
2697
+ from { transform: rotate(0deg); }
2698
+ to { transform: rotate(360deg); }
2699
+ }
2700
+ .spinning {
2701
+ animation: spin 1s linear infinite;
2702
+ }
2703
+
2704
+ /* Pagination */
2705
+ .pagination {
2706
+ display: flex;
2707
+ align-items: center;
2708
+ justify-content: space-between;
2709
+ padding: 16px 24px;
2710
+ background-color: rgba(24, 24, 27, 0.95);
2711
+ border-top: 1px solid var(--border-color);
2712
+ font-size: 14px;
2087
2713
  color: var(--text-muted);
2714
+ position: sticky;
2715
+ bottom: 0;
2716
+ z-index: 5;
2717
+ }
2718
+
2719
+ .pagination-info {
2720
+ display: flex;
2721
+ align-items: center;
2722
+ gap: 16px;
2723
+ }
2724
+
2725
+ .pagination-controls {
2726
+ display: flex;
2727
+ align-items: center;
2728
+ gap: 8px;
2729
+ }
2730
+
2731
+ .pagination-btn {
2732
+ background-color: rgba(255, 255, 255, 0.05);
2733
+ border: 1px solid var(--border-color);
2734
+ color: var(--text-main);
2735
+ padding: 6px 12px;
2736
+ border-radius: 6px;
2737
+ cursor: pointer;
2738
+ font-size: 13px;
2739
+ transition: all 0.2s ease;
2740
+ }
2741
+
2742
+ .pagination-btn:hover:not(:disabled) {
2743
+ background-color: rgba(255, 255, 255, 0.1);
2744
+ }
2745
+
2746
+ .pagination-btn:disabled {
2747
+ opacity: 0.4;
2748
+ cursor: not-allowed;
2749
+ }
2750
+
2751
+ .pagination-btn.active {
2752
+ background-color: var(--accent-color);
2753
+ border-color: var(--accent-color);
2754
+ }
2755
+
2756
+ .page-input {
2757
+ background-color: rgba(255, 255, 255, 0.05);
2758
+ border: 1px solid var(--border-color);
2759
+ color: var(--text-main);
2760
+ padding: 6px 8px;
2761
+ border-radius: 6px;
2762
+ width: 50px;
2763
+ text-align: center;
2764
+ font-size: 13px;
2088
2765
  }
2089
2766
  </style>
2090
2767
  </head>
@@ -2092,7 +2769,7 @@ var htmlTemplate = `
2092
2769
 
2093
2770
  <div class="sidebar">
2094
2771
  <div class="sidebar-header">
2095
- <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
2772
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
2096
2773
  <ellipse cx="12" cy="5" rx="9" ry="3"></ellipse>
2097
2774
  <path d="M21 12c0 1.66-4 3-9 3s-9-1.34-9-3"></path>
2098
2775
  <path d="M3 5v14c0 1.66 4 3 9 3s9-1.34 9-3V5"></path>
@@ -2106,19 +2783,31 @@ var htmlTemplate = `
2106
2783
 
2107
2784
  <div class="main">
2108
2785
  <div class="main-header">
2109
- <h2 id="current-table-name">Select a table</h2>
2786
+ <h2 id="current-table-name">
2787
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="color: var(--text-muted);">
2788
+ <rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
2789
+ <line x1="3" y1="9" x2="21" y2="9"></line>
2790
+ <line x1="9" y1="21" x2="9" y2="9"></line>
2791
+ </svg>
2792
+ Select a table
2793
+ </h2>
2110
2794
  <button id="refresh-btn" class="btn" disabled>
2111
- <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
2795
+ <svg id="refresh-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
2112
2796
  <polyline points="23 4 23 10 17 10"></polyline>
2113
2797
  <polyline points="1 20 1 14 7 14"></polyline>
2114
2798
  <path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"></path>
2115
2799
  </svg>
2116
- Refresh Data
2800
+ Refresh
2117
2801
  </button>
2118
2802
  </div>
2119
2803
  <div class="content-area">
2120
2804
  <div id="data-container" class="empty-state">
2121
- <p>Select a table from the sidebar to view data</p>
2805
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
2806
+ <path d="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z"></path>
2807
+ <polyline points="14 2 14 8 20 8"></polyline>
2808
+ </svg>
2809
+ <h3>No Table Selected</h3>
2810
+ <p>Choose a table from the sidebar to view its data</p>
2122
2811
  </div>
2123
2812
  </div>
2124
2813
  </div>
@@ -2126,21 +2815,38 @@ var htmlTemplate = `
2126
2815
  <script>
2127
2816
  let currentTable = null;
2128
2817
  let tablesData = [];
2818
+ let tableSchemas = {};
2819
+ let currentPage = 1;
2820
+ let totalPages = 1;
2821
+ let totalRecords = 0;
2822
+ let pageSize = 50;
2129
2823
 
2130
2824
  // Format values for the data grid
2131
2825
  function formatValue(val) {
2132
- if (val === null || val === undefined) return '<span class="type-null">null</span>';
2133
- if (typeof val === 'number') return \`<span class="type-number">\${val}</span>\`;
2134
- if (typeof val === 'boolean') return \`<span class="type-boolean">\${val}</span>\`;
2135
- if (val instanceof Date || (typeof val === 'string' && val.match(/^\\d{4}-\\d{2}-\\d{2}T/))) {
2136
- return \`<span class="type-date">\${new Date(val).toLocaleString()}</span>\`;
2826
+ if (val === null || val === undefined) return 'null';
2827
+
2828
+ if (typeof val === 'object') {
2829
+ if (val instanceof Date) {
2830
+ return val.toISOString();
2831
+ }
2832
+ // Handle JSON objects
2833
+ const safeJson = JSON.stringify(val)
2834
+ .replace(/&/g, "&amp;")
2835
+ .replace(/</g, "&lt;")
2836
+ .replace(/>/g, "&gt;");
2837
+ return safeJson;
2137
2838
  }
2138
- // Escape HTML to prevent XSS
2139
- const safeStr = String(val)
2140
- .replace(/&/g, "&amp;")
2141
- .replace(/</g, "&lt;")
2142
- .replace(/>/g, "&gt;");
2143
- return \`<span class="type-string">\${safeStr}</span>\`;
2839
+
2840
+ if (typeof val === 'string') {
2841
+ // Escape HTML to prevent XSS
2842
+ const safeStr = String(val)
2843
+ .replace(/&/g, "&amp;")
2844
+ .replace(/</g, "&lt;")
2845
+ .replace(/>/g, "&gt;");
2846
+ return safeStr;
2847
+ }
2848
+
2849
+ return String(val);
2144
2850
  }
2145
2851
 
2146
2852
  async function loadTables() {
@@ -2150,12 +2856,14 @@ var htmlTemplate = `
2150
2856
 
2151
2857
  const list = document.getElementById('table-list');
2152
2858
  list.innerHTML = '';
2859
+ tableSchemas = {};
2153
2860
 
2154
2861
  tablesData.forEach(t => {
2862
+ tableSchemas[t.name] = t;
2155
2863
  const li = document.createElement('li');
2156
2864
  li.className = 'table-item';
2157
2865
  li.innerHTML = \`
2158
- <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
2866
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
2159
2867
  <rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
2160
2868
  <line x1="3" y1="9" x2="21" y2="9"></line>
2161
2869
  <line x1="9" y1="21" x2="9" y2="9"></line>
@@ -2170,8 +2878,9 @@ var htmlTemplate = `
2170
2878
  }
2171
2879
  }
2172
2880
 
2173
- async function selectTable(name) {
2881
+ async function selectTable(name, page = 1) {
2174
2882
  currentTable = name;
2883
+ currentPage = page;
2175
2884
 
2176
2885
  // Update UI active state
2177
2886
  document.querySelectorAll('.table-item').forEach(el => {
@@ -2179,60 +2888,169 @@ var htmlTemplate = `
2179
2888
  else el.classList.remove('active');
2180
2889
  });
2181
2890
 
2182
- document.getElementById('current-table-name').textContent = name;
2891
+ document.getElementById('current-table-name').innerHTML = \`
2892
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="color: var(--accent-color);">
2893
+ <rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
2894
+ <line x1="3" y1="9" x2="21" y2="9"></line>
2895
+ <line x1="9" y1="21" x2="9" y2="9"></line>
2896
+ </svg>
2897
+ \${name}
2898
+ \`;
2183
2899
  document.getElementById('refresh-btn').disabled = false;
2184
2900
 
2185
2901
  const container = document.getElementById('data-container');
2186
- container.innerHTML = '<div class="empty-state">Loading data...</div>';
2187
- container.className = '';
2902
+ container.innerHTML = \`
2903
+ <div class="empty-state">
2904
+ <svg class="spinning" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
2905
+ <line x1="12" y1="2" x2="12" y2="6"></line>
2906
+ <line x1="12" y1="18" x2="12" y2="22"></line>
2907
+ <line x1="4.93" y1="4.93" x2="7.76" y2="7.76"></line>
2908
+ <line x1="16.24" y1="16.24" x2="19.07" y2="19.07"></line>
2909
+ <line x1="2" y1="12" x2="6" y2="12"></line>
2910
+ <line x1="18" y1="12" x2="22" y2="12"></line>
2911
+ <line x1="4.93" y1="19.07" x2="7.76" y2="16.24"></line>
2912
+ <line x1="16.24" y1="7.76" x2="19.07" y2="4.93"></line>
2913
+ </svg>
2914
+ <p>Loading data...</p>
2915
+ </div>
2916
+ \`;
2188
2917
 
2189
2918
  try {
2190
- const res = await fetch(\`/api/tables/\${name}/data\`);
2919
+ const res = await fetch(\`/api/tables/\${name}/data?page=\${page}&limit=\${pageSize}\`);
2191
2920
  if (!res.ok) throw new Error(await res.text());
2192
- const rows = await res.json();
2921
+ const response = await res.json();
2922
+
2923
+ const rows = response.data || response;
2924
+ totalRecords = response.total || 0;
2925
+ totalPages = response.totalPages || 1;
2926
+
2927
+ const tableSchema = tableSchemas[name];
2193
2928
 
2194
2929
  if (rows.length === 0) {
2195
- container.innerHTML = '<div class="empty-state">No data in this table</div>';
2930
+ container.innerHTML = \`
2931
+ <div class="empty-state">
2932
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
2933
+ <circle cx="12" cy="12" r="10"></circle>
2934
+ <line x1="12" y1="8" x2="12" y2="12"></line>
2935
+ <line x1="12" y1="16" x2="12.01" y2="16"></line>
2936
+ </svg>
2937
+ <h3>Table is Empty</h3>
2938
+ <p>No records found in "\${name}"</p>
2939
+ </div>
2940
+ \`;
2196
2941
  return;
2197
2942
  }
2198
2943
 
2199
- const columns = Object.keys(rows[0]);
2944
+ // Use all columns from the actual data to ensure foreign keys and all data are shown
2945
+ let columns = [];
2946
+ const allKeys = new Set();
2947
+ rows.forEach(row => {
2948
+ Object.keys(row).forEach(key => allKeys.add(key));
2949
+ });
2950
+
2951
+ // Try to get type info from schema if available
2952
+ const schemaColMap = {};
2953
+ if (tableSchema && tableSchema.columns && tableSchema.columns.length > 0) {
2954
+ tableSchema.columns.forEach(col => {
2955
+ schemaColMap[col.name] = col;
2956
+ });
2957
+ }
2958
+
2959
+ columns = Array.from(allKeys).map(key => ({
2960
+ name: key,
2961
+ type: schemaColMap[key] ? schemaColMap[key].type : typeof rows[0][key],
2962
+ primaryKey: schemaColMap[key] ? schemaColMap[key].primaryKey : false
2963
+ }));
2200
2964
 
2201
2965
  let html = '<div class="data-grid-container"><table><thead><tr>';
2202
2966
  columns.forEach(col => {
2203
- html += \`<th>\${col}</th>\`;
2967
+ html += \`
2968
+ <th>
2969
+ <div class="col-header">
2970
+ <div class="col-name">
2971
+ \${col.name}
2972
+ \${col.primaryKey ? '<span class="pk-badge">PK</span>' : ''}
2973
+ </div>
2974
+ <div class="col-type">\${col.type}</div>
2975
+ </div>
2976
+ </th>
2977
+ \`;
2204
2978
  });
2205
2979
  html += '</tr></thead><tbody>';
2206
2980
 
2207
2981
  rows.forEach(row => {
2208
2982
  html += '<tr>';
2209
2983
  columns.forEach(col => {
2210
- html += \`<td>\${formatValue(row[col])}</td>\`;
2984
+ html += \`<td>\${formatValue(row[col.name])}</td>\`;
2211
2985
  });
2212
2986
  html += '</tr>';
2213
2987
  });
2214
2988
 
2215
- html += '</tbody></table></div>';
2989
+ html += '</tbody></table>';
2990
+
2991
+ // Add pagination
2992
+ html += renderPagination();
2993
+
2994
+ html += '</div>';
2216
2995
  container.innerHTML = html;
2217
2996
 
2218
2997
  } catch (e) {
2219
- container.innerHTML = \`<div class="empty-state" style="color: #ff7b72;">Error: \${e.message}</div>\`;
2998
+ container.innerHTML = \`
2999
+ <div class="empty-state">
3000
+ <svg viewBox="0 0 24 24" fill="none" stroke="#ef4444" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
3001
+ <circle cx="12" cy="12" r="10"></circle>
3002
+ <line x1="12" y1="8" x2="12" y2="12"></line>
3003
+ <line x1="12" y1="16" x2="12.01" y2="16"></line>
3004
+ </svg>
3005
+ <h3 style="color: #ef4444;">Error Loading Data</h3>
3006
+ <p>\${e.message}</p>
3007
+ </div>
3008
+ \`;
2220
3009
  }
2221
3010
  }
2222
3011
 
3012
+ function renderPagination() {
3013
+ const startRecord = (currentPage - 1) * pageSize + 1;
3014
+ const endRecord = Math.min(currentPage * pageSize, totalRecords);
3015
+
3016
+ let html = '<div class="pagination">';
3017
+ html += '<div class="pagination-info">';
3018
+ html += \`<span>Total: \${totalRecords} records</span>\`;
3019
+ html += \`<span>Page \${currentPage} of \${totalPages}</span>\`;
3020
+ html += \`<span>Showing \${startRecord}-\${endRecord}</span>\`;
3021
+ html += '</div>';
3022
+
3023
+ html += '<div class="pagination-controls">';
3024
+
3025
+ // Previous button
3026
+ html += \`<button class="pagination-btn" onclick="changePage(\${currentPage - 1})" \${currentPage === 1 ? 'disabled' : ''}>Previous</button>\`;
3027
+
3028
+ // Next button
3029
+ html += \`<button class="pagination-btn" onclick="changePage(\${currentPage + 1})" \${currentPage === totalPages ? 'disabled' : ''}>Next</button>\`;
3030
+
3031
+ html += '</div>';
3032
+ html += '</div>';
3033
+
3034
+ return html;
3035
+ }
3036
+
3037
+ function changePage(page) {
3038
+ if (page < 1 || page > totalPages) return;
3039
+ selectTable(currentTable, page);
3040
+ }
3041
+
2223
3042
  document.getElementById('refresh-btn').addEventListener('click', async () => {
2224
3043
  if (!currentTable) return;
2225
3044
 
2226
- const btn = document.getElementById('refresh-btn');
2227
- const originalText = btn.innerHTML;
2228
- btn.innerHTML = 'Refreshing...';
3045
+ const icon = document.getElementById('refresh-icon');
3046
+ icon.classList.add('spinning');
2229
3047
 
2230
3048
  try {
2231
3049
  await selectTable(currentTable);
2232
3050
  } catch (e) {
2233
3051
  console.error("Failed to refresh data", e);
2234
3052
  } finally {
2235
- setTimeout(() => { btn.innerHTML = originalText; }, 300);
3053
+ setTimeout(() => { icon.classList.remove('spinning'); }, 500);
2236
3054
  }
2237
3055
  });
2238
3056
 
@@ -2308,6 +3126,43 @@ Example query: await db.select().from(users)`);
2308
3126
  });
2309
3127
  }
2310
3128
 
3129
+ // src/config.ts
3130
+ import { resolve as resolve7, join as join6 } from "path";
3131
+ var CONFIG_FILES = [
3132
+ "bungres.config.ts",
3133
+ "bungres.config.js",
3134
+ "bungres.config.mts",
3135
+ "bungres.config.mjs"
3136
+ ];
3137
+ async function loadConfig(cwd = process.cwd()) {
3138
+ let userConfig = {};
3139
+ for (const file of CONFIG_FILES) {
3140
+ const configPath = join6(cwd, file);
3141
+ if (await Bun.file(configPath).exists()) {
3142
+ const mod = await import(resolve7(configPath));
3143
+ userConfig = mod.default ?? mod;
3144
+ break;
3145
+ }
3146
+ }
3147
+ const dbUrl = userConfig.dbCredentials?.url ?? process.env["DATABASE_URL"] ?? process.env["POSTGRES_URL"] ?? "";
3148
+ if (!dbUrl) {
3149
+ console.error(`bungres: No database URL found.
3150
+ Add dbCredentials.url to bungres.config.ts or set DATABASE_URL.`);
3151
+ process.exit(1);
3152
+ }
3153
+ const out = userConfig.out ?? "./migrations";
3154
+ return {
3155
+ schema: userConfig.schema ?? "src/db/schema/**/*.ts",
3156
+ seed: userConfig.seed ?? "src/db/seed.ts",
3157
+ out,
3158
+ dbUrl,
3159
+ dbSchema: userConfig.dbSchema ?? "public",
3160
+ migrationsDir: out,
3161
+ outDir: "./src/db/generated",
3162
+ verbose: userConfig.verbose ?? false
3163
+ };
3164
+ }
3165
+
2311
3166
  // src/ensure-db.ts
2312
3167
  async function ensureDatabase2(dbUrl) {
2313
3168
  let sql2;
@@ -2347,6 +3202,7 @@ ${colorize("Usage:", "yellow")}
2347
3202
  bungres <command> [options]
2348
3203
 
2349
3204
  ${colorize("Commands:", "yellow")}
3205
+ init Initialize bungres project with config file and db folder structure
2350
3206
  generate Generate SQL migration files from your schema definitions
2351
3207
  migrate Run pending migration files against the database
2352
3208
  push Apply schema directly to the DB (no migration files, dev mode)
@@ -2367,6 +3223,7 @@ ${colorize("Options:", "yellow")}
2367
3223
  --help Show this help
2368
3224
 
2369
3225
  ${colorize("Examples:", "yellow")}
3226
+ bungres init
2370
3227
  bungres generate
2371
3228
  bungres migrate
2372
3229
  bungres push
@@ -2391,6 +3248,10 @@ async function main() {
2391
3248
  }
2392
3249
  const command = args[0];
2393
3250
  const flags = parseFlags(args.slice(1));
3251
+ if (command === "init") {
3252
+ await runInit();
3253
+ process.exit(0);
3254
+ }
2394
3255
  const config2 = await loadConfig(process.cwd());
2395
3256
  if (flags.verbose)
2396
3257
  config2.verbose = true;