@bungres/kit 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -13
- package/dist/cli.js +1513 -638
- package/dist/commands/drop.d.ts.map +1 -1
- package/dist/commands/init.d.ts +2 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/migrate.d.ts.map +1 -1
- package/dist/commands/status.d.ts.map +1 -1
- package/dist/commands/studio.d.ts.map +1 -1
- package/dist/config.d.ts +32 -3
- package/dist/config.d.ts.map +1 -1
- package/dist/index.js +609 -191
- package/package.json +20 -4
- package/src/cli.ts +0 -162
- package/src/commands/drop.ts +0 -92
- package/src/commands/fresh.ts +0 -17
- package/src/commands/generate.ts +0 -151
- package/src/commands/migrate.ts +0 -84
- package/src/commands/pull.ts +0 -339
- package/src/commands/push.ts +0 -105
- package/src/commands/refresh.ts +0 -37
- package/src/commands/seed.ts +0 -41
- package/src/commands/status.ts +0 -64
- package/src/commands/studio.ts +0 -471
- package/src/commands/tusky.ts +0 -83
- package/src/config.ts +0 -102
- package/src/differ.ts +0 -236
- package/src/ensure-db.ts +0 -32
- package/src/index.ts +0 -21
- package/src/schema-loader.ts +0 -50
- package/src/utils/colors.ts +0 -4
package/dist/cli.js
CHANGED
|
@@ -1,47 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
// @bun
|
|
3
3
|
|
|
4
|
-
// src/
|
|
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
6
|
|
|
41
|
-
// src/
|
|
42
|
-
import * as fs from "fs";
|
|
43
|
-
|
|
44
|
-
// ../../node_modules/.bun/@bungres+orm@0.2.0/node_modules/@bungres/orm/src/table.ts
|
|
7
|
+
// ../bungres-orm/src/schema/table.ts
|
|
45
8
|
var TableConfigSymbol = Symbol.for("BungresTableConfig");
|
|
46
9
|
function getTableConfig(table) {
|
|
47
10
|
return table[TableConfigSymbol];
|
|
@@ -51,8 +14,13 @@ function camelToSnakeCase(str) {
|
|
|
51
14
|
}
|
|
52
15
|
function createTableFactory(casing) {
|
|
53
16
|
return function(name, columns, extra) {
|
|
17
|
+
let schema;
|
|
18
|
+
if (extra && typeof extra !== "function") {
|
|
19
|
+
schema = extra.schema;
|
|
20
|
+
}
|
|
21
|
+
const qualifiedName = schema ? `"${schema}"."${name}"` : `"${name}"`;
|
|
54
22
|
const columnConfigs = Object.fromEntries(Object.entries(columns).map(([key, config2]) => {
|
|
55
|
-
const c = { ...config2 };
|
|
23
|
+
const c = { ...config2, tableName: qualifiedName };
|
|
56
24
|
if (!c.name) {
|
|
57
25
|
if (casing === "snake") {
|
|
58
26
|
c.name = camelToSnakeCase(key);
|
|
@@ -62,26 +30,50 @@ function createTableFactory(casing) {
|
|
|
62
30
|
}
|
|
63
31
|
return [key, c];
|
|
64
32
|
}));
|
|
65
|
-
const
|
|
66
|
-
const
|
|
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
|
|
73
|
-
checks
|
|
74
|
-
primaryKeys
|
|
63
|
+
indexes,
|
|
64
|
+
checks,
|
|
65
|
+
primaryKeys,
|
|
66
|
+
foreignKeys,
|
|
75
67
|
qualifiedName
|
|
76
68
|
}
|
|
77
69
|
};
|
|
78
70
|
return Object.assign(tableObj, columnConfigs);
|
|
79
71
|
};
|
|
80
72
|
}
|
|
81
|
-
var table = createTableFactory("
|
|
73
|
+
var table = createTableFactory("snake");
|
|
82
74
|
var snakeCase = { table: createTableFactory("snake") };
|
|
83
75
|
var camelCase = { table: createTableFactory("camel") };
|
|
84
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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]) =>
|
|
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
|
|
604
|
+
return `${qName}."${getTableConfig(this._table).columns[c]?.name ?? c}" AS "${c}"`;
|
|
208
605
|
}
|
|
209
|
-
return
|
|
606
|
+
return `${c.tableName ? c.tableName + "." : ""}"${c.name}" AS "${c.alias || c.name}"`;
|
|
210
607
|
}).join(", ");
|
|
211
608
|
} else {
|
|
212
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
228
|
-
|
|
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
|
|
688
|
+
// ../bungres-orm/src/builders/update.ts
|
|
689
|
+
class UpdateBuilder {
|
|
259
690
|
_table;
|
|
260
691
|
_executor;
|
|
261
|
-
|
|
262
|
-
|
|
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
|
-
|
|
276
|
-
|
|
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
|
-
|
|
285
|
-
|
|
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
|
-
|
|
298
|
-
|
|
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
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
params.
|
|
307
|
-
|
|
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
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
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 += "
|
|
318
|
-
params.push(...
|
|
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,111 +771,10 @@ class InsertBuilder {
|
|
|
323
771
|
return { sql: query, params };
|
|
324
772
|
}
|
|
325
773
|
}
|
|
774
|
+
// ../bungres-orm/src/builders/relational.ts
|
|
775
|
+
var _relationsCache = new WeakMap;
|
|
326
776
|
|
|
327
|
-
class
|
|
328
|
-
_table;
|
|
329
|
-
_executor;
|
|
330
|
-
_set = {};
|
|
331
|
-
_where = [];
|
|
332
|
-
_returning;
|
|
333
|
-
_comment;
|
|
334
|
-
constructor(table2, executor) {
|
|
335
|
-
this._table = table2;
|
|
336
|
-
this._executor = executor;
|
|
337
|
-
}
|
|
338
|
-
then(onfulfilled, onrejected) {
|
|
339
|
-
return this._executor.execute(this).then(onfulfilled, onrejected);
|
|
340
|
-
}
|
|
341
|
-
async single() {
|
|
342
|
-
return this._executor.executeSingle(this);
|
|
343
|
-
}
|
|
344
|
-
set(data) {
|
|
345
|
-
this._set = { ...this._set, ...data };
|
|
346
|
-
return this;
|
|
347
|
-
}
|
|
348
|
-
where(condition) {
|
|
349
|
-
this._where.push(condition);
|
|
350
|
-
return this;
|
|
351
|
-
}
|
|
352
|
-
returning(...columns) {
|
|
353
|
-
this._returning = columns.length > 0 ? columns : ["*"];
|
|
354
|
-
return this;
|
|
355
|
-
}
|
|
356
|
-
comment(tag) {
|
|
357
|
-
this._comment = tag;
|
|
358
|
-
return this;
|
|
359
|
-
}
|
|
360
|
-
toSQL() {
|
|
361
|
-
const entries = Object.entries(this._set);
|
|
362
|
-
if (entries.length === 0) {
|
|
363
|
-
throw new Error("UpdateBuilder: no fields to set");
|
|
364
|
-
}
|
|
365
|
-
const params = [];
|
|
366
|
-
const setClauses = entries.map(([key, value]) => {
|
|
367
|
-
params.push(value);
|
|
368
|
-
const dbCol = getTableConfig(this._table).columns[key]?.name ?? key;
|
|
369
|
-
return `"${dbCol}" = $${params.length}`;
|
|
370
|
-
});
|
|
371
|
-
let query = `UPDATE ${getTableConfig(this._table).qualifiedName} SET ${setClauses.join(", ")}`;
|
|
372
|
-
if (this._where.length > 0) {
|
|
373
|
-
const combined = sqlJoin(this._where, " AND ");
|
|
374
|
-
const offset = params.length;
|
|
375
|
-
query += " WHERE " + combined.sql.replace(/\$(\d+)/g, (_, n) => `$${parseInt(n) + offset}`);
|
|
376
|
-
params.push(...combined.params);
|
|
377
|
-
}
|
|
378
|
-
if (this._returning) {
|
|
379
|
-
query += " RETURNING " + (this._returning[0] === "*" ? Object.keys(getTableConfig(this._table).columns).map((c) => `"${getTableConfig(this._table).columns[c].name}" AS "${c}"`).join(", ") : this._returning.map((c) => `"${getTableConfig(this._table).columns[c]?.name ?? c}" AS "${c}"`).join(", "));
|
|
380
|
-
}
|
|
381
|
-
return { sql: query, params };
|
|
382
|
-
}
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
class DeleteBuilder {
|
|
386
|
-
_table;
|
|
387
|
-
_executor;
|
|
388
|
-
_where = [];
|
|
389
|
-
_returning;
|
|
390
|
-
_comment;
|
|
391
|
-
constructor(table2, executor) {
|
|
392
|
-
this._table = table2;
|
|
393
|
-
this._executor = executor;
|
|
394
|
-
}
|
|
395
|
-
then(onfulfilled, onrejected) {
|
|
396
|
-
return this._executor.execute(this).then(onfulfilled, onrejected);
|
|
397
|
-
}
|
|
398
|
-
async single() {
|
|
399
|
-
return this._executor.executeSingle(this);
|
|
400
|
-
}
|
|
401
|
-
where(condition) {
|
|
402
|
-
this._where.push(condition);
|
|
403
|
-
return this;
|
|
404
|
-
}
|
|
405
|
-
returning(...columns) {
|
|
406
|
-
this._returning = columns.length > 0 ? columns : ["*"];
|
|
407
|
-
return this;
|
|
408
|
-
}
|
|
409
|
-
comment(tag) {
|
|
410
|
-
this._comment = tag;
|
|
411
|
-
return this;
|
|
412
|
-
}
|
|
413
|
-
toSQL() {
|
|
414
|
-
let query = `DELETE FROM ${getTableConfig(this._table).qualifiedName}`;
|
|
415
|
-
const params = [];
|
|
416
|
-
if (this._where.length > 0) {
|
|
417
|
-
const combined = sqlJoin(this._where, " AND ");
|
|
418
|
-
query += " WHERE " + combined.sql;
|
|
419
|
-
params.push(...combined.params);
|
|
420
|
-
}
|
|
421
|
-
if (this._returning) {
|
|
422
|
-
query += " RETURNING " + (this._returning[0] === "*" ? Object.keys(getTableConfig(this._table).columns).map((c) => `"${getTableConfig(this._table).columns[c].name}" AS "${c}"`).join(", ") : this._returning.map((c) => `"${getTableConfig(this._table).columns[c]?.name ?? c}" AS "${c}"`).join(", "));
|
|
423
|
-
}
|
|
424
|
-
return { sql: query, params };
|
|
425
|
-
}
|
|
426
|
-
}
|
|
427
|
-
// ../../node_modules/.bun/@bungres+orm@0.2.0/node_modules/@bungres/orm/src/relational.ts
|
|
428
|
-
var _relationsCache = new WeakMap;
|
|
429
|
-
|
|
430
|
-
class RelationalQueryBuilder {
|
|
777
|
+
class RelationalQueryBuilder {
|
|
431
778
|
_executor;
|
|
432
779
|
_schema;
|
|
433
780
|
_tableName;
|
|
@@ -508,7 +855,7 @@ class RelationalQueryBuilder {
|
|
|
508
855
|
const ones = {};
|
|
509
856
|
const manys = {};
|
|
510
857
|
const manyToManys = {};
|
|
511
|
-
for (const [
|
|
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 [
|
|
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 (
|
|
562
|
-
|
|
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
|
|
973
|
+
if (args.where) {
|
|
622
974
|
const offset = params.length;
|
|
623
|
-
|
|
624
|
-
|
|
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
|
-
//
|
|
1021
|
+
// ../bungres-orm/src/core/db.ts
|
|
655
1022
|
function parseDBName(url) {
|
|
656
1023
|
try {
|
|
657
1024
|
return new URL(url).pathname.slice(1);
|
|
@@ -795,7 +1162,7 @@ class BungresTransaction {
|
|
|
795
1162
|
return Array.from(result2);
|
|
796
1163
|
}
|
|
797
1164
|
}
|
|
798
|
-
function
|
|
1165
|
+
function bungres(config2) {
|
|
799
1166
|
const db2 = new BungresDB(config2);
|
|
800
1167
|
if (typeof config2 === "object" && config2.schema) {
|
|
801
1168
|
const schema = config2.schema;
|
|
@@ -817,7 +1184,7 @@ function createDB(config2) {
|
|
|
817
1184
|
}
|
|
818
1185
|
return db2;
|
|
819
1186
|
}
|
|
820
|
-
//
|
|
1187
|
+
// ../bungres-orm/src/ddl.ts
|
|
821
1188
|
function generateCreateTable(config2, ifNotExists = true) {
|
|
822
1189
|
const tableName = config2.schema ? `"${config2.schema}"."${config2.name}"` : `"${config2.name}"`;
|
|
823
1190
|
const exists = ifNotExists ? " IF NOT EXISTS" : "";
|
|
@@ -936,6 +1303,186 @@ 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 userSchema = config2.dbSchema;
|
|
1349
|
+
const existingUserTablesResult = await sql2.unsafe(`SELECT table_name FROM information_schema.tables WHERE table_schema = $1`, [userSchema]);
|
|
1350
|
+
const existingTableNames = new Set(existingUserTablesResult.map((r) => r.table_name));
|
|
1351
|
+
const migTableCheck = await sql2.unsafe(`SELECT EXISTS (
|
|
1352
|
+
SELECT 1 FROM information_schema.tables
|
|
1353
|
+
WHERE table_schema = $1 AND table_name = $2
|
|
1354
|
+
) AS exists`, [config2.migrationsSchema, config2.migrationsTable]);
|
|
1355
|
+
const migrationTableExists = migTableCheck[0]?.exists ?? false;
|
|
1356
|
+
const pushTableCheck = await sql2.unsafe(`SELECT EXISTS (
|
|
1357
|
+
SELECT 1 FROM information_schema.tables
|
|
1358
|
+
WHERE table_schema = $1 AND table_name = $2
|
|
1359
|
+
) AS exists`, [config2.migrationsSchema, "__bungres_push"]);
|
|
1360
|
+
const pushTableExists = pushTableCheck[0]?.exists ?? false;
|
|
1361
|
+
const tablesToDrop = schemas2.filter((s) => existingTableNames.has(s.config.name));
|
|
1362
|
+
if (tablesToDrop.length === 0 && !migrationTableExists && !pushTableExists) {
|
|
1363
|
+
console.log("No tables to drop (they either don't exist or were already dropped).");
|
|
1364
|
+
return;
|
|
1365
|
+
}
|
|
1366
|
+
const tableNamesToPrint = tablesToDrop.map((s) => (s.config.schema ? s.config.schema + "." : "") + s.config.name);
|
|
1367
|
+
if (migrationTableExists) {
|
|
1368
|
+
tableNamesToPrint.push(`${config2.migrationsSchema}.${config2.migrationsTable}`);
|
|
1369
|
+
}
|
|
1370
|
+
if (pushTableExists) {
|
|
1371
|
+
tableNamesToPrint.push(`${config2.migrationsSchema}.__bungres_push`);
|
|
1372
|
+
}
|
|
1373
|
+
if (!opts.force) {
|
|
1374
|
+
console.warn(colorize(`
|
|
1375
|
+
\u26A0\uFE0F WARNING: This will drop the following tables and ALL their data:
|
|
1376
|
+
`, "red"));
|
|
1377
|
+
for (const name of tableNamesToPrint)
|
|
1378
|
+
console.log(colorize(` - ${name}`, "yellow"));
|
|
1379
|
+
process.stdout.write(colorize(`
|
|
1380
|
+
Are you sure? Type YES to continue: `, "cyan"));
|
|
1381
|
+
for await (const line2 of console) {
|
|
1382
|
+
if (line2.trim().toLowerCase() !== "yes") {
|
|
1383
|
+
console.log("Aborted.");
|
|
1384
|
+
return;
|
|
1385
|
+
}
|
|
1386
|
+
break;
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
for (const entry of tablesToDrop) {
|
|
1390
|
+
const ddl = `DROP TABLE IF EXISTS "${entry.config.name}" CASCADE;`;
|
|
1391
|
+
await sql2.unsafe(ddl);
|
|
1392
|
+
console.log(colorize(` \u2713 dropped ${entry.config.name}`, "green"));
|
|
1393
|
+
}
|
|
1394
|
+
if (migrationTableExists) {
|
|
1395
|
+
const qualifiedMigTable = `"${config2.migrationsSchema}"."${config2.migrationsTable}"`;
|
|
1396
|
+
await sql2.unsafe(`DROP TABLE IF EXISTS ${qualifiedMigTable} CASCADE`);
|
|
1397
|
+
console.log(colorize(` \u2713 dropped ${config2.migrationsSchema}.${config2.migrationsTable}`, "green"));
|
|
1398
|
+
}
|
|
1399
|
+
if (pushTableExists) {
|
|
1400
|
+
await sql2.unsafe(`DROP TABLE IF EXISTS "${config2.migrationsSchema}"."__bungres_push" CASCADE`);
|
|
1401
|
+
console.log(colorize(` \u2713 dropped ${config2.migrationsSchema}.__bungres_push`, "green"));
|
|
1402
|
+
}
|
|
1403
|
+
console.log(colorize(`
|
|
1404
|
+
Drop complete.`, "green"));
|
|
1405
|
+
} finally {
|
|
1406
|
+
await sql2.end();
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1410
|
+
// src/commands/migrate.ts
|
|
1411
|
+
import { join as join2, resolve as resolve2 } from "path";
|
|
1412
|
+
async function runMigrate(config2) {
|
|
1413
|
+
const migrationsDir = resolve2(config2.out);
|
|
1414
|
+
const sql2 = new Bun.SQL(config2.dbUrl);
|
|
1415
|
+
const table2 = config2.migrationsTable;
|
|
1416
|
+
const schema = config2.migrationsSchema;
|
|
1417
|
+
const qualifiedTable = `"${schema}"."${table2}"`;
|
|
1418
|
+
const createSchema = `CREATE SCHEMA IF NOT EXISTS "${schema}";`;
|
|
1419
|
+
const createMigrationsTable = `
|
|
1420
|
+
CREATE TABLE IF NOT EXISTS ${qualifiedTable} (
|
|
1421
|
+
id SERIAL PRIMARY KEY,
|
|
1422
|
+
name TEXT NOT NULL UNIQUE,
|
|
1423
|
+
applied_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
1424
|
+
);`.trim();
|
|
1425
|
+
try {
|
|
1426
|
+
await sql2.unsafe(createSchema);
|
|
1427
|
+
await sql2.unsafe(createMigrationsTable);
|
|
1428
|
+
const glob = new Bun.Glob("*.sql");
|
|
1429
|
+
const files = [];
|
|
1430
|
+
for await (const file of glob.scan({ cwd: migrationsDir, absolute: false })) {
|
|
1431
|
+
files.push(file);
|
|
1432
|
+
}
|
|
1433
|
+
files.sort();
|
|
1434
|
+
if (files.length === 0) {
|
|
1435
|
+
console.log(colorize("No migration files found in " + migrationsDir, "yellow"));
|
|
1436
|
+
console.log(colorize("Run `bungres generate` first.", "yellow"));
|
|
1437
|
+
return;
|
|
1438
|
+
}
|
|
1439
|
+
const applied = await sql2.unsafe(`SELECT name FROM ${qualifiedTable}`);
|
|
1440
|
+
const appliedSet = new Set(applied.map((r) => r.name));
|
|
1441
|
+
const pending = files.filter((f) => !appliedSet.has(f));
|
|
1442
|
+
if (pending.length === 0) {
|
|
1443
|
+
console.log(colorize("Everything is up to date.", "green"));
|
|
1444
|
+
return;
|
|
1445
|
+
}
|
|
1446
|
+
console.log(colorize(`
|
|
1447
|
+
Running ${pending.length} pending migration(s)...
|
|
1448
|
+
`, "cyan"));
|
|
1449
|
+
for (const file of pending) {
|
|
1450
|
+
const content = await Bun.file(join2(migrationsDir, file)).text();
|
|
1451
|
+
if (config2.verbose) {
|
|
1452
|
+
console.log(`-- ${file} --
|
|
1453
|
+
${content}
|
|
1454
|
+
`);
|
|
1455
|
+
}
|
|
1456
|
+
await sql2.transaction(async (txSql) => {
|
|
1457
|
+
const statements = config2.breakpoints ? content.split(/-->statement-breakpoint/g).flatMap((chunk) => chunk.split(";").map((s) => s.trim()).filter(Boolean)) : content.split(";").map((s) => s.trim()).filter(Boolean);
|
|
1458
|
+
for (const stmt of statements) {
|
|
1459
|
+
await txSql.unsafe(stmt + ";");
|
|
1460
|
+
}
|
|
1461
|
+
await txSql.unsafe(`INSERT INTO ${qualifiedTable} (name) VALUES ($1)`, [file]);
|
|
1462
|
+
});
|
|
1463
|
+
console.log(colorize(` \u2713 ${file}`, "green"));
|
|
1464
|
+
}
|
|
1465
|
+
console.log(colorize(`
|
|
1466
|
+
Done.`, "green"));
|
|
1467
|
+
} finally {
|
|
1468
|
+
await sql2.end();
|
|
1469
|
+
}
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1472
|
+
// src/commands/fresh.ts
|
|
1473
|
+
async function runFresh(config2) {
|
|
1474
|
+
console.log("Dropping all tables...");
|
|
1475
|
+
await runDrop(config2, { force: true });
|
|
1476
|
+
console.log(`
|
|
1477
|
+
Re-running migrations...`);
|
|
1478
|
+
await runMigrate(config2);
|
|
1479
|
+
console.log(`
|
|
1480
|
+
Fresh complete.`);
|
|
1481
|
+
}
|
|
1482
|
+
|
|
1483
|
+
// src/commands/generate.ts
|
|
1484
|
+
import { join as join3, resolve as resolve3 } from "path";
|
|
1485
|
+
|
|
939
1486
|
// src/differ.ts
|
|
940
1487
|
function diffSchemas(prev, next) {
|
|
941
1488
|
const statements = [];
|
|
@@ -1021,11 +1568,11 @@ function diffSchemas(prev, next) {
|
|
|
1021
1568
|
const idxName = idx.name ?? `idx_${tableName}_${idx.columns.join("_")}`;
|
|
1022
1569
|
if (!prevIdxNames.has(idxName)) {
|
|
1023
1570
|
const tbl = nextConfig.schema ? `"${nextConfig.schema}"."${tableName}"` : `"${tableName}"`;
|
|
1024
|
-
const
|
|
1571
|
+
const unique2 = idx.unique ? "UNIQUE " : "";
|
|
1025
1572
|
const using = idx.using ? ` USING ${idx.using.toUpperCase()}` : "";
|
|
1026
1573
|
const cols = idx.columns.map((c) => `"${c}"`).join(", ");
|
|
1027
1574
|
const where = idx.where ? ` WHERE ${idx.where}` : "";
|
|
1028
|
-
statements.push(`CREATE ${
|
|
1575
|
+
statements.push(`CREATE ${unique2}INDEX IF NOT EXISTS "${idxName}" ON ${tbl}${using} (${cols})${where};`);
|
|
1029
1576
|
summary.push(`CREATE INDEX ${idxName} ON ${tableName}`);
|
|
1030
1577
|
}
|
|
1031
1578
|
}
|
|
@@ -1036,177 +1583,69 @@ function diffSchemas(prev, next) {
|
|
|
1036
1583
|
statements.push(`DROP INDEX IF EXISTS "${idxName}";`);
|
|
1037
1584
|
summary.push(`DROP INDEX ${idxName}`);
|
|
1038
1585
|
}
|
|
1039
|
-
}
|
|
1040
|
-
}
|
|
1041
|
-
return { statements, summary, warnings };
|
|
1042
|
-
}
|
|
1043
|
-
function topoSortConfigs(tables) {
|
|
1044
|
-
const byName = new Map(tables.map((t) => [t.name, t]));
|
|
1045
|
-
const visited = new Set;
|
|
1046
|
-
const result2 = [];
|
|
1047
|
-
function deps(config2) {
|
|
1048
|
-
return Object.values(config2.columns).map((col2) => col2.references?.table).filter((t) => t !== undefined && byName.has(t) && t !== config2.name);
|
|
1049
|
-
}
|
|
1050
|
-
function visit(name) {
|
|
1051
|
-
if (visited.has(name))
|
|
1052
|
-
return;
|
|
1053
|
-
visited.add(name);
|
|
1054
|
-
const config2 = byName.get(name);
|
|
1055
|
-
if (!config2)
|
|
1056
|
-
return;
|
|
1057
|
-
for (const dep of deps(config2))
|
|
1058
|
-
visit(dep);
|
|
1059
|
-
result2.push(config2);
|
|
1060
|
-
}
|
|
1061
|
-
for (const table2 of tables)
|
|
1062
|
-
visit(table2.name);
|
|
1063
|
-
return result2;
|
|
1064
|
-
}
|
|
1065
|
-
function diffColumn(prev, next) {
|
|
1066
|
-
const changes = [];
|
|
1067
|
-
const col2 = `"${next.name}"`;
|
|
1068
|
-
if (prev.dataType !== next.dataType) {
|
|
1069
|
-
changes.push(`ALTER COLUMN ${col2} TYPE ${next.dataType.toUpperCase()} USING ${col2}::${next.dataType}`);
|
|
1070
|
-
}
|
|
1071
|
-
if (!prev.notNull && next.notNull) {
|
|
1072
|
-
changes.push(`ALTER COLUMN ${col2} SET NOT NULL`);
|
|
1073
|
-
}
|
|
1074
|
-
if (prev.notNull && !next.notNull) {
|
|
1075
|
-
changes.push(`ALTER COLUMN ${col2} DROP NOT NULL`);
|
|
1076
|
-
}
|
|
1077
|
-
const prevDef = prev.defaultFn ?? (prev.defaultValue !== undefined ? String(prev.defaultValue) : undefined);
|
|
1078
|
-
const nextDef = next.defaultFn ?? (next.defaultValue !== undefined ? String(next.defaultValue) : undefined);
|
|
1079
|
-
if (prevDef !== nextDef) {
|
|
1080
|
-
if (nextDef !== undefined) {
|
|
1081
|
-
const val = next.defaultFn ?? formatDefault(next.defaultValue, next.dataType);
|
|
1082
|
-
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();
|
|
1586
|
+
}
|
|
1195
1587
|
}
|
|
1588
|
+
return { statements, summary, warnings };
|
|
1196
1589
|
}
|
|
1197
|
-
|
|
1198
|
-
const
|
|
1199
|
-
const
|
|
1200
|
-
|
|
1590
|
+
function topoSortConfigs(tables) {
|
|
1591
|
+
const byName = new Map(tables.map((t) => [t.name, t]));
|
|
1592
|
+
const visited = new Set;
|
|
1593
|
+
const result2 = [];
|
|
1594
|
+
function deps(config2) {
|
|
1595
|
+
return Object.values(config2.columns).map((col2) => col2.references?.table).filter((t) => t !== undefined && byName.has(t) && t !== config2.name);
|
|
1596
|
+
}
|
|
1597
|
+
function visit(name) {
|
|
1598
|
+
if (visited.has(name))
|
|
1599
|
+
return;
|
|
1600
|
+
visited.add(name);
|
|
1601
|
+
const config2 = byName.get(name);
|
|
1602
|
+
if (!config2)
|
|
1603
|
+
return;
|
|
1604
|
+
for (const dep of deps(config2))
|
|
1605
|
+
visit(dep);
|
|
1606
|
+
result2.push(config2);
|
|
1607
|
+
}
|
|
1608
|
+
for (const table2 of tables)
|
|
1609
|
+
visit(table2.name);
|
|
1610
|
+
return result2;
|
|
1201
1611
|
}
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1612
|
+
function diffColumn(prev, next) {
|
|
1613
|
+
const changes = [];
|
|
1614
|
+
const col2 = `"${next.name}"`;
|
|
1615
|
+
if (prev.dataType !== next.dataType) {
|
|
1616
|
+
changes.push(`ALTER COLUMN ${col2} TYPE ${next.dataType.toUpperCase()} USING ${col2}::${next.dataType}`);
|
|
1617
|
+
}
|
|
1618
|
+
if (!prev.notNull && next.notNull) {
|
|
1619
|
+
changes.push(`ALTER COLUMN ${col2} SET NOT NULL`);
|
|
1620
|
+
}
|
|
1621
|
+
if (prev.notNull && !next.notNull) {
|
|
1622
|
+
changes.push(`ALTER COLUMN ${col2} DROP NOT NULL`);
|
|
1623
|
+
}
|
|
1624
|
+
const prevDef = prev.defaultFn ?? (prev.defaultValue !== undefined ? String(prev.defaultValue) : undefined);
|
|
1625
|
+
const nextDef = next.defaultFn ?? (next.defaultValue !== undefined ? String(next.defaultValue) : undefined);
|
|
1626
|
+
if (prevDef !== nextDef) {
|
|
1627
|
+
if (nextDef !== undefined) {
|
|
1628
|
+
const val = next.defaultFn ?? formatDefault(next.defaultValue, next.dataType);
|
|
1629
|
+
changes.push(`ALTER COLUMN ${col2} SET DEFAULT ${val}`);
|
|
1630
|
+
} else {
|
|
1631
|
+
changes.push(`ALTER COLUMN ${col2} DROP DEFAULT`);
|
|
1632
|
+
}
|
|
1633
|
+
}
|
|
1634
|
+
return changes;
|
|
1635
|
+
}
|
|
1636
|
+
function formatDefault(value, dataType) {
|
|
1637
|
+
if (value === null || value === undefined)
|
|
1638
|
+
return "NULL";
|
|
1639
|
+
if (typeof value === "boolean")
|
|
1640
|
+
return value ? "TRUE" : "FALSE";
|
|
1641
|
+
if (typeof value === "number")
|
|
1642
|
+
return String(value);
|
|
1643
|
+
if (typeof value === "string") {
|
|
1644
|
+
if (dataType === "boolean")
|
|
1645
|
+
return value;
|
|
1646
|
+
return `'${value.replace(/'/g, "''")}'`;
|
|
1647
|
+
}
|
|
1648
|
+
return `'${JSON.stringify(value)}'`;
|
|
1210
1649
|
}
|
|
1211
1650
|
|
|
1212
1651
|
// src/commands/generate.ts
|
|
@@ -1218,7 +1657,7 @@ async function runGenerate(config2, name) {
|
|
|
1218
1657
|
console.warn(colorize("No table definitions found. Check your schema glob pattern.", "yellow"));
|
|
1219
1658
|
return;
|
|
1220
1659
|
}
|
|
1221
|
-
const migrationsDir = resolve3(config2.
|
|
1660
|
+
const migrationsDir = resolve3(config2.out);
|
|
1222
1661
|
await Bun.$`mkdir -p ${migrationsDir}`.quiet();
|
|
1223
1662
|
const currentSnapshot = Object.fromEntries(schemas2.map((s) => [s.config.name, s.config]));
|
|
1224
1663
|
const snapshotPath = join3(migrationsDir, SNAPSHOT_FILE);
|
|
@@ -1310,69 +1749,93 @@ function topoSort(schemas2) {
|
|
|
1310
1749
|
return result2;
|
|
1311
1750
|
}
|
|
1312
1751
|
|
|
1313
|
-
// src/commands/
|
|
1314
|
-
import {
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
);
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
const migrationsDir = resolve4(config2.migrationsDir);
|
|
1325
|
-
const sql2 = new Bun.SQL(config2.dbUrl);
|
|
1752
|
+
// src/commands/init.ts
|
|
1753
|
+
import { existsSync } from "fs";
|
|
1754
|
+
import { mkdir } from "fs/promises";
|
|
1755
|
+
import { join as join4 } from "path";
|
|
1756
|
+
async function runInit(cwd = process.cwd()) {
|
|
1757
|
+
const configPath = join4(cwd, "bungres.config.ts");
|
|
1758
|
+
const dbDir = join4(cwd, "src", "db");
|
|
1759
|
+
if (existsSync(configPath)) {
|
|
1760
|
+
console.log("Config file already exists at bungres.config.ts");
|
|
1761
|
+
return;
|
|
1762
|
+
}
|
|
1326
1763
|
try {
|
|
1327
|
-
await
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1764
|
+
await mkdir(dbDir, { recursive: true });
|
|
1765
|
+
console.log("Created src/db directory");
|
|
1766
|
+
} catch (e) {
|
|
1767
|
+
console.error("Failed to create src/db directory:", e);
|
|
1768
|
+
return;
|
|
1769
|
+
}
|
|
1770
|
+
const configContent = `import { defineConfig } from "@bungres/kit";
|
|
1771
|
+
|
|
1772
|
+
export default defineConfig({
|
|
1773
|
+
schema: "./src/db/schema.ts",
|
|
1774
|
+
out: "./bungres",
|
|
1775
|
+
dbCredentials: {
|
|
1776
|
+
url: process.env.DATABASE_URL!,
|
|
1777
|
+
},
|
|
1778
|
+
});
|
|
1779
|
+
`;
|
|
1780
|
+
try {
|
|
1781
|
+
await Bun.write(configPath, configContent);
|
|
1782
|
+
console.log("Created bungres.config.ts");
|
|
1783
|
+
} catch (e) {
|
|
1784
|
+
console.error("Failed to create config file:", e);
|
|
1785
|
+
return;
|
|
1786
|
+
}
|
|
1787
|
+
const schemaContent = `import {
|
|
1788
|
+
uuid,
|
|
1789
|
+
varchar,
|
|
1790
|
+
timestamptz,
|
|
1791
|
+
table,
|
|
1792
|
+
unique,
|
|
1793
|
+
index
|
|
1794
|
+
} from "@bungres/orm";
|
|
1795
|
+
|
|
1796
|
+
export const users = table("users", {
|
|
1797
|
+
id: uuid({ primaryKey: true }),
|
|
1798
|
+
name: varchar({ length: 255, notNull: true }),
|
|
1799
|
+
email: varchar({ length: 255, notNull: true }),
|
|
1800
|
+
createdAt: timestamptz({ notNull: true, defaultRaw: "NOW()" }),
|
|
1801
|
+
}, (t) => [
|
|
1802
|
+
unique().on(t.email),
|
|
1803
|
+
index().on(t.email),
|
|
1804
|
+
]);
|
|
1805
|
+
`;
|
|
1806
|
+
try {
|
|
1807
|
+
await Bun.write(join4(dbDir, "schema.ts"), schemaContent);
|
|
1808
|
+
console.log("Created src/db/schema.ts with example table");
|
|
1809
|
+
} catch (e) {
|
|
1810
|
+
console.error("Failed to create schema file:", e);
|
|
1369
1811
|
}
|
|
1812
|
+
const clientContent = `import { bungres } from "@bungres/orm";
|
|
1813
|
+
import * as schema from "./schema";
|
|
1814
|
+
|
|
1815
|
+
const url = Bun.env.DATABASE_URL || "postgres://postgres:postgres@localhost:5432/postgres";
|
|
1816
|
+
|
|
1817
|
+
export const db = bungres({ url, schema });
|
|
1818
|
+
`;
|
|
1819
|
+
try {
|
|
1820
|
+
await Bun.write(join4(dbDir, "client.ts"), clientContent);
|
|
1821
|
+
console.log("Created src/db/client.ts");
|
|
1822
|
+
} catch (e) {
|
|
1823
|
+
console.error("Failed to create client file:", e);
|
|
1824
|
+
}
|
|
1825
|
+
console.log(`
|
|
1826
|
+
\u2728 Bungres project initialized!`);
|
|
1827
|
+
console.log(`
|
|
1828
|
+
Next steps:`);
|
|
1829
|
+
console.log(" 1. Set DATABASE_URL in your .env file");
|
|
1830
|
+
console.log(" 2. Edit src/db/schema.ts to define your tables");
|
|
1831
|
+
console.log(" 3. Run 'bungres generate' to create migrations");
|
|
1832
|
+
console.log(" 4. Run 'bungres migrate' to apply migrations");
|
|
1370
1833
|
}
|
|
1371
1834
|
|
|
1372
1835
|
// src/commands/pull.ts
|
|
1373
|
-
import { resolve as
|
|
1836
|
+
import { resolve as resolve4, join as join5 } from "path";
|
|
1374
1837
|
async function introspectDb(sql2, dbSchema) {
|
|
1375
|
-
const
|
|
1838
|
+
const columns2 = await sql2.unsafe(`SELECT
|
|
1376
1839
|
c.table_name,
|
|
1377
1840
|
c.column_name,
|
|
1378
1841
|
c.data_type,
|
|
@@ -1406,7 +1869,7 @@ async function introspectDb(sql2, dbSchema) {
|
|
|
1406
1869
|
const indexes = await sql2.unsafe(`SELECT tablename, indexname, indexdef
|
|
1407
1870
|
FROM pg_indexes
|
|
1408
1871
|
WHERE schemaname = $1`, [dbSchema]);
|
|
1409
|
-
return groupByTable(
|
|
1872
|
+
return groupByTable(columns2, constraints, indexes);
|
|
1410
1873
|
}
|
|
1411
1874
|
async function runPull(config2) {
|
|
1412
1875
|
console.log("@bungres/kit pull: introspecting database...");
|
|
@@ -1418,7 +1881,7 @@ async function runPull(config2) {
|
|
|
1418
1881
|
console.log("No tables found in schema:", dbSchema);
|
|
1419
1882
|
return;
|
|
1420
1883
|
}
|
|
1421
|
-
const outDir =
|
|
1884
|
+
const outDir = resolve4(config2.outDir);
|
|
1422
1885
|
await Bun.$`mkdir -p ${outDir}`.quiet();
|
|
1423
1886
|
const outPath = join5(outDir, "schema.ts");
|
|
1424
1887
|
const code2 = generateSchemaTS(tableMap, dbSchema);
|
|
@@ -1429,9 +1892,9 @@ async function runPull(config2) {
|
|
|
1429
1892
|
await sql2.end();
|
|
1430
1893
|
}
|
|
1431
1894
|
}
|
|
1432
|
-
function groupByTable(
|
|
1895
|
+
function groupByTable(columns2, constraints, indexes) {
|
|
1433
1896
|
const map = new Map;
|
|
1434
|
-
for (const col2 of
|
|
1897
|
+
for (const col2 of columns2) {
|
|
1435
1898
|
if (!map.has(col2.table_name)) {
|
|
1436
1899
|
map.set(col2.table_name, {
|
|
1437
1900
|
tableName: col2.table_name,
|
|
@@ -1467,7 +1930,7 @@ function generateSchemaTS(tableMap, dbSchema) {
|
|
|
1467
1930
|
`// Generated at: ${new Date().toISOString()}`,
|
|
1468
1931
|
``,
|
|
1469
1932
|
`import {`,
|
|
1470
|
-
`
|
|
1933
|
+
` table,`,
|
|
1471
1934
|
` text, varchar, char, integer, bigint, smallint,`,
|
|
1472
1935
|
` serial, bigserial, boolean, real, doublePrecision,`,
|
|
1473
1936
|
` numeric, decimal, json, jsonb,`,
|
|
@@ -1479,7 +1942,7 @@ function generateSchemaTS(tableMap, dbSchema) {
|
|
|
1479
1942
|
];
|
|
1480
1943
|
for (const [, table2] of tableMap) {
|
|
1481
1944
|
const varName = toCamelCase(table2.tableName);
|
|
1482
|
-
lines.push(`export const ${varName} =
|
|
1945
|
+
lines.push(`export const ${varName} = table("${table2.tableName}", {`);
|
|
1483
1946
|
for (const col2 of table2.columns) {
|
|
1484
1947
|
const colExpr = buildColumnExpression(col2);
|
|
1485
1948
|
lines.push(` ${toCamelCase(col2.name)}: ${colExpr},`);
|
|
@@ -1625,126 +2088,76 @@ function toCamelCase(str) {
|
|
|
1625
2088
|
return str.replace(/_([a-z])/g, (_, c) => c.toUpperCase());
|
|
1626
2089
|
}
|
|
1627
2090
|
|
|
1628
|
-
// src/commands/
|
|
1629
|
-
import
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
const migrationsDir = resolve6(config2.migrationsDir);
|
|
1633
|
-
const sql2 = new Bun.SQL(config2.dbUrl);
|
|
1634
|
-
try {
|
|
1635
|
-
const tableCheck = await sql2.unsafe(`SELECT EXISTS (
|
|
1636
|
-
SELECT 1 FROM information_schema.tables
|
|
1637
|
-
WHERE table_name = $1
|
|
1638
|
-
) AS exists`, [MIGRATIONS_TABLE2]);
|
|
1639
|
-
const trackingExists = tableCheck[0]?.exists ?? false;
|
|
1640
|
-
const glob = new Bun.Glob("*.sql");
|
|
1641
|
-
const files = [];
|
|
1642
|
-
for await (const file of glob.scan({ cwd: migrationsDir, absolute: false })) {
|
|
1643
|
-
files.push(file);
|
|
1644
|
-
}
|
|
1645
|
-
files.sort();
|
|
1646
|
-
if (files.length === 0) {
|
|
1647
|
-
console.log(colorize("No migration files found. Run `bungres generate` first.", "yellow"));
|
|
1648
|
-
return;
|
|
1649
|
-
}
|
|
1650
|
-
let appliedSet = new Set;
|
|
1651
|
-
if (trackingExists) {
|
|
1652
|
-
const applied = await sql2.unsafe(`SELECT name FROM "${MIGRATIONS_TABLE2}" ORDER BY applied_at`);
|
|
1653
|
-
appliedSet = new Set(applied.map((r) => r.name));
|
|
1654
|
-
}
|
|
1655
|
-
console.log(colorize(`
|
|
1656
|
-
Migration status:
|
|
1657
|
-
`, "cyan"));
|
|
1658
|
-
let pendingCount = 0;
|
|
1659
|
-
for (const file of files) {
|
|
1660
|
-
const isApplied = appliedSet.has(file);
|
|
1661
|
-
const status = isApplied ? colorize("\u2713 applied ", "green") : colorize("\u2717 pending ", "yellow");
|
|
1662
|
-
if (!isApplied)
|
|
1663
|
-
pendingCount++;
|
|
1664
|
-
console.log(` ${status} ${file}`);
|
|
1665
|
-
}
|
|
1666
|
-
console.log(`
|
|
1667
|
-
${colorize(appliedSet.size.toString(), "green")} applied, ${colorize(pendingCount.toString(), "yellow")} pending.
|
|
1668
|
-
`);
|
|
1669
|
-
} finally {
|
|
1670
|
-
await sql2.end();
|
|
1671
|
-
}
|
|
1672
|
-
}
|
|
1673
|
-
|
|
1674
|
-
// src/commands/drop.ts
|
|
1675
|
-
async function runDrop(config2, opts = {}) {
|
|
2091
|
+
// src/commands/push.ts
|
|
2092
|
+
import * as fs from "fs";
|
|
2093
|
+
async function runPush(config2, opts = {}) {
|
|
2094
|
+
console.log("@bungres/kit push: loading schemas...");
|
|
1676
2095
|
const schemas2 = await loadSchemas(config2.schema);
|
|
1677
2096
|
if (schemas2.length === 0) {
|
|
1678
|
-
console.warn("No table definitions found
|
|
2097
|
+
console.warn("No table definitions found. Check your schema glob pattern.");
|
|
1679
2098
|
return;
|
|
1680
2099
|
}
|
|
1681
2100
|
const sql2 = new Bun.SQL(config2.dbUrl);
|
|
1682
2101
|
try {
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
const
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
console.log("No tables to drop (they either don't exist or were already dropped).");
|
|
1694
|
-
return;
|
|
2102
|
+
await sql2.unsafe(`
|
|
2103
|
+
CREATE TABLE IF NOT EXISTS public.__bungres_push (
|
|
2104
|
+
id SERIAL PRIMARY KEY,
|
|
2105
|
+
snapshot JSONB NOT NULL
|
|
2106
|
+
);
|
|
2107
|
+
`);
|
|
2108
|
+
const rows = await sql2.unsafe(`SELECT snapshot FROM public.__bungres_push ORDER BY id DESC LIMIT 1;`);
|
|
2109
|
+
let prevSnapshot = {};
|
|
2110
|
+
if (rows.length > 0) {
|
|
2111
|
+
prevSnapshot = typeof rows[0].snapshot === "string" ? JSON.parse(rows[0].snapshot) : rows[0].snapshot;
|
|
1695
2112
|
}
|
|
1696
|
-
const
|
|
1697
|
-
|
|
1698
|
-
|
|
2113
|
+
const currentSnapshot = Object.fromEntries(schemas2.map((s) => [s.config.name, s.config]));
|
|
2114
|
+
const diff = diffSchemas(prevSnapshot, currentSnapshot);
|
|
2115
|
+
if (diff.statements.length === 0) {
|
|
2116
|
+
console.log(`
|
|
2117
|
+
No schema changes detected. Database is up to date.`);
|
|
2118
|
+
return;
|
|
1699
2119
|
}
|
|
1700
|
-
|
|
1701
|
-
|
|
2120
|
+
console.log(`
|
|
2121
|
+
Changes to apply:`);
|
|
2122
|
+
for (const s of diff.summary)
|
|
2123
|
+
console.log(` + ${s}`);
|
|
2124
|
+
if (diff.warnings && diff.warnings.length > 0) {
|
|
2125
|
+
console.warn(`
|
|
2126
|
+
\u26A0\uFE0F WARNING: Data Loss Detected!`);
|
|
2127
|
+
for (const w of diff.warnings)
|
|
2128
|
+
console.warn(` ! ${w}`);
|
|
2129
|
+
console.warn(`
|
|
2130
|
+
These changes will be immediately executed against the database!`);
|
|
1702
2131
|
}
|
|
1703
2132
|
if (!opts.force) {
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
console.log(
|
|
1709
|
-
|
|
1710
|
-
Are you sure? Type YES to continue: `, "cyan"));
|
|
1711
|
-
for await (const line2 of console) {
|
|
1712
|
-
if (line2.trim().toLowerCase() !== "yes") {
|
|
1713
|
-
console.log("Aborted.");
|
|
1714
|
-
return;
|
|
1715
|
-
}
|
|
1716
|
-
break;
|
|
2133
|
+
process.stdout.write(`
|
|
2134
|
+
Are you sure you want to push these changes? Type YES to continue: `);
|
|
2135
|
+
const answer = await readLine();
|
|
2136
|
+
if (answer.trim().toLowerCase() !== "yes") {
|
|
2137
|
+
console.log("Aborted.");
|
|
2138
|
+
return;
|
|
1717
2139
|
}
|
|
1718
2140
|
}
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
}
|
|
1728
|
-
if (pushTableExists) {
|
|
1729
|
-
await sql2.unsafe("DROP TABLE IF EXISTS public.__bungres_push CASCADE");
|
|
1730
|
-
console.log(colorize(` \u2713 dropped __bungres_push`, "green"));
|
|
2141
|
+
console.log(`
|
|
2142
|
+
Pushing changes...`);
|
|
2143
|
+
await sql2.unsafe(`CREATE EXTENSION IF NOT EXISTS "pgcrypto";`);
|
|
2144
|
+
for (const stmt of diff.statements) {
|
|
2145
|
+
if (config2.verbose) {
|
|
2146
|
+
console.log(`-- ${stmt}`);
|
|
2147
|
+
}
|
|
2148
|
+
await sql2.unsafe(stmt);
|
|
1731
2149
|
}
|
|
1732
|
-
|
|
1733
|
-
|
|
2150
|
+
await sql2.unsafe(`INSERT INTO public.__bungres_push (snapshot) VALUES ($1);`, [JSON.stringify(currentSnapshot)]);
|
|
2151
|
+
console.log(`
|
|
2152
|
+
Push complete.`);
|
|
1734
2153
|
} finally {
|
|
1735
2154
|
await sql2.end();
|
|
1736
2155
|
}
|
|
1737
2156
|
}
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
await runDrop(config2, { force: true });
|
|
1743
|
-
console.log(`
|
|
1744
|
-
Re-running migrations...`);
|
|
1745
|
-
await runMigrate(config2);
|
|
1746
|
-
console.log(`
|
|
1747
|
-
Fresh complete.`);
|
|
2157
|
+
async function readLine() {
|
|
2158
|
+
const buf = Buffer.alloc(256);
|
|
2159
|
+
const n = fs.readSync(0, buf, 0, 256, null);
|
|
2160
|
+
return buf.subarray(0, n).toString().trim();
|
|
1748
2161
|
}
|
|
1749
2162
|
|
|
1750
2163
|
// src/commands/refresh.ts
|
|
@@ -1771,13 +2184,13 @@ Refresh complete. All tables are now empty.`, "green"));
|
|
|
1771
2184
|
}
|
|
1772
2185
|
|
|
1773
2186
|
// src/commands/seed.ts
|
|
1774
|
-
import { resolve as
|
|
2187
|
+
import { resolve as resolve5 } from "path";
|
|
1775
2188
|
async function runSeed(config2) {
|
|
1776
2189
|
if (!config2.seed) {
|
|
1777
2190
|
console.log(colorize(`No seed file configured in bungres.config.ts`, "yellow"));
|
|
1778
2191
|
return;
|
|
1779
2192
|
}
|
|
1780
|
-
const seedPath =
|
|
2193
|
+
const seedPath = resolve5(process.cwd(), config2.seed);
|
|
1781
2194
|
const file = Bun.file(seedPath);
|
|
1782
2195
|
if (!await file.exists()) {
|
|
1783
2196
|
console.error(colorize(`Seed file not found at ${seedPath}`, "red"));
|
|
@@ -1789,7 +2202,7 @@ Running seeder: ${config2.seed}...`, "cyan"));
|
|
|
1789
2202
|
cwd: process.cwd(),
|
|
1790
2203
|
stdout: "inherit",
|
|
1791
2204
|
stderr: "inherit",
|
|
1792
|
-
env: { ...
|
|
2205
|
+
env: { ...Bun.env, DATABASE_URL: config2.dbUrl }
|
|
1793
2206
|
});
|
|
1794
2207
|
const exitCode = await proc.exited;
|
|
1795
2208
|
if (exitCode === 0) {
|
|
@@ -1802,6 +2215,54 @@ Seed failed with exit code ${exitCode}.`);
|
|
|
1802
2215
|
}
|
|
1803
2216
|
}
|
|
1804
2217
|
|
|
2218
|
+
// src/commands/status.ts
|
|
2219
|
+
import { resolve as resolve6 } from "path";
|
|
2220
|
+
async function runStatus(config2) {
|
|
2221
|
+
const migrationsDir = resolve6(config2.out);
|
|
2222
|
+
const sql2 = new Bun.SQL(config2.dbUrl);
|
|
2223
|
+
const table2 = config2.migrationsTable;
|
|
2224
|
+
const schema = config2.migrationsSchema;
|
|
2225
|
+
const qualifiedTable = `"${schema}"."${table2}"`;
|
|
2226
|
+
try {
|
|
2227
|
+
const tableCheck = await sql2.unsafe(`SELECT EXISTS (
|
|
2228
|
+
SELECT 1 FROM information_schema.tables
|
|
2229
|
+
WHERE table_schema = $1 AND table_name = $2
|
|
2230
|
+
) AS exists`, [schema, table2]);
|
|
2231
|
+
const trackingExists = tableCheck[0]?.exists ?? false;
|
|
2232
|
+
const glob = new Bun.Glob("*.sql");
|
|
2233
|
+
const files = [];
|
|
2234
|
+
for await (const file of glob.scan({ cwd: migrationsDir, absolute: false })) {
|
|
2235
|
+
files.push(file);
|
|
2236
|
+
}
|
|
2237
|
+
files.sort();
|
|
2238
|
+
if (files.length === 0) {
|
|
2239
|
+
console.log(colorize("No migration files found. Run `bungres generate` first.", "yellow"));
|
|
2240
|
+
return;
|
|
2241
|
+
}
|
|
2242
|
+
let appliedSet = new Set;
|
|
2243
|
+
if (trackingExists) {
|
|
2244
|
+
const applied = await sql2.unsafe(`SELECT name FROM ${qualifiedTable} ORDER BY applied_at`);
|
|
2245
|
+
appliedSet = new Set(applied.map((r) => r.name));
|
|
2246
|
+
}
|
|
2247
|
+
console.log(colorize(`
|
|
2248
|
+
Migration status:
|
|
2249
|
+
`, "cyan"));
|
|
2250
|
+
let pendingCount = 0;
|
|
2251
|
+
for (const file of files) {
|
|
2252
|
+
const isApplied = appliedSet.has(file);
|
|
2253
|
+
const status = isApplied ? colorize("\u2713 applied ", "green") : colorize("\u2717 pending ", "yellow");
|
|
2254
|
+
if (!isApplied)
|
|
2255
|
+
pendingCount++;
|
|
2256
|
+
console.log(` ${status} ${file}`);
|
|
2257
|
+
}
|
|
2258
|
+
console.log(`
|
|
2259
|
+
${colorize(appliedSet.size.toString(), "green")} applied, ${colorize(pendingCount.toString(), "yellow")} pending.
|
|
2260
|
+
`);
|
|
2261
|
+
} finally {
|
|
2262
|
+
await sql2.end();
|
|
2263
|
+
}
|
|
2264
|
+
}
|
|
2265
|
+
|
|
1805
2266
|
// src/commands/studio.ts
|
|
1806
2267
|
async function runStudio(config2) {
|
|
1807
2268
|
const schemas2 = await loadSchemas(config2.schema);
|
|
@@ -1813,8 +2274,8 @@ async function runStudio(config2) {
|
|
|
1813
2274
|
for (const s of schemas2) {
|
|
1814
2275
|
schemaObj2[s.exportName] = s.table;
|
|
1815
2276
|
}
|
|
1816
|
-
const db2 =
|
|
1817
|
-
const port =
|
|
2277
|
+
const db2 = bungres({ url: config2.dbUrl, schema: schemaObj2 });
|
|
2278
|
+
const port = Bun.env.PORT ? parseInt(Bun.env.PORT, 10) : 5555;
|
|
1818
2279
|
const server = Bun.serve({
|
|
1819
2280
|
port,
|
|
1820
2281
|
async fetch(req) {
|
|
@@ -1840,8 +2301,19 @@ async function runStudio(config2) {
|
|
|
1840
2301
|
return new Response("Table not found", { status: 404 });
|
|
1841
2302
|
}
|
|
1842
2303
|
try {
|
|
1843
|
-
const
|
|
1844
|
-
|
|
2304
|
+
const page = parseInt(url.searchParams.get("page") || "1", 10);
|
|
2305
|
+
const limit = parseInt(url.searchParams.get("limit") || "50", 10);
|
|
2306
|
+
const offset = (page - 1) * limit;
|
|
2307
|
+
const countResult = await db2.select({ count: schema.table }).from(schema.table);
|
|
2308
|
+
const total = Array.isArray(countResult) ? countResult.length : 0;
|
|
2309
|
+
const data = await db2.select().from(schema.table).limit(limit).offset(offset);
|
|
2310
|
+
return new Response(JSON.stringify({
|
|
2311
|
+
data,
|
|
2312
|
+
total,
|
|
2313
|
+
page,
|
|
2314
|
+
limit,
|
|
2315
|
+
totalPages: Math.ceil(total / limit)
|
|
2316
|
+
}), {
|
|
1845
2317
|
headers: { "Content-Type": "application/json" }
|
|
1846
2318
|
});
|
|
1847
2319
|
} catch (e) {
|
|
@@ -1891,18 +2363,26 @@ var htmlTemplate = `
|
|
|
1891
2363
|
<meta charset="UTF-8">
|
|
1892
2364
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
1893
2365
|
<title>Bungres Studio</title>
|
|
1894
|
-
<link href="https://fonts.googleapis.com/css2?family=
|
|
2366
|
+
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
|
1895
2367
|
<style>
|
|
1896
2368
|
:root {
|
|
1897
|
-
--bg-color: #
|
|
1898
|
-
--panel-bg:
|
|
1899
|
-
--border-color: #
|
|
1900
|
-
--text-main: #
|
|
1901
|
-
--text-muted: #
|
|
1902
|
-
--accent-color: #
|
|
1903
|
-
--accent-hover: #
|
|
1904
|
-
--header-bg:
|
|
1905
|
-
--row-hover:
|
|
2369
|
+
--bg-color: #09090b;
|
|
2370
|
+
--panel-bg: rgba(24, 24, 27, 0.7);
|
|
2371
|
+
--border-color: #27272a;
|
|
2372
|
+
--text-main: #f4f4f5;
|
|
2373
|
+
--text-muted: #a1a1aa;
|
|
2374
|
+
--accent-color: #8b5cf6;
|
|
2375
|
+
--accent-hover: #7c3aed;
|
|
2376
|
+
--header-bg: rgba(9, 9, 11, 0.85);
|
|
2377
|
+
--row-hover: rgba(39, 39, 42, 0.8);
|
|
2378
|
+
--glass-border: rgba(255, 255, 255, 0.08);
|
|
2379
|
+
|
|
2380
|
+
--type-number: #f472b6;
|
|
2381
|
+
--type-string: #60a5fa;
|
|
2382
|
+
--type-boolean: #34d399;
|
|
2383
|
+
--type-date: #c084fc;
|
|
2384
|
+
--type-json: #fbbf24;
|
|
2385
|
+
--type-null: #71717a;
|
|
1906
2386
|
}
|
|
1907
2387
|
|
|
1908
2388
|
* { box-sizing: border-box; }
|
|
@@ -1910,7 +2390,7 @@ var htmlTemplate = `
|
|
|
1910
2390
|
body {
|
|
1911
2391
|
margin: 0;
|
|
1912
2392
|
padding: 0;
|
|
1913
|
-
font-family: '
|
|
2393
|
+
font-family: 'Outfit', -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
|
|
1914
2394
|
background-color: var(--bg-color);
|
|
1915
2395
|
color: var(--text-main);
|
|
1916
2396
|
display: flex;
|
|
@@ -1920,56 +2400,91 @@ var htmlTemplate = `
|
|
|
1920
2400
|
|
|
1921
2401
|
/* Sidebar */
|
|
1922
2402
|
.sidebar {
|
|
1923
|
-
width:
|
|
2403
|
+
width: 280px;
|
|
1924
2404
|
background-color: var(--panel-bg);
|
|
2405
|
+
backdrop-filter: blur(12px);
|
|
2406
|
+
-webkit-backdrop-filter: blur(12px);
|
|
1925
2407
|
border-right: 1px solid var(--border-color);
|
|
1926
2408
|
display: flex;
|
|
1927
2409
|
flex-direction: column;
|
|
2410
|
+
box-shadow: 4px 0 24px rgba(0,0,0,0.2);
|
|
2411
|
+
z-index: 20;
|
|
1928
2412
|
}
|
|
1929
2413
|
|
|
1930
2414
|
.sidebar-header {
|
|
1931
|
-
padding:
|
|
1932
|
-
border-bottom: 1px solid var(--border
|
|
2415
|
+
padding: 24px;
|
|
2416
|
+
border-bottom: 1px solid var(--glass-border);
|
|
1933
2417
|
display: flex;
|
|
1934
2418
|
align-items: center;
|
|
1935
|
-
gap:
|
|
2419
|
+
gap: 12px;
|
|
2420
|
+
background: linear-gradient(to bottom, rgba(255,255,255,0.03), transparent);
|
|
1936
2421
|
}
|
|
1937
2422
|
|
|
1938
2423
|
.sidebar-header h1 {
|
|
1939
2424
|
margin: 0;
|
|
1940
|
-
font-size:
|
|
2425
|
+
font-size: 18px;
|
|
1941
2426
|
font-weight: 600;
|
|
1942
|
-
|
|
2427
|
+
background: linear-gradient(to right, #fff, #a1a1aa);
|
|
2428
|
+
-webkit-background-clip: text;
|
|
2429
|
+
-webkit-text-fill-color: transparent;
|
|
2430
|
+
letter-spacing: -0.5px;
|
|
2431
|
+
}
|
|
2432
|
+
|
|
2433
|
+
.sidebar-header svg {
|
|
2434
|
+
color: var(--accent-color);
|
|
2435
|
+
filter: drop-shadow(0 0 8px rgba(139, 92, 246, 0.5));
|
|
1943
2436
|
}
|
|
1944
2437
|
|
|
1945
2438
|
.table-list {
|
|
1946
2439
|
flex: 1;
|
|
1947
2440
|
overflow-y: auto;
|
|
1948
|
-
padding:
|
|
2441
|
+
padding: 16px 12px;
|
|
1949
2442
|
list-style: none;
|
|
1950
2443
|
margin: 0;
|
|
1951
2444
|
}
|
|
1952
2445
|
|
|
2446
|
+
.table-list::-webkit-scrollbar {
|
|
2447
|
+
width: 6px;
|
|
2448
|
+
}
|
|
2449
|
+
.table-list::-webkit-scrollbar-thumb {
|
|
2450
|
+
background: #3f3f46;
|
|
2451
|
+
border-radius: 4px;
|
|
2452
|
+
}
|
|
2453
|
+
|
|
1953
2454
|
.table-item {
|
|
1954
|
-
padding:
|
|
2455
|
+
padding: 12px 16px;
|
|
1955
2456
|
cursor: pointer;
|
|
1956
|
-
font-size:
|
|
2457
|
+
font-size: 15px;
|
|
2458
|
+
font-weight: 500;
|
|
1957
2459
|
color: var(--text-muted);
|
|
1958
|
-
|
|
2460
|
+
border-radius: 8px;
|
|
2461
|
+
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
|
1959
2462
|
display: flex;
|
|
1960
2463
|
align-items: center;
|
|
1961
|
-
gap:
|
|
2464
|
+
gap: 10px;
|
|
2465
|
+
margin-bottom: 4px;
|
|
2466
|
+
border: 1px solid transparent;
|
|
1962
2467
|
}
|
|
1963
2468
|
|
|
1964
2469
|
.table-item:hover {
|
|
1965
|
-
background-color:
|
|
2470
|
+
background-color: rgba(255, 255, 255, 0.05);
|
|
1966
2471
|
color: var(--text-main);
|
|
2472
|
+
transform: translateX(4px);
|
|
1967
2473
|
}
|
|
1968
2474
|
|
|
1969
2475
|
.table-item.active {
|
|
1970
|
-
background-color: rgba(
|
|
2476
|
+
background-color: rgba(139, 92, 246, 0.15);
|
|
2477
|
+
color: #fff;
|
|
2478
|
+
border: 1px solid rgba(139, 92, 246, 0.3);
|
|
2479
|
+
box-shadow: 0 4px 12px rgba(139, 92, 246, 0.1);
|
|
2480
|
+
}
|
|
2481
|
+
|
|
2482
|
+
.table-item svg {
|
|
2483
|
+
transition: transform 0.3s ease;
|
|
2484
|
+
}
|
|
2485
|
+
.table-item.active svg {
|
|
1971
2486
|
color: var(--accent-color);
|
|
1972
|
-
|
|
2487
|
+
transform: scale(1.1);
|
|
1973
2488
|
}
|
|
1974
2489
|
|
|
1975
2490
|
/* Main Content */
|
|
@@ -1977,92 +2492,168 @@ var htmlTemplate = `
|
|
|
1977
2492
|
flex: 1;
|
|
1978
2493
|
display: flex;
|
|
1979
2494
|
flex-direction: column;
|
|
1980
|
-
|
|
2495
|
+
position: relative;
|
|
1981
2496
|
}
|
|
1982
2497
|
|
|
1983
2498
|
.main-header {
|
|
1984
|
-
height:
|
|
1985
|
-
padding: 0
|
|
2499
|
+
height: 70px;
|
|
2500
|
+
padding: 0 24px;
|
|
1986
2501
|
border-bottom: 1px solid var(--border-color);
|
|
1987
2502
|
display: flex;
|
|
1988
2503
|
align-items: center;
|
|
1989
2504
|
justify-content: space-between;
|
|
1990
2505
|
background-color: var(--header-bg);
|
|
2506
|
+
backdrop-filter: blur(12px);
|
|
2507
|
+
-webkit-backdrop-filter: blur(12px);
|
|
2508
|
+
z-index: 10;
|
|
1991
2509
|
}
|
|
1992
2510
|
|
|
1993
2511
|
.main-header h2 {
|
|
1994
2512
|
margin: 0;
|
|
1995
|
-
font-size:
|
|
1996
|
-
font-weight:
|
|
2513
|
+
font-size: 20px;
|
|
2514
|
+
font-weight: 600;
|
|
1997
2515
|
color: white;
|
|
2516
|
+
letter-spacing: -0.5px;
|
|
2517
|
+
display: flex;
|
|
2518
|
+
align-items: center;
|
|
2519
|
+
gap: 10px;
|
|
1998
2520
|
}
|
|
1999
2521
|
|
|
2000
2522
|
.btn {
|
|
2001
|
-
background-color:
|
|
2002
|
-
|
|
2003
|
-
|
|
2523
|
+
background-color: rgba(255, 255, 255, 0.05);
|
|
2524
|
+
border: 1px solid var(--border-color);
|
|
2525
|
+
color: var(--text-main);
|
|
2004
2526
|
padding: 6px 12px;
|
|
2005
2527
|
border-radius: 6px;
|
|
2006
|
-
font-size:
|
|
2528
|
+
font-size: 13px;
|
|
2007
2529
|
font-weight: 500;
|
|
2530
|
+
font-family: 'Outfit', sans-serif;
|
|
2008
2531
|
cursor: pointer;
|
|
2009
|
-
transition:
|
|
2532
|
+
transition: all 0.2s ease;
|
|
2010
2533
|
display: flex;
|
|
2011
2534
|
align-items: center;
|
|
2012
|
-
gap:
|
|
2535
|
+
gap: 8px;
|
|
2013
2536
|
}
|
|
2014
2537
|
|
|
2015
|
-
.btn:hover {
|
|
2016
|
-
background-color:
|
|
2538
|
+
.btn:hover:not(:disabled) {
|
|
2539
|
+
background-color: rgba(255, 255, 255, 0.1);
|
|
2540
|
+
}
|
|
2541
|
+
|
|
2542
|
+
.btn:active:not(:disabled) {
|
|
2543
|
+
transform: translateY(1px);
|
|
2017
2544
|
}
|
|
2018
2545
|
|
|
2019
2546
|
.btn:disabled {
|
|
2020
|
-
opacity: 0.
|
|
2547
|
+
opacity: 0.4;
|
|
2021
2548
|
cursor: not-allowed;
|
|
2022
2549
|
}
|
|
2023
2550
|
|
|
2024
2551
|
.content-area {
|
|
2025
2552
|
flex: 1;
|
|
2026
2553
|
overflow: auto;
|
|
2027
|
-
padding:
|
|
2554
|
+
padding: 0;
|
|
2555
|
+
position: relative;
|
|
2556
|
+
z-index: 1;
|
|
2557
|
+
text-align: left;
|
|
2028
2558
|
}
|
|
2029
2559
|
|
|
2030
2560
|
/* Data Table */
|
|
2031
2561
|
.data-grid-container {
|
|
2032
|
-
background-color:
|
|
2562
|
+
background-color: rgba(24, 24, 27, 0.5);
|
|
2033
2563
|
border: 1px solid var(--border-color);
|
|
2034
|
-
border-radius:
|
|
2035
|
-
overflow:
|
|
2564
|
+
border-radius: 0;
|
|
2565
|
+
overflow: auto;
|
|
2566
|
+
box-shadow: 0 8px 32px rgba(0,0,0,0.2);
|
|
2567
|
+
backdrop-filter: blur(8px);
|
|
2568
|
+
max-height: 100%;
|
|
2569
|
+
width: 100%;
|
|
2570
|
+
}
|
|
2571
|
+
|
|
2572
|
+
.data-grid-container::-webkit-scrollbar {
|
|
2573
|
+
width: 8px;
|
|
2574
|
+
height: 8px;
|
|
2575
|
+
}
|
|
2576
|
+
.data-grid-container::-webkit-scrollbar-corner {
|
|
2577
|
+
background: transparent;
|
|
2578
|
+
}
|
|
2579
|
+
.data-grid-container::-webkit-scrollbar-thumb {
|
|
2580
|
+
background: #3f3f46;
|
|
2581
|
+
border-radius: 4px;
|
|
2036
2582
|
}
|
|
2037
2583
|
|
|
2038
2584
|
table {
|
|
2039
|
-
width:
|
|
2040
|
-
border-collapse:
|
|
2585
|
+
width: auto;
|
|
2586
|
+
border-collapse: separate;
|
|
2587
|
+
border-spacing: 0;
|
|
2041
2588
|
text-align: left;
|
|
2042
|
-
font-size:
|
|
2589
|
+
font-size: 14px;
|
|
2590
|
+
table-layout: auto;
|
|
2043
2591
|
}
|
|
2044
2592
|
|
|
2045
2593
|
th {
|
|
2046
|
-
background-color: rgba(
|
|
2047
|
-
|
|
2594
|
+
background-color: rgba(24, 24, 27, 0.95);
|
|
2595
|
+
backdrop-filter: blur(4px);
|
|
2596
|
+
color: var(--text-main);
|
|
2048
2597
|
font-weight: 500;
|
|
2049
|
-
padding:
|
|
2598
|
+
padding: 12px 20px;
|
|
2050
2599
|
border-bottom: 1px solid var(--border-color);
|
|
2600
|
+
border-right: 1px solid var(--border-color);
|
|
2051
2601
|
white-space: nowrap;
|
|
2052
2602
|
position: sticky;
|
|
2053
2603
|
top: 0;
|
|
2054
2604
|
z-index: 10;
|
|
2055
2605
|
}
|
|
2606
|
+
|
|
2607
|
+
|
|
2608
|
+
th::after {
|
|
2609
|
+
content: '';
|
|
2610
|
+
position: absolute;
|
|
2611
|
+
bottom: -1px; left: 0; right: 0;
|
|
2612
|
+
height: 1px;
|
|
2613
|
+
background: var(--border-color);
|
|
2614
|
+
}
|
|
2615
|
+
|
|
2616
|
+
.col-header {
|
|
2617
|
+
display: flex;
|
|
2618
|
+
flex-direction: column;
|
|
2619
|
+
gap: 4px;
|
|
2620
|
+
}
|
|
2621
|
+
|
|
2622
|
+
.col-name {
|
|
2623
|
+
font-weight: 600;
|
|
2624
|
+
display: flex;
|
|
2625
|
+
align-items: center;
|
|
2626
|
+
gap: 6px;
|
|
2627
|
+
}
|
|
2628
|
+
|
|
2629
|
+
.pk-badge {
|
|
2630
|
+
font-size: 10px;
|
|
2631
|
+
background: rgba(251, 191, 36, 0.2);
|
|
2632
|
+
color: #fbbf24;
|
|
2633
|
+
padding: 2px 6px;
|
|
2634
|
+
border-radius: 4px;
|
|
2635
|
+
font-weight: 600;
|
|
2636
|
+
letter-spacing: 0.5px;
|
|
2637
|
+
}
|
|
2638
|
+
|
|
2639
|
+
.col-type {
|
|
2640
|
+
font-size: 12px;
|
|
2641
|
+
color: var(--text-muted);
|
|
2642
|
+
font-family: monospace;
|
|
2643
|
+
}
|
|
2056
2644
|
|
|
2057
2645
|
td {
|
|
2058
|
-
padding:
|
|
2646
|
+
padding: 12px 20px;
|
|
2059
2647
|
border-bottom: 1px solid var(--border-color);
|
|
2648
|
+
border-right: 1px solid var(--border-color);
|
|
2060
2649
|
color: var(--text-main);
|
|
2061
2650
|
max-width: 300px;
|
|
2062
2651
|
overflow: hidden;
|
|
2063
2652
|
text-overflow: ellipsis;
|
|
2064
2653
|
white-space: nowrap;
|
|
2654
|
+
transition: background-color 0.2s ease;
|
|
2065
2655
|
}
|
|
2656
|
+
|
|
2066
2657
|
|
|
2067
2658
|
tr:last-child td {
|
|
2068
2659
|
border-bottom: none;
|
|
@@ -2072,11 +2663,17 @@ var htmlTemplate = `
|
|
|
2072
2663
|
background-color: var(--row-hover);
|
|
2073
2664
|
}
|
|
2074
2665
|
|
|
2075
|
-
.type-number { color:
|
|
2076
|
-
.type-string { color:
|
|
2077
|
-
.type-boolean { color:
|
|
2078
|
-
.type-date { color:
|
|
2079
|
-
.type-
|
|
2666
|
+
.type-number { color: var(--type-number); }
|
|
2667
|
+
.type-string { color: var(--type-string); }
|
|
2668
|
+
.type-boolean { color: var(--type-boolean); }
|
|
2669
|
+
.type-date { color: var(--type-date); }
|
|
2670
|
+
.type-json { color: var(--type-json); font-family: monospace; }
|
|
2671
|
+
.type-null { color: var(--type-null); font-style: italic; }
|
|
2672
|
+
|
|
2673
|
+
.cell-value {
|
|
2674
|
+
font-family: 'JetBrains Mono', 'Fira Code', monospace;
|
|
2675
|
+
font-size: 13px;
|
|
2676
|
+
}
|
|
2080
2677
|
|
|
2081
2678
|
.empty-state {
|
|
2082
2679
|
display: flex;
|
|
@@ -2084,7 +2681,98 @@ var htmlTemplate = `
|
|
|
2084
2681
|
align-items: center;
|
|
2085
2682
|
justify-content: center;
|
|
2086
2683
|
height: 100%;
|
|
2684
|
+
width: 100%;
|
|
2685
|
+
color: var(--text-muted);
|
|
2686
|
+
animation: fadeIn 0.5s ease-out;
|
|
2687
|
+
}
|
|
2688
|
+
|
|
2689
|
+
.empty-state svg {
|
|
2690
|
+
margin-bottom: 16px;
|
|
2691
|
+
color: #3f3f46;
|
|
2692
|
+
width: 64px;
|
|
2693
|
+
height: 64px;
|
|
2694
|
+
}
|
|
2695
|
+
|
|
2696
|
+
.empty-state h3 {
|
|
2697
|
+
color: var(--text-main);
|
|
2698
|
+
margin: 0 0 8px 0;
|
|
2699
|
+
font-size: 18px;
|
|
2700
|
+
}
|
|
2701
|
+
|
|
2702
|
+
@keyframes fadeIn {
|
|
2703
|
+
from { opacity: 0; transform: translateY(10px); }
|
|
2704
|
+
to { opacity: 1; transform: translateY(0); }
|
|
2705
|
+
}
|
|
2706
|
+
|
|
2707
|
+
@keyframes spin {
|
|
2708
|
+
from { transform: rotate(0deg); }
|
|
2709
|
+
to { transform: rotate(360deg); }
|
|
2710
|
+
}
|
|
2711
|
+
.spinning {
|
|
2712
|
+
animation: spin 1s linear infinite;
|
|
2713
|
+
}
|
|
2714
|
+
|
|
2715
|
+
/* Pagination */
|
|
2716
|
+
.pagination {
|
|
2717
|
+
display: flex;
|
|
2718
|
+
align-items: center;
|
|
2719
|
+
justify-content: space-between;
|
|
2720
|
+
padding: 16px 24px;
|
|
2721
|
+
background-color: rgba(24, 24, 27, 0.95);
|
|
2722
|
+
border-top: 1px solid var(--border-color);
|
|
2723
|
+
font-size: 14px;
|
|
2087
2724
|
color: var(--text-muted);
|
|
2725
|
+
position: sticky;
|
|
2726
|
+
bottom: 0;
|
|
2727
|
+
z-index: 5;
|
|
2728
|
+
}
|
|
2729
|
+
|
|
2730
|
+
.pagination-info {
|
|
2731
|
+
display: flex;
|
|
2732
|
+
align-items: center;
|
|
2733
|
+
gap: 16px;
|
|
2734
|
+
}
|
|
2735
|
+
|
|
2736
|
+
.pagination-controls {
|
|
2737
|
+
display: flex;
|
|
2738
|
+
align-items: center;
|
|
2739
|
+
gap: 8px;
|
|
2740
|
+
}
|
|
2741
|
+
|
|
2742
|
+
.pagination-btn {
|
|
2743
|
+
background-color: rgba(255, 255, 255, 0.05);
|
|
2744
|
+
border: 1px solid var(--border-color);
|
|
2745
|
+
color: var(--text-main);
|
|
2746
|
+
padding: 6px 12px;
|
|
2747
|
+
border-radius: 6px;
|
|
2748
|
+
cursor: pointer;
|
|
2749
|
+
font-size: 13px;
|
|
2750
|
+
transition: all 0.2s ease;
|
|
2751
|
+
}
|
|
2752
|
+
|
|
2753
|
+
.pagination-btn:hover:not(:disabled) {
|
|
2754
|
+
background-color: rgba(255, 255, 255, 0.1);
|
|
2755
|
+
}
|
|
2756
|
+
|
|
2757
|
+
.pagination-btn:disabled {
|
|
2758
|
+
opacity: 0.4;
|
|
2759
|
+
cursor: not-allowed;
|
|
2760
|
+
}
|
|
2761
|
+
|
|
2762
|
+
.pagination-btn.active {
|
|
2763
|
+
background-color: var(--accent-color);
|
|
2764
|
+
border-color: var(--accent-color);
|
|
2765
|
+
}
|
|
2766
|
+
|
|
2767
|
+
.page-input {
|
|
2768
|
+
background-color: rgba(255, 255, 255, 0.05);
|
|
2769
|
+
border: 1px solid var(--border-color);
|
|
2770
|
+
color: var(--text-main);
|
|
2771
|
+
padding: 6px 8px;
|
|
2772
|
+
border-radius: 6px;
|
|
2773
|
+
width: 50px;
|
|
2774
|
+
text-align: center;
|
|
2775
|
+
font-size: 13px;
|
|
2088
2776
|
}
|
|
2089
2777
|
</style>
|
|
2090
2778
|
</head>
|
|
@@ -2092,7 +2780,7 @@ var htmlTemplate = `
|
|
|
2092
2780
|
|
|
2093
2781
|
<div class="sidebar">
|
|
2094
2782
|
<div class="sidebar-header">
|
|
2095
|
-
<svg width="
|
|
2783
|
+
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
2096
2784
|
<ellipse cx="12" cy="5" rx="9" ry="3"></ellipse>
|
|
2097
2785
|
<path d="M21 12c0 1.66-4 3-9 3s-9-1.34-9-3"></path>
|
|
2098
2786
|
<path d="M3 5v14c0 1.66 4 3 9 3s9-1.34 9-3V5"></path>
|
|
@@ -2106,19 +2794,31 @@ var htmlTemplate = `
|
|
|
2106
2794
|
|
|
2107
2795
|
<div class="main">
|
|
2108
2796
|
<div class="main-header">
|
|
2109
|
-
<h2 id="current-table-name">
|
|
2797
|
+
<h2 id="current-table-name">
|
|
2798
|
+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="color: var(--text-muted);">
|
|
2799
|
+
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
|
|
2800
|
+
<line x1="3" y1="9" x2="21" y2="9"></line>
|
|
2801
|
+
<line x1="9" y1="21" x2="9" y2="9"></line>
|
|
2802
|
+
</svg>
|
|
2803
|
+
Select a table
|
|
2804
|
+
</h2>
|
|
2110
2805
|
<button id="refresh-btn" class="btn" disabled>
|
|
2111
|
-
<svg width="
|
|
2806
|
+
<svg id="refresh-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
2112
2807
|
<polyline points="23 4 23 10 17 10"></polyline>
|
|
2113
2808
|
<polyline points="1 20 1 14 7 14"></polyline>
|
|
2114
2809
|
<path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"></path>
|
|
2115
2810
|
</svg>
|
|
2116
|
-
Refresh
|
|
2811
|
+
Refresh
|
|
2117
2812
|
</button>
|
|
2118
2813
|
</div>
|
|
2119
2814
|
<div class="content-area">
|
|
2120
2815
|
<div id="data-container" class="empty-state">
|
|
2121
|
-
<
|
|
2816
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
|
2817
|
+
<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>
|
|
2818
|
+
<polyline points="14 2 14 8 20 8"></polyline>
|
|
2819
|
+
</svg>
|
|
2820
|
+
<h3>No Table Selected</h3>
|
|
2821
|
+
<p>Choose a table from the sidebar to view its data</p>
|
|
2122
2822
|
</div>
|
|
2123
2823
|
</div>
|
|
2124
2824
|
</div>
|
|
@@ -2126,21 +2826,38 @@ var htmlTemplate = `
|
|
|
2126
2826
|
<script>
|
|
2127
2827
|
let currentTable = null;
|
|
2128
2828
|
let tablesData = [];
|
|
2829
|
+
let tableSchemas = {};
|
|
2830
|
+
let currentPage = 1;
|
|
2831
|
+
let totalPages = 1;
|
|
2832
|
+
let totalRecords = 0;
|
|
2833
|
+
let pageSize = 50;
|
|
2129
2834
|
|
|
2130
2835
|
// Format values for the data grid
|
|
2131
2836
|
function formatValue(val) {
|
|
2132
|
-
if (val === null || val === undefined) return '
|
|
2133
|
-
|
|
2134
|
-
if (typeof val === '
|
|
2135
|
-
|
|
2136
|
-
|
|
2837
|
+
if (val === null || val === undefined) return 'null';
|
|
2838
|
+
|
|
2839
|
+
if (typeof val === 'object') {
|
|
2840
|
+
if (val instanceof Date) {
|
|
2841
|
+
return val.toISOString();
|
|
2842
|
+
}
|
|
2843
|
+
// Handle JSON objects
|
|
2844
|
+
const safeJson = JSON.stringify(val)
|
|
2845
|
+
.replace(/&/g, "&")
|
|
2846
|
+
.replace(/</g, "<")
|
|
2847
|
+
.replace(/>/g, ">");
|
|
2848
|
+
return safeJson;
|
|
2849
|
+
}
|
|
2850
|
+
|
|
2851
|
+
if (typeof val === 'string') {
|
|
2852
|
+
// Escape HTML to prevent XSS
|
|
2853
|
+
const safeStr = String(val)
|
|
2854
|
+
.replace(/&/g, "&")
|
|
2855
|
+
.replace(/</g, "<")
|
|
2856
|
+
.replace(/>/g, ">");
|
|
2857
|
+
return safeStr;
|
|
2137
2858
|
}
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
.replace(/&/g, "&")
|
|
2141
|
-
.replace(/</g, "<")
|
|
2142
|
-
.replace(/>/g, ">");
|
|
2143
|
-
return \`<span class="type-string">\${safeStr}</span>\`;
|
|
2859
|
+
|
|
2860
|
+
return String(val);
|
|
2144
2861
|
}
|
|
2145
2862
|
|
|
2146
2863
|
async function loadTables() {
|
|
@@ -2150,12 +2867,14 @@ var htmlTemplate = `
|
|
|
2150
2867
|
|
|
2151
2868
|
const list = document.getElementById('table-list');
|
|
2152
2869
|
list.innerHTML = '';
|
|
2870
|
+
tableSchemas = {};
|
|
2153
2871
|
|
|
2154
2872
|
tablesData.forEach(t => {
|
|
2873
|
+
tableSchemas[t.name] = t;
|
|
2155
2874
|
const li = document.createElement('li');
|
|
2156
2875
|
li.className = 'table-item';
|
|
2157
2876
|
li.innerHTML = \`
|
|
2158
|
-
<svg width="
|
|
2877
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
2159
2878
|
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
|
|
2160
2879
|
<line x1="3" y1="9" x2="21" y2="9"></line>
|
|
2161
2880
|
<line x1="9" y1="21" x2="9" y2="9"></line>
|
|
@@ -2170,8 +2889,9 @@ var htmlTemplate = `
|
|
|
2170
2889
|
}
|
|
2171
2890
|
}
|
|
2172
2891
|
|
|
2173
|
-
async function selectTable(name) {
|
|
2892
|
+
async function selectTable(name, page = 1) {
|
|
2174
2893
|
currentTable = name;
|
|
2894
|
+
currentPage = page;
|
|
2175
2895
|
|
|
2176
2896
|
// Update UI active state
|
|
2177
2897
|
document.querySelectorAll('.table-item').forEach(el => {
|
|
@@ -2179,60 +2899,169 @@ var htmlTemplate = `
|
|
|
2179
2899
|
else el.classList.remove('active');
|
|
2180
2900
|
});
|
|
2181
2901
|
|
|
2182
|
-
document.getElementById('current-table-name').
|
|
2902
|
+
document.getElementById('current-table-name').innerHTML = \`
|
|
2903
|
+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="color: var(--accent-color);">
|
|
2904
|
+
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
|
|
2905
|
+
<line x1="3" y1="9" x2="21" y2="9"></line>
|
|
2906
|
+
<line x1="9" y1="21" x2="9" y2="9"></line>
|
|
2907
|
+
</svg>
|
|
2908
|
+
\${name}
|
|
2909
|
+
\`;
|
|
2183
2910
|
document.getElementById('refresh-btn').disabled = false;
|
|
2184
2911
|
|
|
2185
2912
|
const container = document.getElementById('data-container');
|
|
2186
|
-
container.innerHTML =
|
|
2187
|
-
|
|
2913
|
+
container.innerHTML = \`
|
|
2914
|
+
<div class="empty-state">
|
|
2915
|
+
<svg class="spinning" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
2916
|
+
<line x1="12" y1="2" x2="12" y2="6"></line>
|
|
2917
|
+
<line x1="12" y1="18" x2="12" y2="22"></line>
|
|
2918
|
+
<line x1="4.93" y1="4.93" x2="7.76" y2="7.76"></line>
|
|
2919
|
+
<line x1="16.24" y1="16.24" x2="19.07" y2="19.07"></line>
|
|
2920
|
+
<line x1="2" y1="12" x2="6" y2="12"></line>
|
|
2921
|
+
<line x1="18" y1="12" x2="22" y2="12"></line>
|
|
2922
|
+
<line x1="4.93" y1="19.07" x2="7.76" y2="16.24"></line>
|
|
2923
|
+
<line x1="16.24" y1="7.76" x2="19.07" y2="4.93"></line>
|
|
2924
|
+
</svg>
|
|
2925
|
+
<p>Loading data...</p>
|
|
2926
|
+
</div>
|
|
2927
|
+
\`;
|
|
2188
2928
|
|
|
2189
2929
|
try {
|
|
2190
|
-
const res = await fetch(\`/api/tables/\${name}/data\`);
|
|
2930
|
+
const res = await fetch(\`/api/tables/\${name}/data?page=\${page}&limit=\${pageSize}\`);
|
|
2191
2931
|
if (!res.ok) throw new Error(await res.text());
|
|
2192
|
-
const
|
|
2932
|
+
const response = await res.json();
|
|
2933
|
+
|
|
2934
|
+
const rows = response.data || response;
|
|
2935
|
+
totalRecords = response.total || 0;
|
|
2936
|
+
totalPages = response.totalPages || 1;
|
|
2937
|
+
|
|
2938
|
+
const tableSchema = tableSchemas[name];
|
|
2193
2939
|
|
|
2194
2940
|
if (rows.length === 0) {
|
|
2195
|
-
container.innerHTML =
|
|
2941
|
+
container.innerHTML = \`
|
|
2942
|
+
<div class="empty-state">
|
|
2943
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
|
2944
|
+
<circle cx="12" cy="12" r="10"></circle>
|
|
2945
|
+
<line x1="12" y1="8" x2="12" y2="12"></line>
|
|
2946
|
+
<line x1="12" y1="16" x2="12.01" y2="16"></line>
|
|
2947
|
+
</svg>
|
|
2948
|
+
<h3>Table is Empty</h3>
|
|
2949
|
+
<p>No records found in "\${name}"</p>
|
|
2950
|
+
</div>
|
|
2951
|
+
\`;
|
|
2196
2952
|
return;
|
|
2197
2953
|
}
|
|
2198
2954
|
|
|
2199
|
-
|
|
2955
|
+
// Use all columns from the actual data to ensure foreign keys and all data are shown
|
|
2956
|
+
let columns = [];
|
|
2957
|
+
const allKeys = new Set();
|
|
2958
|
+
rows.forEach(row => {
|
|
2959
|
+
Object.keys(row).forEach(key => allKeys.add(key));
|
|
2960
|
+
});
|
|
2961
|
+
|
|
2962
|
+
// Try to get type info from schema if available
|
|
2963
|
+
const schemaColMap = {};
|
|
2964
|
+
if (tableSchema && tableSchema.columns && tableSchema.columns.length > 0) {
|
|
2965
|
+
tableSchema.columns.forEach(col => {
|
|
2966
|
+
schemaColMap[col.name] = col;
|
|
2967
|
+
});
|
|
2968
|
+
}
|
|
2969
|
+
|
|
2970
|
+
columns = Array.from(allKeys).map(key => ({
|
|
2971
|
+
name: key,
|
|
2972
|
+
type: schemaColMap[key] ? schemaColMap[key].type : typeof rows[0][key],
|
|
2973
|
+
primaryKey: schemaColMap[key] ? schemaColMap[key].primaryKey : false
|
|
2974
|
+
}));
|
|
2200
2975
|
|
|
2201
2976
|
let html = '<div class="data-grid-container"><table><thead><tr>';
|
|
2202
2977
|
columns.forEach(col => {
|
|
2203
|
-
html +=
|
|
2978
|
+
html += \`
|
|
2979
|
+
<th>
|
|
2980
|
+
<div class="col-header">
|
|
2981
|
+
<div class="col-name">
|
|
2982
|
+
\${col.name}
|
|
2983
|
+
\${col.primaryKey ? '<span class="pk-badge">PK</span>' : ''}
|
|
2984
|
+
</div>
|
|
2985
|
+
<div class="col-type">\${col.type}</div>
|
|
2986
|
+
</div>
|
|
2987
|
+
</th>
|
|
2988
|
+
\`;
|
|
2204
2989
|
});
|
|
2205
2990
|
html += '</tr></thead><tbody>';
|
|
2206
2991
|
|
|
2207
2992
|
rows.forEach(row => {
|
|
2208
2993
|
html += '<tr>';
|
|
2209
2994
|
columns.forEach(col => {
|
|
2210
|
-
html += \`<td>\${formatValue(row[col])}</td>\`;
|
|
2995
|
+
html += \`<td>\${formatValue(row[col.name])}</td>\`;
|
|
2211
2996
|
});
|
|
2212
2997
|
html += '</tr>';
|
|
2213
2998
|
});
|
|
2214
2999
|
|
|
2215
|
-
html += '</tbody></table
|
|
3000
|
+
html += '</tbody></table>';
|
|
3001
|
+
|
|
3002
|
+
// Add pagination
|
|
3003
|
+
html += renderPagination();
|
|
3004
|
+
|
|
3005
|
+
html += '</div>';
|
|
2216
3006
|
container.innerHTML = html;
|
|
2217
3007
|
|
|
2218
3008
|
} catch (e) {
|
|
2219
|
-
container.innerHTML =
|
|
3009
|
+
container.innerHTML = \`
|
|
3010
|
+
<div class="empty-state">
|
|
3011
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="#ef4444" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
|
3012
|
+
<circle cx="12" cy="12" r="10"></circle>
|
|
3013
|
+
<line x1="12" y1="8" x2="12" y2="12"></line>
|
|
3014
|
+
<line x1="12" y1="16" x2="12.01" y2="16"></line>
|
|
3015
|
+
</svg>
|
|
3016
|
+
<h3 style="color: #ef4444;">Error Loading Data</h3>
|
|
3017
|
+
<p>\${e.message}</p>
|
|
3018
|
+
</div>
|
|
3019
|
+
\`;
|
|
2220
3020
|
}
|
|
2221
3021
|
}
|
|
2222
3022
|
|
|
3023
|
+
function renderPagination() {
|
|
3024
|
+
const startRecord = (currentPage - 1) * pageSize + 1;
|
|
3025
|
+
const endRecord = Math.min(currentPage * pageSize, totalRecords);
|
|
3026
|
+
|
|
3027
|
+
let html = '<div class="pagination">';
|
|
3028
|
+
html += '<div class="pagination-info">';
|
|
3029
|
+
html += \`<span>Total: \${totalRecords} records</span>\`;
|
|
3030
|
+
html += \`<span>Page \${currentPage} of \${totalPages}</span>\`;
|
|
3031
|
+
html += \`<span>Showing \${startRecord}-\${endRecord}</span>\`;
|
|
3032
|
+
html += '</div>';
|
|
3033
|
+
|
|
3034
|
+
html += '<div class="pagination-controls">';
|
|
3035
|
+
|
|
3036
|
+
// Previous button
|
|
3037
|
+
html += \`<button class="pagination-btn" onclick="changePage(\${currentPage - 1})" \${currentPage === 1 ? 'disabled' : ''}>Previous</button>\`;
|
|
3038
|
+
|
|
3039
|
+
// Next button
|
|
3040
|
+
html += \`<button class="pagination-btn" onclick="changePage(\${currentPage + 1})" \${currentPage === totalPages ? 'disabled' : ''}>Next</button>\`;
|
|
3041
|
+
|
|
3042
|
+
html += '</div>';
|
|
3043
|
+
html += '</div>';
|
|
3044
|
+
|
|
3045
|
+
return html;
|
|
3046
|
+
}
|
|
3047
|
+
|
|
3048
|
+
function changePage(page) {
|
|
3049
|
+
if (page < 1 || page > totalPages) return;
|
|
3050
|
+
selectTable(currentTable, page);
|
|
3051
|
+
}
|
|
3052
|
+
|
|
2223
3053
|
document.getElementById('refresh-btn').addEventListener('click', async () => {
|
|
2224
3054
|
if (!currentTable) return;
|
|
2225
3055
|
|
|
2226
|
-
const
|
|
2227
|
-
|
|
2228
|
-
btn.innerHTML = 'Refreshing...';
|
|
3056
|
+
const icon = document.getElementById('refresh-icon');
|
|
3057
|
+
icon.classList.add('spinning');
|
|
2229
3058
|
|
|
2230
3059
|
try {
|
|
2231
3060
|
await selectTable(currentTable);
|
|
2232
3061
|
} catch (e) {
|
|
2233
3062
|
console.error("Failed to refresh data", e);
|
|
2234
3063
|
} finally {
|
|
2235
|
-
setTimeout(() => {
|
|
3064
|
+
setTimeout(() => { icon.classList.remove('spinning'); }, 500);
|
|
2236
3065
|
}
|
|
2237
3066
|
});
|
|
2238
3067
|
|
|
@@ -2251,7 +3080,7 @@ async function runTusky(config) {
|
|
|
2251
3080
|
for (const s of schemas) {
|
|
2252
3081
|
schemaObj[s.exportName] = s.table;
|
|
2253
3082
|
}
|
|
2254
|
-
const db =
|
|
3083
|
+
const db = bungres({ url: config.dbUrl, schema: schemaObj });
|
|
2255
3084
|
console.log("=========================================");
|
|
2256
3085
|
console.log("\uD83D\uDC18 Welcome to Bungres REPL (Tusky)");
|
|
2257
3086
|
console.log("=========================================");
|
|
@@ -2308,6 +3137,46 @@ Example query: await db.select().from(users)`);
|
|
|
2308
3137
|
});
|
|
2309
3138
|
}
|
|
2310
3139
|
|
|
3140
|
+
// src/config.ts
|
|
3141
|
+
import { join as join6, resolve as resolve7 } from "path";
|
|
3142
|
+
var CONFIG_FILES = [
|
|
3143
|
+
"bungres.config.ts",
|
|
3144
|
+
"bungres.config.js",
|
|
3145
|
+
"bungres.config.mts",
|
|
3146
|
+
"bungres.config.mjs"
|
|
3147
|
+
];
|
|
3148
|
+
async function loadConfig(cwd = process.cwd()) {
|
|
3149
|
+
let userConfig = {};
|
|
3150
|
+
for (const file of CONFIG_FILES) {
|
|
3151
|
+
const configPath = join6(cwd, file);
|
|
3152
|
+
if (await Bun.file(configPath).exists()) {
|
|
3153
|
+
const mod = await import(resolve7(configPath));
|
|
3154
|
+
userConfig = mod.default ?? mod;
|
|
3155
|
+
break;
|
|
3156
|
+
}
|
|
3157
|
+
}
|
|
3158
|
+
const dbUrl = userConfig.dbCredentials?.url ?? Bun.env.DATABASE_URL ?? Bun.env.POSTGRES_URL ?? "";
|
|
3159
|
+
if (!dbUrl) {
|
|
3160
|
+
console.error(`bungres: No database URL found.
|
|
3161
|
+
Add dbCredentials.url to bungres.config.ts or set DATABASE_URL.`);
|
|
3162
|
+
process.exit(1);
|
|
3163
|
+
}
|
|
3164
|
+
const out = userConfig.out ?? "./migrations";
|
|
3165
|
+
return {
|
|
3166
|
+
schema: userConfig.schema ?? "src/db/schema/**/*.ts",
|
|
3167
|
+
seed: userConfig.seed ?? "src/db/seed.ts",
|
|
3168
|
+
out,
|
|
3169
|
+
dbUrl,
|
|
3170
|
+
dbSchema: userConfig.dbSchema ?? "public",
|
|
3171
|
+
migrationsTable: userConfig.migrations?.table ?? "__bungres_migrations",
|
|
3172
|
+
migrationsSchema: userConfig.migrations?.schema ?? "bungres",
|
|
3173
|
+
breakpoints: userConfig.breakpoints ?? true,
|
|
3174
|
+
strict: userConfig.strict ?? false,
|
|
3175
|
+
outDir: "./src/db/generated",
|
|
3176
|
+
verbose: userConfig.verbose ?? false
|
|
3177
|
+
};
|
|
3178
|
+
}
|
|
3179
|
+
|
|
2311
3180
|
// src/ensure-db.ts
|
|
2312
3181
|
async function ensureDatabase2(dbUrl) {
|
|
2313
3182
|
let sql2;
|
|
@@ -2347,6 +3216,7 @@ ${colorize("Usage:", "yellow")}
|
|
|
2347
3216
|
bungres <command> [options]
|
|
2348
3217
|
|
|
2349
3218
|
${colorize("Commands:", "yellow")}
|
|
3219
|
+
init Initialize bungres project with config file and db folder structure
|
|
2350
3220
|
generate Generate SQL migration files from your schema definitions
|
|
2351
3221
|
migrate Run pending migration files against the database
|
|
2352
3222
|
push Apply schema directly to the DB (no migration files, dev mode)
|
|
@@ -2367,6 +3237,7 @@ ${colorize("Options:", "yellow")}
|
|
|
2367
3237
|
--help Show this help
|
|
2368
3238
|
|
|
2369
3239
|
${colorize("Examples:", "yellow")}
|
|
3240
|
+
bungres init
|
|
2370
3241
|
bungres generate
|
|
2371
3242
|
bungres migrate
|
|
2372
3243
|
bungres push
|
|
@@ -2391,6 +3262,10 @@ async function main() {
|
|
|
2391
3262
|
}
|
|
2392
3263
|
const command = args[0];
|
|
2393
3264
|
const flags = parseFlags(args.slice(1));
|
|
3265
|
+
if (command === "init") {
|
|
3266
|
+
await runInit();
|
|
3267
|
+
process.exit(0);
|
|
3268
|
+
}
|
|
2394
3269
|
const config2 = await loadConfig(process.cwd());
|
|
2395
3270
|
if (flags.verbose)
|
|
2396
3271
|
config2.verbose = true;
|