@bungres/kit 0.1.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/LICENSE +21 -0
- package/README.md +65 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +2435 -0
- package/dist/commands/drop.d.ts +5 -0
- package/dist/commands/drop.d.ts.map +1 -0
- package/dist/commands/fresh.d.ts +3 -0
- package/dist/commands/fresh.d.ts.map +1 -0
- package/dist/commands/generate.d.ts +3 -0
- package/dist/commands/generate.d.ts.map +1 -0
- package/dist/commands/migrate.d.ts +3 -0
- package/dist/commands/migrate.d.ts.map +1 -0
- package/dist/commands/pull.d.ts +29 -0
- package/dist/commands/pull.d.ts.map +1 -0
- package/dist/commands/push.d.ts +5 -0
- package/dist/commands/push.d.ts.map +1 -0
- package/dist/commands/refresh.d.ts +3 -0
- package/dist/commands/refresh.d.ts.map +1 -0
- package/dist/commands/seed.d.ts +3 -0
- package/dist/commands/seed.d.ts.map +1 -0
- package/dist/commands/status.d.ts +3 -0
- package/dist/commands/status.d.ts.map +1 -0
- package/dist/commands/studio.d.ts +3 -0
- package/dist/commands/studio.d.ts.map +1 -0
- package/dist/commands/tusky.d.ts +3 -0
- package/dist/commands/tusky.d.ts.map +1 -0
- package/dist/config.d.ts +44 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/differ.d.ts +11 -0
- package/dist/differ.d.ts.map +1 -0
- package/dist/ensure-db.d.ts +2 -0
- package/dist/ensure-db.d.ts.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1720 -0
- package/dist/schema-loader.d.ts +9 -0
- package/dist/schema-loader.d.ts.map +1 -0
- package/dist/utils/colors.d.ts +2 -0
- package/dist/utils/colors.d.ts.map +1 -0
- package/package.json +43 -0
- package/src/cli.ts +162 -0
- package/src/commands/drop.ts +92 -0
- package/src/commands/fresh.ts +17 -0
- package/src/commands/generate.ts +151 -0
- package/src/commands/migrate.ts +84 -0
- package/src/commands/pull.ts +339 -0
- package/src/commands/push.ts +105 -0
- package/src/commands/refresh.ts +37 -0
- package/src/commands/seed.ts +41 -0
- package/src/commands/status.ts +64 -0
- package/src/commands/studio.ts +471 -0
- package/src/commands/tusky.ts +83 -0
- package/src/config.ts +102 -0
- package/src/differ.ts +236 -0
- package/src/ensure-db.ts +32 -0
- package/src/index.ts +21 -0
- package/src/schema-loader.ts +50 -0
- package/src/utils/colors.ts +4 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1720 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// src/config.ts
|
|
3
|
+
import { resolve, join } from "path";
|
|
4
|
+
var CONFIG_FILES = [
|
|
5
|
+
"bungres.config.ts",
|
|
6
|
+
"bungres.config.js",
|
|
7
|
+
"bungres.config.mts",
|
|
8
|
+
"bungres.config.mjs"
|
|
9
|
+
];
|
|
10
|
+
async function loadConfig(cwd = process.cwd()) {
|
|
11
|
+
let userConfig = {};
|
|
12
|
+
for (const file of CONFIG_FILES) {
|
|
13
|
+
const configPath = join(cwd, file);
|
|
14
|
+
if (await Bun.file(configPath).exists()) {
|
|
15
|
+
const mod = await import(resolve(configPath));
|
|
16
|
+
userConfig = mod.default ?? mod;
|
|
17
|
+
break;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
const dbUrl = userConfig.dbCredentials?.url ?? process.env["DATABASE_URL"] ?? process.env["POSTGRES_URL"] ?? "";
|
|
21
|
+
if (!dbUrl) {
|
|
22
|
+
console.error(`bungres: No database URL found.
|
|
23
|
+
Add dbCredentials.url to bungres.config.ts or set DATABASE_URL.`);
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}
|
|
26
|
+
const out = userConfig.out ?? "./migrations";
|
|
27
|
+
return {
|
|
28
|
+
schema: userConfig.schema ?? "src/db/schema/**/*.ts",
|
|
29
|
+
seed: userConfig.seed ?? "src/db/seed.ts",
|
|
30
|
+
out,
|
|
31
|
+
dbUrl,
|
|
32
|
+
dbSchema: userConfig.dbSchema ?? "public",
|
|
33
|
+
migrationsDir: out,
|
|
34
|
+
outDir: "./src/db/generated",
|
|
35
|
+
verbose: userConfig.verbose ?? false
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
// src/schema-loader.ts
|
|
39
|
+
import { resolve as resolve2, join as join2 } from "path";
|
|
40
|
+
|
|
41
|
+
// ../bungres-orm/src/table.ts
|
|
42
|
+
var TableConfigSymbol = Symbol.for("BungresTableConfig");
|
|
43
|
+
function getTableConfig(table) {
|
|
44
|
+
return table[TableConfigSymbol];
|
|
45
|
+
}
|
|
46
|
+
function camelToSnakeCase(str) {
|
|
47
|
+
return str.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`);
|
|
48
|
+
}
|
|
49
|
+
function createTableFactory(casing) {
|
|
50
|
+
return function(name, columns, extra) {
|
|
51
|
+
const columnConfigs = Object.fromEntries(Object.entries(columns).map(([key, config]) => {
|
|
52
|
+
const c = { ...config };
|
|
53
|
+
if (!c.name) {
|
|
54
|
+
if (casing === "snake") {
|
|
55
|
+
c.name = camelToSnakeCase(key);
|
|
56
|
+
} else {
|
|
57
|
+
c.name = key;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return [key, c];
|
|
61
|
+
}));
|
|
62
|
+
const schema = extra?.schema;
|
|
63
|
+
const qualifiedName = schema ? `"${schema}"."${name}"` : `"${name}"`;
|
|
64
|
+
const tableObj = {
|
|
65
|
+
[TableConfigSymbol]: {
|
|
66
|
+
name,
|
|
67
|
+
schema,
|
|
68
|
+
columns: columnConfigs,
|
|
69
|
+
indexes: extra?.indexes ?? [],
|
|
70
|
+
checks: extra?.checks ?? [],
|
|
71
|
+
primaryKeys: extra?.primaryKeys ?? [],
|
|
72
|
+
qualifiedName
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
return Object.assign(tableObj, columnConfigs);
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
var table = createTableFactory("none");
|
|
79
|
+
var snakeCase = { table: createTableFactory("snake") };
|
|
80
|
+
var camelCase = { table: createTableFactory("camel") };
|
|
81
|
+
// ../bungres-orm/src/column.ts
|
|
82
|
+
function buildColumn(dataType, nameOrOpts, opts) {
|
|
83
|
+
let name = "";
|
|
84
|
+
let options = opts;
|
|
85
|
+
if (typeof nameOrOpts === "string") {
|
|
86
|
+
name = nameOrOpts;
|
|
87
|
+
} else if (nameOrOpts !== undefined) {
|
|
88
|
+
options = nameOrOpts;
|
|
89
|
+
}
|
|
90
|
+
const isPrimary = options?.primaryKey ?? false;
|
|
91
|
+
const isNotNull = isPrimary || (options?.notNull ?? false);
|
|
92
|
+
let defaultFn = options?.defaultRaw;
|
|
93
|
+
if (isPrimary && dataType === "uuid" && !defaultFn) {
|
|
94
|
+
defaultFn = "gen_random_uuid()";
|
|
95
|
+
}
|
|
96
|
+
const config = {
|
|
97
|
+
name,
|
|
98
|
+
dataType,
|
|
99
|
+
notNull: isNotNull,
|
|
100
|
+
primaryKey: isPrimary,
|
|
101
|
+
unique: options?.unique ?? false,
|
|
102
|
+
defaultValue: options?.default,
|
|
103
|
+
...defaultFn !== undefined ? { defaultFn } : {},
|
|
104
|
+
...options?.references !== undefined ? { references: options.references } : {},
|
|
105
|
+
...options?.check !== undefined ? { check: options.check } : {}
|
|
106
|
+
};
|
|
107
|
+
return Object.assign(config, {
|
|
108
|
+
as(alias) {
|
|
109
|
+
return Object.assign({}, this, { alias });
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
var col = (dataType) => (nameOrOpts, opts) => buildColumn(dataType, nameOrOpts, opts);
|
|
114
|
+
var integer = col("integer");
|
|
115
|
+
var bigint = col("bigint");
|
|
116
|
+
var smallint = col("smallint");
|
|
117
|
+
var boolean = col("boolean");
|
|
118
|
+
var real = col("real");
|
|
119
|
+
var doublePrecision = col("double precision");
|
|
120
|
+
var numeric = col("numeric");
|
|
121
|
+
var decimal = col("decimal");
|
|
122
|
+
var json = col("json");
|
|
123
|
+
var jsonb = col("jsonb");
|
|
124
|
+
var timestamp = col("timestamp");
|
|
125
|
+
var timestamptz = col("timestamptz");
|
|
126
|
+
var date = col("date");
|
|
127
|
+
var time = col("time");
|
|
128
|
+
var timetz = col("timetz");
|
|
129
|
+
var uuid = col("uuid");
|
|
130
|
+
var bytea = col("bytea");
|
|
131
|
+
var interval = col("interval");
|
|
132
|
+
var inet = col("inet");
|
|
133
|
+
var cidr = col("cidr");
|
|
134
|
+
var macaddr = col("macaddr");
|
|
135
|
+
// ../bungres-orm/src/sql.ts
|
|
136
|
+
function sqlJoin(chunks, separator = ", ") {
|
|
137
|
+
const params = [];
|
|
138
|
+
const parts = [];
|
|
139
|
+
for (const chunk of chunks) {
|
|
140
|
+
const offset = params.length;
|
|
141
|
+
parts.push(chunk.sql.replace(/\$(\d+)/g, (_, n) => `$${parseInt(n) + offset}`));
|
|
142
|
+
params.push(...chunk.params);
|
|
143
|
+
}
|
|
144
|
+
return { sql: parts.join(separator), params };
|
|
145
|
+
}
|
|
146
|
+
// ../bungres-orm/src/query.ts
|
|
147
|
+
class SelectBuilder {
|
|
148
|
+
_table;
|
|
149
|
+
_executor;
|
|
150
|
+
_where = [];
|
|
151
|
+
_orderBy = [];
|
|
152
|
+
_limit;
|
|
153
|
+
_offset;
|
|
154
|
+
_select;
|
|
155
|
+
_selection;
|
|
156
|
+
_joins = [];
|
|
157
|
+
_comment;
|
|
158
|
+
constructor(table2, executor, selection) {
|
|
159
|
+
this._table = table2;
|
|
160
|
+
this._executor = executor;
|
|
161
|
+
this._selection = selection;
|
|
162
|
+
}
|
|
163
|
+
then(onfulfilled, onrejected) {
|
|
164
|
+
return this._executor.execute(this).then(onfulfilled, onrejected);
|
|
165
|
+
}
|
|
166
|
+
select(...columns) {
|
|
167
|
+
this._select = columns;
|
|
168
|
+
return this;
|
|
169
|
+
}
|
|
170
|
+
comment(tag) {
|
|
171
|
+
this._comment = tag;
|
|
172
|
+
return this;
|
|
173
|
+
}
|
|
174
|
+
where(condition) {
|
|
175
|
+
this._where.push(condition);
|
|
176
|
+
return this;
|
|
177
|
+
}
|
|
178
|
+
orderBy(column, dir = "asc") {
|
|
179
|
+
this._orderBy.push({ column: typeof column === "string" ? column : column.name, dir });
|
|
180
|
+
return this;
|
|
181
|
+
}
|
|
182
|
+
limit(n) {
|
|
183
|
+
this._limit = n;
|
|
184
|
+
return this;
|
|
185
|
+
}
|
|
186
|
+
offset(n) {
|
|
187
|
+
this._offset = n;
|
|
188
|
+
return this;
|
|
189
|
+
}
|
|
190
|
+
join(rawClause) {
|
|
191
|
+
this._joins.push(rawClause);
|
|
192
|
+
return this;
|
|
193
|
+
}
|
|
194
|
+
toSQL() {
|
|
195
|
+
let cols = "";
|
|
196
|
+
if (this._selection) {
|
|
197
|
+
cols = Object.entries(this._selection).map(([alias, col2]) => `"${col2.name}" AS "${alias}"`).join(", ");
|
|
198
|
+
} else if (this._select && this._select.length > 0) {
|
|
199
|
+
cols = this._select.map((c) => {
|
|
200
|
+
if (typeof c === "string") {
|
|
201
|
+
return `"${getTableConfig(this._table).columns[c]?.name ?? c}" AS "${c}"`;
|
|
202
|
+
}
|
|
203
|
+
return `"${c.name}" AS "${c.alias || c.name}"`;
|
|
204
|
+
}).join(", ");
|
|
205
|
+
} else {
|
|
206
|
+
cols = Object.keys(getTableConfig(this._table).columns).map((c) => `"${getTableConfig(this._table).columns[c].name}" AS "${c}"`).join(", ");
|
|
207
|
+
}
|
|
208
|
+
let query = `SELECT ${cols} FROM ${getTableConfig(this._table).qualifiedName}`;
|
|
209
|
+
if (this._joins.length > 0) {
|
|
210
|
+
query += " " + this._joins.join(" ");
|
|
211
|
+
}
|
|
212
|
+
const params = [];
|
|
213
|
+
if (this._where.length > 0) {
|
|
214
|
+
const combined = sqlJoin(this._where, " AND ");
|
|
215
|
+
const offset = 0;
|
|
216
|
+
query += " WHERE " + combined.sql.replace(/\$(\d+)/g, (_, n) => `$${parseInt(n) + offset}`);
|
|
217
|
+
params.push(...combined.params);
|
|
218
|
+
}
|
|
219
|
+
if (this._orderBy.length > 0) {
|
|
220
|
+
query += " ORDER BY " + this._orderBy.map((o) => {
|
|
221
|
+
const dbCol = getTableConfig(this._table).columns[o.column]?.name ?? o.column;
|
|
222
|
+
return `"${dbCol}" ${o.dir.toUpperCase()}`;
|
|
223
|
+
}).join(", ");
|
|
224
|
+
}
|
|
225
|
+
if (this._limit !== undefined) {
|
|
226
|
+
params.push(this._limit);
|
|
227
|
+
query += ` LIMIT $${params.length}`;
|
|
228
|
+
}
|
|
229
|
+
if (this._offset !== undefined) {
|
|
230
|
+
params.push(this._offset);
|
|
231
|
+
query += ` OFFSET $${params.length}`;
|
|
232
|
+
}
|
|
233
|
+
if (this._comment) {
|
|
234
|
+
query += ` /* ${this._comment} */`;
|
|
235
|
+
}
|
|
236
|
+
return { sql: query, params };
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
class SelectBuilderIntermediate {
|
|
241
|
+
_executor;
|
|
242
|
+
_selection;
|
|
243
|
+
constructor(_executor, _selection) {
|
|
244
|
+
this._executor = _executor;
|
|
245
|
+
this._selection = _selection;
|
|
246
|
+
}
|
|
247
|
+
from(table2) {
|
|
248
|
+
return new SelectBuilder(table2, this._executor, this._selection);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
class InsertBuilder {
|
|
253
|
+
_table;
|
|
254
|
+
_executor;
|
|
255
|
+
_values = [];
|
|
256
|
+
_onConflict;
|
|
257
|
+
_returning;
|
|
258
|
+
_comment;
|
|
259
|
+
constructor(table2, executor) {
|
|
260
|
+
this._table = table2;
|
|
261
|
+
this._executor = executor;
|
|
262
|
+
}
|
|
263
|
+
then(onfulfilled, onrejected) {
|
|
264
|
+
return this._executor.execute(this).then(onfulfilled, onrejected);
|
|
265
|
+
}
|
|
266
|
+
values(data) {
|
|
267
|
+
const rows = Array.isArray(data) ? data : [data];
|
|
268
|
+
this._values.push(...rows);
|
|
269
|
+
return this;
|
|
270
|
+
}
|
|
271
|
+
onConflictDoNothing() {
|
|
272
|
+
this._onConflict = "do nothing";
|
|
273
|
+
return this;
|
|
274
|
+
}
|
|
275
|
+
onConflictDoUpdate(clause) {
|
|
276
|
+
this._onConflict = clause;
|
|
277
|
+
return this;
|
|
278
|
+
}
|
|
279
|
+
returning(...columns) {
|
|
280
|
+
this._returning = columns.length > 0 ? columns : ["*"];
|
|
281
|
+
return this;
|
|
282
|
+
}
|
|
283
|
+
comment(tag) {
|
|
284
|
+
this._comment = tag;
|
|
285
|
+
return this;
|
|
286
|
+
}
|
|
287
|
+
toSQL() {
|
|
288
|
+
if (this._values.length === 0) {
|
|
289
|
+
throw new Error("InsertBuilder: no values provided");
|
|
290
|
+
}
|
|
291
|
+
const keys = Array.from(new Set(this._values.flatMap((v) => Object.keys(v))));
|
|
292
|
+
const params = [];
|
|
293
|
+
const rowPlaceholders = [];
|
|
294
|
+
for (const row of this._values) {
|
|
295
|
+
const placeholders = [];
|
|
296
|
+
for (const key of keys) {
|
|
297
|
+
params.push(row[key] ?? null);
|
|
298
|
+
placeholders.push(`$${params.length}`);
|
|
299
|
+
}
|
|
300
|
+
rowPlaceholders.push(`(${placeholders.join(", ")})`);
|
|
301
|
+
}
|
|
302
|
+
const colList = keys.map((k) => `"${getTableConfig(this._table).columns[k]?.name ?? k}"`).join(", ");
|
|
303
|
+
let query = `INSERT INTO ${getTableConfig(this._table).qualifiedName} (${colList}) VALUES ${rowPlaceholders.join(", ")}`;
|
|
304
|
+
if (this._onConflict === "do nothing") {
|
|
305
|
+
query += " ON CONFLICT DO NOTHING";
|
|
306
|
+
} else if (this._onConflict && typeof this._onConflict === "object") {
|
|
307
|
+
const offset = params.length;
|
|
308
|
+
query += " ON CONFLICT " + this._onConflict.sql.replace(/\$(\d+)/g, (_, n) => `$${parseInt(n) + offset}`);
|
|
309
|
+
params.push(...this._onConflict.params);
|
|
310
|
+
}
|
|
311
|
+
if (this._returning) {
|
|
312
|
+
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(", "));
|
|
313
|
+
}
|
|
314
|
+
return { sql: query, params };
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
class UpdateBuilder {
|
|
319
|
+
_table;
|
|
320
|
+
_executor;
|
|
321
|
+
_set = {};
|
|
322
|
+
_where = [];
|
|
323
|
+
_returning;
|
|
324
|
+
_comment;
|
|
325
|
+
constructor(table2, executor) {
|
|
326
|
+
this._table = table2;
|
|
327
|
+
this._executor = executor;
|
|
328
|
+
}
|
|
329
|
+
then(onfulfilled, onrejected) {
|
|
330
|
+
return this._executor.execute(this).then(onfulfilled, onrejected);
|
|
331
|
+
}
|
|
332
|
+
set(data) {
|
|
333
|
+
this._set = { ...this._set, ...data };
|
|
334
|
+
return this;
|
|
335
|
+
}
|
|
336
|
+
where(condition) {
|
|
337
|
+
this._where.push(condition);
|
|
338
|
+
return this;
|
|
339
|
+
}
|
|
340
|
+
returning(...columns) {
|
|
341
|
+
this._returning = columns.length > 0 ? columns : ["*"];
|
|
342
|
+
return this;
|
|
343
|
+
}
|
|
344
|
+
comment(tag) {
|
|
345
|
+
this._comment = tag;
|
|
346
|
+
return this;
|
|
347
|
+
}
|
|
348
|
+
toSQL() {
|
|
349
|
+
const entries = Object.entries(this._set);
|
|
350
|
+
if (entries.length === 0) {
|
|
351
|
+
throw new Error("UpdateBuilder: no fields to set");
|
|
352
|
+
}
|
|
353
|
+
const params = [];
|
|
354
|
+
const setClauses = entries.map(([key, value]) => {
|
|
355
|
+
params.push(value);
|
|
356
|
+
const dbCol = getTableConfig(this._table).columns[key]?.name ?? key;
|
|
357
|
+
return `"${dbCol}" = $${params.length}`;
|
|
358
|
+
});
|
|
359
|
+
let query = `UPDATE ${getTableConfig(this._table).qualifiedName} SET ${setClauses.join(", ")}`;
|
|
360
|
+
if (this._where.length > 0) {
|
|
361
|
+
const combined = sqlJoin(this._where, " AND ");
|
|
362
|
+
const offset = params.length;
|
|
363
|
+
query += " WHERE " + combined.sql.replace(/\$(\d+)/g, (_, n) => `$${parseInt(n) + offset}`);
|
|
364
|
+
params.push(...combined.params);
|
|
365
|
+
}
|
|
366
|
+
if (this._returning) {
|
|
367
|
+
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(", "));
|
|
368
|
+
}
|
|
369
|
+
return { sql: query, params };
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
class DeleteBuilder {
|
|
374
|
+
_table;
|
|
375
|
+
_executor;
|
|
376
|
+
_where = [];
|
|
377
|
+
_returning;
|
|
378
|
+
_comment;
|
|
379
|
+
constructor(table2, executor) {
|
|
380
|
+
this._table = table2;
|
|
381
|
+
this._executor = executor;
|
|
382
|
+
}
|
|
383
|
+
then(onfulfilled, onrejected) {
|
|
384
|
+
return this._executor.execute(this).then(onfulfilled, onrejected);
|
|
385
|
+
}
|
|
386
|
+
where(condition) {
|
|
387
|
+
this._where.push(condition);
|
|
388
|
+
return this;
|
|
389
|
+
}
|
|
390
|
+
returning(...columns) {
|
|
391
|
+
this._returning = columns.length > 0 ? columns : ["*"];
|
|
392
|
+
return this;
|
|
393
|
+
}
|
|
394
|
+
comment(tag) {
|
|
395
|
+
this._comment = tag;
|
|
396
|
+
return this;
|
|
397
|
+
}
|
|
398
|
+
toSQL() {
|
|
399
|
+
let query = `DELETE FROM ${getTableConfig(this._table).qualifiedName}`;
|
|
400
|
+
const params = [];
|
|
401
|
+
if (this._where.length > 0) {
|
|
402
|
+
const combined = sqlJoin(this._where, " AND ");
|
|
403
|
+
query += " WHERE " + combined.sql;
|
|
404
|
+
params.push(...combined.params);
|
|
405
|
+
}
|
|
406
|
+
if (this._returning) {
|
|
407
|
+
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(", "));
|
|
408
|
+
}
|
|
409
|
+
return { sql: query, params };
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
// ../bungres-orm/src/relational.ts
|
|
413
|
+
var _relationsCache = new WeakMap;
|
|
414
|
+
|
|
415
|
+
class RelationalQueryBuilder {
|
|
416
|
+
_executor;
|
|
417
|
+
_schema;
|
|
418
|
+
_tableName;
|
|
419
|
+
_args;
|
|
420
|
+
constructor(_executor, _schema, _tableName, args) {
|
|
421
|
+
this._executor = _executor;
|
|
422
|
+
this._schema = _schema;
|
|
423
|
+
this._tableName = _tableName;
|
|
424
|
+
this._args = args ?? {};
|
|
425
|
+
}
|
|
426
|
+
where(condition) {
|
|
427
|
+
return new RelationalQueryBuilder(this._executor, this._schema, this._tableName, {
|
|
428
|
+
...this._args,
|
|
429
|
+
where: condition
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
limit(n) {
|
|
433
|
+
return new RelationalQueryBuilder(this._executor, this._schema, this._tableName, {
|
|
434
|
+
...this._args,
|
|
435
|
+
limit: n
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
offset(n) {
|
|
439
|
+
return new RelationalQueryBuilder(this._executor, this._schema, this._tableName, {
|
|
440
|
+
...this._args,
|
|
441
|
+
offset: n
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
orderBy(order) {
|
|
445
|
+
return new RelationalQueryBuilder(this._executor, this._schema, this._tableName, {
|
|
446
|
+
...this._args,
|
|
447
|
+
orderBy: order
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
select(...fields) {
|
|
451
|
+
const columnsConfig = Object.fromEntries(fields.map((f) => [f, true]));
|
|
452
|
+
return new RelationalQueryBuilder(this._executor, this._schema, this._tableName, {
|
|
453
|
+
...this._args,
|
|
454
|
+
columns: {
|
|
455
|
+
...this._args.columns || {},
|
|
456
|
+
...columnsConfig
|
|
457
|
+
}
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
with(relation, callback) {
|
|
461
|
+
const relations = this._getRuntimeRelations(this._tableName);
|
|
462
|
+
const rel = relations.ones[relation] || relations.manys[relation] || relations.manyToManys[relation];
|
|
463
|
+
if (!rel)
|
|
464
|
+
throw new Error(`Relation ${String(relation)} not found on table ${String(this._tableName)}`);
|
|
465
|
+
const subQb = new RelationalQueryBuilder(this._executor, this._schema, rel.targetTable);
|
|
466
|
+
const configuredSubQb = callback ? callback(subQb) : subQb;
|
|
467
|
+
const newArgs = { ...this._args };
|
|
468
|
+
newArgs.with = { ...newArgs.with || {}, [relation]: callback ? configuredSubQb._args : true };
|
|
469
|
+
return new RelationalQueryBuilder(this._executor, this._schema, this._tableName, newArgs);
|
|
470
|
+
}
|
|
471
|
+
async findMany(args) {
|
|
472
|
+
const finalArgs = { ...this._args, ...args ?? {} };
|
|
473
|
+
const chunk = this.buildSQL(finalArgs);
|
|
474
|
+
const rows = await this._executor.execute(chunk);
|
|
475
|
+
return rows.map((r) => typeof r._data === "string" ? JSON.parse(r._data) : r._data);
|
|
476
|
+
}
|
|
477
|
+
async findFirst(args) {
|
|
478
|
+
const finalArgs = { ...this._args, ...args ?? {}, limit: 1 };
|
|
479
|
+
const res = await this.findMany(finalArgs);
|
|
480
|
+
return res[0] ?? null;
|
|
481
|
+
}
|
|
482
|
+
_getRuntimeRelations(tableName) {
|
|
483
|
+
let schemaCache = _relationsCache.get(this._schema);
|
|
484
|
+
if (!schemaCache) {
|
|
485
|
+
schemaCache = new Map;
|
|
486
|
+
_relationsCache.set(this._schema, schemaCache);
|
|
487
|
+
}
|
|
488
|
+
let cached = schemaCache.get(tableName);
|
|
489
|
+
if (cached)
|
|
490
|
+
return cached;
|
|
491
|
+
const table2 = this._schema[tableName];
|
|
492
|
+
const tConfig = table2[TableConfigSymbol];
|
|
493
|
+
const ones = {};
|
|
494
|
+
const manys = {};
|
|
495
|
+
const manyToManys = {};
|
|
496
|
+
for (const [colName, col2] of Object.entries(tConfig.columns)) {
|
|
497
|
+
if (col2.references) {
|
|
498
|
+
const ref = col2.references;
|
|
499
|
+
const relName = ref.relationName || ref.table;
|
|
500
|
+
ones[relName] = { targetTable: ref.table, sourceColumn: col2.name };
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
for (const [otherName, otherTable] of Object.entries(this._schema)) {
|
|
504
|
+
const otherConfig = otherTable[TableConfigSymbol];
|
|
505
|
+
for (const [colName, col2] of Object.entries(otherConfig.columns)) {
|
|
506
|
+
if (col2.references && col2.references.table === tableName) {
|
|
507
|
+
const ref = col2.references;
|
|
508
|
+
const backRelName = ref.backRelationName || otherName;
|
|
509
|
+
manys[backRelName] = { targetTable: otherName, targetColumn: col2.name };
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
for (const [junctionName, junctionTable] of Object.entries(this._schema)) {
|
|
514
|
+
const junctionConfig = junctionTable[TableConfigSymbol];
|
|
515
|
+
const refs = Object.entries(junctionConfig.columns).filter(([_, c]) => c.references);
|
|
516
|
+
const toThis = refs.find(([_, c]) => c.references.table === tableName);
|
|
517
|
+
if (toThis) {
|
|
518
|
+
for (const [otherColName, otherCol] of refs) {
|
|
519
|
+
if (otherCol === toThis[1])
|
|
520
|
+
continue;
|
|
521
|
+
const ref = otherCol.references;
|
|
522
|
+
const targetTableName = ref.table;
|
|
523
|
+
const relName = toThis[1].references.backRelationName || targetTableName;
|
|
524
|
+
manyToManys[relName] = {
|
|
525
|
+
junctionTable: junctionName,
|
|
526
|
+
targetTable: targetTableName,
|
|
527
|
+
joinSourceColumn: toThis[1].name,
|
|
528
|
+
joinTargetColumn: otherCol.name
|
|
529
|
+
};
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
const result = { ones, manys, manyToManys };
|
|
534
|
+
schemaCache.set(tableName, result);
|
|
535
|
+
return result;
|
|
536
|
+
}
|
|
537
|
+
_buildSelectJson(tableName, args, alias, params, parentAlias, joinCondition, extraJoin) {
|
|
538
|
+
const tableConfig = this._schema[tableName][TableConfigSymbol];
|
|
539
|
+
const relations = this._getRuntimeRelations(tableName);
|
|
540
|
+
const withArgs = args.with || {};
|
|
541
|
+
const jsonFields = [];
|
|
542
|
+
const lateralJoins = [];
|
|
543
|
+
const columnsConfig = args.columns;
|
|
544
|
+
for (const [colKey, colConfig] of Object.entries(tableConfig.columns)) {
|
|
545
|
+
if (columnsConfig) {
|
|
546
|
+
if (columnsConfig[colKey] !== true) {
|
|
547
|
+
continue;
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
jsonFields.push(`'${colKey}', "${alias}"."${colConfig.name}"`);
|
|
551
|
+
}
|
|
552
|
+
for (const [relKey, relArgs] of Object.entries(withArgs)) {
|
|
553
|
+
const isTrue = relArgs === true;
|
|
554
|
+
const rArgs = isTrue ? {} : relArgs;
|
|
555
|
+
if (relations.ones[relKey]) {
|
|
556
|
+
const rel = relations.ones[relKey];
|
|
557
|
+
const subAlias = `${alias}_${relKey}`;
|
|
558
|
+
const joinCond = `"${subAlias}"."${this._schema[rel.targetTable][TableConfigSymbol].columns.id?.name ?? "id"}" = "${alias}"."${rel.sourceColumn}"`;
|
|
559
|
+
const subQuery = this._buildSelectJson(rel.targetTable, rArgs, subAlias, params, alias, joinCond);
|
|
560
|
+
const subSql = `SELECT ${subQuery.sql} ${subQuery.from}`;
|
|
561
|
+
jsonFields.push(`'${relKey}', (${subSql})`);
|
|
562
|
+
} else if (relations.manys[relKey]) {
|
|
563
|
+
const rel = relations.manys[relKey];
|
|
564
|
+
const subAlias = `${alias}_${relKey}`;
|
|
565
|
+
const joinCond = `"${subAlias}"."${rel.targetColumn}" = "${alias}"."${tableConfig.columns.id?.name ?? "id"}"`;
|
|
566
|
+
const subQuery = this._buildSelectJson(rel.targetTable, rArgs, subAlias, params, alias, joinCond);
|
|
567
|
+
const aggAlias = `${subAlias}_agg`;
|
|
568
|
+
const aggSql = `
|
|
569
|
+
SELECT COALESCE(json_agg(_sub.obj), '[]'::json)
|
|
570
|
+
FROM (
|
|
571
|
+
SELECT ${subQuery.sql} AS obj
|
|
572
|
+
${subQuery.from}
|
|
573
|
+
) _sub
|
|
574
|
+
`;
|
|
575
|
+
jsonFields.push(`'${relKey}', (${aggSql})`);
|
|
576
|
+
} else if (relations.manyToManys[relKey]) {
|
|
577
|
+
const rel = relations.manyToManys[relKey];
|
|
578
|
+
const subAlias = `${alias}_${relKey}`;
|
|
579
|
+
const junctionAlias = `${alias}_j_${relKey}`;
|
|
580
|
+
const targetTableConfig = this._schema[rel.targetTable][TableConfigSymbol];
|
|
581
|
+
const junctionTableConfig = this._schema[rel.junctionTable][TableConfigSymbol];
|
|
582
|
+
const fromExtra = `
|
|
583
|
+
INNER JOIN ${junctionTableConfig.qualifiedName} AS "${junctionAlias}"
|
|
584
|
+
ON "${junctionAlias}"."${rel.joinTargetColumn}" = "${subAlias}"."${targetTableConfig.columns.id?.name ?? "id"}"
|
|
585
|
+
`;
|
|
586
|
+
const whereExtra = `"${junctionAlias}"."${rel.joinSourceColumn}" = "${alias}"."${tableConfig.columns.id?.name ?? "id"}"`;
|
|
587
|
+
const subQuery = this._buildSelectJson(rel.targetTable, rArgs, subAlias, params, alias, whereExtra, fromExtra);
|
|
588
|
+
const aggSql = `
|
|
589
|
+
SELECT COALESCE(json_agg(_sub.obj), '[]'::json)
|
|
590
|
+
FROM (
|
|
591
|
+
SELECT ${subQuery.sql} AS obj
|
|
592
|
+
${subQuery.from}
|
|
593
|
+
) _sub
|
|
594
|
+
`;
|
|
595
|
+
jsonFields.push(`'${relKey}', (${aggSql})`);
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
let selectSql = `json_build_object(${jsonFields.join(", ")})`;
|
|
599
|
+
let fromSql = `FROM ${tableConfig.qualifiedName} AS "${alias}"`;
|
|
600
|
+
if (extraJoin) {
|
|
601
|
+
fromSql += ` ${extraJoin}`;
|
|
602
|
+
}
|
|
603
|
+
if (joinCondition) {
|
|
604
|
+
fromSql += ` WHERE ${joinCondition}`;
|
|
605
|
+
}
|
|
606
|
+
if (args.where && args.where.sql) {
|
|
607
|
+
const offset = params.length;
|
|
608
|
+
fromSql += (joinCondition ? " AND " : " WHERE ") + args.where.sql.replace(/\$(\d+)/g, (_, n) => `$${parseInt(n) + offset}`);
|
|
609
|
+
params.push(...args.where.params);
|
|
610
|
+
}
|
|
611
|
+
if (args.orderBy) {
|
|
612
|
+
if (typeof args.orderBy === "string") {
|
|
613
|
+
fromSql += ` ORDER BY ${args.orderBy}`;
|
|
614
|
+
} else if (args.orderBy.sql) {
|
|
615
|
+
const offset = params.length;
|
|
616
|
+
fromSql += ` ORDER BY ` + args.orderBy.sql.replace(/\$(\d+)/g, (_, n) => `$${parseInt(n) + offset}`);
|
|
617
|
+
params.push(...args.orderBy.params);
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
if (args.limit !== undefined) {
|
|
621
|
+
params.push(args.limit);
|
|
622
|
+
fromSql += ` LIMIT $${params.length}`;
|
|
623
|
+
}
|
|
624
|
+
if (args.offset !== undefined) {
|
|
625
|
+
params.push(args.offset);
|
|
626
|
+
fromSql += ` OFFSET $${params.length}`;
|
|
627
|
+
}
|
|
628
|
+
return { sql: selectSql, from: fromSql };
|
|
629
|
+
}
|
|
630
|
+
buildSQL(args) {
|
|
631
|
+
const params = [];
|
|
632
|
+
const rootAlias = "root";
|
|
633
|
+
const subQuery = this._buildSelectJson(this._tableName, args, rootAlias, params);
|
|
634
|
+
const sql2 = `SELECT ${subQuery.sql} AS _data ${subQuery.from}`;
|
|
635
|
+
return { sql: sql2, params };
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
// ../bungres-orm/src/db.ts
|
|
640
|
+
function parseDBName(url) {
|
|
641
|
+
try {
|
|
642
|
+
return new URL(url).pathname.slice(1);
|
|
643
|
+
} catch {
|
|
644
|
+
return "";
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
function maintenanceUrl(url) {
|
|
648
|
+
try {
|
|
649
|
+
const u = new URL(url);
|
|
650
|
+
u.pathname = "/postgres";
|
|
651
|
+
return u.toString();
|
|
652
|
+
} catch {
|
|
653
|
+
return url;
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
async function ensureDatabase(url) {
|
|
657
|
+
const dbName = parseDBName(url);
|
|
658
|
+
if (!dbName || dbName === "postgres")
|
|
659
|
+
return;
|
|
660
|
+
const maintenance = new Bun.SQL(maintenanceUrl(url), { max: 1 });
|
|
661
|
+
try {
|
|
662
|
+
const rows = await maintenance.unsafe(`SELECT 1 FROM pg_database WHERE datname = $1`, [dbName]);
|
|
663
|
+
if (rows.length === 0) {
|
|
664
|
+
if (!/^[a-zA-Z_][a-zA-Z0-9_$]*$/.test(dbName)) {
|
|
665
|
+
throw new Error(`Invalid database name: "${dbName}"`);
|
|
666
|
+
}
|
|
667
|
+
await maintenance.unsafe(`CREATE DATABASE "${dbName}"`);
|
|
668
|
+
console.log(`bungres: created database "${dbName}"`);
|
|
669
|
+
}
|
|
670
|
+
} finally {
|
|
671
|
+
await maintenance.end();
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
class BungresDB {
|
|
676
|
+
_sql;
|
|
677
|
+
_config;
|
|
678
|
+
_ready = null;
|
|
679
|
+
constructor(config) {
|
|
680
|
+
const url = typeof config === "string" ? config : config.url;
|
|
681
|
+
const opts = typeof config === "object" ? config : { url };
|
|
682
|
+
this._config = { autoCreateDB: true, ...opts };
|
|
683
|
+
this._sql = new Bun.SQL(url, {
|
|
684
|
+
max: opts.max ?? 10,
|
|
685
|
+
idleTimeout: opts.idleTimeout ?? 1e4,
|
|
686
|
+
...opts.maxLifetime !== undefined && { maxLifetime: opts.maxLifetime },
|
|
687
|
+
...opts.tls !== undefined && { tls: opts.tls }
|
|
688
|
+
});
|
|
689
|
+
if (this._config.autoCreateDB !== false) {
|
|
690
|
+
this._ready = ensureDatabase(url).catch((err) => {
|
|
691
|
+
console.warn(`bungres: could not auto-create database: ${err.message}`);
|
|
692
|
+
});
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
async ready() {
|
|
696
|
+
if (this._ready) {
|
|
697
|
+
await this._ready;
|
|
698
|
+
this._ready = null;
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
select(tableOrFields) {
|
|
702
|
+
if (tableOrFields) {
|
|
703
|
+
if (TableConfigSymbol in tableOrFields) {
|
|
704
|
+
return new SelectBuilder(tableOrFields, this);
|
|
705
|
+
}
|
|
706
|
+
return new SelectBuilderIntermediate(this, tableOrFields);
|
|
707
|
+
}
|
|
708
|
+
return new SelectBuilderIntermediate(this);
|
|
709
|
+
}
|
|
710
|
+
insert(table2) {
|
|
711
|
+
return new InsertBuilder(table2, this);
|
|
712
|
+
}
|
|
713
|
+
update(table2) {
|
|
714
|
+
return new UpdateBuilder(table2, this);
|
|
715
|
+
}
|
|
716
|
+
delete(table2) {
|
|
717
|
+
return new DeleteBuilder(table2, this);
|
|
718
|
+
}
|
|
719
|
+
async execute(builder) {
|
|
720
|
+
await this.ready();
|
|
721
|
+
const chunk = "toSQL" in builder ? builder.toSQL() : builder;
|
|
722
|
+
const result = await this._sql.unsafe(chunk.sql, chunk.params);
|
|
723
|
+
return Array.from(result);
|
|
724
|
+
}
|
|
725
|
+
async executeSingle(builder) {
|
|
726
|
+
const rows = await this.execute(builder);
|
|
727
|
+
return rows[0] ?? null;
|
|
728
|
+
}
|
|
729
|
+
async raw(query, params = []) {
|
|
730
|
+
await this.ready();
|
|
731
|
+
const result = await this._sql.unsafe(query, params);
|
|
732
|
+
return Array.from(result);
|
|
733
|
+
}
|
|
734
|
+
async transaction(fn) {
|
|
735
|
+
await this.ready();
|
|
736
|
+
return this._sql.transaction(async (txSql) => {
|
|
737
|
+
const tx = new BungresTransaction(txSql);
|
|
738
|
+
return fn(tx);
|
|
739
|
+
});
|
|
740
|
+
}
|
|
741
|
+
async close() {
|
|
742
|
+
await this._sql.end();
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
class BungresTransaction {
|
|
747
|
+
_sql;
|
|
748
|
+
constructor(sql2) {
|
|
749
|
+
this._sql = sql2;
|
|
750
|
+
}
|
|
751
|
+
select(tableOrFields) {
|
|
752
|
+
if (tableOrFields) {
|
|
753
|
+
if (TableConfigSymbol in tableOrFields) {
|
|
754
|
+
return new SelectBuilder(tableOrFields, this);
|
|
755
|
+
}
|
|
756
|
+
return new SelectBuilderIntermediate(this, tableOrFields);
|
|
757
|
+
}
|
|
758
|
+
return new SelectBuilderIntermediate(this);
|
|
759
|
+
}
|
|
760
|
+
insert(table2) {
|
|
761
|
+
return new InsertBuilder(table2, this);
|
|
762
|
+
}
|
|
763
|
+
update(table2) {
|
|
764
|
+
return new UpdateBuilder(table2, this);
|
|
765
|
+
}
|
|
766
|
+
delete(table2) {
|
|
767
|
+
return new DeleteBuilder(table2, this);
|
|
768
|
+
}
|
|
769
|
+
async execute(builder) {
|
|
770
|
+
const chunk = "toSQL" in builder ? builder.toSQL() : builder;
|
|
771
|
+
const result = await this._sql.unsafe(chunk.sql, chunk.params);
|
|
772
|
+
return Array.from(result);
|
|
773
|
+
}
|
|
774
|
+
async executeSingle(builder) {
|
|
775
|
+
const rows = await this.execute(builder);
|
|
776
|
+
return rows[0] ?? null;
|
|
777
|
+
}
|
|
778
|
+
async raw(query, params = []) {
|
|
779
|
+
const result = await this._sql.unsafe(query, params);
|
|
780
|
+
return Array.from(result);
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
function createDB(config) {
|
|
784
|
+
const db = new BungresDB(config);
|
|
785
|
+
if (typeof config === "object" && config.schema) {
|
|
786
|
+
const schema = config.schema;
|
|
787
|
+
return new Proxy(db, {
|
|
788
|
+
get(target, prop) {
|
|
789
|
+
if (prop in target) {
|
|
790
|
+
const value = target[prop];
|
|
791
|
+
if (typeof value === "function") {
|
|
792
|
+
return value.bind(target);
|
|
793
|
+
}
|
|
794
|
+
return value;
|
|
795
|
+
}
|
|
796
|
+
if (typeof prop === "string" && prop in schema) {
|
|
797
|
+
return new RelationalQueryBuilder(target, schema, prop);
|
|
798
|
+
}
|
|
799
|
+
return;
|
|
800
|
+
}
|
|
801
|
+
});
|
|
802
|
+
}
|
|
803
|
+
return db;
|
|
804
|
+
}
|
|
805
|
+
// ../bungres-orm/src/ddl.ts
|
|
806
|
+
function generateCreateTable(config, ifNotExists = true) {
|
|
807
|
+
const tableName = config.schema ? `"${config.schema}"."${config.name}"` : `"${config.name}"`;
|
|
808
|
+
const exists = ifNotExists ? " IF NOT EXISTS" : "";
|
|
809
|
+
const columnDefs = Object.entries(config.columns).map(([key, col2]) => buildColumnDDL(key, col2, config.name));
|
|
810
|
+
if (config.primaryKeys && config.primaryKeys.length > 1) {
|
|
811
|
+
const pkCols = config.primaryKeys.map((c) => `"${c}"`).join(", ");
|
|
812
|
+
columnDefs.push(`PRIMARY KEY (${pkCols})`);
|
|
813
|
+
}
|
|
814
|
+
if (config.checks) {
|
|
815
|
+
for (const check of config.checks) {
|
|
816
|
+
columnDefs.push(`CHECK (${check})`);
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
let sql2 = `CREATE TABLE${exists} ${tableName} (
|
|
820
|
+
${columnDefs.join(`,
|
|
821
|
+
`)}
|
|
822
|
+
);`;
|
|
823
|
+
if (config.indexes && config.indexes.length > 0) {
|
|
824
|
+
sql2 += `
|
|
825
|
+
|
|
826
|
+
` + config.indexes.map((idx) => buildIndex(config, idx)).join(`
|
|
827
|
+
`);
|
|
828
|
+
}
|
|
829
|
+
return sql2;
|
|
830
|
+
}
|
|
831
|
+
function generateAddConstraint(tableName, schema, constraintName, constraintDef) {
|
|
832
|
+
const tbl = schema ? `"${schema}"."${tableName}"` : `"${tableName}"`;
|
|
833
|
+
return `ALTER TABLE ${tbl} ADD CONSTRAINT "${constraintName}" ${constraintDef};`;
|
|
834
|
+
}
|
|
835
|
+
function generateDropConstraint(tableName, schema, constraintName) {
|
|
836
|
+
const tbl = schema ? `"${schema}"."${tableName}"` : `"${tableName}"`;
|
|
837
|
+
return `ALTER TABLE ${tbl} DROP CONSTRAINT IF EXISTS "${constraintName}";`;
|
|
838
|
+
}
|
|
839
|
+
function buildColumnDDL(key, col2, tableName) {
|
|
840
|
+
const name = `"${col2.name || key}"`;
|
|
841
|
+
let type = buildType(col2);
|
|
842
|
+
const parts = [`${name} ${type}`];
|
|
843
|
+
if (col2.primaryKey && !["serial", "bigserial"].includes(col2.dataType)) {
|
|
844
|
+
parts.push("PRIMARY KEY");
|
|
845
|
+
} else if (col2.primaryKey) {
|
|
846
|
+
parts.push("PRIMARY KEY");
|
|
847
|
+
}
|
|
848
|
+
if (col2.notNull && !col2.primaryKey && !["serial", "bigserial"].includes(col2.dataType)) {
|
|
849
|
+
parts.push("NOT NULL");
|
|
850
|
+
}
|
|
851
|
+
if (col2.unique && !col2.primaryKey) {
|
|
852
|
+
parts.push("UNIQUE");
|
|
853
|
+
}
|
|
854
|
+
if (col2.defaultFn) {
|
|
855
|
+
parts.push(`DEFAULT ${col2.defaultFn}`);
|
|
856
|
+
} else if (col2.defaultValue !== undefined) {
|
|
857
|
+
parts.push(`DEFAULT ${formatDefaultValue(col2.defaultValue, col2.dataType)}`);
|
|
858
|
+
}
|
|
859
|
+
if (col2.references) {
|
|
860
|
+
const ref = col2.references;
|
|
861
|
+
const constraintName = `${tableName}_${col2.name || key}_fkey`;
|
|
862
|
+
let fk = `CONSTRAINT "${constraintName}" REFERENCES "${ref.table}"("${ref.column}")`;
|
|
863
|
+
if (ref.onDelete)
|
|
864
|
+
fk += ` ON DELETE ${ref.onDelete.toUpperCase()}`;
|
|
865
|
+
if (ref.onUpdate)
|
|
866
|
+
fk += ` ON UPDATE ${ref.onUpdate.toUpperCase()}`;
|
|
867
|
+
parts.push(fk);
|
|
868
|
+
}
|
|
869
|
+
if (col2.check) {
|
|
870
|
+
parts.push(`CHECK (${col2.check})`);
|
|
871
|
+
}
|
|
872
|
+
return parts.join(" ");
|
|
873
|
+
}
|
|
874
|
+
function buildType(col2) {
|
|
875
|
+
switch (col2.dataType) {
|
|
876
|
+
case "varchar":
|
|
877
|
+
return col2.length ? `VARCHAR(${col2.length})` : "VARCHAR";
|
|
878
|
+
case "char":
|
|
879
|
+
return col2.length ? `CHAR(${col2.length})` : "CHAR";
|
|
880
|
+
case "numeric":
|
|
881
|
+
case "decimal":
|
|
882
|
+
return col2.dataType.toUpperCase();
|
|
883
|
+
case "double precision":
|
|
884
|
+
return "DOUBLE PRECISION";
|
|
885
|
+
case "timestamptz":
|
|
886
|
+
return "TIMESTAMP WITH TIME ZONE";
|
|
887
|
+
case "timetz":
|
|
888
|
+
return "TIME WITH TIME ZONE";
|
|
889
|
+
default:
|
|
890
|
+
return col2.dataType.toUpperCase();
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
function formatDefaultValue(value, dataType) {
|
|
894
|
+
if (value === null)
|
|
895
|
+
return "NULL";
|
|
896
|
+
if (typeof value === "string") {
|
|
897
|
+
if (dataType === "boolean")
|
|
898
|
+
return value;
|
|
899
|
+
return `'${value.replace(/'/g, "''")}'`;
|
|
900
|
+
}
|
|
901
|
+
if (typeof value === "boolean")
|
|
902
|
+
return value ? "TRUE" : "FALSE";
|
|
903
|
+
if (typeof value === "number")
|
|
904
|
+
return String(value);
|
|
905
|
+
return `'${JSON.stringify(value).replace(/'/g, "''")}'`;
|
|
906
|
+
}
|
|
907
|
+
function buildIndex(table2, idx) {
|
|
908
|
+
const tableName = table2.schema ? `"${table2.schema}"."${table2.name}"` : `"${table2.name}"`;
|
|
909
|
+
const unique = idx.unique ? "UNIQUE " : "";
|
|
910
|
+
const using = idx.using ? ` USING ${idx.using.toUpperCase()}` : "";
|
|
911
|
+
const cols = idx.columns.map((c) => `"${c}"`).join(", ");
|
|
912
|
+
const where = idx.where ? ` WHERE ${idx.where}` : "";
|
|
913
|
+
const idxName = idx.name ?? `idx_${table2.name}_${idx.columns.join("_")}`;
|
|
914
|
+
return `CREATE ${unique}INDEX IF NOT EXISTS "${idxName}" ON ${tableName}${using} (${cols})${where};`;
|
|
915
|
+
}
|
|
916
|
+
function generateAddColumn(tableName, schema, key, col2) {
|
|
917
|
+
const tbl = schema ? `"${schema}"."${tableName}"` : `"${tableName}"`;
|
|
918
|
+
return `ALTER TABLE ${tbl} ADD COLUMN IF NOT EXISTS ${buildColumnDDL(key, col2, tableName)};`;
|
|
919
|
+
}
|
|
920
|
+
function generateDropColumn(tableName, schema, columnName) {
|
|
921
|
+
const tbl = schema ? `"${schema}"."${tableName}"` : `"${tableName}"`;
|
|
922
|
+
return `ALTER TABLE ${tbl} DROP COLUMN IF EXISTS "${columnName}";`;
|
|
923
|
+
}
|
|
924
|
+
// src/schema-loader.ts
|
|
925
|
+
async function loadSchemas(patterns, cwd = process.cwd()) {
|
|
926
|
+
const globs = Array.isArray(patterns) ? patterns : [patterns];
|
|
927
|
+
const entries = [];
|
|
928
|
+
for (const pattern of globs) {
|
|
929
|
+
const glob = new Bun.Glob(pattern);
|
|
930
|
+
for await (const file of glob.scan({ cwd, absolute: false })) {
|
|
931
|
+
const absPath = resolve2(join2(cwd, file));
|
|
932
|
+
const mod = await import(absPath);
|
|
933
|
+
for (const [exportName, value] of Object.entries(mod)) {
|
|
934
|
+
if (isTable(value)) {
|
|
935
|
+
entries.push({
|
|
936
|
+
exportName,
|
|
937
|
+
config: value[TableConfigSymbol],
|
|
938
|
+
table: value,
|
|
939
|
+
filePath: absPath
|
|
940
|
+
});
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
return entries;
|
|
946
|
+
}
|
|
947
|
+
function isTable(value) {
|
|
948
|
+
return typeof value === "object" && value !== null && TableConfigSymbol in value;
|
|
949
|
+
}
|
|
950
|
+
// src/commands/push.ts
|
|
951
|
+
import * as fs from "fs";
|
|
952
|
+
|
|
953
|
+
// src/differ.ts
|
|
954
|
+
function diffSchemas(prev, next) {
|
|
955
|
+
const statements = [];
|
|
956
|
+
const summary = [];
|
|
957
|
+
const warnings = [];
|
|
958
|
+
const prevTables = new Set(Object.keys(prev));
|
|
959
|
+
const nextTables = new Set(Object.keys(next));
|
|
960
|
+
const newTableConfigs = [];
|
|
961
|
+
for (const tableName of nextTables) {
|
|
962
|
+
if (!prevTables.has(tableName)) {
|
|
963
|
+
newTableConfigs.push(next[tableName]);
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
for (const config of topoSortConfigs(newTableConfigs)) {
|
|
967
|
+
statements.push(generateCreateTable(config, true));
|
|
968
|
+
summary.push(`CREATE TABLE ${config.name}`);
|
|
969
|
+
}
|
|
970
|
+
for (const tableName of prevTables) {
|
|
971
|
+
if (!nextTables.has(tableName)) {
|
|
972
|
+
const config = prev[tableName];
|
|
973
|
+
const tbl = config.schema ? `"${config.schema}"."${tableName}"` : `"${tableName}"`;
|
|
974
|
+
statements.push(`DROP TABLE IF EXISTS ${tbl};`);
|
|
975
|
+
summary.push(`DROP TABLE ${tableName}`);
|
|
976
|
+
warnings.push(`Data loss warning: Table '${tableName}' will be permanently deleted.`);
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
for (const tableName of nextTables) {
|
|
980
|
+
if (!prevTables.has(tableName))
|
|
981
|
+
continue;
|
|
982
|
+
const prevConfig = prev[tableName];
|
|
983
|
+
const nextConfig = next[tableName];
|
|
984
|
+
const prevCols = prevConfig.columns;
|
|
985
|
+
const nextCols = nextConfig.columns;
|
|
986
|
+
const prevColNames = new Set(Object.keys(prevCols));
|
|
987
|
+
const nextColNames = new Set(Object.keys(nextCols));
|
|
988
|
+
for (const key of nextColNames) {
|
|
989
|
+
if (!prevColNames.has(key)) {
|
|
990
|
+
const col2 = nextCols[key];
|
|
991
|
+
statements.push(generateAddColumn(tableName, nextConfig.schema, key, col2));
|
|
992
|
+
summary.push(`ALTER TABLE ${tableName} ADD COLUMN ${col2.name}`);
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
for (const key of prevColNames) {
|
|
996
|
+
if (!nextColNames.has(key)) {
|
|
997
|
+
const col2 = prevCols[key];
|
|
998
|
+
statements.push(generateDropColumn(tableName, prevConfig.schema, col2.name));
|
|
999
|
+
summary.push(`ALTER TABLE ${tableName} DROP COLUMN ${col2.name}`);
|
|
1000
|
+
warnings.push(`Data loss warning: Column '${col2.name}' in table '${tableName}' will be permanently deleted.`);
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
for (const key of nextColNames) {
|
|
1004
|
+
if (!prevColNames.has(key))
|
|
1005
|
+
continue;
|
|
1006
|
+
const changes = diffColumn(prevCols[key], nextCols[key]);
|
|
1007
|
+
if (changes.length > 0) {
|
|
1008
|
+
const tbl = nextConfig.schema ? `"${nextConfig.schema}"."${tableName}"` : `"${tableName}"`;
|
|
1009
|
+
for (const change of changes) {
|
|
1010
|
+
statements.push(`ALTER TABLE ${tbl} ${change};`);
|
|
1011
|
+
summary.push(`ALTER TABLE ${tableName} ALTER COLUMN ${nextCols[key].name} (${change})`);
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
const prevRef = prevCols[key].references;
|
|
1015
|
+
const nextRef = nextCols[key].references;
|
|
1016
|
+
if (JSON.stringify(prevRef) !== JSON.stringify(nextRef)) {
|
|
1017
|
+
const constraintName = `${tableName}_${nextCols[key].name}_fkey`;
|
|
1018
|
+
if (prevRef) {
|
|
1019
|
+
statements.push(generateDropConstraint(tableName, nextConfig.schema, constraintName));
|
|
1020
|
+
summary.push(`ALTER TABLE ${tableName} DROP CONSTRAINT ${constraintName}`);
|
|
1021
|
+
}
|
|
1022
|
+
if (nextRef) {
|
|
1023
|
+
let fkDef = `FOREIGN KEY ("${nextCols[key].name}") REFERENCES "${nextRef.table}"("${nextRef.column}")`;
|
|
1024
|
+
if (nextRef.onDelete)
|
|
1025
|
+
fkDef += ` ON DELETE ${nextRef.onDelete.toUpperCase()}`;
|
|
1026
|
+
if (nextRef.onUpdate)
|
|
1027
|
+
fkDef += ` ON UPDATE ${nextRef.onUpdate.toUpperCase()}`;
|
|
1028
|
+
statements.push(generateAddConstraint(tableName, nextConfig.schema, constraintName, fkDef));
|
|
1029
|
+
summary.push(`ALTER TABLE ${tableName} ADD CONSTRAINT ${constraintName}`);
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
const prevIdxNames = new Set((prevConfig.indexes ?? []).map((i) => i.name ?? `idx_${tableName}_${i.columns.join("_")}`));
|
|
1034
|
+
for (const idx of nextConfig.indexes ?? []) {
|
|
1035
|
+
const idxName = idx.name ?? `idx_${tableName}_${idx.columns.join("_")}`;
|
|
1036
|
+
if (!prevIdxNames.has(idxName)) {
|
|
1037
|
+
const tbl = nextConfig.schema ? `"${nextConfig.schema}"."${tableName}"` : `"${tableName}"`;
|
|
1038
|
+
const unique = idx.unique ? "UNIQUE " : "";
|
|
1039
|
+
const using = idx.using ? ` USING ${idx.using.toUpperCase()}` : "";
|
|
1040
|
+
const cols = idx.columns.map((c) => `"${c}"`).join(", ");
|
|
1041
|
+
const where = idx.where ? ` WHERE ${idx.where}` : "";
|
|
1042
|
+
statements.push(`CREATE ${unique}INDEX IF NOT EXISTS "${idxName}" ON ${tbl}${using} (${cols})${where};`);
|
|
1043
|
+
summary.push(`CREATE INDEX ${idxName} ON ${tableName}`);
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
const nextIdxNames = new Set((nextConfig.indexes ?? []).map((i) => i.name ?? `idx_${tableName}_${i.columns.join("_")}`));
|
|
1047
|
+
for (const idx of prevConfig.indexes ?? []) {
|
|
1048
|
+
const idxName = idx.name ?? `idx_${tableName}_${idx.columns.join("_")}`;
|
|
1049
|
+
if (!nextIdxNames.has(idxName)) {
|
|
1050
|
+
statements.push(`DROP INDEX IF EXISTS "${idxName}";`);
|
|
1051
|
+
summary.push(`DROP INDEX ${idxName}`);
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
return { statements, summary, warnings };
|
|
1056
|
+
}
|
|
1057
|
+
function topoSortConfigs(tables) {
|
|
1058
|
+
const byName = new Map(tables.map((t) => [t.name, t]));
|
|
1059
|
+
const visited = new Set;
|
|
1060
|
+
const result = [];
|
|
1061
|
+
function deps(config) {
|
|
1062
|
+
return Object.values(config.columns).map((col2) => col2.references?.table).filter((t) => t !== undefined && byName.has(t) && t !== config.name);
|
|
1063
|
+
}
|
|
1064
|
+
function visit(name) {
|
|
1065
|
+
if (visited.has(name))
|
|
1066
|
+
return;
|
|
1067
|
+
visited.add(name);
|
|
1068
|
+
const config = byName.get(name);
|
|
1069
|
+
if (!config)
|
|
1070
|
+
return;
|
|
1071
|
+
for (const dep of deps(config))
|
|
1072
|
+
visit(dep);
|
|
1073
|
+
result.push(config);
|
|
1074
|
+
}
|
|
1075
|
+
for (const table2 of tables)
|
|
1076
|
+
visit(table2.name);
|
|
1077
|
+
return result;
|
|
1078
|
+
}
|
|
1079
|
+
function diffColumn(prev, next) {
|
|
1080
|
+
const changes = [];
|
|
1081
|
+
const col2 = `"${next.name}"`;
|
|
1082
|
+
if (prev.dataType !== next.dataType) {
|
|
1083
|
+
changes.push(`ALTER COLUMN ${col2} TYPE ${next.dataType.toUpperCase()} USING ${col2}::${next.dataType}`);
|
|
1084
|
+
}
|
|
1085
|
+
if (!prev.notNull && next.notNull) {
|
|
1086
|
+
changes.push(`ALTER COLUMN ${col2} SET NOT NULL`);
|
|
1087
|
+
}
|
|
1088
|
+
if (prev.notNull && !next.notNull) {
|
|
1089
|
+
changes.push(`ALTER COLUMN ${col2} DROP NOT NULL`);
|
|
1090
|
+
}
|
|
1091
|
+
const prevDef = prev.defaultFn ?? (prev.defaultValue !== undefined ? String(prev.defaultValue) : undefined);
|
|
1092
|
+
const nextDef = next.defaultFn ?? (next.defaultValue !== undefined ? String(next.defaultValue) : undefined);
|
|
1093
|
+
if (prevDef !== nextDef) {
|
|
1094
|
+
if (nextDef !== undefined) {
|
|
1095
|
+
const val = next.defaultFn ?? formatDefault(next.defaultValue, next.dataType);
|
|
1096
|
+
changes.push(`ALTER COLUMN ${col2} SET DEFAULT ${val}`);
|
|
1097
|
+
} else {
|
|
1098
|
+
changes.push(`ALTER COLUMN ${col2} DROP DEFAULT`);
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
return changes;
|
|
1102
|
+
}
|
|
1103
|
+
function formatDefault(value, dataType) {
|
|
1104
|
+
if (value === null || value === undefined)
|
|
1105
|
+
return "NULL";
|
|
1106
|
+
if (typeof value === "boolean")
|
|
1107
|
+
return value ? "TRUE" : "FALSE";
|
|
1108
|
+
if (typeof value === "number")
|
|
1109
|
+
return String(value);
|
|
1110
|
+
if (typeof value === "string") {
|
|
1111
|
+
if (dataType === "boolean")
|
|
1112
|
+
return value;
|
|
1113
|
+
return `'${value.replace(/'/g, "''")}'`;
|
|
1114
|
+
}
|
|
1115
|
+
return `'${JSON.stringify(value)}'`;
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
// src/commands/push.ts
|
|
1119
|
+
async function runPush(config, opts = {}) {
|
|
1120
|
+
console.log("@bungres/kit push: loading schemas...");
|
|
1121
|
+
const schemas = await loadSchemas(config.schema);
|
|
1122
|
+
if (schemas.length === 0) {
|
|
1123
|
+
console.warn("No table definitions found. Check your schema glob pattern.");
|
|
1124
|
+
return;
|
|
1125
|
+
}
|
|
1126
|
+
const sql2 = new Bun.SQL(config.dbUrl);
|
|
1127
|
+
try {
|
|
1128
|
+
await sql2.unsafe(`
|
|
1129
|
+
CREATE TABLE IF NOT EXISTS public.__bungres_push (
|
|
1130
|
+
id SERIAL PRIMARY KEY,
|
|
1131
|
+
snapshot JSONB NOT NULL
|
|
1132
|
+
);
|
|
1133
|
+
`);
|
|
1134
|
+
const rows = await sql2.unsafe(`SELECT snapshot FROM public.__bungres_push ORDER BY id DESC LIMIT 1;`);
|
|
1135
|
+
let prevSnapshot = {};
|
|
1136
|
+
if (rows.length > 0) {
|
|
1137
|
+
prevSnapshot = typeof rows[0].snapshot === "string" ? JSON.parse(rows[0].snapshot) : rows[0].snapshot;
|
|
1138
|
+
}
|
|
1139
|
+
const currentSnapshot = Object.fromEntries(schemas.map((s) => [s.config.name, s.config]));
|
|
1140
|
+
const diff = diffSchemas(prevSnapshot, currentSnapshot);
|
|
1141
|
+
if (diff.statements.length === 0) {
|
|
1142
|
+
console.log(`
|
|
1143
|
+
No schema changes detected. Database is up to date.`);
|
|
1144
|
+
return;
|
|
1145
|
+
}
|
|
1146
|
+
console.log(`
|
|
1147
|
+
Changes to apply:`);
|
|
1148
|
+
for (const s of diff.summary)
|
|
1149
|
+
console.log(` + ${s}`);
|
|
1150
|
+
if (diff.warnings && diff.warnings.length > 0) {
|
|
1151
|
+
console.warn(`
|
|
1152
|
+
\u26A0\uFE0F WARNING: Data Loss Detected!`);
|
|
1153
|
+
for (const w of diff.warnings)
|
|
1154
|
+
console.warn(` ! ${w}`);
|
|
1155
|
+
console.warn(`
|
|
1156
|
+
These changes will be immediately executed against the database!`);
|
|
1157
|
+
}
|
|
1158
|
+
if (!opts.force) {
|
|
1159
|
+
process.stdout.write(`
|
|
1160
|
+
Are you sure you want to push these changes? Type YES to continue: `);
|
|
1161
|
+
const answer = await readLine();
|
|
1162
|
+
if (answer.trim().toLowerCase() !== "yes") {
|
|
1163
|
+
console.log("Aborted.");
|
|
1164
|
+
return;
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
console.log(`
|
|
1168
|
+
Pushing changes...`);
|
|
1169
|
+
await sql2.unsafe(`CREATE EXTENSION IF NOT EXISTS "pgcrypto";`);
|
|
1170
|
+
for (const stmt of diff.statements) {
|
|
1171
|
+
if (config.verbose) {
|
|
1172
|
+
console.log(`-- ${stmt}`);
|
|
1173
|
+
}
|
|
1174
|
+
await sql2.unsafe(stmt);
|
|
1175
|
+
}
|
|
1176
|
+
await sql2.unsafe(`INSERT INTO public.__bungres_push (snapshot) VALUES ($1);`, [JSON.stringify(currentSnapshot)]);
|
|
1177
|
+
console.log(`
|
|
1178
|
+
Push complete.`);
|
|
1179
|
+
} finally {
|
|
1180
|
+
await sql2.end();
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
async function readLine() {
|
|
1184
|
+
const buf = Buffer.alloc(256);
|
|
1185
|
+
const n = fs.readSync(0, buf, 0, 256, null);
|
|
1186
|
+
return buf.subarray(0, n).toString().trim();
|
|
1187
|
+
}
|
|
1188
|
+
// src/commands/generate.ts
|
|
1189
|
+
import { join as join3, resolve as resolve3 } from "path";
|
|
1190
|
+
|
|
1191
|
+
// src/utils/colors.ts
|
|
1192
|
+
function colorize(text2, color) {
|
|
1193
|
+
const code = Bun.color(color, "ansi");
|
|
1194
|
+
return code ? `${code}${text2}\x1B[0m` : text2;
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
// src/commands/generate.ts
|
|
1198
|
+
var SNAPSHOT_FILE = ".snapshot.json";
|
|
1199
|
+
async function runGenerate(config, name) {
|
|
1200
|
+
console.log("@bungres/kit generate: loading schemas...");
|
|
1201
|
+
const schemas = await loadSchemas(config.schema);
|
|
1202
|
+
if (schemas.length === 0) {
|
|
1203
|
+
console.warn(colorize("No table definitions found. Check your schema glob pattern.", "yellow"));
|
|
1204
|
+
return;
|
|
1205
|
+
}
|
|
1206
|
+
const migrationsDir = resolve3(config.migrationsDir);
|
|
1207
|
+
await Bun.$`mkdir -p ${migrationsDir}`.quiet();
|
|
1208
|
+
const currentSnapshot = Object.fromEntries(schemas.map((s) => [s.config.name, s.config]));
|
|
1209
|
+
const snapshotPath = join3(migrationsDir, SNAPSHOT_FILE);
|
|
1210
|
+
const snapshotFile = Bun.file(snapshotPath);
|
|
1211
|
+
const isFirstMigration = !await snapshotFile.exists();
|
|
1212
|
+
const prevSnapshot = isFirstMigration ? {} : JSON.parse(await snapshotFile.text());
|
|
1213
|
+
const now = new Date;
|
|
1214
|
+
const yyyy = now.getUTCFullYear();
|
|
1215
|
+
const mm = String(now.getUTCMonth() + 1).padStart(2, "0");
|
|
1216
|
+
const dd = String(now.getUTCDate()).padStart(2, "0");
|
|
1217
|
+
const HH = String(now.getUTCHours()).padStart(2, "0");
|
|
1218
|
+
const MM = String(now.getUTCMinutes()).padStart(2, "0");
|
|
1219
|
+
const SS = String(now.getUTCSeconds()).padStart(2, "0");
|
|
1220
|
+
const prefix = `${yyyy}_${mm}_${dd}_${HH}${MM}${SS}`;
|
|
1221
|
+
const filename = name ? `${prefix}_${name}.sql` : `${prefix}.sql`;
|
|
1222
|
+
const outPath = join3(migrationsDir, filename);
|
|
1223
|
+
let statements;
|
|
1224
|
+
let summary;
|
|
1225
|
+
let warnings = [];
|
|
1226
|
+
if (isFirstMigration) {
|
|
1227
|
+
const sorted = topoSort(schemas);
|
|
1228
|
+
statements = [
|
|
1229
|
+
`CREATE EXTENSION IF NOT EXISTS "pgcrypto";`,
|
|
1230
|
+
``,
|
|
1231
|
+
...sorted.flatMap((entry) => [
|
|
1232
|
+
`-- ${entry.exportName}`,
|
|
1233
|
+
generateCreateTable(entry.config, true),
|
|
1234
|
+
``
|
|
1235
|
+
])
|
|
1236
|
+
];
|
|
1237
|
+
summary = sorted.map((s) => `CREATE TABLE ${s.config.name}`);
|
|
1238
|
+
} else {
|
|
1239
|
+
const diff = diffSchemas(prevSnapshot, currentSnapshot);
|
|
1240
|
+
if (diff.statements.length === 0) {
|
|
1241
|
+
console.log(colorize(`
|
|
1242
|
+
No schema changes detected. Nothing to generate.`, "yellow"));
|
|
1243
|
+
return;
|
|
1244
|
+
}
|
|
1245
|
+
statements = diff.statements;
|
|
1246
|
+
summary = diff.summary;
|
|
1247
|
+
warnings = diff.warnings;
|
|
1248
|
+
}
|
|
1249
|
+
const lines = [
|
|
1250
|
+
`-- Migration: ${filename}`,
|
|
1251
|
+
`-- Generated by @bungres/kit at ${new Date().toISOString()}`,
|
|
1252
|
+
`-- Changes: ${summary.join(", ")}`,
|
|
1253
|
+
``,
|
|
1254
|
+
...statements
|
|
1255
|
+
];
|
|
1256
|
+
await Bun.write(outPath, lines.join(`
|
|
1257
|
+
`));
|
|
1258
|
+
await Bun.write(snapshotPath, JSON.stringify(currentSnapshot, null, 2));
|
|
1259
|
+
console.log(colorize(`
|
|
1260
|
+
Generated: ${outPath}`, "green"));
|
|
1261
|
+
console.log(` Changes:`);
|
|
1262
|
+
for (const s of summary)
|
|
1263
|
+
console.log(colorize(` + ${s}`, "cyan"));
|
|
1264
|
+
if (warnings.length > 0) {
|
|
1265
|
+
console.warn(colorize(`
|
|
1266
|
+
\u26A0\uFE0F WARNING: Data Loss Detected!`, "red"));
|
|
1267
|
+
for (const w of warnings)
|
|
1268
|
+
console.warn(colorize(` ! ${w}`, "red"));
|
|
1269
|
+
console.warn(colorize(` Please review the generated migration carefully before applying it.
|
|
1270
|
+
`, "yellow"));
|
|
1271
|
+
}
|
|
1272
|
+
console.log(colorize(`
|
|
1273
|
+
Run \`bungres migrate\` to apply it.`, "cyan"));
|
|
1274
|
+
}
|
|
1275
|
+
function topoSort(schemas) {
|
|
1276
|
+
const byName = new Map(schemas.map((s) => [s.config.name, s]));
|
|
1277
|
+
function deps(config) {
|
|
1278
|
+
return Object.values(config.columns).map((col2) => col2.references?.table).filter((t) => t !== undefined && byName.has(t) && t !== config.name);
|
|
1279
|
+
}
|
|
1280
|
+
const visited = new Set;
|
|
1281
|
+
const result = [];
|
|
1282
|
+
function visit(name) {
|
|
1283
|
+
if (visited.has(name))
|
|
1284
|
+
return;
|
|
1285
|
+
visited.add(name);
|
|
1286
|
+
const entry = byName.get(name);
|
|
1287
|
+
if (!entry)
|
|
1288
|
+
return;
|
|
1289
|
+
for (const dep of deps(entry.config))
|
|
1290
|
+
visit(dep);
|
|
1291
|
+
result.push(entry);
|
|
1292
|
+
}
|
|
1293
|
+
for (const schema of schemas)
|
|
1294
|
+
visit(schema.config.name);
|
|
1295
|
+
return result;
|
|
1296
|
+
}
|
|
1297
|
+
// src/commands/migrate.ts
|
|
1298
|
+
import { join as join4, resolve as resolve4 } from "path";
|
|
1299
|
+
var MIGRATIONS_TABLE = "__bungres_migrations";
|
|
1300
|
+
var CREATE_MIGRATIONS_TABLE = `
|
|
1301
|
+
CREATE TABLE IF NOT EXISTS "${MIGRATIONS_TABLE}" (
|
|
1302
|
+
id SERIAL PRIMARY KEY,
|
|
1303
|
+
name TEXT NOT NULL UNIQUE,
|
|
1304
|
+
applied_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
1305
|
+
);
|
|
1306
|
+
`.trim();
|
|
1307
|
+
async function runMigrate(config) {
|
|
1308
|
+
const migrationsDir = resolve4(config.migrationsDir);
|
|
1309
|
+
const sql2 = new Bun.SQL(config.dbUrl);
|
|
1310
|
+
try {
|
|
1311
|
+
await sql2.unsafe(CREATE_MIGRATIONS_TABLE);
|
|
1312
|
+
const glob = new Bun.Glob("*.sql");
|
|
1313
|
+
const files = [];
|
|
1314
|
+
for await (const file of glob.scan({ cwd: migrationsDir, absolute: false })) {
|
|
1315
|
+
files.push(file);
|
|
1316
|
+
}
|
|
1317
|
+
files.sort();
|
|
1318
|
+
if (files.length === 0) {
|
|
1319
|
+
console.log(colorize("No migration files found in " + migrationsDir, "yellow"));
|
|
1320
|
+
console.log(colorize("Run `bungres generate` first.", "yellow"));
|
|
1321
|
+
return;
|
|
1322
|
+
}
|
|
1323
|
+
const applied = await sql2.unsafe(`SELECT name FROM "${MIGRATIONS_TABLE}"`);
|
|
1324
|
+
const appliedSet = new Set(applied.map((r) => r.name));
|
|
1325
|
+
const pending = files.filter((f) => !appliedSet.has(f));
|
|
1326
|
+
if (pending.length === 0) {
|
|
1327
|
+
console.log(colorize("Everything is up to date.", "green"));
|
|
1328
|
+
return;
|
|
1329
|
+
}
|
|
1330
|
+
console.log(colorize(`
|
|
1331
|
+
Running ${pending.length} pending migration(s)...
|
|
1332
|
+
`, "cyan"));
|
|
1333
|
+
for (const file of pending) {
|
|
1334
|
+
const content = await Bun.file(join4(migrationsDir, file)).text();
|
|
1335
|
+
if (config.verbose) {
|
|
1336
|
+
console.log(`-- ${file} --
|
|
1337
|
+
${content}
|
|
1338
|
+
`);
|
|
1339
|
+
}
|
|
1340
|
+
await sql2.transaction(async (txSql) => {
|
|
1341
|
+
const statements = content.split(";").map((s) => s.trim()).filter(Boolean);
|
|
1342
|
+
for (const stmt of statements) {
|
|
1343
|
+
await txSql.unsafe(stmt + ";");
|
|
1344
|
+
}
|
|
1345
|
+
await txSql.unsafe(`INSERT INTO "${MIGRATIONS_TABLE}" (name) VALUES ($1)`, [file]);
|
|
1346
|
+
});
|
|
1347
|
+
console.log(colorize(` \u2713 ${file}`, "green"));
|
|
1348
|
+
}
|
|
1349
|
+
console.log(colorize(`
|
|
1350
|
+
Done.`, "green"));
|
|
1351
|
+
} finally {
|
|
1352
|
+
await sql2.end();
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
1355
|
+
// src/commands/pull.ts
|
|
1356
|
+
import { resolve as resolve5, join as join5 } from "path";
|
|
1357
|
+
async function introspectDb(sql2, dbSchema) {
|
|
1358
|
+
const columns = await sql2.unsafe(`SELECT
|
|
1359
|
+
c.table_name,
|
|
1360
|
+
c.column_name,
|
|
1361
|
+
c.data_type,
|
|
1362
|
+
c.udt_name,
|
|
1363
|
+
c.is_nullable,
|
|
1364
|
+
c.column_default,
|
|
1365
|
+
c.character_maximum_length
|
|
1366
|
+
FROM information_schema.columns c
|
|
1367
|
+
WHERE c.table_schema = $1
|
|
1368
|
+
AND c.table_name NOT IN ('__bungres_migrations', '__bungres_push')
|
|
1369
|
+
ORDER BY c.table_name, c.ordinal_position`, [dbSchema]);
|
|
1370
|
+
const constraints = await sql2.unsafe(`SELECT
|
|
1371
|
+
tc.table_name,
|
|
1372
|
+
kcu.column_name,
|
|
1373
|
+
tc.constraint_type,
|
|
1374
|
+
ccu.table_name AS foreign_table,
|
|
1375
|
+
ccu.column_name AS foreign_column,
|
|
1376
|
+
rc.delete_rule,
|
|
1377
|
+
rc.update_rule
|
|
1378
|
+
FROM information_schema.table_constraints tc
|
|
1379
|
+
JOIN information_schema.key_column_usage kcu
|
|
1380
|
+
ON tc.constraint_name = kcu.constraint_name
|
|
1381
|
+
AND tc.table_schema = kcu.table_schema
|
|
1382
|
+
LEFT JOIN information_schema.constraint_column_usage ccu
|
|
1383
|
+
ON tc.constraint_name = ccu.constraint_name
|
|
1384
|
+
AND tc.table_schema = ccu.table_schema
|
|
1385
|
+
LEFT JOIN information_schema.referential_constraints rc
|
|
1386
|
+
ON tc.constraint_name = rc.constraint_name
|
|
1387
|
+
AND tc.table_schema = rc.constraint_schema
|
|
1388
|
+
WHERE tc.table_schema = $1`, [dbSchema]);
|
|
1389
|
+
const indexes = await sql2.unsafe(`SELECT tablename, indexname, indexdef
|
|
1390
|
+
FROM pg_indexes
|
|
1391
|
+
WHERE schemaname = $1`, [dbSchema]);
|
|
1392
|
+
return groupByTable(columns, constraints, indexes);
|
|
1393
|
+
}
|
|
1394
|
+
async function runPull(config) {
|
|
1395
|
+
console.log("@bungres/kit pull: introspecting database...");
|
|
1396
|
+
const sql2 = new Bun.SQL(config.dbUrl);
|
|
1397
|
+
try {
|
|
1398
|
+
const dbSchema = config.dbSchema;
|
|
1399
|
+
const tableMap = await introspectDb(sql2, dbSchema);
|
|
1400
|
+
if (tableMap.size === 0) {
|
|
1401
|
+
console.log("No tables found in schema:", dbSchema);
|
|
1402
|
+
return;
|
|
1403
|
+
}
|
|
1404
|
+
const outDir = resolve5(config.outDir);
|
|
1405
|
+
await Bun.$`mkdir -p ${outDir}`.quiet();
|
|
1406
|
+
const outPath = join5(outDir, "schema.ts");
|
|
1407
|
+
const code = generateSchemaTS(tableMap, dbSchema);
|
|
1408
|
+
await Bun.write(outPath, code);
|
|
1409
|
+
console.log(`Generated schema: ${outPath}`);
|
|
1410
|
+
console.log(` Tables: ${[...tableMap.keys()].join(", ")}`);
|
|
1411
|
+
} finally {
|
|
1412
|
+
await sql2.end();
|
|
1413
|
+
}
|
|
1414
|
+
}
|
|
1415
|
+
function groupByTable(columns, constraints, indexes) {
|
|
1416
|
+
const map = new Map;
|
|
1417
|
+
for (const col2 of columns) {
|
|
1418
|
+
if (!map.has(col2.table_name)) {
|
|
1419
|
+
map.set(col2.table_name, {
|
|
1420
|
+
tableName: col2.table_name,
|
|
1421
|
+
columns: [],
|
|
1422
|
+
indexes: indexes.filter((i) => i.tablename === col2.table_name)
|
|
1423
|
+
});
|
|
1424
|
+
}
|
|
1425
|
+
const tableConstraints = constraints.filter((c) => c.table_name === col2.table_name && c.column_name === col2.column_name);
|
|
1426
|
+
const pkConstraint = tableConstraints.find((c) => c.constraint_type === "PRIMARY KEY");
|
|
1427
|
+
const uniqueConstraint = tableConstraints.find((c) => c.constraint_type === "UNIQUE");
|
|
1428
|
+
const fkConstraint = tableConstraints.find((c) => c.constraint_type === "FOREIGN KEY");
|
|
1429
|
+
map.get(col2.table_name).columns.push({
|
|
1430
|
+
name: col2.column_name,
|
|
1431
|
+
dataType: col2.data_type,
|
|
1432
|
+
udtName: col2.udt_name,
|
|
1433
|
+
isNullable: col2.is_nullable === "YES",
|
|
1434
|
+
columnDefault: col2.column_default,
|
|
1435
|
+
maxLength: col2.character_maximum_length,
|
|
1436
|
+
isPrimary: !!pkConstraint,
|
|
1437
|
+
isUnique: !!uniqueConstraint,
|
|
1438
|
+
foreignTable: fkConstraint?.foreign_table ?? undefined,
|
|
1439
|
+
foreignColumn: fkConstraint?.foreign_column ?? undefined,
|
|
1440
|
+
deleteRule: fkConstraint?.delete_rule ?? undefined,
|
|
1441
|
+
updateRule: fkConstraint?.update_rule ?? undefined
|
|
1442
|
+
});
|
|
1443
|
+
}
|
|
1444
|
+
return map;
|
|
1445
|
+
}
|
|
1446
|
+
function generateSchemaTS(tableMap, dbSchema) {
|
|
1447
|
+
const lines = [
|
|
1448
|
+
`// Generated by @bungres/kit pull`,
|
|
1449
|
+
`// Do not edit manually \u2014 re-run \`bungres pull\` to regenerate`,
|
|
1450
|
+
`// Generated at: ${new Date().toISOString()}`,
|
|
1451
|
+
``,
|
|
1452
|
+
`import {`,
|
|
1453
|
+
` table,`,
|
|
1454
|
+
` text, varchar, char, integer, bigint, smallint,`,
|
|
1455
|
+
` serial, bigserial, boolean, real, doublePrecision,`,
|
|
1456
|
+
` numeric, decimal, json, jsonb,`,
|
|
1457
|
+
` timestamp, timestamptz, date, time, uuid,`,
|
|
1458
|
+
` bytea, inet,`,
|
|
1459
|
+
`} from "@bungres/orm";`,
|
|
1460
|
+
``
|
|
1461
|
+
];
|
|
1462
|
+
for (const [, table2] of tableMap) {
|
|
1463
|
+
const varName = toCamelCase(table2.tableName);
|
|
1464
|
+
lines.push(`export const ${varName} = table("${table2.tableName}", {`);
|
|
1465
|
+
for (const col2 of table2.columns) {
|
|
1466
|
+
const colExpr = buildColumnExpression(col2);
|
|
1467
|
+
lines.push(` ${col2.name}: ${colExpr},`);
|
|
1468
|
+
}
|
|
1469
|
+
const options = [];
|
|
1470
|
+
if (dbSchema !== "public") {
|
|
1471
|
+
options.push(`schema: "${dbSchema}"`);
|
|
1472
|
+
}
|
|
1473
|
+
if (table2.indexes.length > 0) {
|
|
1474
|
+
const idxLines = [];
|
|
1475
|
+
for (const idx of table2.indexes) {
|
|
1476
|
+
const m = idx.indexdef.match(/CREATE (UNIQUE )?INDEX (.+) ON (.+) USING (\w+) \((.+)\)(?: WHERE (.+))?/i);
|
|
1477
|
+
if (m) {
|
|
1478
|
+
const isUnique = !!m[1];
|
|
1479
|
+
const name = m[2].trim().replace(/^"|"$/g, "");
|
|
1480
|
+
if (name.endsWith("_pkey") || name.endsWith("_key"))
|
|
1481
|
+
continue;
|
|
1482
|
+
const using = m[4].toLowerCase();
|
|
1483
|
+
const cols = m[5].split(",").map((c) => `"${c.trim().replace(/^"|"$/g, "")}"`);
|
|
1484
|
+
let idxStr = `{ name: "${name}", columns: [${cols.join(", ")}], using: "${using}"`;
|
|
1485
|
+
if (isUnique)
|
|
1486
|
+
idxStr += `, unique: true`;
|
|
1487
|
+
if (m[6])
|
|
1488
|
+
idxStr += `, where: \`${m[6].trim()}\``;
|
|
1489
|
+
idxStr += ` }`;
|
|
1490
|
+
idxLines.push(idxStr);
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1493
|
+
if (idxLines.length > 0) {
|
|
1494
|
+
options.push(`indexes: [
|
|
1495
|
+
${idxLines.join(`,
|
|
1496
|
+
`)}
|
|
1497
|
+
]`);
|
|
1498
|
+
}
|
|
1499
|
+
}
|
|
1500
|
+
if (options.length > 0) {
|
|
1501
|
+
lines.push(`}, {`);
|
|
1502
|
+
lines.push(` ${options.join(`,
|
|
1503
|
+
`)}`);
|
|
1504
|
+
lines.push(`});`);
|
|
1505
|
+
} else {
|
|
1506
|
+
lines.push(`});`);
|
|
1507
|
+
}
|
|
1508
|
+
lines.push(``);
|
|
1509
|
+
}
|
|
1510
|
+
return lines.join(`
|
|
1511
|
+
`);
|
|
1512
|
+
}
|
|
1513
|
+
function buildColumnExpression(col2) {
|
|
1514
|
+
let expr = pgTypeToBungresBuilder(col2);
|
|
1515
|
+
if (col2.isPrimary)
|
|
1516
|
+
expr += `.primaryKey()`;
|
|
1517
|
+
else if (!col2.isNullable)
|
|
1518
|
+
expr += `.notNull()`;
|
|
1519
|
+
if (col2.isUnique && !col2.isPrimary)
|
|
1520
|
+
expr += `.unique()`;
|
|
1521
|
+
if (col2.columnDefault !== null && !col2.isPrimary) {
|
|
1522
|
+
if (col2.columnDefault.includes("(")) {
|
|
1523
|
+
expr += `.defaultRaw("${col2.columnDefault}")`;
|
|
1524
|
+
} else if (col2.dataType === "boolean") {
|
|
1525
|
+
expr += `.default(${col2.columnDefault})`;
|
|
1526
|
+
} else if (col2.dataType.includes("int") || col2.dataType.includes("numeric") || col2.dataType.includes("real") || col2.dataType.includes("double")) {
|
|
1527
|
+
expr += `.default(${col2.columnDefault})`;
|
|
1528
|
+
} else {
|
|
1529
|
+
const cleaned = col2.columnDefault.replace(/^'(.*)'::.*$/, "$1");
|
|
1530
|
+
expr += `.default("${cleaned}")`;
|
|
1531
|
+
}
|
|
1532
|
+
}
|
|
1533
|
+
if (col2.foreignTable && col2.foreignColumn) {
|
|
1534
|
+
const deleteRule = col2.deleteRule?.toLowerCase().replace(" ", " ");
|
|
1535
|
+
const updateRule = col2.updateRule?.toLowerCase().replace(" ", " ");
|
|
1536
|
+
let refOpts = "";
|
|
1537
|
+
if (deleteRule && deleteRule !== "no action") {
|
|
1538
|
+
refOpts += `onDelete: "${deleteRule}", `;
|
|
1539
|
+
}
|
|
1540
|
+
if (updateRule && updateRule !== "no action") {
|
|
1541
|
+
refOpts += `onUpdate: "${updateRule}"`;
|
|
1542
|
+
}
|
|
1543
|
+
const opts = refOpts ? `, { ${refOpts.trim().replace(/,$/, "")} }` : "";
|
|
1544
|
+
expr += `.references("${col2.foreignTable}", "${col2.foreignColumn}"${opts})`;
|
|
1545
|
+
}
|
|
1546
|
+
return expr;
|
|
1547
|
+
}
|
|
1548
|
+
function pgTypeToBungresBuilder(col2) {
|
|
1549
|
+
const dt = col2.dataType;
|
|
1550
|
+
const name = col2.name;
|
|
1551
|
+
if (dt === "uuid")
|
|
1552
|
+
return `uuid("${name}")`;
|
|
1553
|
+
if (dt === "text")
|
|
1554
|
+
return `text("${name}")`;
|
|
1555
|
+
if (dt === "character varying")
|
|
1556
|
+
return col2.maxLength ? `varchar("${name}", ${col2.maxLength})` : `varchar("${name}")`;
|
|
1557
|
+
if (dt === "character")
|
|
1558
|
+
return col2.maxLength ? `char("${name}", ${col2.maxLength})` : `char("${name}")`;
|
|
1559
|
+
if (dt === "integer")
|
|
1560
|
+
return `integer("${name}")`;
|
|
1561
|
+
if (dt === "bigint")
|
|
1562
|
+
return `bigint("${name}")`;
|
|
1563
|
+
if (dt === "smallint")
|
|
1564
|
+
return `smallint("${name}")`;
|
|
1565
|
+
if (dt === "boolean")
|
|
1566
|
+
return `boolean("${name}")`;
|
|
1567
|
+
if (dt === "real")
|
|
1568
|
+
return `real("${name}")`;
|
|
1569
|
+
if (dt === "double precision")
|
|
1570
|
+
return `doublePrecision("${name}")`;
|
|
1571
|
+
if (dt === "numeric" || dt === "decimal")
|
|
1572
|
+
return `numeric("${name}")`;
|
|
1573
|
+
if (dt === "json")
|
|
1574
|
+
return `json("${name}")`;
|
|
1575
|
+
if (dt === "jsonb")
|
|
1576
|
+
return `jsonb("${name}")`;
|
|
1577
|
+
if (dt === "timestamp without time zone")
|
|
1578
|
+
return `timestamp("${name}")`;
|
|
1579
|
+
if (dt === "timestamp with time zone")
|
|
1580
|
+
return `timestamptz("${name}")`;
|
|
1581
|
+
if (dt === "date")
|
|
1582
|
+
return `date("${name}")`;
|
|
1583
|
+
if (dt === "time without time zone")
|
|
1584
|
+
return `time("${name}")`;
|
|
1585
|
+
if (dt === "bytea")
|
|
1586
|
+
return `bytea("${name}")`;
|
|
1587
|
+
if (dt === "inet")
|
|
1588
|
+
return `inet("${name}")`;
|
|
1589
|
+
if (dt === "USER-DEFINED" && col2.udtName === "citext")
|
|
1590
|
+
return `text("${name}")`;
|
|
1591
|
+
return `text("${name}") /* original type: ${dt} */`;
|
|
1592
|
+
}
|
|
1593
|
+
function toCamelCase(str) {
|
|
1594
|
+
return str.replace(/_([a-z])/g, (_, c) => c.toUpperCase());
|
|
1595
|
+
}
|
|
1596
|
+
// src/commands/status.ts
|
|
1597
|
+
import { resolve as resolve6 } from "path";
|
|
1598
|
+
var MIGRATIONS_TABLE2 = "__bungres_migrations";
|
|
1599
|
+
async function runStatus(config) {
|
|
1600
|
+
const migrationsDir = resolve6(config.migrationsDir);
|
|
1601
|
+
const sql2 = new Bun.SQL(config.dbUrl);
|
|
1602
|
+
try {
|
|
1603
|
+
const tableCheck = await sql2.unsafe(`SELECT EXISTS (
|
|
1604
|
+
SELECT 1 FROM information_schema.tables
|
|
1605
|
+
WHERE table_name = $1
|
|
1606
|
+
) AS exists`, [MIGRATIONS_TABLE2]);
|
|
1607
|
+
const trackingExists = tableCheck[0]?.exists ?? false;
|
|
1608
|
+
const glob = new Bun.Glob("*.sql");
|
|
1609
|
+
const files = [];
|
|
1610
|
+
for await (const file of glob.scan({ cwd: migrationsDir, absolute: false })) {
|
|
1611
|
+
files.push(file);
|
|
1612
|
+
}
|
|
1613
|
+
files.sort();
|
|
1614
|
+
if (files.length === 0) {
|
|
1615
|
+
console.log(colorize("No migration files found. Run `bungres generate` first.", "yellow"));
|
|
1616
|
+
return;
|
|
1617
|
+
}
|
|
1618
|
+
let appliedSet = new Set;
|
|
1619
|
+
if (trackingExists) {
|
|
1620
|
+
const applied = await sql2.unsafe(`SELECT name FROM "${MIGRATIONS_TABLE2}" ORDER BY applied_at`);
|
|
1621
|
+
appliedSet = new Set(applied.map((r) => r.name));
|
|
1622
|
+
}
|
|
1623
|
+
console.log(colorize(`
|
|
1624
|
+
Migration status:
|
|
1625
|
+
`, "cyan"));
|
|
1626
|
+
let pendingCount = 0;
|
|
1627
|
+
for (const file of files) {
|
|
1628
|
+
const isApplied = appliedSet.has(file);
|
|
1629
|
+
const status = isApplied ? colorize("\u2713 applied ", "green") : colorize("\u2717 pending ", "yellow");
|
|
1630
|
+
if (!isApplied)
|
|
1631
|
+
pendingCount++;
|
|
1632
|
+
console.log(` ${status} ${file}`);
|
|
1633
|
+
}
|
|
1634
|
+
console.log(`
|
|
1635
|
+
${colorize(appliedSet.size.toString(), "green")} applied, ${colorize(pendingCount.toString(), "yellow")} pending.
|
|
1636
|
+
`);
|
|
1637
|
+
} finally {
|
|
1638
|
+
await sql2.end();
|
|
1639
|
+
}
|
|
1640
|
+
}
|
|
1641
|
+
// src/commands/drop.ts
|
|
1642
|
+
async function runDrop(config, opts = {}) {
|
|
1643
|
+
const schemas = await loadSchemas(config.schema);
|
|
1644
|
+
if (schemas.length === 0) {
|
|
1645
|
+
console.warn("No table definitions found in schema files.");
|
|
1646
|
+
return;
|
|
1647
|
+
}
|
|
1648
|
+
const sql2 = new Bun.SQL(config.dbUrl);
|
|
1649
|
+
try {
|
|
1650
|
+
const existingTablesResult = await sql2`
|
|
1651
|
+
SELECT table_name
|
|
1652
|
+
FROM information_schema.tables
|
|
1653
|
+
WHERE table_schema = 'public' OR table_schema = current_schema()
|
|
1654
|
+
`;
|
|
1655
|
+
const existingTableNames = new Set(existingTablesResult.map((row) => row.table_name));
|
|
1656
|
+
const migrationTableExists = existingTableNames.has("__bungres_migrations");
|
|
1657
|
+
const pushTableExists = existingTableNames.has("__bungres_push");
|
|
1658
|
+
const tablesToDrop = schemas.filter((s) => existingTableNames.has(s.config.name));
|
|
1659
|
+
if (tablesToDrop.length === 0 && !migrationTableExists && !pushTableExists) {
|
|
1660
|
+
console.log("No tables to drop (they either don't exist or were already dropped).");
|
|
1661
|
+
return;
|
|
1662
|
+
}
|
|
1663
|
+
const tableNamesToPrint = tablesToDrop.map((s) => (s.config.schema ? s.config.schema + "." : "") + s.config.name);
|
|
1664
|
+
if (migrationTableExists) {
|
|
1665
|
+
tableNamesToPrint.push("__bungres_migrations");
|
|
1666
|
+
}
|
|
1667
|
+
if (pushTableExists) {
|
|
1668
|
+
tableNamesToPrint.push("__bungres_push");
|
|
1669
|
+
}
|
|
1670
|
+
if (!opts.force) {
|
|
1671
|
+
console.warn(colorize(`
|
|
1672
|
+
\u26A0\uFE0F WARNING: This will drop the following tables and ALL their data:
|
|
1673
|
+
`, "red"));
|
|
1674
|
+
for (const name of tableNamesToPrint)
|
|
1675
|
+
console.log(colorize(` - ${name}`, "yellow"));
|
|
1676
|
+
process.stdout.write(colorize(`
|
|
1677
|
+
Are you sure? Type YES to continue: `, "cyan"));
|
|
1678
|
+
for await (const line of console) {
|
|
1679
|
+
if (line.trim().toLowerCase() !== "yes") {
|
|
1680
|
+
console.log("Aborted.");
|
|
1681
|
+
return;
|
|
1682
|
+
}
|
|
1683
|
+
break;
|
|
1684
|
+
}
|
|
1685
|
+
}
|
|
1686
|
+
for (const entry of tablesToDrop) {
|
|
1687
|
+
const ddl = `DROP TABLE IF EXISTS "${entry.config.name}" CASCADE;`;
|
|
1688
|
+
await sql2.unsafe(ddl);
|
|
1689
|
+
console.log(colorize(` \u2713 dropped ${entry.config.name}`, "green"));
|
|
1690
|
+
}
|
|
1691
|
+
if (migrationTableExists) {
|
|
1692
|
+
await sql2.unsafe("DROP TABLE IF EXISTS public.__bungres_migrations CASCADE");
|
|
1693
|
+
console.log(colorize(` \u2713 dropped __bungres_migrations`, "green"));
|
|
1694
|
+
}
|
|
1695
|
+
if (pushTableExists) {
|
|
1696
|
+
await sql2.unsafe("DROP TABLE IF EXISTS public.__bungres_push CASCADE");
|
|
1697
|
+
console.log(colorize(` \u2713 dropped __bungres_push`, "green"));
|
|
1698
|
+
}
|
|
1699
|
+
console.log(colorize(`
|
|
1700
|
+
Drop complete.`, "green"));
|
|
1701
|
+
} finally {
|
|
1702
|
+
await sql2.end();
|
|
1703
|
+
}
|
|
1704
|
+
}
|
|
1705
|
+
|
|
1706
|
+
// src/index.ts
|
|
1707
|
+
function defineConfig(config) {
|
|
1708
|
+
return config;
|
|
1709
|
+
}
|
|
1710
|
+
export {
|
|
1711
|
+
runStatus,
|
|
1712
|
+
runPush,
|
|
1713
|
+
runPull,
|
|
1714
|
+
runMigrate,
|
|
1715
|
+
runGenerate,
|
|
1716
|
+
runDrop,
|
|
1717
|
+
loadSchemas,
|
|
1718
|
+
loadConfig,
|
|
1719
|
+
defineConfig
|
|
1720
|
+
};
|