@bungres/kit 1.0.0 → 1.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/README.md +3 -1
- package/dist/cli.js +717 -409
- package/dist/commands/drop.d.ts.map +1 -1
- package/dist/commands/generate.d.ts.map +1 -1
- package/dist/commands/push.d.ts.map +1 -1
- package/dist/commands/studio.d.ts.map +1 -1
- package/dist/commands/template.d.ts +4 -1
- package/dist/commands/template.d.ts.map +1 -1
- package/dist/differ.d.ts +8 -1
- package/dist/differ.d.ts.map +1 -1
- package/dist/index.js +249 -50
- package/dist/schema-loader.d.ts +16 -1
- package/dist/schema-loader.d.ts.map +1 -1
- package/package.json +4 -3
package/dist/cli.js
CHANGED
|
@@ -1664,6 +1664,37 @@ function generateDropColumn(tableName, schema, columnName) {
|
|
|
1664
1664
|
const tbl = schema ? `"${schema}"."${tableName}"` : `"${tableName}"`;
|
|
1665
1665
|
return `ALTER TABLE ${tbl} DROP COLUMN IF EXISTS "${columnName}";`;
|
|
1666
1666
|
}
|
|
1667
|
+
function generateCreateEnum(name, values) {
|
|
1668
|
+
const vals = values.map((v) => `'${v.replace(/'/g, "''")}'`).join(", ");
|
|
1669
|
+
return `CREATE TYPE "${name}" AS ENUM (${vals});`;
|
|
1670
|
+
}
|
|
1671
|
+
function generateDropEnum(name, ifExists = true) {
|
|
1672
|
+
return `DROP TYPE${ifExists ? " IF EXISTS" : ""} "${name}";`;
|
|
1673
|
+
}
|
|
1674
|
+
function inlineParams(chunk) {
|
|
1675
|
+
let { sql: sql2, params } = chunk;
|
|
1676
|
+
return sql2.replace(/\$(\d+)/g, (_, n) => {
|
|
1677
|
+
const p = params[parseInt(n) - 1];
|
|
1678
|
+
if (typeof p === "string")
|
|
1679
|
+
return `'${p.replace(/'/g, "''")}'`;
|
|
1680
|
+
if (typeof p === "number")
|
|
1681
|
+
return String(p);
|
|
1682
|
+
if (typeof p === "boolean")
|
|
1683
|
+
return p ? "TRUE" : "FALSE";
|
|
1684
|
+
if (p === null)
|
|
1685
|
+
return "NULL";
|
|
1686
|
+
return `'${String(p).replace(/'/g, "''")}'`;
|
|
1687
|
+
});
|
|
1688
|
+
}
|
|
1689
|
+
function generateCreateView(view) {
|
|
1690
|
+
const kind = view.materialized ? "MATERIALIZED VIEW" : "VIEW";
|
|
1691
|
+
const inlineSql = inlineParams(view.query.toSQL());
|
|
1692
|
+
return `CREATE ${kind} "${view.name}" AS ${inlineSql};`;
|
|
1693
|
+
}
|
|
1694
|
+
function generateDropView(view, ifExists = true) {
|
|
1695
|
+
const kind = view.materialized ? "MATERIALIZED VIEW" : "VIEW";
|
|
1696
|
+
return `DROP ${kind}${ifExists ? " IF EXISTS" : ""} "${view.name}";`;
|
|
1697
|
+
}
|
|
1667
1698
|
|
|
1668
1699
|
// src/schema-loader.ts
|
|
1669
1700
|
async function loadSchemas(patterns, cwd = process.cwd()) {
|
|
@@ -1677,11 +1708,27 @@ async function loadSchemas(patterns, cwd = process.cwd()) {
|
|
|
1677
1708
|
for (const [exportName, value] of Object.entries(mod)) {
|
|
1678
1709
|
if (isTable(value)) {
|
|
1679
1710
|
entries.push({
|
|
1711
|
+
type: "table",
|
|
1680
1712
|
exportName,
|
|
1681
1713
|
config: value[TableConfigSymbol],
|
|
1682
1714
|
table: value,
|
|
1683
1715
|
filePath: absPath
|
|
1684
1716
|
});
|
|
1717
|
+
} else if (isEnum(value)) {
|
|
1718
|
+
entries.push({
|
|
1719
|
+
type: "enum",
|
|
1720
|
+
exportName,
|
|
1721
|
+
enumName: value.enumName,
|
|
1722
|
+
enumValues: value.enumValues,
|
|
1723
|
+
filePath: absPath
|
|
1724
|
+
});
|
|
1725
|
+
} else if (isView(value)) {
|
|
1726
|
+
entries.push({
|
|
1727
|
+
type: "view",
|
|
1728
|
+
exportName,
|
|
1729
|
+
config: value,
|
|
1730
|
+
filePath: absPath
|
|
1731
|
+
});
|
|
1685
1732
|
}
|
|
1686
1733
|
}
|
|
1687
1734
|
}
|
|
@@ -1691,6 +1738,12 @@ async function loadSchemas(patterns, cwd = process.cwd()) {
|
|
|
1691
1738
|
function isTable(value) {
|
|
1692
1739
|
return typeof value === "object" && value !== null && TableConfigSymbol in value;
|
|
1693
1740
|
}
|
|
1741
|
+
function isEnum(value) {
|
|
1742
|
+
return typeof value === "function" && "enumName" in value && "enumValues" in value && Array.isArray(value.enumValues);
|
|
1743
|
+
}
|
|
1744
|
+
function isView(value) {
|
|
1745
|
+
return typeof value === "object" && value !== null && "name" in value && "query" in value && typeof value.query === "object" && typeof value.query.toSQL === "function";
|
|
1746
|
+
}
|
|
1694
1747
|
|
|
1695
1748
|
// ../../node_modules/.bun/@clack+core@1.4.3/node_modules/@clack/core/dist/index.mjs
|
|
1696
1749
|
import { styleText } from "util";
|
|
@@ -3058,9 +3111,12 @@ async function runDrop(config2, opts = {}) {
|
|
|
3058
3111
|
const s = spinner();
|
|
3059
3112
|
s.start("Loading schemas...");
|
|
3060
3113
|
const schemas2 = await loadSchemas(config2.schema);
|
|
3061
|
-
|
|
3114
|
+
const tableSchemas = schemas2.filter((s2) => s2.type === "table");
|
|
3115
|
+
const enumSchemas = schemas2.filter((s2) => s2.type === "enum");
|
|
3116
|
+
const viewSchemas = schemas2.filter((s2) => s2.type === "view");
|
|
3117
|
+
if (tableSchemas.length === 0 && enumSchemas.length === 0 && viewSchemas.length === 0) {
|
|
3062
3118
|
s.stop("No schemas found.");
|
|
3063
|
-
log.warn(import_picocolors.default.yellow("No
|
|
3119
|
+
log.warn(import_picocolors.default.yellow("No definitions found in schema files."));
|
|
3064
3120
|
outro("Failed.");
|
|
3065
3121
|
return;
|
|
3066
3122
|
}
|
|
@@ -3082,25 +3138,31 @@ async function runDrop(config2, opts = {}) {
|
|
|
3082
3138
|
WHERE table_schema = $1 AND table_name = $2
|
|
3083
3139
|
) AS exists`, [config2.migrationsSchema, "__bungres_push"]);
|
|
3084
3140
|
const pushTableExists = pushTableCheck[0]?.exists ?? false;
|
|
3085
|
-
const tablesToDrop =
|
|
3086
|
-
if (tablesToDrop.length === 0 && !migrationTableExists && !pushTableExists) {
|
|
3141
|
+
const tablesToDrop = tableSchemas.filter((sch) => existingTableNames.has(sch.config.name));
|
|
3142
|
+
if (tablesToDrop.length === 0 && enumSchemas.length === 0 && viewSchemas.length === 0 && !migrationTableExists && !pushTableExists) {
|
|
3087
3143
|
ms.stop("Nothing to do.");
|
|
3088
|
-
log.success(import_picocolors.default.green("No
|
|
3144
|
+
log.success(import_picocolors.default.green("No items to drop (they either don't exist or were already dropped)."));
|
|
3089
3145
|
outro("Done.");
|
|
3090
3146
|
return;
|
|
3091
3147
|
}
|
|
3092
3148
|
ms.stop("Database check complete.");
|
|
3093
|
-
const
|
|
3149
|
+
const namesToPrint = tablesToDrop.map((sch) => (sch.config.schema ? sch.config.schema + "." : "") + sch.config.name + " (table)");
|
|
3150
|
+
for (const v of viewSchemas) {
|
|
3151
|
+
namesToPrint.push(`${v.config.name} (view)`);
|
|
3152
|
+
}
|
|
3153
|
+
for (const e of enumSchemas) {
|
|
3154
|
+
namesToPrint.push(`${e.enumName} (enum)`);
|
|
3155
|
+
}
|
|
3094
3156
|
if (migrationTableExists) {
|
|
3095
|
-
|
|
3157
|
+
namesToPrint.push(`${config2.migrationsSchema}.${config2.migrationsTable} (table)`);
|
|
3096
3158
|
}
|
|
3097
3159
|
if (pushTableExists) {
|
|
3098
|
-
|
|
3160
|
+
namesToPrint.push(`${config2.migrationsSchema}.__bungres_push (table)`);
|
|
3099
3161
|
}
|
|
3100
3162
|
if (!opts.force) {
|
|
3101
3163
|
log.warn(import_picocolors.default.bgRed(import_picocolors.default.white(" \u26A0\uFE0F WARNING ")));
|
|
3102
|
-
log.message(import_picocolors.default.bold(import_picocolors.default.red("This will drop the following
|
|
3103
|
-
for (const name of
|
|
3164
|
+
log.message(import_picocolors.default.bold(import_picocolors.default.red("This will drop the following items and ALL their data:")));
|
|
3165
|
+
for (const name of namesToPrint) {
|
|
3104
3166
|
log.info(import_picocolors.default.yellow(` - ${name}`));
|
|
3105
3167
|
}
|
|
3106
3168
|
const confirm2 = await confirm({
|
|
@@ -3114,11 +3176,22 @@ async function runDrop(config2, opts = {}) {
|
|
|
3114
3176
|
}
|
|
3115
3177
|
const exSpinner = spinner();
|
|
3116
3178
|
exSpinner.start("Dropping tables...");
|
|
3179
|
+
for (const entry of viewSchemas) {
|
|
3180
|
+
const isMat = entry.config.materialized ? "MATERIALIZED VIEW" : "VIEW";
|
|
3181
|
+
const ddl = `DROP ${isMat} IF EXISTS "${entry.config.name}" CASCADE;`;
|
|
3182
|
+
await sql2.unsafe(ddl);
|
|
3183
|
+
log.step(import_picocolors.default.gray(`Dropped ${entry.config.name}`));
|
|
3184
|
+
}
|
|
3117
3185
|
for (const entry of tablesToDrop) {
|
|
3118
3186
|
const ddl = `DROP TABLE IF EXISTS "${entry.config.name}" CASCADE;`;
|
|
3119
3187
|
await sql2.unsafe(ddl);
|
|
3120
3188
|
log.step(import_picocolors.default.gray(`Dropped ${entry.config.name}`));
|
|
3121
3189
|
}
|
|
3190
|
+
for (const entry of enumSchemas) {
|
|
3191
|
+
const ddl = `DROP TYPE IF EXISTS "${entry.enumName}" CASCADE;`;
|
|
3192
|
+
await sql2.unsafe(ddl);
|
|
3193
|
+
log.step(import_picocolors.default.gray(`Dropped ${entry.enumName}`));
|
|
3194
|
+
}
|
|
3122
3195
|
if (migrationTableExists) {
|
|
3123
3196
|
const qualifiedMigTable = `"${config2.migrationsSchema}"."${config2.migrationsTable}"`;
|
|
3124
3197
|
await sql2.unsafe(`DROP TABLE IF EXISTS ${qualifiedMigTable} CASCADE`);
|
|
@@ -3128,7 +3201,7 @@ async function runDrop(config2, opts = {}) {
|
|
|
3128
3201
|
await sql2.unsafe(`DROP TABLE IF EXISTS "${config2.migrationsSchema}"."__bungres_push" CASCADE`);
|
|
3129
3202
|
log.step(import_picocolors.default.gray(`Dropped ${config2.migrationsSchema}.__bungres_push`));
|
|
3130
3203
|
}
|
|
3131
|
-
exSpinner.stop("
|
|
3204
|
+
exSpinner.stop("Items dropped.");
|
|
3132
3205
|
outro(import_picocolors.default.cyan("\u2728 Drop complete."));
|
|
3133
3206
|
} catch (err) {
|
|
3134
3207
|
log.error(import_picocolors.default.red(`Drop failed: ${err.message}`));
|
|
@@ -3235,12 +3308,56 @@ function diffSchemas(prev, next) {
|
|
|
3235
3308
|
const statements = [];
|
|
3236
3309
|
const summary = [];
|
|
3237
3310
|
const warnings = [];
|
|
3238
|
-
const
|
|
3239
|
-
const
|
|
3311
|
+
const prevEnums = prev.enums || {};
|
|
3312
|
+
const nextEnums = next.enums || {};
|
|
3313
|
+
const prevEnumNames = new Set(Object.keys(prevEnums));
|
|
3314
|
+
const nextEnumNames = new Set(Object.keys(nextEnums));
|
|
3315
|
+
for (const enumName of nextEnumNames) {
|
|
3316
|
+
if (!prevEnumNames.has(enumName)) {
|
|
3317
|
+
const e = nextEnums[enumName];
|
|
3318
|
+
statements.push(generateCreateEnum(e.enumName, e.enumValues));
|
|
3319
|
+
summary.push(`CREATE TYPE ${e.enumName}`);
|
|
3320
|
+
}
|
|
3321
|
+
}
|
|
3322
|
+
for (const enumName of prevEnumNames) {
|
|
3323
|
+
if (!nextEnumNames.has(enumName)) {
|
|
3324
|
+
const e = prevEnums[enumName];
|
|
3325
|
+
statements.push(generateDropEnum(e.enumName, true));
|
|
3326
|
+
summary.push(`DROP TYPE ${e.enumName}`);
|
|
3327
|
+
}
|
|
3328
|
+
}
|
|
3329
|
+
for (const enumName of nextEnumNames) {
|
|
3330
|
+
if (prevEnumNames.has(enumName)) {
|
|
3331
|
+
const p2 = prevEnums[enumName];
|
|
3332
|
+
const n2 = nextEnums[enumName];
|
|
3333
|
+
if (JSON.stringify(p2.enumValues) !== JSON.stringify(n2.enumValues)) {
|
|
3334
|
+
warnings.push(`Enum '${n2.enumName}' values have changed. Bungres Kit currently requires manual migration for ALTER TYPE ... ADD VALUE.`);
|
|
3335
|
+
}
|
|
3336
|
+
}
|
|
3337
|
+
}
|
|
3338
|
+
const prevViews = prev.views || {};
|
|
3339
|
+
const nextViews = next.views || {};
|
|
3340
|
+
const prevViewNames = new Set(Object.keys(prevViews));
|
|
3341
|
+
const nextViewNames = new Set(Object.keys(nextViews));
|
|
3342
|
+
for (const viewName of prevViewNames) {
|
|
3343
|
+
if (!nextViewNames.has(viewName)) {
|
|
3344
|
+
statements.push(generateDropView(prevViews[viewName]));
|
|
3345
|
+
summary.push(`DROP VIEW ${viewName}`);
|
|
3346
|
+
} else {
|
|
3347
|
+
const pSql = generateCreateView(prevViews[viewName]);
|
|
3348
|
+
const nSql = generateCreateView(nextViews[viewName]);
|
|
3349
|
+
if (pSql !== nSql) {
|
|
3350
|
+
statements.push(generateDropView(prevViews[viewName]));
|
|
3351
|
+
summary.push(`DROP VIEW ${viewName} (for recreation)`);
|
|
3352
|
+
}
|
|
3353
|
+
}
|
|
3354
|
+
}
|
|
3355
|
+
const prevTables = new Set(Object.keys(prev.tables || {}));
|
|
3356
|
+
const nextTables = new Set(Object.keys(next.tables || {}));
|
|
3240
3357
|
const newTableConfigs = [];
|
|
3241
3358
|
for (const tableName of nextTables) {
|
|
3242
3359
|
if (!prevTables.has(tableName)) {
|
|
3243
|
-
newTableConfigs.push(next[tableName]);
|
|
3360
|
+
newTableConfigs.push(next.tables[tableName]);
|
|
3244
3361
|
}
|
|
3245
3362
|
}
|
|
3246
3363
|
for (const config2 of topoSortConfigs(newTableConfigs)) {
|
|
@@ -3249,7 +3366,7 @@ function diffSchemas(prev, next) {
|
|
|
3249
3366
|
}
|
|
3250
3367
|
for (const tableName of prevTables) {
|
|
3251
3368
|
if (!nextTables.has(tableName)) {
|
|
3252
|
-
const config2 = prev[tableName];
|
|
3369
|
+
const config2 = prev.tables[tableName];
|
|
3253
3370
|
const tbl = config2.schema ? `"${config2.schema}"."${tableName}"` : `"${tableName}"`;
|
|
3254
3371
|
statements.push(`DROP TABLE IF EXISTS ${tbl};`);
|
|
3255
3372
|
summary.push(`DROP TABLE ${tableName}`);
|
|
@@ -3259,8 +3376,8 @@ function diffSchemas(prev, next) {
|
|
|
3259
3376
|
for (const tableName of nextTables) {
|
|
3260
3377
|
if (!prevTables.has(tableName))
|
|
3261
3378
|
continue;
|
|
3262
|
-
const prevConfig = prev[tableName];
|
|
3263
|
-
const nextConfig = next[tableName];
|
|
3379
|
+
const prevConfig = prev.tables[tableName];
|
|
3380
|
+
const nextConfig = next.tables[tableName];
|
|
3264
3381
|
const prevCols = prevConfig.columns;
|
|
3265
3382
|
const nextCols = nextConfig.columns;
|
|
3266
3383
|
const prevColNames = new Set(Object.keys(prevCols));
|
|
@@ -3332,6 +3449,19 @@ function diffSchemas(prev, next) {
|
|
|
3332
3449
|
}
|
|
3333
3450
|
}
|
|
3334
3451
|
}
|
|
3452
|
+
for (const viewName of nextViewNames) {
|
|
3453
|
+
if (!prevViewNames.has(viewName)) {
|
|
3454
|
+
statements.push(generateCreateView(nextViews[viewName]));
|
|
3455
|
+
summary.push(`CREATE VIEW ${viewName}`);
|
|
3456
|
+
} else {
|
|
3457
|
+
const pSql = generateCreateView(prevViews[viewName]);
|
|
3458
|
+
const nSql = generateCreateView(nextViews[viewName]);
|
|
3459
|
+
if (pSql !== nSql) {
|
|
3460
|
+
statements.push(generateCreateView(nextViews[viewName]));
|
|
3461
|
+
summary.push(`CREATE VIEW ${viewName} (recreated)`);
|
|
3462
|
+
}
|
|
3463
|
+
}
|
|
3464
|
+
}
|
|
3335
3465
|
return { statements, summary, warnings };
|
|
3336
3466
|
}
|
|
3337
3467
|
function topoSortConfigs(tables) {
|
|
@@ -3359,7 +3489,14 @@ function topoSortConfigs(tables) {
|
|
|
3359
3489
|
function diffColumn(prev, next) {
|
|
3360
3490
|
const changes = [];
|
|
3361
3491
|
const col2 = `"${next.name}"`;
|
|
3492
|
+
const prevDef = prev.defaultFn ?? (prev.defaultValue !== undefined ? String(prev.defaultValue) : undefined);
|
|
3493
|
+
const nextDef = next.defaultFn ?? (next.defaultValue !== undefined ? String(next.defaultValue) : undefined);
|
|
3494
|
+
let typeChanged = false;
|
|
3362
3495
|
if (prev.dataType !== next.dataType) {
|
|
3496
|
+
typeChanged = true;
|
|
3497
|
+
if (prevDef !== undefined) {
|
|
3498
|
+
changes.push(`ALTER COLUMN ${col2} DROP DEFAULT`);
|
|
3499
|
+
}
|
|
3363
3500
|
changes.push(`ALTER COLUMN ${col2} TYPE ${next.dataType.toUpperCase()} USING ${col2}::${next.dataType}`);
|
|
3364
3501
|
} else {
|
|
3365
3502
|
const prevLen = prev.length;
|
|
@@ -3376,9 +3513,12 @@ function diffColumn(prev, next) {
|
|
|
3376
3513
|
if (prev.notNull && !next.notNull) {
|
|
3377
3514
|
changes.push(`ALTER COLUMN ${col2} DROP NOT NULL`);
|
|
3378
3515
|
}
|
|
3379
|
-
|
|
3380
|
-
|
|
3381
|
-
|
|
3516
|
+
if (typeChanged) {
|
|
3517
|
+
if (nextDef !== undefined) {
|
|
3518
|
+
const val = next.defaultFn ?? formatDefault(next.defaultValue, next.dataType);
|
|
3519
|
+
changes.push(`ALTER COLUMN ${col2} SET DEFAULT ${val}`);
|
|
3520
|
+
}
|
|
3521
|
+
} else if (prevDef !== nextDef) {
|
|
3382
3522
|
if (nextDef !== undefined) {
|
|
3383
3523
|
const val = next.defaultFn ?? formatDefault(next.defaultValue, next.dataType);
|
|
3384
3524
|
changes.push(`ALTER COLUMN ${col2} SET DEFAULT ${val}`);
|
|
@@ -3421,19 +3561,46 @@ async function runGenerate(config2, name) {
|
|
|
3421
3561
|
const metaDir = join3(migrationsDir, "meta");
|
|
3422
3562
|
await Bun.$`mkdir -p ${migrationsDir}`.quiet();
|
|
3423
3563
|
await Bun.$`mkdir -p ${metaDir}`.quiet();
|
|
3424
|
-
const currentSnapshot =
|
|
3425
|
-
|
|
3564
|
+
const currentSnapshot = { tables: {}, enums: {}, views: {} };
|
|
3565
|
+
for (const s2 of schemas2) {
|
|
3566
|
+
if (s2.type === "table") {
|
|
3567
|
+
currentSnapshot.tables[s2.config.name] = s2.config;
|
|
3568
|
+
} else if (s2.type === "enum") {
|
|
3569
|
+
currentSnapshot.enums[s2.enumName] = { enumName: s2.enumName, enumValues: s2.enumValues };
|
|
3570
|
+
} else if (s2.type === "view") {
|
|
3571
|
+
currentSnapshot.views[s2.config.name] = s2.config;
|
|
3572
|
+
}
|
|
3573
|
+
}
|
|
3574
|
+
let prevSnapshot = { tables: {}, enums: {}, views: {} };
|
|
3426
3575
|
let isFirstMigration = true;
|
|
3427
3576
|
try {
|
|
3428
3577
|
const files = readdirSync(metaDir).filter((f2) => f2.endsWith("_snapshot.json")).sort();
|
|
3429
3578
|
if (files.length > 0) {
|
|
3430
3579
|
const latest = files[files.length - 1];
|
|
3431
|
-
|
|
3580
|
+
const parsed = JSON.parse(await Bun.file(join3(metaDir, latest)).text());
|
|
3581
|
+
if (parsed.tables || parsed.enums || parsed.views) {
|
|
3582
|
+
prevSnapshot = {
|
|
3583
|
+
tables: parsed.tables || {},
|
|
3584
|
+
enums: parsed.enums || {},
|
|
3585
|
+
views: parsed.views || {}
|
|
3586
|
+
};
|
|
3587
|
+
} else {
|
|
3588
|
+
prevSnapshot = { tables: parsed, enums: {}, views: {} };
|
|
3589
|
+
}
|
|
3432
3590
|
isFirstMigration = false;
|
|
3433
3591
|
} else {
|
|
3434
3592
|
const legacyFile2 = Bun.file(join3(migrationsDir, ".snapshot.json"));
|
|
3435
3593
|
if (await legacyFile2.exists()) {
|
|
3436
|
-
|
|
3594
|
+
const parsed = JSON.parse(await legacyFile2.text());
|
|
3595
|
+
if (parsed.tables || parsed.enums || parsed.views) {
|
|
3596
|
+
prevSnapshot = {
|
|
3597
|
+
tables: parsed.tables || {},
|
|
3598
|
+
enums: parsed.enums || {},
|
|
3599
|
+
views: parsed.views || {}
|
|
3600
|
+
};
|
|
3601
|
+
} else {
|
|
3602
|
+
prevSnapshot = { tables: parsed, enums: {}, views: {} };
|
|
3603
|
+
}
|
|
3437
3604
|
isFirstMigration = false;
|
|
3438
3605
|
}
|
|
3439
3606
|
}
|
|
@@ -3453,17 +3620,30 @@ async function runGenerate(config2, name) {
|
|
|
3453
3620
|
let summary;
|
|
3454
3621
|
let warnings = [];
|
|
3455
3622
|
if (isFirstMigration) {
|
|
3456
|
-
const
|
|
3457
|
-
|
|
3458
|
-
|
|
3459
|
-
|
|
3460
|
-
|
|
3461
|
-
]
|
|
3462
|
-
|
|
3623
|
+
const tableSchemas = schemas2.filter((s2) => s2.type === "table");
|
|
3624
|
+
const enumSchemas = schemas2.filter((s2) => s2.type === "enum");
|
|
3625
|
+
const viewSchemas = schemas2.filter((s2) => s2.type === "view");
|
|
3626
|
+
const sorted = topoSort(tableSchemas);
|
|
3627
|
+
upStatements = [];
|
|
3628
|
+
downStatements = [];
|
|
3629
|
+
summary = [];
|
|
3630
|
+
for (const e of enumSchemas) {
|
|
3631
|
+
upStatements.push(`-- ${e.exportName}`, `CREATE TYPE "${e.enumName}" AS ENUM (${e.enumValues.map((v) => `'${v.replace(/'/g, "''")}'`).join(", ")});`, ``);
|
|
3632
|
+
downStatements.unshift(`DROP TYPE IF EXISTS "${e.enumName}";`);
|
|
3633
|
+
summary.push(`CREATE TYPE ${e.enumName}`);
|
|
3634
|
+
}
|
|
3635
|
+
for (const entry of sorted) {
|
|
3636
|
+
upStatements.push(`-- ${entry.exportName}`, generateCreateTable(entry.config, true), ``);
|
|
3463
3637
|
const tbl = entry.config.schema ? `"${entry.config.schema}"."${entry.config.name}"` : `"${entry.config.name}"`;
|
|
3464
|
-
|
|
3465
|
-
|
|
3466
|
-
|
|
3638
|
+
downStatements.unshift(`DROP TABLE IF EXISTS ${tbl};`);
|
|
3639
|
+
summary.push(`CREATE TABLE ${entry.config.name}`);
|
|
3640
|
+
}
|
|
3641
|
+
for (const v of viewSchemas) {
|
|
3642
|
+
upStatements.push(`-- ${v.exportName}`, generateCreateView(v.config), ``);
|
|
3643
|
+
const isMat = v.config.materialized ? "MATERIALIZED VIEW" : "VIEW";
|
|
3644
|
+
downStatements.unshift(`DROP ${isMat} IF EXISTS "${v.config.name}";`);
|
|
3645
|
+
summary.push(`CREATE ${isMat} ${v.config.name}`);
|
|
3646
|
+
}
|
|
3467
3647
|
} else {
|
|
3468
3648
|
const upDiff = diffSchemas(prevSnapshot, currentSnapshot);
|
|
3469
3649
|
if (upDiff.statements.length === 0) {
|
|
@@ -3595,12 +3775,12 @@ export default defineConfig({
|
|
|
3595
3775
|
uuid,
|
|
3596
3776
|
varchar,
|
|
3597
3777
|
timestamptz,
|
|
3598
|
-
|
|
3778
|
+
pgTable,
|
|
3599
3779
|
unique,
|
|
3600
3780
|
index
|
|
3601
3781
|
} from "@bungres/orm";
|
|
3602
3782
|
|
|
3603
|
-
export const users =
|
|
3783
|
+
export const users = pgTable("users", {
|
|
3604
3784
|
id: uuid({ primaryKey: true }),
|
|
3605
3785
|
name: varchar({ length: 255, notNull: true }),
|
|
3606
3786
|
email: varchar({ length: 255, notNull: true }),
|
|
@@ -3736,19 +3916,18 @@ function generateSchemaTS(tableMap, dbSchema) {
|
|
|
3736
3916
|
`// Generated at: ${new Date().toISOString()}`,
|
|
3737
3917
|
``,
|
|
3738
3918
|
`import {`,
|
|
3739
|
-
`
|
|
3919
|
+
` pgTable,`,
|
|
3740
3920
|
` text, varchar, char, integer, bigint, smallint,`,
|
|
3741
3921
|
` serial, bigserial, boolean, real, doublePrecision,`,
|
|
3742
3922
|
` numeric, decimal, json, jsonb,`,
|
|
3743
3923
|
` timestamp, timestamptz, date, time, uuid,`,
|
|
3744
3924
|
` bytea, inet,`,
|
|
3745
|
-
` textArray, integerArray, varcharArray, uuidArray,`,
|
|
3746
3925
|
`} from "@bungres/orm";`,
|
|
3747
3926
|
``
|
|
3748
3927
|
];
|
|
3749
3928
|
for (const [, table2] of tableMap) {
|
|
3750
3929
|
const varName = toCamelCase(table2.tableName);
|
|
3751
|
-
lines.push(`export const ${varName} =
|
|
3930
|
+
lines.push(`export const ${varName} = pgTable("${table2.tableName}", {`);
|
|
3752
3931
|
for (const col2 of table2.columns) {
|
|
3753
3932
|
const colExpr = buildColumnExpression(col2);
|
|
3754
3933
|
lines.push(` ${toCamelCase(col2.name)}: ${colExpr},`);
|
|
@@ -3829,11 +4008,13 @@ function buildColumnExpression(col2) {
|
|
|
3829
4008
|
refOpts += `, onUpdate: "${updateRule}"`;
|
|
3830
4009
|
opts.push(`references: { ${refOpts} }`);
|
|
3831
4010
|
}
|
|
3832
|
-
let
|
|
3833
|
-
|
|
3834
|
-
|
|
3835
|
-
}
|
|
3836
|
-
|
|
4011
|
+
let builderResult = pgTypeToBungresBuilderName(col2);
|
|
4012
|
+
let builderName = typeof builderResult === "string" ? builderResult : builderResult.base;
|
|
4013
|
+
let isArray = typeof builderResult !== "string" && builderResult.isArray;
|
|
4014
|
+
let expr = opts.length > 0 ? `${builderName}({ ${opts.join(", ")} })` : `${builderName}()`;
|
|
4015
|
+
if (isArray)
|
|
4016
|
+
expr += `.array()`;
|
|
4017
|
+
return expr;
|
|
3837
4018
|
}
|
|
3838
4019
|
function pgTypeToBungresBuilderName(col2) {
|
|
3839
4020
|
const dt = col2.dataType;
|
|
@@ -3879,14 +4060,14 @@ function pgTypeToBungresBuilderName(col2) {
|
|
|
3879
4060
|
return "text";
|
|
3880
4061
|
if (dt === "ARRAY") {
|
|
3881
4062
|
if (col2.udtName === "_text")
|
|
3882
|
-
return "
|
|
4063
|
+
return { base: "text", isArray: true };
|
|
3883
4064
|
if (col2.udtName === "_int4" || col2.udtName === "_int8")
|
|
3884
|
-
return "
|
|
4065
|
+
return { base: "integer", isArray: true };
|
|
3885
4066
|
if (col2.udtName === "_varchar")
|
|
3886
|
-
return "
|
|
4067
|
+
return { base: "varchar", isArray: true };
|
|
3887
4068
|
if (col2.udtName === "_uuid")
|
|
3888
|
-
return "
|
|
3889
|
-
return "
|
|
4069
|
+
return { base: "uuid", isArray: true };
|
|
4070
|
+
return { base: "text", isArray: true };
|
|
3890
4071
|
}
|
|
3891
4072
|
return "text";
|
|
3892
4073
|
}
|
|
@@ -3921,11 +4102,29 @@ async function runPush(config2, opts = {}) {
|
|
|
3921
4102
|
);
|
|
3922
4103
|
`);
|
|
3923
4104
|
const rows = await sql2.unsafe(`SELECT snapshot FROM ${qualifiedPush} ORDER BY id DESC LIMIT 1;`);
|
|
3924
|
-
let prevSnapshot = {};
|
|
4105
|
+
let prevSnapshot = { tables: {}, enums: {}, views: {} };
|
|
3925
4106
|
if (rows.length > 0) {
|
|
3926
|
-
|
|
4107
|
+
const parsed = typeof rows[0].snapshot === "string" ? JSON.parse(rows[0].snapshot) : rows[0].snapshot;
|
|
4108
|
+
if (parsed.tables || parsed.enums || parsed.views) {
|
|
4109
|
+
prevSnapshot = {
|
|
4110
|
+
tables: parsed.tables || {},
|
|
4111
|
+
enums: parsed.enums || {},
|
|
4112
|
+
views: parsed.views || {}
|
|
4113
|
+
};
|
|
4114
|
+
} else {
|
|
4115
|
+
prevSnapshot = { tables: parsed, enums: {}, views: {} };
|
|
4116
|
+
}
|
|
4117
|
+
}
|
|
4118
|
+
const currentSnapshot = { tables: {}, enums: {}, views: {} };
|
|
4119
|
+
for (const s2 of schemas2) {
|
|
4120
|
+
if (s2.type === "table") {
|
|
4121
|
+
currentSnapshot.tables[s2.config.name] = s2.config;
|
|
4122
|
+
} else if (s2.type === "enum") {
|
|
4123
|
+
currentSnapshot.enums[s2.enumName] = { enumName: s2.enumName, enumValues: s2.enumValues };
|
|
4124
|
+
} else if (s2.type === "view") {
|
|
4125
|
+
currentSnapshot.views[s2.config.name] = s2.config;
|
|
4126
|
+
}
|
|
3927
4127
|
}
|
|
3928
|
-
const currentSnapshot = Object.fromEntries(schemas2.map((schemaEntry) => [schemaEntry.config.name, schemaEntry.config]));
|
|
3929
4128
|
const diff = diffSchemas(prevSnapshot, currentSnapshot);
|
|
3930
4129
|
if (diff.statements.length === 0) {
|
|
3931
4130
|
ms.stop("Up to date.");
|
|
@@ -3982,7 +4181,7 @@ async function runPush(config2, opts = {}) {
|
|
|
3982
4181
|
// src/commands/refresh.ts
|
|
3983
4182
|
var import_picocolors6 = __toESM(require_picocolors(), 1);
|
|
3984
4183
|
async function runRefresh(config2) {
|
|
3985
|
-
const schemas2 = await loadSchemas(config2.schema);
|
|
4184
|
+
const schemas2 = (await loadSchemas(config2.schema)).filter((s) => s.type === "table");
|
|
3986
4185
|
if (schemas2.length === 0) {
|
|
3987
4186
|
console.warn("No table definitions found in schema files.");
|
|
3988
4187
|
return;
|
|
@@ -4116,7 +4315,7 @@ async function runSeed(config2) {
|
|
|
4116
4315
|
}
|
|
4117
4316
|
const s = spinner();
|
|
4118
4317
|
s.start("Loading schemas for auto-seeding...");
|
|
4119
|
-
const schemas2 = await loadSchemas(config2.schema);
|
|
4318
|
+
const schemas2 = (await loadSchemas(config2.schema)).filter((s2) => s2.type === "table");
|
|
4120
4319
|
if (schemas2.length === 0) {
|
|
4121
4320
|
s.stop("No schemas.");
|
|
4122
4321
|
outro("Nothing to seed.");
|
|
@@ -4248,7 +4447,8 @@ async function runStatus(config2) {
|
|
|
4248
4447
|
var import_picocolors10 = __toESM(require_picocolors(), 1);
|
|
4249
4448
|
|
|
4250
4449
|
// src/commands/template.ts
|
|
4251
|
-
|
|
4450
|
+
function renderIndexHtml(opts) {
|
|
4451
|
+
return `<!DOCTYPE html>
|
|
4252
4452
|
<html lang="en">
|
|
4253
4453
|
<head>
|
|
4254
4454
|
<meta charset="UTF-8">
|
|
@@ -4257,8 +4457,7 @@ var indexHtml = `<!DOCTYPE html>
|
|
|
4257
4457
|
|
|
4258
4458
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
4259
4459
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
4260
|
-
<link href="https://fonts.googleapis.com/css2?family=
|
|
4261
|
-
<link href="https://fonts.googleapis.com/css2?family=Fira+Code:wght@400;500&display=swap" rel="stylesheet">
|
|
4460
|
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
|
|
4262
4461
|
|
|
4263
4462
|
<script src="https://unpkg.com/htmx.org@2.0.0" defer></script>
|
|
4264
4463
|
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
|
|
@@ -4270,38 +4469,17 @@ var indexHtml = `<!DOCTYPE html>
|
|
|
4270
4469
|
theme: {
|
|
4271
4470
|
extend: {
|
|
4272
4471
|
fontFamily: {
|
|
4273
|
-
sans: ['
|
|
4274
|
-
mono: ['"
|
|
4472
|
+
sans: ['Inter', 'sans-serif'],
|
|
4473
|
+
mono: ['"JetBrains Mono"', 'monospace'],
|
|
4275
4474
|
},
|
|
4276
4475
|
colors: {
|
|
4277
|
-
|
|
4278
|
-
|
|
4279
|
-
|
|
4280
|
-
|
|
4281
|
-
|
|
4282
|
-
|
|
4283
|
-
|
|
4284
|
-
'primary-foreground': 'var(--primary-foreground)',
|
|
4285
|
-
secondary: 'var(--secondary)',
|
|
4286
|
-
'secondary-foreground': 'var(--secondary-foreground)',
|
|
4287
|
-
muted: 'var(--muted)',
|
|
4288
|
-
'muted-foreground': 'var(--muted-foreground)',
|
|
4289
|
-
accent: 'var(--accent)',
|
|
4290
|
-
'accent-foreground': 'var(--accent-foreground)',
|
|
4291
|
-
destructive: 'var(--destructive)',
|
|
4292
|
-
border: 'var(--border)',
|
|
4293
|
-
input: 'var(--input)',
|
|
4294
|
-
ring: 'var(--ring)',
|
|
4295
|
-
sidebar: {
|
|
4296
|
-
DEFAULT: 'var(--sidebar)',
|
|
4297
|
-
foreground: 'var(--sidebar-foreground)',
|
|
4298
|
-
primary: 'var(--sidebar-primary)',
|
|
4299
|
-
'primary-foreground': 'var(--sidebar-primary-foreground)',
|
|
4300
|
-
accent: 'var(--sidebar-accent)',
|
|
4301
|
-
'accent-foreground': 'var(--sidebar-accent-foreground)',
|
|
4302
|
-
border: 'var(--sidebar-border)',
|
|
4303
|
-
ring: 'var(--sidebar-ring)',
|
|
4304
|
-
}
|
|
4476
|
+
bg: '#161618',
|
|
4477
|
+
panel: '#1c1c1f',
|
|
4478
|
+
border: '#2c2c2f',
|
|
4479
|
+
text: '#ededed',
|
|
4480
|
+
muted: '#8f8f91',
|
|
4481
|
+
accent: '#00e599',
|
|
4482
|
+
hover: '#28282c'
|
|
4305
4483
|
}
|
|
4306
4484
|
}
|
|
4307
4485
|
}
|
|
@@ -4309,265 +4487,319 @@ var indexHtml = `<!DOCTYPE html>
|
|
|
4309
4487
|
</script>
|
|
4310
4488
|
|
|
4311
4489
|
<style>
|
|
4312
|
-
:
|
|
4313
|
-
|
|
4314
|
-
--foreground: oklch(0.148 0.004 228.8);
|
|
4315
|
-
--card: oklch(1 0 0);
|
|
4316
|
-
--card-foreground: oklch(0.148 0.004 228.8);
|
|
4317
|
-
--popover: oklch(1 0 0);
|
|
4318
|
-
--popover-foreground: oklch(0.148 0.004 228.8);
|
|
4319
|
-
--primary: oklch(0.508 0.118 165.612);
|
|
4320
|
-
--primary-foreground: oklch(0.979 0.021 166.113);
|
|
4321
|
-
--secondary: oklch(0.967 0.001 286.375);
|
|
4322
|
-
--secondary-foreground: oklch(0.21 0.006 285.885);
|
|
4323
|
-
--muted: oklch(0.963 0.002 197.1);
|
|
4324
|
-
--muted-foreground: oklch(0.56 0.021 213.5);
|
|
4325
|
-
--accent: oklch(0.963 0.002 197.1);
|
|
4326
|
-
--accent-foreground: oklch(0.218 0.008 223.9);
|
|
4327
|
-
--destructive: oklch(0.577 0.245 27.325);
|
|
4328
|
-
--border: oklch(0.925 0.005 214.3);
|
|
4329
|
-
--input: oklch(0.925 0.005 214.3);
|
|
4330
|
-
--ring: oklch(0.723 0.014 214.4);
|
|
4331
|
-
--chart-1: oklch(0.845 0.143 164.978);
|
|
4332
|
-
--chart-2: oklch(0.696 0.17 162.48);
|
|
4333
|
-
--chart-3: oklch(0.596 0.145 163.225);
|
|
4334
|
-
--chart-4: oklch(0.508 0.118 165.612);
|
|
4335
|
-
--chart-5: oklch(0.432 0.095 166.913);
|
|
4336
|
-
--radius: 0.625rem;
|
|
4337
|
-
--sidebar: oklch(0.987 0.002 197.1);
|
|
4338
|
-
--sidebar-foreground: oklch(0.148 0.004 228.8);
|
|
4339
|
-
--sidebar-primary: oklch(0.596 0.145 163.225);
|
|
4340
|
-
--sidebar-primary-foreground: oklch(0.979 0.021 166.113);
|
|
4341
|
-
--sidebar-accent: oklch(0.963 0.002 197.1);
|
|
4342
|
-
--sidebar-accent-foreground: oklch(0.218 0.008 223.9);
|
|
4343
|
-
--sidebar-border: oklch(0.925 0.005 214.3);
|
|
4344
|
-
--sidebar-ring: oklch(0.723 0.014 214.4);
|
|
4345
|
-
}
|
|
4346
|
-
|
|
4347
|
-
.dark {
|
|
4348
|
-
--background: oklch(0.148 0.004 228.8);
|
|
4349
|
-
--foreground: oklch(0.987 0.002 197.1);
|
|
4350
|
-
--card: oklch(0.218 0.008 223.9);
|
|
4351
|
-
--card-foreground: oklch(0.987 0.002 197.1);
|
|
4352
|
-
--popover: oklch(0.218 0.008 223.9);
|
|
4353
|
-
--popover-foreground: oklch(0.987 0.002 197.1);
|
|
4354
|
-
--primary: oklch(0.432 0.095 166.913);
|
|
4355
|
-
--primary-foreground: oklch(0.979 0.021 166.113);
|
|
4356
|
-
--secondary: oklch(0.274 0.006 286.033);
|
|
4357
|
-
--secondary-foreground: oklch(0.985 0 0);
|
|
4358
|
-
--muted: oklch(0.275 0.011 216.9);
|
|
4359
|
-
--muted-foreground: oklch(0.723 0.014 214.4);
|
|
4360
|
-
--accent: oklch(0.275 0.011 216.9);
|
|
4361
|
-
--accent-foreground: oklch(0.987 0.002 197.1);
|
|
4362
|
-
--destructive: oklch(0.704 0.191 22.216);
|
|
4363
|
-
--border: oklch(1 0 0 / 10%);
|
|
4364
|
-
--input: oklch(1 0 0 / 15%);
|
|
4365
|
-
--ring: oklch(0.56 0.021 213.5);
|
|
4366
|
-
--chart-1: oklch(0.845 0.143 164.978);
|
|
4367
|
-
--chart-2: oklch(0.696 0.17 162.48);
|
|
4368
|
-
--chart-3: oklch(0.596 0.145 163.225);
|
|
4369
|
-
--chart-4: oklch(0.508 0.118 165.612);
|
|
4370
|
-
--chart-5: oklch(0.432 0.095 166.913);
|
|
4371
|
-
--sidebar: oklch(0.218 0.008 223.9);
|
|
4372
|
-
--sidebar-foreground: oklch(0.987 0.002 197.1);
|
|
4373
|
-
--sidebar-primary: oklch(0.696 0.17 162.48);
|
|
4374
|
-
--sidebar-primary-foreground: oklch(0.262 0.051 172.552);
|
|
4375
|
-
--sidebar-accent: oklch(0.275 0.011 216.9);
|
|
4376
|
-
--sidebar-accent-foreground: oklch(0.987 0.002 197.1);
|
|
4377
|
-
--sidebar-border: oklch(1 0 0 / 10%);
|
|
4378
|
-
--sidebar-ring: oklch(0.56 0.021 213.5);
|
|
4379
|
-
}
|
|
4380
|
-
|
|
4381
|
-
::-webkit-scrollbar { width: 6px; height: 6px; }
|
|
4490
|
+
body { background-color: #161618; color: #ededed; }
|
|
4491
|
+
::-webkit-scrollbar { width: 8px; height: 8px; }
|
|
4382
4492
|
::-webkit-scrollbar-track { background: transparent; }
|
|
4383
|
-
::-webkit-scrollbar-thumb { background:
|
|
4384
|
-
::-webkit-scrollbar-thumb:hover { background:
|
|
4493
|
+
::-webkit-scrollbar-thumb { background: #3a3a3e; border-radius: 4px; }
|
|
4494
|
+
::-webkit-scrollbar-thumb:hover { background: #4a4a4e; }
|
|
4385
4495
|
::-webkit-scrollbar-corner { background: transparent; }
|
|
4386
4496
|
[x-cloak] { display: none !important; }
|
|
4387
|
-
|
|
4388
|
-
.
|
|
4389
|
-
.
|
|
4497
|
+
|
|
4498
|
+
.tab-active { background-color: #1c1c1f; border-top: 2px solid #00e599; color: #ededed; }
|
|
4499
|
+
.tab-inactive { background-color: #161618; border-top: 2px solid transparent; color: #8f8f91; }
|
|
4500
|
+
|
|
4501
|
+
table { border-spacing: 0; }
|
|
4502
|
+
th { border-bottom: 1px solid #2c2c2f; border-right: 1px solid #2c2c2f; background: #1c1c1f; font-weight: 500; font-size: 13px; color: #8f8f91; }
|
|
4503
|
+
td { border-bottom: 1px solid #2c2c2f; border-right: 1px solid #2c2c2f; font-size: 13px; }
|
|
4504
|
+
tr:hover td { background-color: #28282c; }
|
|
4390
4505
|
</style>
|
|
4391
4506
|
</head>
|
|
4392
|
-
<body class="
|
|
4393
|
-
<div x-data="
|
|
4394
|
-
|
|
4395
|
-
|
|
4396
|
-
|
|
4397
|
-
|
|
4398
|
-
|
|
4399
|
-
<
|
|
4400
|
-
|
|
4401
|
-
|
|
4507
|
+
<body class="font-sans h-screen flex overflow-hidden text-sm selection:bg-accent/30">
|
|
4508
|
+
<div x-data="studioState()" class="flex h-screen w-full relative">
|
|
4509
|
+
|
|
4510
|
+
<!-- JSON Modal -->
|
|
4511
|
+
<div x-show="jsonModalOpen" x-cloak class="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm transition-opacity" style="display: none;">
|
|
4512
|
+
<div @click.away="jsonModalOpen = false" class="bg-panel border border-border rounded-lg shadow-xl w-full max-w-2xl flex flex-col max-h-[80vh]">
|
|
4513
|
+
<div class="flex items-center justify-between p-3 border-b border-border bg-bg/50">
|
|
4514
|
+
<h3 class="font-medium text-text text-xs flex items-center gap-2">
|
|
4515
|
+
<svg class="w-4 h-4 text-accent" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="16 18 22 12 16 6"></polyline><polyline points="8 6 2 12 8 18"></polyline></svg>
|
|
4516
|
+
Data Viewer
|
|
4517
|
+
</h3>
|
|
4518
|
+
<button @click="jsonModalOpen = false" class="text-muted hover:text-text p-1 rounded hover:bg-hover transition-colors"><svg class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg></button>
|
|
4519
|
+
</div>
|
|
4520
|
+
<div class="p-4 overflow-auto flex-1">
|
|
4521
|
+
<pre class="font-mono text-xs text-text bg-bg p-4 rounded border border-border whitespace-pre-wrap break-all" x-text="jsonModalData"></pre>
|
|
4522
|
+
</div>
|
|
4523
|
+
</div>
|
|
4524
|
+
</div>
|
|
4525
|
+
|
|
4526
|
+
<!-- Sidebar -->
|
|
4527
|
+
<aside class="w-64 bg-bg border-r border-border flex flex-col z-20 shrink-0">
|
|
4528
|
+
|
|
4529
|
+
<!-- Schema Selector -->
|
|
4530
|
+
<div class="p-3 border-b border-border">
|
|
4531
|
+
<select x-model="currentSchema" @change="loadSidebar()" class="w-full bg-panel border border-border text-text text-sm rounded px-2 py-1.5 focus:outline-none focus:border-muted cursor-pointer font-medium">
|
|
4532
|
+
<template x-for="s in schemas" :key="s">
|
|
4533
|
+
<option :value="s" x-text="s"></option>
|
|
4534
|
+
</template>
|
|
4535
|
+
</select>
|
|
4536
|
+
</div>
|
|
4537
|
+
|
|
4538
|
+
<!-- Search Box -->
|
|
4539
|
+
<div class="p-3 border-b border-border bg-bg">
|
|
4540
|
+
<div class="relative">
|
|
4541
|
+
<svg class="w-4 h-4 text-muted absolute left-2.5 top-2" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line></svg>
|
|
4542
|
+
<input type="text" x-model="searchQuery" placeholder="Filter tables.." class="w-full bg-panel border border-border rounded pl-8 pr-2 py-1.5 text-sm focus:outline-none focus:border-muted text-text placeholder-muted">
|
|
4543
|
+
</div>
|
|
4402
4544
|
</div>
|
|
4403
|
-
|
|
4404
|
-
|
|
4545
|
+
|
|
4546
|
+
<!-- Tables List -->
|
|
4547
|
+
<div class="flex-1 overflow-y-auto pt-2" id="sidebar-content" :hx-get="'/htmx/sidebar?schema=' + currentSchema" hx-trigger="load">
|
|
4548
|
+
<div class="p-4 text-xs text-muted animate-pulse text-center">Loading...</div>
|
|
4405
4549
|
</div>
|
|
4406
4550
|
</aside>
|
|
4407
|
-
|
|
4408
|
-
|
|
4409
|
-
|
|
4410
|
-
|
|
4411
|
-
|
|
4412
|
-
|
|
4413
|
-
|
|
4414
|
-
|
|
4415
|
-
|
|
4416
|
-
|
|
4417
|
-
|
|
4418
|
-
|
|
4419
|
-
|
|
4420
|
-
<line x1="3" y1="9" x2="21" y2="9"></line>
|
|
4421
|
-
|
|
4422
|
-
|
|
4423
|
-
|
|
4424
|
-
|
|
4425
|
-
|
|
4426
|
-
|
|
4427
|
-
|
|
4428
|
-
|
|
4429
|
-
<
|
|
4430
|
-
|
|
4431
|
-
|
|
4432
|
-
|
|
4551
|
+
|
|
4552
|
+
<!-- Main Content -->
|
|
4553
|
+
<main class="flex-1 flex flex-col min-w-0 bg-panel">
|
|
4554
|
+
<!-- Tabs Header -->
|
|
4555
|
+
<header class="h-10 border-b border-border flex items-center bg-bg shrink-0 overflow-x-auto overflow-y-hidden">
|
|
4556
|
+
<template x-for="tab in tabs" :key="tab.id">
|
|
4557
|
+
<div
|
|
4558
|
+
@click="activeTab = tab.id"
|
|
4559
|
+
:class="activeTab === tab.id ? 'tab-active' : 'tab-inactive hover:bg-panel hover:text-text'"
|
|
4560
|
+
class="h-full flex items-center gap-2 pl-4 pr-2 cursor-pointer border-r border-border min-w-max transition-colors group relative select-none"
|
|
4561
|
+
>
|
|
4562
|
+
<!-- Icon -->
|
|
4563
|
+
<template x-if="tab.type === 'table'">
|
|
4564
|
+
<svg class="w-4 h-4 text-muted" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect><line x1="3" y1="9" x2="21" y2="9"></line><line x1="9" y1="21" x2="9" y2="9"></line></svg>
|
|
4565
|
+
</template>
|
|
4566
|
+
<template x-if="tab.type === 'query'">
|
|
4567
|
+
<svg class="w-4 h-4 text-accent" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="16 18 22 12 16 6"></polyline><polyline points="8 6 2 12 8 18"></polyline></svg>
|
|
4568
|
+
</template>
|
|
4569
|
+
|
|
4570
|
+
<span x-text="tab.title" class="font-medium text-xs mr-2"></span>
|
|
4571
|
+
|
|
4572
|
+
<!-- Close Button -->
|
|
4573
|
+
<button @click.stop="closeTab(tab.id)" class="opacity-0 group-hover:opacity-100 p-0.5 rounded hover:bg-hover text-muted hover:text-text transition-opacity">
|
|
4574
|
+
<svg class="w-3 h-3" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>
|
|
4575
|
+
</button>
|
|
4576
|
+
</div>
|
|
4577
|
+
</template>
|
|
4578
|
+
<button @click="openQueryEditor()" class="h-full px-3 text-muted hover:text-text hover:bg-panel flex items-center justify-center transition-colors">
|
|
4579
|
+
<svg class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="12" y1="5" x2="12" y2="19"></line><line x1="5" y1="12" x2="19" y2="12"></line></svg>
|
|
4433
4580
|
</button>
|
|
4434
4581
|
</header>
|
|
4435
|
-
|
|
4436
|
-
|
|
4437
|
-
|
|
4438
|
-
|
|
4439
|
-
|
|
4440
|
-
|
|
4441
|
-
|
|
4442
|
-
<
|
|
4443
|
-
<p class="text-sm">Choose a table from the sidebar to view its data</p>
|
|
4582
|
+
|
|
4583
|
+
<!-- Active Tab Content Container -->
|
|
4584
|
+
<div class="flex-1 relative overflow-hidden bg-panel">
|
|
4585
|
+
|
|
4586
|
+
<!-- Empty State -->
|
|
4587
|
+
<div x-show="tabs.length === 0" class="absolute inset-0 flex flex-col items-center justify-center text-muted">
|
|
4588
|
+
<div class="w-12 h-12 rounded-lg bg-hover border border-border flex items-center justify-center mb-4">
|
|
4589
|
+
<svg class="w-6 h-6 text-text" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect><line x1="3" y1="9" x2="21" y2="9"></line><line x1="9" y1="21" x2="9" y2="9"></line></svg>
|
|
4444
4590
|
</div>
|
|
4591
|
+
<p class="text-sm">Select a table to view</p>
|
|
4445
4592
|
</div>
|
|
4593
|
+
|
|
4594
|
+
<!-- Render Tabs -->
|
|
4595
|
+
<template x-for="tab in tabs" :key="tab.id">
|
|
4596
|
+
<div x-show="activeTab === tab.id" class="absolute inset-0 flex flex-col h-full w-full">
|
|
4597
|
+
|
|
4598
|
+
<!-- Table View -->
|
|
4599
|
+
<template x-if="tab.type === 'table'">
|
|
4600
|
+
<div class="flex flex-col h-full w-full">
|
|
4601
|
+
<!-- Table Header Toolbars -->
|
|
4602
|
+
<div class="h-12 border-b border-border flex items-center justify-between px-3 shrink-0 bg-panel">
|
|
4603
|
+
<div class="flex items-center gap-2">
|
|
4604
|
+
<span class="text-xs text-muted font-medium ml-1 flex items-center gap-2">
|
|
4605
|
+
<svg class="w-3.5 h-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect><line x1="3" y1="9" x2="21" y2="9"></line><line x1="9" y1="21" x2="9" y2="9"></line></svg>
|
|
4606
|
+
<span x-text="tab.title"></span>
|
|
4607
|
+
</span>
|
|
4608
|
+
</div>
|
|
4609
|
+
<!-- HTMX will swap the pagination controls into this div -->
|
|
4610
|
+
<div :id="'pagination-' + tab.id" class="flex items-center gap-2"></div>
|
|
4611
|
+
</div>
|
|
4612
|
+
|
|
4613
|
+
<!-- Table Data Grid Container -->
|
|
4614
|
+
<div class="flex-1 overflow-auto bg-bg" :id="'data-' + tab.id"
|
|
4615
|
+
x-init="htmx.ajax('GET', '/htmx/tables/' + tab.name + '?schema=' + tab.schema + '&tabId=' + tab.id, {target: '#data-' + tab.id})">
|
|
4616
|
+
<div class="p-8 text-center text-muted">Loading data...</div>
|
|
4617
|
+
</div>
|
|
4618
|
+
</div>
|
|
4619
|
+
</template>
|
|
4620
|
+
|
|
4621
|
+
<!-- Query Editor View -->
|
|
4622
|
+
<template x-if="tab.type === 'query'">
|
|
4623
|
+
<div class="flex flex-col h-full w-full" x-data="{ query: 'SELECT 1;' }">
|
|
4624
|
+
<div class="h-12 border-b border-border flex items-center px-3 shrink-0 gap-2 bg-panel">
|
|
4625
|
+
<button
|
|
4626
|
+
@click="htmx.ajax('POST', '/htmx/query', { target: '#query-result-' + tab.id, values: { query: query } })"
|
|
4627
|
+
class="bg-accent text-bg px-3 py-1.5 rounded text-xs font-semibold hover:opacity-90 flex items-center gap-1.5 transition-opacity"
|
|
4628
|
+
>
|
|
4629
|
+
<svg class="w-3 h-3" viewBox="0 0 24 24" fill="currentColor"><polygon points="5 3 19 12 5 21 5 3"></polygon></svg> Run
|
|
4630
|
+
</button>
|
|
4631
|
+
<span class="text-muted text-[10px] font-mono ml-2">Press Ctrl+Enter to run</span>
|
|
4632
|
+
</div>
|
|
4633
|
+
<div class="flex-none h-48 border-b border-border relative">
|
|
4634
|
+
<textarea
|
|
4635
|
+
x-model="query"
|
|
4636
|
+
@keydown.ctrl.enter.prevent="htmx.ajax('POST', '/htmx/query', { target: '#query-result-' + tab.id, values: { query: query } })"
|
|
4637
|
+
class="absolute inset-0 w-full h-full bg-bg text-text font-mono text-sm p-4 focus:outline-none resize-none"
|
|
4638
|
+
placeholder="SELECT 1;"
|
|
4639
|
+
></textarea>
|
|
4640
|
+
</div>
|
|
4641
|
+
<div class="flex-1 overflow-auto bg-bg flex flex-col" :id="'query-result-' + tab.id">
|
|
4642
|
+
<div class="h-full flex items-center justify-center text-muted">
|
|
4643
|
+
<div class="flex flex-col items-center">
|
|
4644
|
+
<svg class="w-6 h-6 mb-2 opacity-50" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="5 3 19 12 5 21 5 3"></polygon></svg>
|
|
4645
|
+
<span class="text-xs">Run a query to see results</span>
|
|
4646
|
+
</div>
|
|
4647
|
+
</div>
|
|
4648
|
+
</div>
|
|
4649
|
+
</div>
|
|
4650
|
+
</template>
|
|
4651
|
+
|
|
4652
|
+
</div>
|
|
4653
|
+
</template>
|
|
4446
4654
|
</div>
|
|
4447
4655
|
</main>
|
|
4448
4656
|
</div>
|
|
4449
4657
|
<script>
|
|
4450
|
-
|
|
4658
|
+
const INITIAL_SCHEMAS = ${JSON.stringify(opts.schemas)};
|
|
4659
|
+
const INITIAL_CURRENT_SCHEMA = ${JSON.stringify(opts.currentSchema)};
|
|
4660
|
+
|
|
4661
|
+
function studioState() {
|
|
4662
|
+
return {
|
|
4663
|
+
schemas: INITIAL_SCHEMAS,
|
|
4664
|
+
currentSchema: INITIAL_CURRENT_SCHEMA,
|
|
4665
|
+
searchQuery: '',
|
|
4666
|
+
tabs: [],
|
|
4667
|
+
activeTab: null,
|
|
4668
|
+
queryEditorCount: 0,
|
|
4669
|
+
jsonModalOpen: false,
|
|
4670
|
+
jsonModalData: '',
|
|
4671
|
+
|
|
4672
|
+
init() {
|
|
4673
|
+
this.$watch('activeTab', (value) => {
|
|
4674
|
+
const tab = this.tabs.find(t => t.id === value);
|
|
4675
|
+
if (tab && tab.schema && tab.schema !== this.currentSchema) {
|
|
4676
|
+
this.currentSchema = tab.schema;
|
|
4677
|
+
this.loadSidebar();
|
|
4678
|
+
}
|
|
4679
|
+
});
|
|
4680
|
+
|
|
4681
|
+
// Listen for table clicks from the sidebar
|
|
4682
|
+
window.addEventListener('open-table', (e) => {
|
|
4683
|
+
const tableName = e.detail.tableName;
|
|
4684
|
+
const schema = this.currentSchema;
|
|
4685
|
+
const tabId = 'table_' + schema + '_' + tableName;
|
|
4686
|
+
|
|
4687
|
+
if (!this.tabs.find(t => t.id === tabId)) {
|
|
4688
|
+
this.tabs.push({ id: tabId, type: 'table', title: tableName, name: tableName, schema: schema });
|
|
4689
|
+
}
|
|
4690
|
+
this.activeTab = tabId;
|
|
4691
|
+
});
|
|
4692
|
+
|
|
4693
|
+
window.addEventListener('open-json-modal', (e) => {
|
|
4694
|
+
try {
|
|
4695
|
+
const str = atob(e.detail);
|
|
4696
|
+
this.jsonModalData = JSON.stringify(JSON.parse(str), null, 2);
|
|
4697
|
+
} catch(err) {
|
|
4698
|
+
this.jsonModalData = 'Invalid JSON data';
|
|
4699
|
+
}
|
|
4700
|
+
this.jsonModalOpen = true;
|
|
4701
|
+
});
|
|
4702
|
+
},
|
|
4703
|
+
|
|
4704
|
+
loadSidebar() {
|
|
4705
|
+
htmx.ajax('GET', '/htmx/sidebar?schema=' + this.currentSchema, {target: '#sidebar-content'});
|
|
4706
|
+
},
|
|
4707
|
+
|
|
4708
|
+
openQueryEditor() {
|
|
4709
|
+
this.queryEditorCount++;
|
|
4710
|
+
const tabId = 'query_' + this.queryEditorCount;
|
|
4711
|
+
this.tabs.push({ id: tabId, type: 'query', title: 'Query Editor ' + (this.queryEditorCount > 1 ? this.queryEditorCount : ''), schema: this.currentSchema });
|
|
4712
|
+
this.activeTab = tabId;
|
|
4713
|
+
},
|
|
4714
|
+
|
|
4715
|
+
closeTab(id) {
|
|
4716
|
+
const idx = this.tabs.findIndex(t => t.id === id);
|
|
4717
|
+
if (idx === -1) return;
|
|
4718
|
+
|
|
4719
|
+
this.tabs.splice(idx, 1);
|
|
4720
|
+
if (this.activeTab === id) {
|
|
4721
|
+
if (this.tabs.length > 0) {
|
|
4722
|
+
this.activeTab = this.tabs[Math.max(0, idx - 1)].id;
|
|
4723
|
+
} else {
|
|
4724
|
+
this.activeTab = null;
|
|
4725
|
+
}
|
|
4726
|
+
}
|
|
4727
|
+
}
|
|
4728
|
+
}
|
|
4729
|
+
}
|
|
4451
4730
|
</script>
|
|
4452
4731
|
</body>
|
|
4453
4732
|
</html>`;
|
|
4733
|
+
}
|
|
4454
4734
|
|
|
4455
4735
|
// src/commands/studio.ts
|
|
4456
4736
|
async function runStudio(config2) {
|
|
4457
|
-
const schemas2 = await loadSchemas(config2.schema);
|
|
4737
|
+
const schemas2 = (await loadSchemas(config2.schema)).filter((s) => s.type === "table");
|
|
4458
4738
|
if (schemas2.length === 0) {
|
|
4459
|
-
console.warn("No table definitions found in schema files.");
|
|
4460
|
-
return;
|
|
4739
|
+
console.warn("No table definitions found in schema files. Connecting anyway to browse DB...");
|
|
4461
4740
|
}
|
|
4462
4741
|
const schemaObj2 = {};
|
|
4463
4742
|
for (const s of schemas2) {
|
|
4464
4743
|
schemaObj2[s.exportName] = s.table;
|
|
4465
4744
|
}
|
|
4466
4745
|
const db2 = bungres({ url: config2.dbUrl, schema: schemaObj2 });
|
|
4746
|
+
let allSchemas = ["public"];
|
|
4747
|
+
try {
|
|
4748
|
+
const res = await db2.execute(rawSql(`SELECT schema_name FROM information_schema.schemata WHERE schema_name NOT LIKE 'pg_%' AND schema_name != 'information_schema'`));
|
|
4749
|
+
if (Array.isArray(res) && res.length > 0) {
|
|
4750
|
+
allSchemas = res.map((r2) => r2.schema_name);
|
|
4751
|
+
}
|
|
4752
|
+
} catch (e) {}
|
|
4467
4753
|
const port = Bun.env.PORT ? parseInt(Bun.env.PORT, 10) : 5555;
|
|
4468
4754
|
const server = Bun.serve({
|
|
4469
4755
|
port,
|
|
4470
4756
|
async fetch(req) {
|
|
4471
4757
|
const url = new URL(req.url);
|
|
4472
|
-
if (req.method === "GET" && url.pathname === "/
|
|
4473
|
-
const
|
|
4474
|
-
|
|
4475
|
-
if (s.config.foreignKeys) {
|
|
4476
|
-
s.config.foreignKeys.forEach((fk) => {
|
|
4477
|
-
fk.columns.forEach((col2) => fkColumns.add(col2));
|
|
4478
|
-
});
|
|
4479
|
-
}
|
|
4480
|
-
const columns = Object.entries(s.config.columns).map(([key, col2]) => {
|
|
4481
|
-
const fk = col2.references ? {
|
|
4482
|
-
table: col2.references.table,
|
|
4483
|
-
column: col2.references.column
|
|
4484
|
-
} : fkColumns.has(col2.name) ? { isForeignKey: true } : null;
|
|
4485
|
-
return {
|
|
4486
|
-
name: col2.name,
|
|
4487
|
-
type: col2.dataType,
|
|
4488
|
-
primaryKey: col2.primaryKey,
|
|
4489
|
-
foreignKey: fk
|
|
4490
|
-
};
|
|
4491
|
-
});
|
|
4492
|
-
return {
|
|
4493
|
-
name: s.config.name,
|
|
4494
|
-
exportName: s.exportName,
|
|
4495
|
-
columns,
|
|
4496
|
-
foreignKeys: s.config.foreignKeys || []
|
|
4497
|
-
};
|
|
4498
|
-
});
|
|
4499
|
-
return new Response(JSON.stringify(tables), {
|
|
4500
|
-
headers: { "Content-Type": "application/json" }
|
|
4501
|
-
});
|
|
4502
|
-
}
|
|
4503
|
-
if (req.method === "GET" && url.pathname.startsWith("/api/tables/") && url.pathname.endsWith("/data")) {
|
|
4504
|
-
const tableName = url.pathname.split("/")[3];
|
|
4505
|
-
const schema = schemas2.find((s) => s.config.name === tableName);
|
|
4506
|
-
if (!schema) {
|
|
4507
|
-
return new Response("Table not found", { status: 404 });
|
|
4508
|
-
}
|
|
4509
|
-
try {
|
|
4510
|
-
const page = parseInt(url.searchParams.get("page") || "1", 10);
|
|
4511
|
-
const limit = parseInt(url.searchParams.get("limit") || "50", 10);
|
|
4512
|
-
const offset = (page - 1) * limit;
|
|
4513
|
-
const countResult = await db2.select({ count: schema.table }).from(schema.table);
|
|
4514
|
-
const total = Array.isArray(countResult) ? countResult.length : 0;
|
|
4515
|
-
const data = await db2.select().from(schema.table).limit(limit).offset(offset);
|
|
4516
|
-
return new Response(JSON.stringify({
|
|
4517
|
-
data,
|
|
4518
|
-
total,
|
|
4519
|
-
page,
|
|
4520
|
-
limit,
|
|
4521
|
-
totalPages: Math.ceil(total / limit)
|
|
4522
|
-
}), {
|
|
4523
|
-
headers: { "Content-Type": "application/json" }
|
|
4524
|
-
});
|
|
4525
|
-
} catch (e) {
|
|
4526
|
-
return new Response(JSON.stringify({ error: e.message }), {
|
|
4527
|
-
status: 500,
|
|
4528
|
-
headers: { "Content-Type": "application/json" }
|
|
4529
|
-
});
|
|
4530
|
-
}
|
|
4531
|
-
}
|
|
4532
|
-
if (req.method === "POST" && url.pathname === "/api/editor/open") {
|
|
4758
|
+
if (req.method === "GET" && url.pathname === "/htmx/sidebar") {
|
|
4759
|
+
const currentSchema = url.searchParams.get("schema") || config2.dbSchema || "public";
|
|
4760
|
+
let items = [];
|
|
4533
4761
|
try {
|
|
4534
|
-
const
|
|
4535
|
-
|
|
4536
|
-
|
|
4537
|
-
|
|
4538
|
-
|
|
4539
|
-
|
|
4540
|
-
|
|
4541
|
-
|
|
4542
|
-
|
|
4543
|
-
|
|
4544
|
-
|
|
4545
|
-
});
|
|
4762
|
+
const query = `
|
|
4763
|
+
SELECT c.relname as name, c.reltuples as count, c.relkind as type
|
|
4764
|
+
FROM pg_class c
|
|
4765
|
+
JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
|
|
4766
|
+
WHERE n.nspname = '${currentSchema}'
|
|
4767
|
+
AND c.relkind IN ('r', 'v', 'm')
|
|
4768
|
+
ORDER BY c.relname
|
|
4769
|
+
`;
|
|
4770
|
+
const res = await db2.execute(rawSql(query));
|
|
4771
|
+
if (Array.isArray(res)) {
|
|
4772
|
+
items = res;
|
|
4546
4773
|
}
|
|
4547
|
-
return new Response("Schema not found", { status: 404 });
|
|
4548
4774
|
} catch (e) {
|
|
4549
|
-
|
|
4775
|
+
items = schemas2.map((s) => ({ name: s.config.name, count: 0, type: "r" }));
|
|
4550
4776
|
}
|
|
4551
|
-
|
|
4552
|
-
|
|
4553
|
-
|
|
4554
|
-
|
|
4555
|
-
|
|
4777
|
+
const formatCount = (n2) => {
|
|
4778
|
+
if (n2 >= 1e6)
|
|
4779
|
+
return (n2 / 1e6).toFixed(1) + "M";
|
|
4780
|
+
if (n2 >= 1000)
|
|
4781
|
+
return (n2 / 1000).toFixed(1) + "k";
|
|
4782
|
+
return Math.floor(n2).toString();
|
|
4783
|
+
};
|
|
4784
|
+
let html = '<div class="px-3 py-2 text-[10px] font-semibold text-muted tracking-wider flex justify-between items-center"><div class="flex items-center gap-1.5"><svg class="w-3 h-3" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="22 12 18 12 15 21 9 3 6 12 2 12"></polyline></svg> TABLES</div></div>';
|
|
4785
|
+
html += '<ul class="flex flex-col gap-0.5 p-2 m-0 list-none">';
|
|
4786
|
+
items.forEach((item) => {
|
|
4787
|
+
let icon = '<svg class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect><line x1="3" y1="9" x2="21" y2="9"></line><line x1="9" y1="21" x2="9" y2="9"></line></svg>';
|
|
4788
|
+
if (item.type === "v")
|
|
4789
|
+
icon = '<svg class="w-4 h-4 text-purple-400" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path><circle cx="12" cy="12" r="3"></circle></svg>';
|
|
4790
|
+
if (item.type === "m")
|
|
4791
|
+
icon = '<svg class="w-4 h-4 text-orange-400" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="12 2 2 7 12 12 22 7 12 2"></polygon><polyline points="2 17 12 22 22 17"></polyline><polyline points="2 12 12 17 22 12"></polyline></svg>';
|
|
4556
4792
|
html += `
|
|
4557
|
-
<li>
|
|
4793
|
+
<li x-show="searchQuery === '' || '${item.name}'.toLowerCase().includes(searchQuery.toLowerCase())">
|
|
4558
4794
|
<button
|
|
4559
|
-
|
|
4560
|
-
|
|
4561
|
-
@click="currentTable = '${tableName}'; pageSize = 25"
|
|
4562
|
-
:class="currentTable === '${tableName}' ? 'bg-primary text-primary-foreground' : 'text-muted-foreground hover:bg-sidebar-accent hover:text-sidebar-accent-foreground'"
|
|
4563
|
-
class="w-full text-left px-3 py-2 rounded-md text-sm font-medium flex items-center gap-2 transition-colors focus:outline-none"
|
|
4795
|
+
@click="$dispatch('open-table', { tableName: '${item.name}' })"
|
|
4796
|
+
class="w-full text-left px-2 py-1 rounded text-sm font-medium flex items-center justify-between transition-colors focus:outline-none hover:bg-hover hover:text-text group text-muted"
|
|
4564
4797
|
>
|
|
4565
|
-
<
|
|
4566
|
-
|
|
4567
|
-
<
|
|
4568
|
-
|
|
4569
|
-
</
|
|
4570
|
-
${tableName}
|
|
4798
|
+
<div class="flex items-center gap-2 truncate">
|
|
4799
|
+
${icon}
|
|
4800
|
+
<span class="truncate">${item.name}</span>
|
|
4801
|
+
</div>
|
|
4802
|
+
<span class="text-[10px] font-mono text-muted/50 group-hover:text-muted">${formatCount(Math.max(0, item.count))}</span>
|
|
4571
4803
|
</button>
|
|
4572
4804
|
</li>
|
|
4573
4805
|
`;
|
|
@@ -4577,140 +4809,215 @@ async function runStudio(config2) {
|
|
|
4577
4809
|
}
|
|
4578
4810
|
if (req.method === "GET" && url.pathname.startsWith("/htmx/tables/")) {
|
|
4579
4811
|
const tableName = url.pathname.split("/")[3];
|
|
4580
|
-
const
|
|
4581
|
-
|
|
4582
|
-
return new Response(`<div class="p-4 text-red-500">Table not found</div>`, { status: 404, headers: { "Content-Type": "text/html" } });
|
|
4583
|
-
}
|
|
4812
|
+
const reqSchema = url.searchParams.get("schema") || config2.dbSchema || "public";
|
|
4813
|
+
const tabId = url.searchParams.get("tabId") || `table_${tableName}`;
|
|
4584
4814
|
try {
|
|
4585
4815
|
const page = parseInt(url.searchParams.get("page") || "1", 10);
|
|
4586
4816
|
const limit = parseInt(url.searchParams.get("limit") || "25", 10);
|
|
4587
4817
|
const offset = (page - 1) * limit;
|
|
4588
|
-
|
|
4589
|
-
|
|
4818
|
+
let countResult;
|
|
4819
|
+
let data = [];
|
|
4820
|
+
const tsSchema = schemas2.find((s) => s.config.name === tableName);
|
|
4821
|
+
let colConfigs = {};
|
|
4822
|
+
let fkColumns = new Set;
|
|
4823
|
+
try {
|
|
4824
|
+
if (tsSchema) {
|
|
4825
|
+
colConfigs = tsSchema.config.columns || {};
|
|
4826
|
+
if (tsSchema.config.foreignKeys) {
|
|
4827
|
+
tsSchema.config.foreignKeys.forEach((fk) => fk.columns.forEach((col2) => fkColumns.add(col2)));
|
|
4828
|
+
}
|
|
4829
|
+
const countRes = await db2.select({ count: tsSchema.table }).from(tsSchema.table);
|
|
4830
|
+
countResult = Array.isArray(countRes) ? [{ count: countRes.length }] : [{ count: 0 }];
|
|
4831
|
+
data = await db2.select().from(tsSchema.table).limit(limit).offset(offset);
|
|
4832
|
+
} else {
|
|
4833
|
+
countResult = await db2.execute(rawSql(`SELECT COUNT(*) as count FROM "${reqSchema}"."${tableName}"`));
|
|
4834
|
+
data = await db2.execute(rawSql(`SELECT * FROM "${reqSchema}"."${tableName}" LIMIT ${limit} OFFSET ${offset}`));
|
|
4835
|
+
const colsRes = await db2.execute(rawSql(`SELECT column_name, data_type FROM information_schema.columns WHERE table_schema = '${reqSchema}' AND table_name = '${tableName}'`));
|
|
4836
|
+
if (Array.isArray(colsRes)) {
|
|
4837
|
+
colsRes.forEach((c2) => {
|
|
4838
|
+
colConfigs[c2.column_name] = { dataType: c2.data_type };
|
|
4839
|
+
});
|
|
4840
|
+
}
|
|
4841
|
+
}
|
|
4842
|
+
} catch (e) {
|
|
4843
|
+
return new Response(`<div class="p-4 text-red-500">Table not found or query error</div>`, { status: 404, headers: { "Content-Type": "text/html" } });
|
|
4844
|
+
}
|
|
4845
|
+
const total = Array.isArray(countResult) && countResult.length > 0 ? parseInt(countResult[0].count, 10) : 0;
|
|
4590
4846
|
const totalPages = Math.ceil(total / limit) || 1;
|
|
4591
|
-
|
|
4847
|
+
if (!Array.isArray(data))
|
|
4848
|
+
data = [];
|
|
4592
4849
|
const formatValue = (val) => {
|
|
4593
4850
|
if (val === null || val === undefined)
|
|
4594
|
-
return '<span class="italic text-muted
|
|
4595
|
-
if (typeof val === "
|
|
4596
|
-
|
|
4597
|
-
|
|
4598
|
-
|
|
4599
|
-
|
|
4600
|
-
|
|
4601
|
-
|
|
4602
|
-
return `<span class="text-foreground font-mono text-xs">${JSON.stringify(val).replace(/&/g, "&").replace(/</g, "<")}</span>`;
|
|
4603
|
-
return `<span class="text-foreground">${String(val).replace(/&/g, "&").replace(/</g, "<")}</span>`;
|
|
4851
|
+
return '<span class="italic text-muted/50">NULL</span>';
|
|
4852
|
+
if (typeof val === "object" && !(val instanceof Date)) {
|
|
4853
|
+
const rawJson = JSON.stringify(val);
|
|
4854
|
+
const b64 = Buffer.from(rawJson).toString("base64");
|
|
4855
|
+
const safeJson = rawJson.replace(/&/g, "&").replace(/</g, "<");
|
|
4856
|
+
return `<button onclick="window.dispatchEvent(new CustomEvent('open-json-modal', { detail: '${b64}' }))" class="text-text font-mono text-xs hover:underline text-left truncate w-full max-w-[250px] block cursor-pointer" title="Click to view formatted data">${safeJson}</button>`;
|
|
4857
|
+
}
|
|
4858
|
+
return `<span class="text-text">${String(val).replace(/&/g, "&").replace(/</g, "<")}</span>`;
|
|
4604
4859
|
};
|
|
4605
|
-
|
|
4606
|
-
|
|
4607
|
-
|
|
4608
|
-
|
|
4609
|
-
|
|
4610
|
-
|
|
4611
|
-
|
|
4612
|
-
|
|
4613
|
-
|
|
4614
|
-
|
|
4860
|
+
const columns = tsSchema ? Object.keys(colConfigs) : Object.keys(data[0] || {});
|
|
4861
|
+
let html = '<div class="h-full w-full overflow-auto bg-bg">';
|
|
4862
|
+
if (data.length === 0 && columns.length === 0) {
|
|
4863
|
+
html += '<div class="p-8 text-center text-muted">Table is Empty</div></div>';
|
|
4864
|
+
} else {
|
|
4865
|
+
html += '<table class="text-left border-collapse whitespace-nowrap min-w-max">';
|
|
4866
|
+
html += "<thead><tr>";
|
|
4867
|
+
html += `<th class="w-10 px-3 py-2 sticky top-0 z-10 text-center bg-panel"><input type="checkbox" @change="document.querySelectorAll('#data-${tabId} .row-checkbox').forEach(cb => cb.checked = $event.target.checked)" class="w-3.5 h-3.5 rounded border-muted bg-transparent cursor-pointer"></th>`;
|
|
4868
|
+
columns.forEach((col2) => {
|
|
4869
|
+
const colConfig = colConfigs[col2];
|
|
4870
|
+
let typeLabel = colConfig ? colConfig.dataType : "unknown";
|
|
4871
|
+
let badges = "";
|
|
4872
|
+
if (colConfig?.primaryKey) {
|
|
4873
|
+
badges += '<span class="ml-1.5 text-[9px] bg-emerald-500/20 text-emerald-400 px-1 py-0.5 rounded uppercase font-bold" title="Primary Key">PK</span>';
|
|
4874
|
+
}
|
|
4875
|
+
if (tsSchema && tsSchema.config.indexes) {
|
|
4876
|
+
const isIndexed = tsSchema.config.indexes.some((idx) => idx.columns.includes(col2));
|
|
4877
|
+
if (isIndexed)
|
|
4878
|
+
badges += '<span class="ml-1.5 text-[9px] bg-blue-500/20 text-blue-400 px-1 py-0.5 rounded uppercase font-bold" title="Indexed">IDX</span>';
|
|
4879
|
+
}
|
|
4880
|
+
if (colConfig?.unique)
|
|
4881
|
+
badges += '<span class="ml-1.5 text-[9px] bg-amber-500/20 text-amber-400 px-1 py-0.5 rounded uppercase font-bold" title="Unique">UQ</span>';
|
|
4882
|
+
if (fkColumns.has(col2))
|
|
4883
|
+
badges += '<span class="ml-1.5 text-[9px] bg-purple-500/20 text-purple-400 px-1 py-0.5 rounded uppercase font-bold" title="Foreign Key">FK</span>';
|
|
4884
|
+
html += `<th class="px-4 py-2 font-medium sticky top-0 z-10 whitespace-nowrap bg-panel text-muted hover:text-text cursor-pointer transition-colors border-b border-r border-border">
|
|
4885
|
+
<div class="flex items-center gap-2">
|
|
4886
|
+
<span class="flex items-center">${col2}${badges}</span>
|
|
4887
|
+
<span class="text-[10px] font-mono opacity-50">${typeLabel}</span>
|
|
4888
|
+
</div>
|
|
4889
|
+
</th>`;
|
|
4890
|
+
});
|
|
4891
|
+
html += "</tr></thead><tbody>";
|
|
4892
|
+
data.forEach((row) => {
|
|
4893
|
+
html += '<tr class="hover:bg-hover transition-colors">';
|
|
4894
|
+
html += '<td class="px-3 py-1.5 text-center bg-bg border-b border-r border-border"><input type="checkbox" class="row-checkbox w-3.5 h-3.5 rounded border-muted bg-transparent cursor-pointer"></td>';
|
|
4895
|
+
columns.forEach((col2) => {
|
|
4896
|
+
html += `<td class="px-4 py-1.5 max-w-[300px] overflow-hidden text-ellipsis font-mono border-b border-r border-border bg-bg">${formatValue(row[col2])}</td>`;
|
|
4897
|
+
});
|
|
4898
|
+
html += "</tr>";
|
|
4899
|
+
});
|
|
4900
|
+
html += "</tbody></table></div>";
|
|
4901
|
+
}
|
|
4902
|
+
const startRecord = total === 0 ? 0 : (page - 1) * limit + 1;
|
|
4903
|
+
const endRecord = Math.min(page * limit, total);
|
|
4904
|
+
let paginationHtml = `
|
|
4905
|
+
<div id="pagination-${tabId}" hx-swap-oob="true" class="flex items-center gap-4 text-xs text-muted font-mono">
|
|
4906
|
+
<span>${startRecord}-${endRecord} of ${total}</span>
|
|
4907
|
+
|
|
4908
|
+
<div class="flex items-center gap-3">
|
|
4909
|
+
<div class="relative flex items-center border border-border rounded bg-panel overflow-hidden group">
|
|
4910
|
+
<select
|
|
4911
|
+
class="bg-transparent text-text pl-2 pr-6 py-0.5 focus:outline-none cursor-pointer appearance-none text-center relative z-10 w-full"
|
|
4912
|
+
@change="htmx.ajax('GET', '/htmx/tables/${tableName}?schema=${reqSchema}&tabId=${tabId}&page=1&limit=' + $event.target.value, {target: '#data-${tabId}'})"
|
|
4913
|
+
>
|
|
4914
|
+
<option class="bg-bg text-text" style="background-color: #161618; color: #ededed;" value="10" ${limit === 10 ? "selected" : ""}>10</option>
|
|
4915
|
+
<option class="bg-bg text-text" style="background-color: #161618; color: #ededed;" value="25" ${limit === 25 ? "selected" : ""}>25</option>
|
|
4916
|
+
<option class="bg-bg text-text" style="background-color: #161618; color: #ededed;" value="50" ${limit === 50 ? "selected" : ""}>50</option>
|
|
4917
|
+
<option class="bg-bg text-text" style="background-color: #161618; color: #ededed;" value="100" ${limit === 100 ? "selected" : ""}>100</option>
|
|
4918
|
+
</select>
|
|
4919
|
+
<div class="absolute right-1 text-muted pointer-events-none z-0 group-hover:text-text transition-colors">
|
|
4920
|
+
<svg class="w-3 h-3" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="6 9 12 15 18 9"></polyline></svg>
|
|
4921
|
+
</div>
|
|
4922
|
+
</div>
|
|
4923
|
+
|
|
4924
|
+
<div class="flex items-center border border-border rounded bg-panel overflow-hidden">
|
|
4925
|
+
<div class="px-2 py-0.5 text-text text-center min-w-[30px]">${page}</div>
|
|
4926
|
+
<div class="px-1 py-0.5 text-muted border-l border-border text-[10px] flex items-center justify-center pointer-events-none bg-panel">
|
|
4927
|
+
of ${totalPages}
|
|
4928
|
+
</div>
|
|
4929
|
+
<button
|
|
4930
|
+
${page <= 1 ? "disabled" : ""}
|
|
4931
|
+
hx-get="/htmx/tables/${tableName}?schema=${reqSchema}&tabId=${tabId}&page=${page - 1}&limit=${limit}"
|
|
4932
|
+
hx-target="#data-${tabId}"
|
|
4933
|
+
class="px-2 py-1 hover:bg-hover hover:text-text disabled:opacity-50 disabled:cursor-not-allowed border-l border-r border-border transition-colors flex items-center justify-center"
|
|
4934
|
+
><svg class="w-3.5 h-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="15 18 9 12 15 6"></polyline></svg></button>
|
|
4935
|
+
<button
|
|
4936
|
+
${page >= totalPages ? "disabled" : ""}
|
|
4937
|
+
hx-get="/htmx/tables/${tableName}?schema=${reqSchema}&tabId=${tabId}&page=${page + 1}&limit=${limit}"
|
|
4938
|
+
hx-target="#data-${tabId}"
|
|
4939
|
+
class="px-2 py-1 hover:bg-hover hover:text-text disabled:opacity-50 disabled:cursor-not-allowed transition-colors flex items-center justify-center"
|
|
4940
|
+
><svg class="w-3.5 h-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="9 18 15 12 9 6"></polyline></svg></button>
|
|
4941
|
+
</div>
|
|
4942
|
+
</div>
|
|
4943
|
+
</div>
|
|
4944
|
+
`;
|
|
4945
|
+
return new Response(html + paginationHtml, { headers: { "Content-Type": "text/html" } });
|
|
4946
|
+
} catch (e) {
|
|
4947
|
+
return new Response(`
|
|
4948
|
+
<div class="p-6">
|
|
4949
|
+
<div class="bg-red-500/10 border border-red-500/20 text-red-400 p-4 rounded-lg">
|
|
4950
|
+
<h3 class="font-semibold mb-1">Error Loading Data</h3>
|
|
4951
|
+
<p class="text-sm opacity-80">${e.message}</p>
|
|
4615
4952
|
</div>
|
|
4616
|
-
|
|
4953
|
+
</div>
|
|
4954
|
+
`, { headers: { "Content-Type": "text/html" } });
|
|
4955
|
+
}
|
|
4956
|
+
}
|
|
4957
|
+
if (req.method === "POST" && url.pathname === "/htmx/query") {
|
|
4958
|
+
try {
|
|
4959
|
+
const body = await req.formData();
|
|
4960
|
+
const query = body.get("query")?.toString() || "";
|
|
4961
|
+
if (!query.trim()) {
|
|
4962
|
+
return new Response('<div class="p-6 text-muted">No query provided.</div>', { headers: { "Content-Type": "text/html" } });
|
|
4963
|
+
}
|
|
4964
|
+
const startMs = Date.now();
|
|
4965
|
+
const res = await db2.execute(rawSql(query));
|
|
4966
|
+
const duration2 = Date.now() - startMs;
|
|
4967
|
+
const data = Array.isArray(res) ? res : [res];
|
|
4968
|
+
if (data.length === 0 || data.length === 1 && typeof data[0] === "object" && Object.keys(data[0]).length === 0) {
|
|
4969
|
+
return new Response(`<div class="p-4 text-accent text-xs font-mono border-b border-border">Query executed successfully in ${duration2}ms. No data returned.</div>`, { headers: { "Content-Type": "text/html" } });
|
|
4617
4970
|
}
|
|
4618
4971
|
const columns = Object.keys(data[0] || {});
|
|
4619
|
-
|
|
4620
|
-
|
|
4621
|
-
|
|
4972
|
+
const formatValue = (val) => {
|
|
4973
|
+
if (val === null || val === undefined)
|
|
4974
|
+
return '<span class="italic text-muted/50">NULL</span>';
|
|
4975
|
+
if (typeof val === "object" && !(val instanceof Date)) {
|
|
4976
|
+
const rawJson = JSON.stringify(val);
|
|
4977
|
+
const b64 = Buffer.from(rawJson).toString("base64");
|
|
4978
|
+
const safeJson = rawJson.replace(/&/g, "&").replace(/</g, "<");
|
|
4979
|
+
return `<button onclick="window.dispatchEvent(new CustomEvent('open-json-modal', { detail: '${b64}' }))" class="text-text font-mono text-xs hover:underline text-left truncate w-full max-w-[250px] block cursor-pointer" title="Click to view formatted data">${safeJson}</button>`;
|
|
4980
|
+
}
|
|
4981
|
+
return `<span class="text-text">${String(val).replace(/&/g, "&").replace(/</g, "<")}</span>`;
|
|
4982
|
+
};
|
|
4983
|
+
let html = '<div class="h-full w-full overflow-auto bg-bg">';
|
|
4984
|
+
html += '<table class="text-left border-collapse whitespace-nowrap min-w-max">';
|
|
4622
4985
|
html += "<thead><tr>";
|
|
4623
4986
|
columns.forEach((col2) => {
|
|
4624
|
-
const
|
|
4625
|
-
|
|
4626
|
-
|
|
4627
|
-
|
|
4628
|
-
|
|
4629
|
-
|
|
4630
|
-
if (colConfig?.unique) {
|
|
4631
|
-
indexLabel += '<span class="ml-1 text-[10px] bg-amber-500/20 text-amber-500 px-1 rounded uppercase" title="Unique">UQ</span>';
|
|
4632
|
-
}
|
|
4633
|
-
if (colConfig?.references) {
|
|
4634
|
-
indexLabel += '<span class="ml-1 text-[10px] bg-yellow-500/20 text-yellow-500 px-1 rounded uppercase" title="Foreign Key">FK</span>';
|
|
4635
|
-
}
|
|
4636
|
-
if (schema.config.indexes) {
|
|
4637
|
-
const isIndexed = schema.config.indexes.some((idx) => idx.columns.includes(col2));
|
|
4638
|
-
if (isIndexed) {
|
|
4639
|
-
indexLabel += '<span class="ml-1 text-[10px] bg-blue-500/20 text-blue-500 px-1 rounded uppercase" title="Indexed">IDX</span>';
|
|
4640
|
-
}
|
|
4641
|
-
}
|
|
4642
|
-
html += `<th class="bg-card border-b border-r border-border px-4 py-2.5 font-medium text-muted-foreground sticky top-0 z-10">
|
|
4643
|
-
<div class="flex flex-col gap-0.5">
|
|
4644
|
-
<span class="text-foreground">${col2}</span>
|
|
4645
|
-
<div class="flex items-center flex-wrap gap-1 mt-0.5">
|
|
4646
|
-
<span class="text-[10px] font-mono text-muted-foreground/70 mr-1">${typeLabel}</span>
|
|
4647
|
-
${indexLabel}
|
|
4648
|
-
</div>
|
|
4987
|
+
const row0 = data.length > 0 ? data[0] : null;
|
|
4988
|
+
const typeLabel = row0 && row0[col2] != null ? typeof row0[col2] : "unknown";
|
|
4989
|
+
html += `<th class="px-4 py-2 font-medium sticky top-0 z-10 whitespace-nowrap bg-panel text-muted hover:text-text cursor-pointer transition-colors border-b border-r border-border">
|
|
4990
|
+
<div class="flex items-center gap-2">
|
|
4991
|
+
<span class="text-text">${col2}</span>
|
|
4992
|
+
<span class="text-[10px] font-mono opacity-50">${typeLabel}</span>
|
|
4649
4993
|
</div>
|
|
4650
4994
|
</th>`;
|
|
4651
4995
|
});
|
|
4652
4996
|
html += "</tr></thead><tbody>";
|
|
4653
4997
|
data.forEach((row) => {
|
|
4654
|
-
html += '<tr class="hover:bg-
|
|
4998
|
+
html += '<tr class="hover:bg-hover transition-colors">';
|
|
4655
4999
|
columns.forEach((col2) => {
|
|
4656
|
-
html += `<td class="
|
|
5000
|
+
html += `<td class="px-4 py-1.5 max-w-[300px] overflow-hidden text-ellipsis font-mono border-b border-r border-border bg-bg">${formatValue(row[col2])}</td>`;
|
|
4657
5001
|
});
|
|
4658
5002
|
html += "</tr>";
|
|
4659
5003
|
});
|
|
4660
5004
|
html += "</tbody></table></div>";
|
|
4661
|
-
|
|
4662
|
-
const endRecord = Math.min(page * limit, total);
|
|
4663
|
-
html += `
|
|
4664
|
-
<div class="flex items-center justify-between px-4 py-3 bg-card border-t border-border text-xs text-muted-foreground shrink-0 sticky bottom-0 z-10 w-full">
|
|
4665
|
-
<div class="flex items-center gap-4">
|
|
4666
|
-
<span>${total} records</span>
|
|
4667
|
-
<span>${startRecord}-${endRecord}</span>
|
|
4668
|
-
</div>
|
|
4669
|
-
<div class="flex items-center gap-2">
|
|
4670
|
-
<select
|
|
4671
|
-
name="limit"
|
|
4672
|
-
class="bg-background border border-border text-foreground px-2 py-1.5 rounded-md focus:outline-none focus:ring-1 focus:ring-ring cursor-pointer"
|
|
4673
|
-
@change="pageSize = $event.target.value; htmx.ajax('GET', '/htmx/tables/${tableName}?page=1&limit=' + pageSize, {target: '#data-container'})"
|
|
4674
|
-
>
|
|
4675
|
-
<option value="10" ${limit === 10 ? "selected" : ""}>10</option>
|
|
4676
|
-
<option value="25" ${limit === 25 ? "selected" : ""}>25</option>
|
|
4677
|
-
<option value="50" ${limit === 50 ? "selected" : ""}>50</option>
|
|
4678
|
-
<option value="100" ${limit === 100 ? "selected" : ""}>100</option>
|
|
4679
|
-
</select>
|
|
4680
|
-
|
|
4681
|
-
<button
|
|
4682
|
-
${page <= 1 ? "disabled" : ""}
|
|
4683
|
-
hx-get="/htmx/tables/${tableName}?page=${page - 1}&limit=${limit}"
|
|
4684
|
-
hx-target="#data-container"
|
|
4685
|
-
class="bg-background border border-border text-foreground px-3 py-1.5 rounded-md hover:bg-accent hover:text-accent-foreground disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
|
4686
|
-
>←</button>
|
|
4687
|
-
|
|
4688
|
-
<span class="min-w-[40px] text-center">${page} / ${totalPages}</span>
|
|
4689
|
-
|
|
4690
|
-
<button
|
|
4691
|
-
${page >= totalPages ? "disabled" : ""}
|
|
4692
|
-
hx-get="/htmx/tables/${tableName}?page=${page + 1}&limit=${limit}"
|
|
4693
|
-
hx-target="#data-container"
|
|
4694
|
-
class="bg-background border border-border text-foreground px-3 py-1.5 rounded-md hover:bg-accent hover:text-accent-foreground disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
|
4695
|
-
>→</button>
|
|
4696
|
-
</div>
|
|
4697
|
-
</div>
|
|
4698
|
-
`;
|
|
4699
|
-
html += "</div>";
|
|
5005
|
+
html += `<div class="px-4 py-2 text-[10px] text-muted border-t border-border font-mono shrink-0 bg-panel sticky bottom-0">${data.length} row(s) returned in ${duration2}ms</div>`;
|
|
4700
5006
|
return new Response(html, { headers: { "Content-Type": "text/html" } });
|
|
4701
5007
|
} catch (e) {
|
|
4702
5008
|
return new Response(`
|
|
4703
5009
|
<div class="p-6">
|
|
4704
5010
|
<div class="bg-red-500/10 border border-red-500/20 text-red-400 p-4 rounded-lg">
|
|
4705
|
-
<h3 class="font-semibold mb-1">Error
|
|
4706
|
-
<p class="text-sm opacity-80">${e.message}</p>
|
|
5011
|
+
<h3 class="font-semibold mb-1">Query Error</h3>
|
|
5012
|
+
<p class="text-sm opacity-80 font-mono break-words whitespace-pre-wrap">${e.message}</p>
|
|
4707
5013
|
</div>
|
|
4708
5014
|
</div>
|
|
4709
5015
|
`, { headers: { "Content-Type": "text/html" } });
|
|
4710
5016
|
}
|
|
4711
5017
|
}
|
|
4712
5018
|
if (req.method === "GET" && url.pathname === "/") {
|
|
4713
|
-
|
|
5019
|
+
const currentSchema = url.searchParams.get("schema") || config2.dbSchema || "public";
|
|
5020
|
+
return new Response(renderIndexHtml({ schemas: allSchemas, currentSchema }), {
|
|
4714
5021
|
headers: { "Content-Type": "text/html" }
|
|
4715
5022
|
});
|
|
4716
5023
|
}
|
|
@@ -4725,7 +5032,7 @@ async function runStudio(config2) {
|
|
|
4725
5032
|
// src/commands/tusky.ts
|
|
4726
5033
|
var import_picocolors11 = __toESM(require_picocolors(), 1);
|
|
4727
5034
|
async function runTusky(config) {
|
|
4728
|
-
const schemas = await loadSchemas(config.schema);
|
|
5035
|
+
const schemas = (await loadSchemas(config.schema)).filter((s) => s.type === "table");
|
|
4729
5036
|
const schemaObj = {};
|
|
4730
5037
|
for (const s of schemas) {
|
|
4731
5038
|
schemaObj[s.exportName] = s.table;
|
|
@@ -4855,7 +5162,7 @@ async function ensureDatabase2(dbUrl) {
|
|
|
4855
5162
|
// package.json
|
|
4856
5163
|
var package_default = {
|
|
4857
5164
|
name: "@bungres/kit",
|
|
4858
|
-
version: "1.
|
|
5165
|
+
version: "1.1.0",
|
|
4859
5166
|
description: "CLI toolkit for @bungres/orm \u2014 migrate, push, generate, pull",
|
|
4860
5167
|
license: "MIT",
|
|
4861
5168
|
engines: {
|
|
@@ -4870,9 +5177,10 @@ var package_default = {
|
|
|
4870
5177
|
types: "./dist/index.d.ts",
|
|
4871
5178
|
exports: {
|
|
4872
5179
|
".": {
|
|
4873
|
-
|
|
5180
|
+
types: "./dist/index.d.ts",
|
|
4874
5181
|
import: "./dist/index.js",
|
|
4875
|
-
|
|
5182
|
+
require: "./dist/index.js",
|
|
5183
|
+
default: "./dist/index.js"
|
|
4876
5184
|
}
|
|
4877
5185
|
},
|
|
4878
5186
|
files: [
|