@bungres/kit 0.1.1 → 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
- // ../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
- // ../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
- // ../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
- // ../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
- // ../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
- // ../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);
@@ -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();
1195
- }
1196
- }
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();
1621
+ } else {
1622
+ changes.push(`ALTER COLUMN ${col2} DROP DEFAULT`);
1623
+ }
1624
+ }
1625
+ return changes;
1201
1626
  }
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,
@@ -1467,21 +1921,22 @@ function generateSchemaTS(tableMap, dbSchema) {
1467
1921
  `// Generated at: ${new Date().toISOString()}`,
1468
1922
  ``,
1469
1923
  `import {`,
1470
- ` table,`,
1924
+ ` snakeCase,`,
1471
1925
  ` text, varchar, char, integer, bigint, smallint,`,
1472
1926
  ` serial, bigserial, boolean, real, doublePrecision,`,
1473
1927
  ` numeric, decimal, json, jsonb,`,
1474
1928
  ` timestamp, timestamptz, date, time, uuid,`,
1475
1929
  ` bytea, inet,`,
1930
+ ` textArray, integerArray, varcharArray, uuidArray,`,
1476
1931
  `} from "@bungres/orm";`,
1477
1932
  ``
1478
1933
  ];
1479
1934
  for (const [, table2] of tableMap) {
1480
1935
  const varName = toCamelCase(table2.tableName);
1481
- lines.push(`export const ${varName} = table("${table2.tableName}", {`);
1936
+ lines.push(`export const ${varName} = snakeCase.table("${table2.tableName}", {`);
1482
1937
  for (const col2 of table2.columns) {
1483
1938
  const colExpr = buildColumnExpression(col2);
1484
- lines.push(` ${col2.name}: ${colExpr},`);
1939
+ lines.push(` ${toCamelCase(col2.name)}: ${colExpr},`);
1485
1940
  }
1486
1941
  const options = [];
1487
1942
  if (dbSchema !== "public") {
@@ -1528,209 +1983,172 @@ function generateSchemaTS(tableMap, dbSchema) {
1528
1983
  `);
1529
1984
  }
1530
1985
  function buildColumnExpression(col2) {
1531
- let expr = pgTypeToBungresBuilder(col2);
1986
+ const opts = [];
1987
+ if (col2.dataType === "character varying" || col2.dataType === "character") {
1988
+ if (col2.maxLength)
1989
+ opts.push(`length: ${col2.maxLength}`);
1990
+ }
1532
1991
  if (col2.isPrimary)
1533
- expr += `.primaryKey()`;
1992
+ opts.push(`primaryKey: true`);
1534
1993
  else if (!col2.isNullable)
1535
- expr += `.notNull()`;
1994
+ opts.push(`notNull: true`);
1536
1995
  if (col2.isUnique && !col2.isPrimary)
1537
- expr += `.unique()`;
1996
+ opts.push(`unique: true`);
1538
1997
  if (col2.columnDefault !== null && !col2.isPrimary) {
1539
1998
  if (col2.columnDefault.includes("(")) {
1540
- expr += `.defaultRaw("${col2.columnDefault}")`;
1541
- } else if (col2.dataType === "boolean") {
1542
- expr += `.default(${col2.columnDefault})`;
1543
- } else if (col2.dataType.includes("int") || col2.dataType.includes("numeric") || col2.dataType.includes("real") || col2.dataType.includes("double")) {
1544
- expr += `.default(${col2.columnDefault})`;
1999
+ opts.push(`defaultRaw: "${col2.columnDefault}"`);
2000
+ } else if (col2.dataType === "boolean" || col2.dataType.includes("int") || col2.dataType.includes("numeric") || col2.dataType.includes("real") || col2.dataType.includes("double")) {
2001
+ opts.push(`default: ${col2.columnDefault}`);
1545
2002
  } else {
1546
2003
  const cleaned = col2.columnDefault.replace(/^'(.*)'::.*$/, "$1");
1547
- expr += `.default("${cleaned}")`;
2004
+ opts.push(`default: "${cleaned}"`);
1548
2005
  }
1549
2006
  }
1550
2007
  if (col2.foreignTable && col2.foreignColumn) {
1551
2008
  const deleteRule = col2.deleteRule?.toLowerCase().replace(" ", " ");
1552
2009
  const updateRule = col2.updateRule?.toLowerCase().replace(" ", " ");
1553
- let refOpts = "";
1554
- if (deleteRule && deleteRule !== "no action") {
1555
- refOpts += `onDelete: "${deleteRule}", `;
1556
- }
1557
- if (updateRule && updateRule !== "no action") {
1558
- refOpts += `onUpdate: "${updateRule}"`;
1559
- }
1560
- const opts = refOpts ? `, { ${refOpts.trim().replace(/,$/, "")} }` : "";
1561
- expr += `.references("${col2.foreignTable}", "${col2.foreignColumn}"${opts})`;
1562
- }
1563
- return expr;
2010
+ let refOpts = `table: "${col2.foreignTable}", column: "${col2.foreignColumn}"`;
2011
+ if (deleteRule && deleteRule !== "no action")
2012
+ refOpts += `, onDelete: "${deleteRule}"`;
2013
+ if (updateRule && updateRule !== "no action")
2014
+ refOpts += `, onUpdate: "${updateRule}"`;
2015
+ opts.push(`references: { ${refOpts} }`);
2016
+ }
2017
+ let builderName = pgTypeToBungresBuilderName(col2);
2018
+ if (opts.length > 0) {
2019
+ return `${builderName}({ ${opts.join(", ")} })`;
2020
+ }
2021
+ return `${builderName}()`;
1564
2022
  }
1565
- function pgTypeToBungresBuilder(col2) {
2023
+ function pgTypeToBungresBuilderName(col2) {
1566
2024
  const dt = col2.dataType;
1567
- const name = col2.name;
1568
2025
  if (dt === "uuid")
1569
- return `uuid("${name}")`;
2026
+ return "uuid";
1570
2027
  if (dt === "text")
1571
- return `text("${name}")`;
2028
+ return "text";
1572
2029
  if (dt === "character varying")
1573
- return col2.maxLength ? `varchar("${name}", ${col2.maxLength})` : `varchar("${name}")`;
2030
+ return "varchar";
1574
2031
  if (dt === "character")
1575
- return col2.maxLength ? `char("${name}", ${col2.maxLength})` : `char("${name}")`;
2032
+ return "char";
1576
2033
  if (dt === "integer")
1577
- return `integer("${name}")`;
2034
+ return "integer";
1578
2035
  if (dt === "bigint")
1579
- return `bigint("${name}")`;
2036
+ return "bigint";
1580
2037
  if (dt === "smallint")
1581
- return `smallint("${name}")`;
2038
+ return "smallint";
1582
2039
  if (dt === "boolean")
1583
- return `boolean("${name}")`;
2040
+ return "boolean";
1584
2041
  if (dt === "real")
1585
- return `real("${name}")`;
2042
+ return "real";
1586
2043
  if (dt === "double precision")
1587
- return `doublePrecision("${name}")`;
2044
+ return "doublePrecision";
1588
2045
  if (dt === "numeric" || dt === "decimal")
1589
- return `numeric("${name}")`;
2046
+ return "numeric";
1590
2047
  if (dt === "json")
1591
- return `json("${name}")`;
2048
+ return "json";
1592
2049
  if (dt === "jsonb")
1593
- return `jsonb("${name}")`;
2050
+ return "jsonb";
1594
2051
  if (dt === "timestamp without time zone")
1595
- return `timestamp("${name}")`;
2052
+ return "timestamp";
1596
2053
  if (dt === "timestamp with time zone")
1597
- return `timestamptz("${name}")`;
2054
+ return "timestamptz";
1598
2055
  if (dt === "date")
1599
- return `date("${name}")`;
2056
+ return "date";
1600
2057
  if (dt === "time without time zone")
1601
- return `time("${name}")`;
2058
+ return "time";
1602
2059
  if (dt === "bytea")
1603
- return `bytea("${name}")`;
2060
+ return "bytea";
1604
2061
  if (dt === "inet")
1605
- return `inet("${name}")`;
2062
+ return "inet";
1606
2063
  if (dt === "USER-DEFINED" && col2.udtName === "citext")
1607
- return `text("${name}")`;
1608
- return `text("${name}") /* original type: ${dt} */`;
2064
+ return "text";
2065
+ if (dt === "ARRAY") {
2066
+ if (col2.udtName === "_text")
2067
+ return "textArray";
2068
+ if (col2.udtName === "_int4" || col2.udtName === "_int8")
2069
+ return "integerArray";
2070
+ if (col2.udtName === "_varchar")
2071
+ return "varcharArray";
2072
+ if (col2.udtName === "_uuid")
2073
+ return "uuidArray";
2074
+ return "textArray";
2075
+ }
2076
+ return "text";
1609
2077
  }
1610
2078
  function toCamelCase(str) {
1611
2079
  return str.replace(/_([a-z])/g, (_, c) => c.toUpperCase());
1612
2080
  }
1613
2081
 
1614
- // src/commands/status.ts
1615
- import { resolve as resolve6 } from "path";
1616
- var MIGRATIONS_TABLE2 = "__bungres_migrations";
1617
- async function runStatus(config2) {
1618
- const migrationsDir = resolve6(config2.migrationsDir);
1619
- const sql2 = new Bun.SQL(config2.dbUrl);
1620
- try {
1621
- const tableCheck = await sql2.unsafe(`SELECT EXISTS (
1622
- SELECT 1 FROM information_schema.tables
1623
- WHERE table_name = $1
1624
- ) AS exists`, [MIGRATIONS_TABLE2]);
1625
- const trackingExists = tableCheck[0]?.exists ?? false;
1626
- const glob = new Bun.Glob("*.sql");
1627
- const files = [];
1628
- for await (const file of glob.scan({ cwd: migrationsDir, absolute: false })) {
1629
- files.push(file);
1630
- }
1631
- files.sort();
1632
- if (files.length === 0) {
1633
- console.log(colorize("No migration files found. Run `bungres generate` first.", "yellow"));
1634
- return;
1635
- }
1636
- let appliedSet = new Set;
1637
- if (trackingExists) {
1638
- const applied = await sql2.unsafe(`SELECT name FROM "${MIGRATIONS_TABLE2}" ORDER BY applied_at`);
1639
- appliedSet = new Set(applied.map((r) => r.name));
1640
- }
1641
- console.log(colorize(`
1642
- Migration status:
1643
- `, "cyan"));
1644
- let pendingCount = 0;
1645
- for (const file of files) {
1646
- const isApplied = appliedSet.has(file);
1647
- const status = isApplied ? colorize("\u2713 applied ", "green") : colorize("\u2717 pending ", "yellow");
1648
- if (!isApplied)
1649
- pendingCount++;
1650
- console.log(` ${status} ${file}`);
1651
- }
1652
- console.log(`
1653
- ${colorize(appliedSet.size.toString(), "green")} applied, ${colorize(pendingCount.toString(), "yellow")} pending.
1654
- `);
1655
- } finally {
1656
- await sql2.end();
1657
- }
1658
- }
1659
-
1660
- // src/commands/drop.ts
1661
- 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...");
1662
2086
  const schemas2 = await loadSchemas(config2.schema);
1663
2087
  if (schemas2.length === 0) {
1664
- console.warn("No table definitions found in schema files.");
2088
+ console.warn("No table definitions found. Check your schema glob pattern.");
1665
2089
  return;
1666
2090
  }
1667
2091
  const sql2 = new Bun.SQL(config2.dbUrl);
1668
2092
  try {
1669
- const existingTablesResult = await sql2`
1670
- SELECT table_name
1671
- FROM information_schema.tables
1672
- WHERE table_schema = 'public' OR table_schema = current_schema()
1673
- `;
1674
- const existingTableNames = new Set(existingTablesResult.map((row) => row.table_name));
1675
- const migrationTableExists = existingTableNames.has("__bungres_migrations");
1676
- const pushTableExists = existingTableNames.has("__bungres_push");
1677
- const tablesToDrop = schemas2.filter((s) => existingTableNames.has(s.config.name));
1678
- if (tablesToDrop.length === 0 && !migrationTableExists && !pushTableExists) {
1679
- console.log("No tables to drop (they either don't exist or were already dropped).");
1680
- 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;
1681
2103
  }
1682
- const tableNamesToPrint = tablesToDrop.map((s) => (s.config.schema ? s.config.schema + "." : "") + s.config.name);
1683
- if (migrationTableExists) {
1684
- 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;
1685
2110
  }
1686
- if (pushTableExists) {
1687
- 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!`);
1688
2122
  }
1689
2123
  if (!opts.force) {
1690
- console.warn(colorize(`
1691
- \u26A0\uFE0F WARNING: This will drop the following tables and ALL their data:
1692
- `, "red"));
1693
- for (const name of tableNamesToPrint)
1694
- console.log(colorize(` - ${name}`, "yellow"));
1695
- process.stdout.write(colorize(`
1696
- Are you sure? Type YES to continue: `, "cyan"));
1697
- for await (const line2 of console) {
1698
- if (line2.trim().toLowerCase() !== "yes") {
1699
- console.log("Aborted.");
1700
- return;
1701
- }
1702
- 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;
1703
2130
  }
1704
2131
  }
1705
- for (const entry of tablesToDrop) {
1706
- const ddl = `DROP TABLE IF EXISTS "${entry.config.name}" CASCADE;`;
1707
- await sql2.unsafe(ddl);
1708
- console.log(colorize(` \u2713 dropped ${entry.config.name}`, "green"));
1709
- }
1710
- if (migrationTableExists) {
1711
- await sql2.unsafe("DROP TABLE IF EXISTS public.__bungres_migrations CASCADE");
1712
- console.log(colorize(` \u2713 dropped __bungres_migrations`, "green"));
1713
- }
1714
- if (pushTableExists) {
1715
- await sql2.unsafe("DROP TABLE IF EXISTS public.__bungres_push CASCADE");
1716
- 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);
1717
2140
  }
