simplemdg-dev-cli 2.4.5 → 2.6.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 +33 -0
- package/USER_GUIDE.md +57 -0
- package/dist/commands/cache.command.d.ts +2 -0
- package/dist/commands/cache.command.js +129 -0
- package/dist/commands/cache.command.js.map +1 -0
- package/dist/commands/cf.command.js +201 -122
- package/dist/commands/cf.command.js.map +1 -1
- package/dist/commands/gitlab.command.js +33 -23
- package/dist/commands/gitlab.command.js.map +1 -1
- package/dist/core/cache/smart-cache-events.d.ts +3 -0
- package/dist/core/cache/smart-cache-events.js +20 -0
- package/dist/core/cache/smart-cache-events.js.map +1 -0
- package/dist/core/cache/smart-cache-manager.d.ts +20 -0
- package/dist/core/cache/smart-cache-manager.js +148 -0
- package/dist/core/cache/smart-cache-manager.js.map +1 -0
- package/dist/core/cache/smart-cache-store.d.ts +8 -0
- package/dist/core/cache/smart-cache-store.js +74 -0
- package/dist/core/cache/smart-cache-store.js.map +1 -0
- package/dist/core/cache/smart-cache.d.ts +18 -0
- package/dist/core/cache/smart-cache.js +117 -0
- package/dist/core/cache/smart-cache.js.map +1 -0
- package/dist/core/cache/smart-cache.types.d.ts +62 -0
- package/dist/core/cache/smart-cache.types.js +17 -0
- package/dist/core/cache/smart-cache.types.js.map +1 -0
- package/dist/core/cf/cf-target-cache.d.ts +7 -0
- package/dist/core/cf/cf-target-cache.js +58 -0
- package/dist/core/cf/cf-target-cache.js.map +1 -0
- package/dist/core/cf/cf-target.types.d.ts +11 -0
- package/dist/core/cf/cf-target.types.js +11 -0
- package/dist/core/cf/cf-target.types.js.map +1 -0
- package/dist/core/db/db-studio-client.d.ts +1 -1
- package/dist/core/db/db-studio-client.js +173 -44
- package/dist/core/db/db-studio-client.js.map +1 -1
- package/dist/core/db/db-studio-server.js +125 -0
- package/dist/core/db/db-studio-server.js.map +1 -1
- package/dist/core/db/db-studio-styles.d.ts +1 -1
- package/dist/core/db/db-studio-styles.js +36 -0
- package/dist/core/db/db-studio-styles.js.map +1 -1
- package/dist/core/db/db-types.d.ts +54 -0
- package/dist/core/db/studio/sql-formatter.d.ts +25 -0
- package/dist/core/db/studio/sql-formatter.js +139 -0
- package/dist/core/db/studio/sql-formatter.js.map +1 -0
- package/dist/core/db/studio/studio-settings.d.ts +4 -0
- package/dist/core/db/studio/studio-settings.js +39 -0
- package/dist/core/db/studio/studio-settings.js.map +1 -0
- package/dist/core/db/studio/workspace-cache.d.ts +3 -0
- package/dist/core/db/studio/workspace-cache.js +51 -0
- package/dist/core/db/studio/workspace-cache.js.map +1 -0
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/commands/cache.command.ts +159 -0
- package/src/commands/cf.command.ts +232 -129
- package/src/commands/gitlab.command.ts +37 -21
- package/src/core/cache/smart-cache-events.ts +20 -0
- package/src/core/cache/smart-cache-manager.ts +169 -0
- package/src/core/cache/smart-cache-store.ts +83 -0
- package/src/core/cache/smart-cache.ts +97 -0
- package/src/core/cache/smart-cache.types.ts +79 -0
- package/src/core/cf/cf-target-cache.ts +61 -0
- package/src/core/cf/cf-target.types.ts +17 -0
- package/src/core/db/db-studio-client.ts +173 -44
- package/src/core/db/db-studio-server.ts +109 -1
- package/src/core/db/db-studio-styles.ts +36 -0
- package/src/core/db/db-types.ts +61 -0
- package/src/core/db/studio/sql-formatter.ts +139 -0
- package/src/core/db/studio/studio-settings.ts +36 -0
- package/src/core/db/studio/workspace-cache.ts +51 -0
- package/src/index.ts +3 -1
|
@@ -23,7 +23,7 @@ import {
|
|
|
23
23
|
generateSelectSql,
|
|
24
24
|
looksLikeProduction,
|
|
25
25
|
} from "./db-metadata";
|
|
26
|
-
import type { TResolvedDatabaseConnection, TTableChangeSet } from "./db-types";
|
|
26
|
+
import type { TGridSortState, TResolvedDatabaseConnection, TStudioWorkspaceState, TTableChangeSet } from "./db-types";
|
|
27
27
|
import {
|
|
28
28
|
deleteSavedQuery,
|
|
29
29
|
listSavedQueries,
|
|
@@ -32,6 +32,15 @@ import {
|
|
|
32
32
|
} from "./db-query-files";
|
|
33
33
|
import { deleteRow, insertRow, saveTableChanges, updateRow } from "./db-row";
|
|
34
34
|
import { appendQueryHistory, listQueryHistory } from "./db-query-history";
|
|
35
|
+
import { readWorkspace, writeWorkspace } from "./studio/workspace-cache";
|
|
36
|
+
import { readStudioSettings, writeStudioSettings } from "./studio/studio-settings";
|
|
37
|
+
import {
|
|
38
|
+
formatSql,
|
|
39
|
+
generateInsertTemplate,
|
|
40
|
+
generateTableQuery,
|
|
41
|
+
generateUpdateTemplate,
|
|
42
|
+
splitStatements,
|
|
43
|
+
} from "./studio/sql-formatter";
|
|
35
44
|
import {
|
|
36
45
|
detectAppDatabaseServices,
|
|
37
46
|
ensureCloudFoundrySession,
|
|
@@ -39,6 +48,7 @@ import {
|
|
|
39
48
|
importConnectionFromApp,
|
|
40
49
|
listCloudFoundryAppsWithCache,
|
|
41
50
|
} from "./db-btp";
|
|
51
|
+
import { onCacheEvent } from "../cache/smart-cache";
|
|
42
52
|
import type { TDatabaseObjectKind, TDatabaseType } from "./db-types";
|
|
43
53
|
|
|
44
54
|
export type TStudioServerOptions = {
|
|
@@ -200,6 +210,25 @@ export async function startStudioServer(options: TStudioServerOptions = {}): Pro
|
|
|
200
210
|
return;
|
|
201
211
|
}
|
|
202
212
|
|
|
213
|
+
// Server-Sent Events: stream smart-cache background-refresh notifications.
|
|
214
|
+
if (pathname === "/api/events" && method === "GET") {
|
|
215
|
+
res.writeHead(200, {
|
|
216
|
+
"content-type": "text/event-stream; charset=utf-8",
|
|
217
|
+
"cache-control": "no-cache",
|
|
218
|
+
connection: "keep-alive",
|
|
219
|
+
});
|
|
220
|
+
res.write(": connected\n\n");
|
|
221
|
+
const unsubscribe = onCacheEvent((event) => {
|
|
222
|
+
res.write(`data: ${JSON.stringify(event)}\n\n`);
|
|
223
|
+
});
|
|
224
|
+
const keepAlive = setInterval(() => res.write(": ping\n\n"), 25000);
|
|
225
|
+
req.on("close", () => {
|
|
226
|
+
clearInterval(keepAlive);
|
|
227
|
+
unsubscribe();
|
|
228
|
+
});
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
|
|
203
232
|
// --- Connections ---------------------------------------------------------
|
|
204
233
|
if (pathname === "/api/connections" && method === "GET") {
|
|
205
234
|
sendJson(res, { connections: await listPublicConnections() });
|
|
@@ -565,12 +594,91 @@ export async function startStudioServer(options: TStudioServerOptions = {}): Pro
|
|
|
565
594
|
return;
|
|
566
595
|
}
|
|
567
596
|
|
|
597
|
+
// --- Workspace + settings ------------------------------------------------
|
|
598
|
+
if (pathname === "/api/studio/workspace" && method === "GET") {
|
|
599
|
+
sendJson(res, { workspace: await readWorkspace() });
|
|
600
|
+
return;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
if (pathname === "/api/studio/workspace" && method === "PUT") {
|
|
604
|
+
const body = await readJsonBody(req);
|
|
605
|
+
const workspace = await writeWorkspace(body as unknown as TStudioWorkspaceState);
|
|
606
|
+
sendJson(res, { workspace });
|
|
607
|
+
return;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
if (pathname === "/api/studio/settings" && method === "GET") {
|
|
611
|
+
sendJson(res, { settings: await readStudioSettings() });
|
|
612
|
+
return;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
if (pathname === "/api/studio/settings" && method === "PUT") {
|
|
616
|
+
const body = await readJsonBody(req);
|
|
617
|
+
const settings = await writeStudioSettings(body);
|
|
618
|
+
sendJson(res, { settings });
|
|
619
|
+
return;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
// --- SQL helpers ---------------------------------------------------------
|
|
623
|
+
if (pathname === "/api/sql/format" && method === "POST") {
|
|
624
|
+
const body = await readJsonBody(req);
|
|
625
|
+
sendJson(res, { sql: formatSql(getString(body, "sql")) });
|
|
626
|
+
return;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
if (pathname === "/api/sql/parse-statements" && method === "POST") {
|
|
630
|
+
const body = await readJsonBody(req);
|
|
631
|
+
sendJson(res, { statements: splitStatements(getString(body, "sql")) });
|
|
632
|
+
return;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
if (pathname === "/api/sql/generate-table-query" && method === "POST") {
|
|
636
|
+
const body = await readJsonBody(req);
|
|
637
|
+
const adapter = await pool.getAdapter(getString(body, "connectionId"));
|
|
638
|
+
const sql = generateTableQuery({
|
|
639
|
+
type: adapter.type,
|
|
640
|
+
schema: getString(body, "schema"),
|
|
641
|
+
table: getString(body, "table"),
|
|
642
|
+
where: getString(body, "where") || undefined,
|
|
643
|
+
sort: Array.isArray(body.sort) ? (body.sort as TGridSortState[]) : undefined,
|
|
644
|
+
limit: getNumber(body, "limit", 100),
|
|
645
|
+
offset: getNumber(body, "offset", 0),
|
|
646
|
+
});
|
|
647
|
+
sendJson(res, { sql });
|
|
648
|
+
return;
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
if (pathname === "/api/table/generate-sql" && method === "POST") {
|
|
652
|
+
const body = await readJsonBody(req);
|
|
653
|
+
const adapter = await pool.getAdapter(getString(body, "connectionId"));
|
|
654
|
+
const schema = getString(body, "schema");
|
|
655
|
+
const table = getString(body, "table");
|
|
656
|
+
const [columns, primaryKey] = await Promise.all([
|
|
657
|
+
adapter.listColumns(schema, table).catch(() => []),
|
|
658
|
+
adapter.getPrimaryKey(schema, table).catch(() => ({ columns: [] })),
|
|
659
|
+
]);
|
|
660
|
+
sendJson(res, {
|
|
661
|
+
select: generateSelectSql(adapter.type, schema, table, getNumber(body, "limit", 100)),
|
|
662
|
+
count: generateCountSql(adapter.type, schema, table),
|
|
663
|
+
insert: generateInsertTemplate(adapter.type, schema, table, columns),
|
|
664
|
+
update: generateUpdateTemplate(adapter.type, schema, table, columns, primaryKey.columns),
|
|
665
|
+
});
|
|
666
|
+
return;
|
|
667
|
+
}
|
|
668
|
+
|
|
568
669
|
// --- History -------------------------------------------------------------
|
|
569
670
|
if (pathname === "/api/history" && method === "GET") {
|
|
570
671
|
sendJson(res, { history: await listQueryHistory(100) });
|
|
571
672
|
return;
|
|
572
673
|
}
|
|
573
674
|
|
|
675
|
+
if (pathname === "/api/history" && method === "DELETE") {
|
|
676
|
+
const { clearQueryHistory } = await import("./db-query-history");
|
|
677
|
+
await clearQueryHistory();
|
|
678
|
+
sendJson(res, { cleared: true });
|
|
679
|
+
return;
|
|
680
|
+
}
|
|
681
|
+
|
|
574
682
|
// --- Export --------------------------------------------------------------
|
|
575
683
|
if (pathname === "/api/export/csv" && method === "POST") {
|
|
576
684
|
const body = await readJsonBody(req);
|
|
@@ -218,4 +218,40 @@ table.grid tr.row-err td{box-shadow:inset 0 0 0 1px var(--red)}
|
|
|
218
218
|
.toast.ok{border-left-color:var(--green)}.toast.err{border-left-color:var(--red)}.toast.warn{border-left-color:var(--yellow)}
|
|
219
219
|
@keyframes slideup{from{transform:translateY(8px);opacity:0}to{transform:translateY(0);opacity:1}}
|
|
220
220
|
.empty{color:var(--faint);padding:16px;text-align:center}
|
|
221
|
+
mark.hl{background:#facc15;color:#10151f;border-radius:3px;padding:0 1px}
|
|
222
|
+
/* tabs: pin + drag */
|
|
223
|
+
.wtab.pinned{background:#101a2b}
|
|
224
|
+
.wtab.pinned .t-title{max-width:90px}
|
|
225
|
+
.wtab.dragging{opacity:.5}
|
|
226
|
+
.wtab.dragover{box-shadow:inset 2px 0 0 var(--accent)}
|
|
227
|
+
.wtab .pin{color:var(--amber)}
|
|
228
|
+
/* sql preview popover */
|
|
229
|
+
.popover{position:absolute;z-index:55;background:var(--bg-2);border:1px solid var(--border-2);border-radius:10px;box-shadow:0 16px 50px rgba(0,0,0,.5);padding:10px;width:560px;max-width:90vw}
|
|
230
|
+
.popover pre{margin:0;background:#0a1018;border:1px solid var(--border);border-radius:8px;padding:10px;color:#cfe7ff;font-family:Consolas,monospace;font-size:12.5px;white-space:pre-wrap;max-height:240px;overflow:auto}
|
|
231
|
+
/* change summary bar */
|
|
232
|
+
.changebar{display:flex;align-items:center;gap:10px;padding:7px 10px;background:#2a230a;border:1px solid #a16207;border-radius:9px;margin:0 10px 8px;color:#fde68a}
|
|
233
|
+
.changebar .grow{flex:1}
|
|
234
|
+
/* breadcrumb */
|
|
235
|
+
.crumbs{display:flex;align-items:center;gap:6px;padding:6px 10px;color:var(--muted);font-size:12px;border-bottom:1px solid var(--border);background:var(--bg-2)}
|
|
236
|
+
.crumbs a{color:#8fc6ff;cursor:pointer}
|
|
237
|
+
.crumbs .sep{color:var(--faint)}
|
|
238
|
+
/* command palette */
|
|
239
|
+
.palette{position:fixed;top:80px;left:50%;transform:translateX(-50%);width:560px;max-width:92vw;background:var(--bg-2);border:1px solid var(--border-2);border-radius:12px;box-shadow:0 24px 80px rgba(0,0,0,.55);z-index:85;overflow:hidden}
|
|
240
|
+
.palette input{width:100%;background:#0a1018;border:0;border-bottom:1px solid var(--border);padding:13px 15px;color:#fff;font-size:15px}
|
|
241
|
+
.palette .pitems{max-height:50vh;overflow:auto}
|
|
242
|
+
.palette .pitem{padding:9px 14px;cursor:pointer;display:flex;justify-content:space-between;gap:10px}
|
|
243
|
+
.palette .pitem.sel,.palette .pitem:hover{background:var(--accent);color:#fff}
|
|
244
|
+
.palette .pitem .kbd{color:var(--faint);font-size:11px}
|
|
245
|
+
.kbd{font-family:Consolas,monospace;background:var(--chip);border:1px solid var(--border);border-radius:5px;padding:1px 6px;font-size:11px}
|
|
246
|
+
.shorts{display:grid;grid-template-columns:1fr 1fr;gap:6px 20px}
|
|
247
|
+
.shorts .srow{display:flex;justify-content:space-between;gap:10px;padding:3px 0;border-bottom:1px solid var(--border)}
|
|
248
|
+
/* editor gutter */
|
|
249
|
+
.editwrap{display:flex;border:1px solid var(--border);border-radius:8px;overflow:hidden;background:#0a1018}
|
|
250
|
+
.gutter{padding:11px 8px;text-align:right;color:var(--faint);font-family:Consolas,monospace;font-size:13px;line-height:1.5;user-select:none;background:#0c131e;min-width:42px;overflow:hidden}
|
|
251
|
+
.editwrap .editor{border:0;border-radius:0;flex:1;line-height:1.5}
|
|
252
|
+
.ac{position:absolute;z-index:70;background:var(--bg-2);border:1px solid var(--border-2);border-radius:8px;box-shadow:0 12px 40px rgba(0,0,0,.5);max-height:220px;overflow:auto;min-width:220px}
|
|
253
|
+
.ac .aci{padding:6px 11px;cursor:pointer;display:flex;justify-content:space-between;gap:10px}
|
|
254
|
+
.ac .aci.sel,.ac .aci:hover{background:var(--accent);color:#fff}
|
|
255
|
+
.ac .aci .t{color:var(--faint);font-size:11px}
|
|
256
|
+
.toggle{display:flex;align-items:center;gap:8px;padding:6px 0}
|
|
221
257
|
`;
|
package/src/core/db/db-types.ts
CHANGED
|
@@ -221,6 +221,67 @@ export type TSaveTableChangesResult = {
|
|
|
221
221
|
rowResults: TSaveRowResult[];
|
|
222
222
|
};
|
|
223
223
|
|
|
224
|
+
export type TGridSortState = {
|
|
225
|
+
column: string;
|
|
226
|
+
direction: "asc" | "desc";
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
export type TStudioTabType = "welcome" | "sql" | "data-grid" | "metadata" | "query-file" | "history";
|
|
230
|
+
|
|
231
|
+
export type TStudioTabState = {
|
|
232
|
+
id: string;
|
|
233
|
+
type: TStudioTabType;
|
|
234
|
+
title: string;
|
|
235
|
+
groupId?: string;
|
|
236
|
+
pinned?: boolean;
|
|
237
|
+
dirty?: boolean;
|
|
238
|
+
connectionId?: string;
|
|
239
|
+
schema?: string;
|
|
240
|
+
objectName?: string;
|
|
241
|
+
objectType?: "table" | "view";
|
|
242
|
+
sql?: string;
|
|
243
|
+
filter?: string;
|
|
244
|
+
pageSize?: number;
|
|
245
|
+
pageIndex?: number;
|
|
246
|
+
sort?: TGridSortState[];
|
|
247
|
+
openedAt: string;
|
|
248
|
+
updatedAt: string;
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
export type TStudioTabGroup = {
|
|
252
|
+
id: string;
|
|
253
|
+
name: string;
|
|
254
|
+
color: string;
|
|
255
|
+
collapsed?: boolean;
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
export type TStudioLayoutState = {
|
|
259
|
+
sidebarWidth?: number;
|
|
260
|
+
readOnly?: boolean;
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
export type TStudioWorkspaceState = {
|
|
264
|
+
version: number;
|
|
265
|
+
activeTabId?: string;
|
|
266
|
+
tabs: TStudioTabState[];
|
|
267
|
+
tabGroups: TStudioTabGroup[];
|
|
268
|
+
layout: TStudioLayoutState;
|
|
269
|
+
updatedAt: string;
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
export type TStudioSettings = {
|
|
273
|
+
restoreWorkspace: boolean;
|
|
274
|
+
defaultRowLimit: number;
|
|
275
|
+
defaultSchema?: string;
|
|
276
|
+
readOnlyByDefault: boolean;
|
|
277
|
+
queryTimeoutMs: number;
|
|
278
|
+
autoFormatGeneratedSql: boolean;
|
|
279
|
+
autoSaveDelayMs: number;
|
|
280
|
+
maxHistoryItems: number;
|
|
281
|
+
showProductionWarning: boolean;
|
|
282
|
+
theme: string;
|
|
283
|
+
};
|
|
284
|
+
|
|
224
285
|
export interface IDatabaseAdapter {
|
|
225
286
|
readonly type: TDatabaseType;
|
|
226
287
|
connect(): Promise<void>;
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { buildQualifiedName, quoteIdentifier } from "../db-metadata";
|
|
2
|
+
import type { TDatabaseColumn, TDatabaseType, TGridSortState } from "../db-types";
|
|
3
|
+
|
|
4
|
+
const CLAUSE_KEYWORDS = [
|
|
5
|
+
"select", "from", "where", "group by", "having", "order by", "limit", "offset",
|
|
6
|
+
"union all", "union", "left join", "right join", "inner join", "full join", "join", "on",
|
|
7
|
+
"insert into", "values", "update", "set", "delete from", "create table", "alter table",
|
|
8
|
+
];
|
|
9
|
+
|
|
10
|
+
/** Lightweight SQL pretty printer: uppercases clause keywords and breaks lines. */
|
|
11
|
+
export function formatSql(sql: string): string {
|
|
12
|
+
let text = sql.replace(/\s+/g, " ").trim();
|
|
13
|
+
|
|
14
|
+
for (const keyword of CLAUSE_KEYWORDS) {
|
|
15
|
+
const pattern = new RegExp(`\\s*\\b${keyword.replace(/ /g, "\\s+")}\\b\\s*`, "gi");
|
|
16
|
+
text = text.replace(pattern, (match) => {
|
|
17
|
+
const upper = keyword.toUpperCase();
|
|
18
|
+
const isJoinOrOn = /join|^on$/i.test(keyword);
|
|
19
|
+
return (isJoinOrOn ? "\n " : "\n") + upper + " ";
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return text.replace(/\n\s*\n/g, "\n").replace(/[ \t]+\n/g, "\n").trim();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export type TStatementRange = { sql: string; start: number; end: number };
|
|
27
|
+
|
|
28
|
+
/** Split SQL into statements on `;`, ignoring semicolons inside strings/comments. */
|
|
29
|
+
export function splitStatementRanges(sql: string): TStatementRange[] {
|
|
30
|
+
const ranges: TStatementRange[] = [];
|
|
31
|
+
let buffer = "";
|
|
32
|
+
let bufferStart = -1;
|
|
33
|
+
let inString = false;
|
|
34
|
+
let quote = "";
|
|
35
|
+
let inLine = false;
|
|
36
|
+
let inBlock = false;
|
|
37
|
+
|
|
38
|
+
const push = (endIndex: number): void => {
|
|
39
|
+
if (buffer.trim()) {
|
|
40
|
+
ranges.push({ sql: buffer.trim(), start: bufferStart, end: endIndex });
|
|
41
|
+
}
|
|
42
|
+
buffer = "";
|
|
43
|
+
bufferStart = -1;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
for (let i = 0; i < sql.length; i += 1) {
|
|
47
|
+
const ch = sql[i];
|
|
48
|
+
const next = sql[i + 1];
|
|
49
|
+
|
|
50
|
+
if (bufferStart === -1 && !/\s/.test(ch)) {
|
|
51
|
+
bufferStart = i;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (inLine) {
|
|
55
|
+
buffer += ch;
|
|
56
|
+
if (ch === "\n") inLine = false;
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
if (inBlock) {
|
|
60
|
+
buffer += ch;
|
|
61
|
+
if (ch === "*" && next === "/") { buffer += next; i += 1; inBlock = false; }
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
if (inString) {
|
|
65
|
+
buffer += ch;
|
|
66
|
+
if (ch === quote) inString = false;
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
if (ch === "-" && next === "-") { inLine = true; buffer += ch; continue; }
|
|
70
|
+
if (ch === "/" && next === "*") { inBlock = true; buffer += ch; continue; }
|
|
71
|
+
if (ch === "'" || ch === '"') { inString = true; quote = ch; buffer += ch; continue; }
|
|
72
|
+
if (ch === ";") { push(i); continue; }
|
|
73
|
+
buffer += ch;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
push(sql.length);
|
|
77
|
+
return ranges;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function splitStatements(sql: string): string[] {
|
|
81
|
+
return splitStatementRanges(sql).map((range) => range.sql);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** The statement that contains the given cursor offset (for "Run current statement"). */
|
|
85
|
+
export function statementAtOffset(sql: string, offset: number): string {
|
|
86
|
+
const ranges = splitStatementRanges(sql);
|
|
87
|
+
const hit = ranges.find((range) => offset >= range.start && offset <= range.end + 1);
|
|
88
|
+
return (hit ?? ranges[ranges.length - 1])?.sql ?? sql.trim();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export type TGenerateTableQueryInput = {
|
|
92
|
+
type: TDatabaseType;
|
|
93
|
+
schema: string;
|
|
94
|
+
table: string;
|
|
95
|
+
where?: string;
|
|
96
|
+
sort?: TGridSortState[];
|
|
97
|
+
limit?: number;
|
|
98
|
+
offset?: number;
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
export function generateTableQuery(input: TGenerateTableQueryInput): string {
|
|
102
|
+
const qualified = buildQualifiedName(input.type, input.schema, input.table);
|
|
103
|
+
const lines = ["SELECT *", `FROM ${qualified}`];
|
|
104
|
+
|
|
105
|
+
if (input.where && input.where.trim()) {
|
|
106
|
+
lines.push(`WHERE ${input.where.trim()}`);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (input.sort && input.sort.length > 0) {
|
|
110
|
+
const order = input.sort
|
|
111
|
+
.map((sort) => `${quoteIdentifier(input.type, sort.column)} ${sort.direction === "desc" ? "DESC" : "ASC"}`)
|
|
112
|
+
.join(", ");
|
|
113
|
+
lines.push(`ORDER BY ${order}`);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (input.limit && input.limit > 0) {
|
|
117
|
+
lines.push(`LIMIT ${input.limit}`);
|
|
118
|
+
lines.push(`OFFSET ${input.offset ?? 0}`);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return `${lines.join("\n")};`;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function generateInsertTemplate(type: TDatabaseType, schema: string, table: string, columns: TDatabaseColumn[]): string {
|
|
125
|
+
const cols = columns.length > 0 ? columns : [{ name: "column1" } as TDatabaseColumn, { name: "column2" } as TDatabaseColumn];
|
|
126
|
+
const colNames = cols.map((column) => quoteIdentifier(type, column.name));
|
|
127
|
+
const placeholders = cols.map((column) => `<${column.name}>`);
|
|
128
|
+
return `INSERT INTO ${buildQualifiedName(type, schema, table)} (\n ${colNames.join(", ")}\n) VALUES (\n ${placeholders.join(", ")}\n);`;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function generateUpdateTemplate(type: TDatabaseType, schema: string, table: string, columns: TDatabaseColumn[], primaryKey: string[]): string {
|
|
132
|
+
const keyCols = primaryKey.length > 0 ? primaryKey : columns.slice(0, 1).map((column) => column.name);
|
|
133
|
+
const setCols = columns.filter((column) => !keyCols.includes(column.name));
|
|
134
|
+
const setClause = (setCols.length > 0 ? setCols : columns)
|
|
135
|
+
.map((column) => `${quoteIdentifier(type, column.name)} = <${column.name}>`)
|
|
136
|
+
.join(",\n ");
|
|
137
|
+
const whereClause = keyCols.map((column) => `${quoteIdentifier(type, column)} = <${column}>`).join("\n AND ");
|
|
138
|
+
return `UPDATE ${buildQualifiedName(type, schema, table)}\nSET\n ${setClause}\nWHERE\n ${whereClause};`;
|
|
139
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import os from "node:os";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import fs from "fs-extra";
|
|
4
|
+
import type { TStudioSettings } from "../db-types";
|
|
5
|
+
|
|
6
|
+
const SETTINGS_PATH = path.join(os.homedir(), ".simplemdg", "db-studio-settings.json");
|
|
7
|
+
|
|
8
|
+
export const DEFAULT_SETTINGS: TStudioSettings = {
|
|
9
|
+
restoreWorkspace: true,
|
|
10
|
+
defaultRowLimit: 100,
|
|
11
|
+
defaultSchema: undefined,
|
|
12
|
+
readOnlyByDefault: false,
|
|
13
|
+
queryTimeoutMs: 30000,
|
|
14
|
+
autoFormatGeneratedSql: true,
|
|
15
|
+
autoSaveDelayMs: 500,
|
|
16
|
+
maxHistoryItems: 300,
|
|
17
|
+
showProductionWarning: true,
|
|
18
|
+
theme: "dark",
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export async function readStudioSettings(): Promise<TStudioSettings> {
|
|
22
|
+
if (!(await fs.pathExists(SETTINGS_PATH))) {
|
|
23
|
+
return { ...DEFAULT_SETTINGS };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const parsed = await fs.readJson(SETTINGS_PATH).catch(() => ({})) as Partial<TStudioSettings>;
|
|
27
|
+
return { ...DEFAULT_SETTINGS, ...parsed };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function writeStudioSettings(patch: Partial<TStudioSettings>): Promise<TStudioSettings> {
|
|
31
|
+
const current = await readStudioSettings();
|
|
32
|
+
const next: TStudioSettings = { ...current, ...patch };
|
|
33
|
+
await fs.ensureDir(path.dirname(SETTINGS_PATH));
|
|
34
|
+
await fs.writeJson(SETTINGS_PATH, next, { spaces: 2 });
|
|
35
|
+
return next;
|
|
36
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import os from "node:os";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import fs from "fs-extra";
|
|
4
|
+
import type { TStudioWorkspaceState } from "../db-types";
|
|
5
|
+
|
|
6
|
+
const WORKSPACE_PATH = path.join(os.homedir(), ".simplemdg", "db-studio-workspace.json");
|
|
7
|
+
const WORKSPACE_VERSION = 1;
|
|
8
|
+
|
|
9
|
+
const EMPTY_WORKSPACE: TStudioWorkspaceState = {
|
|
10
|
+
version: WORKSPACE_VERSION,
|
|
11
|
+
activeTabId: undefined,
|
|
12
|
+
tabs: [],
|
|
13
|
+
tabGroups: [],
|
|
14
|
+
layout: {},
|
|
15
|
+
updatedAt: new Date(0).toISOString(),
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export async function readWorkspace(): Promise<TStudioWorkspaceState> {
|
|
19
|
+
if (!(await fs.pathExists(WORKSPACE_PATH))) {
|
|
20
|
+
return { ...EMPTY_WORKSPACE };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const parsed = await fs.readJson(WORKSPACE_PATH).catch(() => undefined) as Partial<TStudioWorkspaceState> | undefined;
|
|
24
|
+
|
|
25
|
+
if (!parsed || !Array.isArray(parsed.tabs)) {
|
|
26
|
+
return { ...EMPTY_WORKSPACE };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return {
|
|
30
|
+
version: WORKSPACE_VERSION,
|
|
31
|
+
activeTabId: parsed.activeTabId,
|
|
32
|
+
tabs: parsed.tabs,
|
|
33
|
+
tabGroups: Array.isArray(parsed.tabGroups) ? parsed.tabGroups : [],
|
|
34
|
+
layout: parsed.layout ?? {},
|
|
35
|
+
updatedAt: parsed.updatedAt ?? new Date().toISOString(),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export async function writeWorkspace(state: TStudioWorkspaceState): Promise<TStudioWorkspaceState> {
|
|
40
|
+
const next: TStudioWorkspaceState = {
|
|
41
|
+
version: WORKSPACE_VERSION,
|
|
42
|
+
activeTabId: state.activeTabId,
|
|
43
|
+
tabs: Array.isArray(state.tabs) ? state.tabs : [],
|
|
44
|
+
tabGroups: Array.isArray(state.tabGroups) ? state.tabGroups : [],
|
|
45
|
+
layout: state.layout ?? {},
|
|
46
|
+
updatedAt: new Date().toISOString(),
|
|
47
|
+
};
|
|
48
|
+
await fs.ensureDir(path.dirname(WORKSPACE_PATH));
|
|
49
|
+
await fs.writeJson(WORKSPACE_PATH, next, { spaces: 2 });
|
|
50
|
+
return next;
|
|
51
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -23,6 +23,7 @@ import { registerCloudFoundryCommands } from "./commands/cf.command";
|
|
|
23
23
|
import { registerCdsCommands } from "./commands/cds.command";
|
|
24
24
|
import { registerNpmrcCommands } from "./commands/npmrc.command";
|
|
25
25
|
import { registerGitLabCommands } from "./commands/gitlab.command";
|
|
26
|
+
import { registerCacheCommands } from "./commands/cache.command";
|
|
26
27
|
import { enableInteractiveNavigation, runGroupNavigator } from "./core/navigator";
|
|
27
28
|
import type { TInstallCommandOptions, TKeyValueMap } from "./types-local";
|
|
28
29
|
|
|
@@ -319,7 +320,7 @@ async function runInstallCommand(options: TInstallCommandOptions): Promise<void>
|
|
|
319
320
|
process.exitCode = installResult.exitCode;
|
|
320
321
|
}
|
|
321
322
|
|
|
322
|
-
program.name("simplemdg").description("SimpleMDG local development helper").version("2.
|
|
323
|
+
program.name("simplemdg").description("SimpleMDG local development helper").version("2.6.0");
|
|
323
324
|
|
|
324
325
|
|
|
325
326
|
program
|
|
@@ -418,6 +419,7 @@ registerCloudFoundryCommands(program);
|
|
|
418
419
|
registerCdsCommands(program);
|
|
419
420
|
registerNpmrcCommands(program);
|
|
420
421
|
registerGitLabCommands(program);
|
|
422
|
+
registerCacheCommands(program);
|
|
421
423
|
|
|
422
424
|
// Turn every group command (cf, cf db, cds, npmrc, gitlab, ...) into an
|
|
423
425
|
// interactive menu so a partial command like `smdg cf` or `smdg cf db` lists
|