1718
- console.log(colorize(`
1719
- Drop complete.`, "green"));
1720
- } finally {
1721
- await sql2.end();
1722
- }
1723
- }
1724
-
1725
- // src/commands/fresh.ts
1726
- async function runFresh(config2) {
1727
- console.log("Dropping all tables...");
1728
- await runDrop(config2, { force: true });
1729
- console.log(`
1730
- Re-running migrations...`);
1731
- await runMigrate(config2);
1732
- console.log(`
1733
- 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();
1734
2152
  }
1735
2153
 
1736
2154
  // src/commands/refresh.ts
@@ -1757,13 +2175,13 @@ Refresh complete. All tables are now empty.`, "green"));
1757
2175
  }
1758
2176
 
1759
2177
  // src/commands/seed.ts
1760
- import { resolve as resolve7 } from "path";
2178
+ import { resolve as resolve5 } from "path";
1761
2179
  async function runSeed(config2) {
1762
2180
  if (!config2.seed) {
1763
2181
  console.log(colorize(`No seed file configured in bungres.config.ts`, "yellow"));
1764
2182
  return;
1765
2183
  }
1766
- const seedPath = resolve7(process.cwd(), config2.seed);
2184
+ const seedPath = resolve5(process.cwd(), config2.seed);
1767
2185
  const file = Bun.file(seedPath);
1768
2186
  if (!await file.exists()) {
1769
2187
  console.error(colorize(`Seed file not found at ${seedPath}`, "red"));
@@ -1788,6 +2206,52 @@ Seed failed with exit code ${exitCode}.`);
1788
2206
  }
1789
2207
  }
1790
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
+
1791
2255
  // src/commands/studio.ts
1792
2256
  async function runStudio(config2) {
1793
2257
  const schemas2 = await loadSchemas(config2.schema);
@@ -1826,8 +2290,19 @@ async function runStudio(config2) {
1826
2290
  return new Response("Table not found", { status: 404 });
1827
2291
  }
1828
2292
  try {
1829
- const data = await db2.select().from(schema.table).limit(100);
1830
- 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
+ }), {
1831
2306
  headers: { "Content-Type": "application/json" }
1832
2307
  });
1833
2308
  } catch (e) {
@@ -1877,18 +2352,26 @@ var htmlTemplate = `
1877
2352
  <meta charset="UTF-8">
1878
2353
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
1879
2354
  <title>Bungres Studio</title>
1880
- <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">
1881
2356
  <style>
1882
2357
  :root {
1883
- --bg-color: #0d1117;
1884
- --panel-bg: #161b22;
1885
- --border-color: #30363d;
1886
- --text-main: #c9d1d9;
1887
- --text-muted: #8b949e;
1888
- --accent-color: #58a6ff;
1889
- --accent-hover: #1f6feb;
1890
- --header-bg: #161b22;
1891
- --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;
1892
2375
  }
1893
2376
 
1894
2377
  * { box-sizing: border-box; }
@@ -1896,7 +2379,7 @@ var htmlTemplate = `
1896
2379
  body {
1897
2380
  margin: 0;
1898
2381
  padding: 0;
1899
- 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;
1900
2383
  background-color: var(--bg-color);
1901
2384
  color: var(--text-main);
1902
2385
  display: flex;
@@ -1906,56 +2389,91 @@ var htmlTemplate = `
1906
2389
 
1907
2390
  /* Sidebar */
1908
2391
  .sidebar {
1909
- width: 250px;
2392
+ width: 280px;
1910
2393
  background-color: var(--panel-bg);
2394
+ backdrop-filter: blur(12px);
2395
+ -webkit-backdrop-filter: blur(12px);
1911
2396
  border-right: 1px solid var(--border-color);
1912
2397
  display: flex;
1913
2398
  flex-direction: column;
2399
+ box-shadow: 4px 0 24px rgba(0,0,0,0.2);
2400
+ z-index: 20;
1914
2401
  }
1915
2402
 
1916
2403
  .sidebar-header {
1917
- padding: 20px;
1918
- border-bottom: 1px solid var(--border-color);
2404
+ padding: 24px;
2405
+ border-bottom: 1px solid var(--glass-border);
1919
2406
  display: flex;
1920
2407
  align-items: center;
1921
- gap: 10px;
2408
+ gap: 12px;
2409
+ background: linear-gradient(to bottom, rgba(255,255,255,0.03), transparent);
1922
2410
  }
1923
2411
 
1924
2412
  .sidebar-header h1 {
1925
2413
  margin: 0;
1926
- font-size: 16px;
2414
+ font-size: 18px;
1927
2415
  font-weight: 600;
1928
- 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));
1929
2425
  }
1930
2426
 
1931
2427
  .table-list {
1932
2428
  flex: 1;
1933
2429
  overflow-y: auto;
1934
- padding: 10px 0;
2430
+ padding: 16px 12px;
1935
2431
  list-style: none;
1936
2432
  margin: 0;
1937
2433
  }
1938
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
+
1939
2443
  .table-item {
1940
- padding: 8px 20px;
2444
+ padding: 12px 16px;
1941
2445
  cursor: pointer;
1942
- font-size: 14px;
2446
+ font-size: 15px;
2447
+ font-weight: 500;
1943
2448
  color: var(--text-muted);
1944
- transition: all 0.2s ease;
2449
+ border-radius: 8px;
2450
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
1945
2451
  display: flex;
1946
2452
  align-items: center;
1947
- gap: 8px;
2453
+ gap: 10px;
2454
+ margin-bottom: 4px;
2455
+ border: 1px solid transparent;
1948
2456
  }
1949
2457
 
1950
2458
  .table-item:hover {
1951
- background-color: var(--row-hover);
2459
+ background-color: rgba(255, 255, 255, 0.05);
1952
2460
  color: var(--text-main);
2461
+ transform: translateX(4px);
1953
2462
  }
1954
2463
 
1955
2464
  .table-item.active {
1956
- 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 {
1957
2475
  color: var(--accent-color);
1958
- border-right: 3px solid var(--accent-color);
2476
+ transform: scale(1.1);
1959
2477
  }
1960
2478
 
1961
2479
  /* Main Content */
@@ -1963,92 +2481,168 @@ var htmlTemplate = `
1963
2481
  flex: 1;
1964
2482
  display: flex;
1965
2483
  flex-direction: column;
1966
- background-color: var(--bg-color);
2484
+ position: relative;
1967
2485
  }
1968
2486
 
1969
2487
  .main-header {
1970
- height: 60px;
1971
- padding: 0 20px;
2488
+ height: 70px;
2489
+ padding: 0 24px;
1972
2490
  border-bottom: 1px solid var(--border-color);
1973
2491
  display: flex;
1974
2492
  align-items: center;
1975
2493
  justify-content: space-between;
1976
2494
  background-color: var(--header-bg);
2495
+ backdrop-filter: blur(12px);
2496
+ -webkit-backdrop-filter: blur(12px);
2497
+ z-index: 10;
1977
2498
  }
1978
2499
 
1979
2500
  .main-header h2 {
1980
2501
  margin: 0;
1981
- font-size: 16px;
1982
- font-weight: 500;
2502
+ font-size: 20px;
2503
+ font-weight: 600;
1983
2504
  color: white;
2505
+ letter-spacing: -0.5px;
2506
+ display: flex;
2507
+ align-items: center;
2508
+ gap: 10px;
1984
2509
  }
1985
2510
 
1986
2511
  .btn {
1987
- background-color: var(--accent-color);
1988
- color: white;
1989
- border: none;
2512
+ background-color: rgba(255, 255, 255, 0.05);
2513
+ border: 1px solid var(--border-color);
2514
+ color: var(--text-main);
1990
2515
  padding: 6px 12px;
1991
2516
  border-radius: 6px;
1992
- font-size: 12px;
2517
+ font-size: 13px;
1993
2518
  font-weight: 500;
2519
+ font-family: 'Outfit', sans-serif;
1994
2520
  cursor: pointer;
1995
- transition: background-color 0.2s ease;
2521
+ transition: all 0.2s ease;
1996
2522
  display: flex;
1997
2523
  align-items: center;
1998
- gap: 6px;
2524
+ gap: 8px;
1999
2525
  }
2000
2526
 
2001
- .btn:hover {
2002
- 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);
2003
2533
  }
2004
2534
 
2005
2535
  .btn:disabled {
2006
- opacity: 0.5;
2536
+ opacity: 0.4;
2007
2537
  cursor: not-allowed;
2008
2538
  }
2009
2539
 
2010
2540
  .content-area {
2011
2541
  flex: 1;
2012
2542
  overflow: auto;
2013
- padding: 20px;
2543
+ padding: 0;
2544
+ position: relative;
2545
+ z-index: 1;
2546
+ text-align: left;
2014
2547
  }
2015
2548
 
2016
2549
  /* Data Table */
2017
2550
  .data-grid-container {
2018
- background-color: var(--panel-bg);
2551
+ background-color: rgba(24, 24, 27, 0.5);
2019
2552
  border: 1px solid var(--border-color);
2020
- border-radius: 8px;
2021
- 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;
2022
2571
  }
2023
2572
 
2024
2573
  table {
2025
- width: 100%;
2026
- border-collapse: collapse;
2574
+ width: auto;
2575
+ border-collapse: separate;
2576
+ border-spacing: 0;
2027
2577
  text-align: left;
2028
- font-size: 13px;
2578
+ font-size: 14px;
2579
+ table-layout: auto;
2029
2580
  }
2030
2581
 
2031
2582
  th {
2032
- background-color: rgba(255,255,255,0.02);
2033
- color: var(--text-muted);
2583
+ background-color: rgba(24, 24, 27, 0.95);
2584
+ backdrop-filter: blur(4px);
2585
+ color: var(--text-main);
2034
2586
  font-weight: 500;
2035
- padding: 10px 16px;
2587
+ padding: 12px 20px;
2036
2588
  border-bottom: 1px solid var(--border-color);
2589
+ border-right: 1px solid var(--border-color);
2037
2590
  white-space: nowrap;
2038
2591
  position: sticky;
2039
2592
  top: 0;
2040
2593
  z-index: 10;
2041
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
+ }
2042
2633
 
2043
2634
  td {
2044
- padding: 10px 16px;
2635
+ padding: 12px 20px;
2045
2636
  border-bottom: 1px solid var(--border-color);
2637
+ border-right: 1px solid var(--border-color);
2046
2638
  color: var(--text-main);
2047
2639
  max-width: 300px;
2048
2640
  overflow: hidden;
2049
2641
  text-overflow: ellipsis;
2050
2642
  white-space: nowrap;
2643
+ transition: background-color 0.2s ease;
2051
2644
  }
2645
+
2052
2646
 
2053
2647
  tr:last-child td {
2054
2648
  border-bottom: none;
@@ -2058,11 +2652,17 @@ var htmlTemplate = `
2058
2652
  background-color: var(--row-hover);
2059
2653
  }
2060
2654
 
2061
- .type-number { color: #79c0ff; }
2062
- .type-string { color: #a5d6ff; }
2063
- .type-boolean { color: #ff7b72; }
2064
- .type-date { color: #d2a8ff; }
2065
- .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
+ }
2066
2666
 
2067
2667
  .empty-state {
2068
2668
  display: flex;
@@ -2070,7 +2670,98 @@ var htmlTemplate = `
2070
2670
  align-items: center;
2071
2671
  justify-content: center;
2072
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;
2073
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;
2074
2765
  }
2075
2766
  </style>
2076
2767
  </head>
@@ -2078,7 +2769,7 @@ var htmlTemplate = `
2078
2769
 
2079
2770
  <div class="sidebar">
2080
2771
  <div class="sidebar-header">
2081
- <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">
2082
2773
  <ellipse cx="12" cy="5" rx="9" ry="3"></ellipse>
2083
2774
  <path d="M21 12c0 1.66-4 3-9 3s-9-1.34-9-3"></path>
2084
2775
  <path d="M3 5v14c0 1.66 4 3 9 3s9-1.34 9-3V5"></path>
@@ -2092,19 +2783,31 @@ var htmlTemplate = `
2092
2783
 
2093
2784
  <div class="main">
2094
2785
  <div class="main-header">
2095
- <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>
2096
2794
  <button id="refresh-btn" class="btn" disabled>
2097
- <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">
2098
2796
  <polyline points="23 4 23 10 17 10"></polyline>
2099
2797
  <polyline points="1 20 1 14 7 14"></polyline>
2100
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>
2101
2799
  </svg>
2102
- Refresh Data
2800
+ Refresh
2103
2801
  </button>
2104
2802
  </div>
2105
2803
  <div class="content-area">
2106
2804
  <div id="data-container" class="empty-state">
2107
- <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>
2108
2811
  </div>
2109
2812
  </div>
2110
2813
  </div>
@@ -2112,21 +2815,38 @@ var htmlTemplate = `
2112
2815
  <script>
2113
2816
  let currentTable = null;
2114
2817
  let tablesData = [];
2818
+ let tableSchemas = {};
2819
+ let currentPage = 1;
2820
+ let totalPages = 1;
2821
+ let totalRecords = 0;
2822
+ let pageSize = 50;
2115
2823
 
2116
2824
  // Format values for the data grid
2117
2825
  function formatValue(val) {
2118
- if (val === null || val === undefined) return '<span class="type-null">null</span>';
2119
- if (typeof val === 'number') return \`<span class="type-number">\${val}</span>\`;
2120
- if (typeof val === 'boolean') return \`<span class="type-boolean">\${val}</span>\`;
2121
- if (val instanceof Date || (typeof val === 'string' && val.match(/^\\d{4}-\\d{2}-\\d{2}T/))) {
2122
- 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;
2123
2838
  }
2124
- // Escape HTML to prevent XSS
2125
- const safeStr = String(val)
2126
- .replace(/&/g, "&amp;")
2127
- .replace(/</g, "&lt;")
2128
- .replace(/>/g, "&gt;");
2129
- 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);
2130
2850
  }
2131
2851
 
2132
2852
  async function loadTables() {
@@ -2136,12 +2856,14 @@ var htmlTemplate = `
2136
2856
 
2137
2857
  const list = document.getElementById('table-list');
2138
2858
  list.innerHTML = '';
2859
+ tableSchemas = {};
2139
2860
 
2140
2861
  tablesData.forEach(t => {
2862
+ tableSchemas[t.name] = t;
2141
2863
  const li = document.createElement('li');
2142
2864
  li.className = 'table-item';
2143
2865
  li.innerHTML = \`
2144
- <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">
2145
2867
  <rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
2146
2868
  <line x1="3" y1="9" x2="21" y2="9"></line>
2147
2869
  <line x1="9" y1="21" x2="9" y2="9"></line>
@@ -2156,8 +2878,9 @@ var htmlTemplate = `
2156
2878
  }
2157
2879
  }
2158
2880
 
2159
- async function selectTable(name) {
2881
+ async function selectTable(name, page = 1) {
2160
2882
  currentTable = name;
2883
+ currentPage = page;
2161
2884
 
2162
2885
  // Update UI active state
2163
2886
  document.querySelectorAll('.table-item').forEach(el => {
@@ -2165,60 +2888,169 @@ var htmlTemplate = `
2165
2888
  else el.classList.remove('active');
2166
2889
  });
2167
2890
 
2168
- 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
+ \`;
2169
2899
  document.getElementById('refresh-btn').disabled = false;
2170
2900
 
2171
2901
  const container = document.getElementById('data-container');
2172
- container.innerHTML = '<div class="empty-state">Loading data...</div>';
2173
- 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
+ \`;
2174
2917
 
2175
2918
  try {
2176
- const res = await fetch(\`/api/tables/\${name}/data\`);
2919
+ const res = await fetch(\`/api/tables/\${name}/data?page=\${page}&limit=\${pageSize}\`);
2177
2920
  if (!res.ok) throw new Error(await res.text());
2178
- 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];
2179
2928
 
2180
2929
  if (rows.length === 0) {
2181
- 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
+ \`;
2182
2941
  return;
2183
2942
  }
2184
2943
 
2185
- 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
+ }));
2186
2964
 
2187
2965
  let html = '<div class="data-grid-container"><table><thead><tr>';
2188
2966
  columns.forEach(col => {
2189
- 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
+ \`;
2190
2978
  });
2191
2979
  html += '</tr></thead><tbody>';
2192
2980
 
2193
2981
  rows.forEach(row => {
2194
2982
  html += '<tr>';
2195
2983
  columns.forEach(col => {
2196
- html += \`<td>\${formatValue(row[col])}</td>\`;
2984
+ html += \`<td>\${formatValue(row[col.name])}</td>\`;
2197
2985
  });
2198
2986
  html += '</tr>';
2199
2987
  });
2200
2988
 
2201
- html += '</tbody></table></div>';
2989
+ html += '</tbody></table>';
2990
+
2991
+ // Add pagination
2992
+ html += renderPagination();
2993
+
2994
+ html += '</div>';
2202
2995
  container.innerHTML = html;
2203
2996
 
2204
2997
  } catch (e) {
2205
- 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
+ \`;
2206
3009
  }
2207
3010
  }
2208
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
+
2209
3042
  document.getElementById('refresh-btn').addEventListener('click', async () => {
2210
3043
  if (!currentTable) return;
2211
3044
 
2212
- const btn = document.getElementById('refresh-btn');
2213
- const originalText = btn.innerHTML;
2214
- btn.innerHTML = 'Refreshing...';
3045
+ const icon = document.getElementById('refresh-icon');
3046
+ icon.classList.add('spinning');
2215
3047
 
2216
3048
  try {
2217
3049
  await selectTable(currentTable);
2218
3050
  } catch (e) {
2219
3051
  console.error("Failed to refresh data", e);
2220
3052
  } finally {
2221
- setTimeout(() => { btn.innerHTML = originalText; }, 300);
3053
+ setTimeout(() => { icon.classList.remove('spinning'); }, 500);
2222
3054
  }
2223
3055
  });
2224
3056
 
@@ -2294,6 +3126,43 @@ Example query: await db.select().from(users)`);
2294
3126
  });
2295
3127
  }
2296
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
+
2297
3166
  // src/ensure-db.ts
2298
3167
  async function ensureDatabase2(dbUrl) {
2299
3168
  let sql2;
@@ -2333,6 +3202,7 @@ ${colorize("Usage:", "yellow")}
2333
3202
  bungres <command> [options]
2334
3203
 
2335
3204
  ${colorize("Commands:", "yellow")}
3205
+ init Initialize bungres project with config file and db folder structure
2336
3206
  generate Generate SQL migration files from your schema definitions
2337
3207
  migrate Run pending migration files against the database
2338
3208
  push Apply schema directly to the DB (no migration files, dev mode)
@@ -2353,6 +3223,7 @@ ${colorize("Options:", "yellow")}
2353
3223
  --help Show this help
2354
3224
 
2355
3225
  ${colorize("Examples:", "yellow")}
3226
+ bungres init
2356
3227
  bungres generate
2357
3228
  bungres migrate
2358
3229
  bungres push
@@ -2377,6 +3248,10 @@ async function main() {
2377
3248
  }
2378
3249
  const command = args[0];
2379
3250
  const flags = parseFlags(args.slice(1));
3251
+ if (command === "init") {
3252
+ await runInit();
3253
+ process.exit(0);
3254
+ }
2380
3255
  const config2 = await loadConfig(process.cwd());
2381
3256
  if (flags.verbose)
2382
3257
  config2.verbose = true;