admindb 1.0.3 → 1.2.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.
Files changed (45) hide show
  1. package/README.md +151 -47
  2. package/dist/app.d.ts +12 -0
  3. package/dist/app.js +7 -0
  4. package/dist/app.js.map +1 -1
  5. package/dist/args.d.ts +28 -0
  6. package/dist/args.js +187 -0
  7. package/dist/args.js.map +1 -0
  8. package/dist/cli.js +85 -15
  9. package/dist/cli.js.map +1 -1
  10. package/dist/db/database.d.ts +58 -37
  11. package/dist/db/database.js +239 -42
  12. package/dist/db/database.js.map +1 -1
  13. package/dist/db/manager.d.ts +8 -2
  14. package/dist/db/manager.js +33 -1
  15. package/dist/db/manager.js.map +1 -1
  16. package/dist/public/css/app.css +1 -1
  17. package/dist/public/css/input.css +53 -0
  18. package/dist/public/js/browse.js +847 -86
  19. package/dist/public/js/databases.js +99 -1
  20. package/dist/routes/api.js +261 -97
  21. package/dist/routes/api.js.map +1 -1
  22. package/dist/routes/databases.d.ts +10 -0
  23. package/dist/routes/databases.js +97 -0
  24. package/dist/routes/databases.js.map +1 -1
  25. package/dist/routes/pages.js +50 -30
  26. package/dist/routes/pages.js.map +1 -1
  27. package/dist/sql/generator.d.ts +0 -22
  28. package/dist/sql/generator.js +0 -48
  29. package/dist/sql/generator.js.map +1 -1
  30. package/dist/test/database.test.js +241 -63
  31. package/dist/test/database.test.js.map +1 -1
  32. package/dist/test/generator.test.js +0 -32
  33. package/dist/test/generator.test.js.map +1 -1
  34. package/dist/util.d.ts +18 -1
  35. package/dist/util.js +37 -2
  36. package/dist/util.js.map +1 -1
  37. package/dist/views/layouts/main.hbs +1 -1
  38. package/dist/views/pages/databases.hbs +41 -0
  39. package/dist/views/pages/table.hbs +81 -28
  40. package/dist/views/partials/sidebar.hbs +0 -12
  41. package/package.json +1 -1
  42. package/dist/public/js/triggers.js +0 -155
  43. package/dist/public/js/views.js +0 -176
  44. package/dist/views/pages/triggers.hbs +0 -162
  45. package/dist/views/pages/views.hbs +0 -134
package/dist/util.js CHANGED
@@ -1,7 +1,11 @@
1
1
  "use strict";
2
2
  /** Small shared helpers used across the server. */
3
+ var __importDefault = (this && this.__importDefault) || function (mod) {
4
+ return (mod && mod.__esModule) ? mod : { "default": mod };
5
+ };
3
6
  Object.defineProperty(exports, "__esModule", { value: true });
4
7
  exports.errorMessage = errorMessage;
8
+ exports.isPathWithinRoot = isPathWithinRoot;
5
9
  exports.parseFilters = parseFilters;
6
10
  exports.filtersToQS = filtersToQS;
7
11
  exports.isInternalTable = isInternalTable;
@@ -10,19 +14,38 @@ exports.decodePk = decodePk;
10
14
  exports.normalizeCell = normalizeCell;
11
15
  exports.normalizeRow = normalizeRow;
12
16
  exports.coerceFormValue = coerceFormValue;
17
+ const node_path_1 = __importDefault(require("node:path"));
13
18
  function errorMessage(err) {
14
19
  if (err instanceof Error)
15
20
  return err.message;
16
21
  return String(err);
17
22
  }
23
+ /**
24
+ * True when `p` is equal to `root` or lives inside it (both treated as
25
+ * absolute). Used to sandbox the filesystem file-browser to an allowed folder.
26
+ */
27
+ function isPathWithinRoot(root, p) {
28
+ const rel = node_path_1.default.relative(node_path_1.default.resolve(root), node_path_1.default.resolve(p));
29
+ return rel === '' || (!rel.startsWith('..') && !node_path_1.default.isAbsolute(rel));
30
+ }
18
31
  /** Parse the `f` query parameter (URL-encoded JSON object) into row filters. */
19
32
  function parseFilters(raw) {
20
33
  const out = {};
21
34
  const add = (obj) => {
22
35
  if (obj && typeof obj === 'object') {
23
36
  for (const [k, v] of Object.entries(obj)) {
24
- if (v !== undefined && v !== null && v !== '')
37
+ if (v === undefined || v === null || v === '')
38
+ continue;
39
+ if (Array.isArray(v)) {
40
+ if (v.length)
41
+ out[k] = v;
42
+ }
43
+ else if (typeof v === 'object') {
44
+ out[k] = v;
45
+ }
46
+ else {
25
47
  out[k] = String(v);
48
+ }
26
49
  }
27
50
  }
28
51
  };
@@ -43,7 +66,19 @@ function parseFilters(raw) {
43
66
  function filtersToQS(filters) {
44
67
  const out = {};
45
68
  for (const [k, v] of Object.entries(filters ?? {})) {
46
- if (v !== undefined && v !== null && String(v).trim() !== '')
69
+ if (v === undefined || v === null)
70
+ continue;
71
+ if (Array.isArray(v)) {
72
+ if (v.length)
73
+ out[k] = v;
74
+ continue;
75
+ }
76
+ if (typeof v === 'object') {
77
+ if (Object.keys(v).length)
78
+ out[k] = v;
79
+ continue;
80
+ }
81
+ if (String(v).trim() !== '')
47
82
  out[k] = String(v).trim();
48
83
  }
49
84
  const keys = Object.keys(out);
package/dist/util.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"util.js","sourceRoot":"","sources":["../src/util.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"util.js","sourceRoot":"","sources":["../src/util.ts"],"names":[],"mappings":"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"}
@@ -36,7 +36,7 @@
36
36
  {{{body}}}
37
37
  </main>
38
38
  <footer class="border-t border-base-300 px-6 py-4 text-center text-xs text-base-content/40">
39
- AdminDB · a browser tool for SQLite databases
39
+ AdminDB{{#if version}} v{{version}}{{/if}} · a browser tool for SQLite databases
40
40
  </footer>
41
41
  </div>
42
42
  <div class="drawer-side z-40">
@@ -28,6 +28,47 @@
28
28
  </p>
29
29
  </div>
30
30
 
31
+ <!-- Open an existing database file -->
32
+ {{#if allowBrowse}}
33
+ <div class="app-card p-5">
34
+ <div class="mb-3 flex flex-wrap items-center justify-between gap-2">
35
+ <h2 class="text-sm font-semibold uppercase tracking-wide text-base-content/45">Open an existing database</h2>
36
+ <span class="text-xs text-base-content/40">.db · .sqlite · .sqlite3</span>
37
+ </div>
38
+ <div class="flex flex-wrap items-center gap-2">
39
+ <input id="fs-path" class="field-input font-mono sm:!w-96" placeholder="Absolute path to a folder, e.g. C:\data" autocomplete="off" spellcheck="false" />
40
+ <button id="fs-go" type="button" class="btn btn-outline btn-sm gap-1.5">
41
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" /></svg>
42
+ Go
43
+ </button>
44
+ <button id="fs-up" type="button" class="btn btn-ghost btn-sm gap-1.5">
45
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M3 10h13M3 10l4-4m-4 4l4 4" /></svg>
46
+ Up
47
+ </button>
48
+ </div>
49
+ <div class="mt-3 overflow-hidden rounded-xl border border-base-300">
50
+ <div id="fs-current" class="border-b border-base-200 bg-base-200/50 px-3 py-2 font-mono text-xs text-base-content/60"></div>
51
+ <div id="fs-listing" class="max-h-80 divide-y divide-base-200/70 overflow-auto"></div>
52
+ </div>
53
+ <p class="mt-2.5 text-xs text-base-content/45">
54
+ Browse the filesystem to select an existing database; opening it adds it to the list above so you can
55
+ switch between databases freely.
56
+ {{#if browseRoot}}
57
+ <span class="font-medium text-base-content/60">Browsing is limited to <code class="font-mono">{{browseRoot}}</code>.</span>
58
+ {{/if}}
59
+ <span class="font-medium text-warning">This server has no authentication — treat it like a remote filesystem &amp; database shell.</span>
60
+ </p>
61
+ </div>
62
+ {{else}}
63
+ <div class="app-card p-5">
64
+ <p class="text-sm text-base-content/60">
65
+ <b class="text-base-content/80">File browsing is disabled.</b>
66
+ This server was started with specific database files, so the filesystem browser is turned off — use the
67
+ list of available databases below.
68
+ </p>
69
+ </div>
70
+ {{/if}}
71
+
31
72
  <!-- List -->
32
73
  <div>
33
74
  <div class="mb-3 flex items-center justify-between">
@@ -52,34 +52,31 @@
52
52
  </div>
53
53
  </div>
54
54
 
55
- <!-- Row filters -->
56
- <form id="filter-form" class="app-card p-4">
57
- <div class="mb-3 flex flex-wrap items-center justify-between gap-2">
55
+ <!-- Row filters (collapsed by default; auto-expands when filters are active) -->
56
+ <details class="app-card overflow-hidden" {{#if filterQS}}open{{/if}}>
57
+ <summary class="group flex cursor-pointer list-none items-center justify-between gap-2 px-4 py-3 transition hover:bg-base-200/40 [&::-webkit-details-marker]:hidden">
58
58
  <span class="flex items-center gap-2 text-sm font-semibold uppercase tracking-wide text-base-content/45">
59
59
  <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M3 4a1 1 0 011-1h16a1 1 0 011 1v2.586a1 1 0 01-.293.707l-6.414 6.414A1 1 0 0014 15.414V20a1 1 0 01-1 1h-2a1 1 0 01-1-1v-4.586a1 1 0 00-.293-.707L3.293 7.293A1 1 0 013 6.586V4z" /></svg>
60
60
  Filter rows
61
+ {{#if filterQS}}<span class="chip chip-pk">active</span>{{/if}}
61
62
  </span>
62
- <span class="text-xs text-base-content/40">Tip: <code>=exact</code> · <code>&gt;5</code> · <code>&lt;=10</code> · <code>pre*</code> starts with · plain text = contains</span>
63
- </div>
64
- <div class="grid grid-cols-2 gap-2 md:grid-cols-3 xl:grid-cols-4">
65
- {{#each colNames}}
66
- <label class="block">
67
- <span class="field-label !mb-1 font-mono">{{this}}</span>
68
- <input type="text" class="field-input filter-input !py-1.5 !text-[13px]" name="{{this}}" value="{{lookup ../filters this}}" placeholder="filter…" autocomplete="off" spellcheck="false" />
69
- </label>
70
- {{/each}}
71
- </div>
72
- <div class="mt-3 flex flex-wrap items-center gap-2">
73
- <button type="submit" class="btn btn-primary btn-sm gap-1.5">
74
- <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" /></svg>
75
- Apply filter
76
- </button>
77
- <button type="button" id="clear-filter" class="btn btn-ghost btn-sm">Clear</button>
78
- {{#if filterQS}}
79
- <span class="text-xs text-base-content/50">Filtering <b class="tabular-nums">{{count}}</b> row(s).</span>
80
- {{/if}}
81
- </div>
82
- </form>
63
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 shrink-0 text-base-content/40 transition-transform group-open:rotate-180" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M19 9l-7 7-7-7" /></svg>
64
+ </summary>
65
+ <form id="filter-form" class="border-t border-base-200 p-4">
66
+ <p class="mb-3 text-xs text-base-content/40">Filters adapt to each column's type: foreign keys, booleans, dates and numbers get dedicated controls. Text columns keep the operator syntax — <code>=exact</code> · <code>&gt;5</code> · <code>pre*</code> starts with · plain text = contains.</p>
67
+ <div id="filter-fields" class="grid grid-cols-2 gap-2 md:grid-cols-3 xl:grid-cols-4"></div>
68
+ <div class="mt-3 flex flex-wrap items-center gap-2">
69
+ <button type="submit" class="btn btn-primary btn-sm gap-1.5">
70
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" /></svg>
71
+ Apply filter
72
+ </button>
73
+ <button type="button" id="clear-filter" class="btn btn-ghost btn-sm">Clear</button>
74
+ {{#if filterQS}}
75
+ <span class="text-xs text-base-content/50">Filtering <b class="tabular-nums">{{count}}</b> row(s).</span>
76
+ {{/if}}
77
+ </div>
78
+ </form>
79
+ </details>
83
80
 
84
81
  <!-- CSV import -->
85
82
  <div id="import-panel" class="app-card hidden p-4">
@@ -113,10 +110,37 @@
113
110
  </label>
114
111
  </div>
115
112
 
113
+ <!-- Bulk selection bar -->
114
+ <div id="bulk-bar" class="hidden items-center justify-between gap-2 border-b border-base-200 bg-primary/[0.04] px-4 py-2.5">
115
+ <span class="text-sm font-medium text-base-content/80">
116
+ <b id="bulk-count" class="tabular-nums text-primary">0</b> row(s) selected
117
+ </span>
118
+ <div class="flex flex-wrap items-center gap-1.5">
119
+ <button id="bulk-export-csv" type="button" class="btn btn-ghost btn-xs gap-1 text-base-content/70 hover:text-primary">
120
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" /></svg>
121
+ Export CSV
122
+ </button>
123
+ <button id="bulk-export-json" type="button" class="btn btn-ghost btn-xs gap-1 text-base-content/70 hover:text-primary">
124
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" /></svg>
125
+ Export JSON
126
+ </button>
127
+ <button id="bulk-delete" type="button" class="btn btn-error btn-xs gap-1" {{#if readonly}}disabled{{/if}}>
128
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" /></svg>
129
+ Delete
130
+ </button>
131
+ <button id="bulk-clear" type="button" class="btn btn-ghost btn-xs text-base-content/60">Clear</button>
132
+ </div>
133
+ </div>
134
+
116
135
  <div class="overflow-x-auto">
117
136
  <table class="table table-sm table-pin-rows">
118
137
  <thead>
119
138
  <tr class="bg-base-200/70 text-xs uppercase tracking-wide text-base-content/50">
139
+ {{#if hasPk}}
140
+ <th class="w-10 text-center font-semibold">
141
+ <input type="checkbox" id="select-all-rows" class="checkbox checkbox-primary checkbox-xs" title="Select all rows on this page" />
142
+ </th>
143
+ {{/if}}
120
144
  {{#each colHeaders}}
121
145
  <th class="whitespace-nowrap font-semibold">
122
146
  <a href="{{this.href}}" class="sort-link {{#if this.active}}active{{/if}}" title="Sort by {{this.name}}">
@@ -127,17 +151,30 @@
127
151
  </th>
128
152
  {{/each}}
129
153
  <th class="text-right font-semibold">Actions</th>
154
+ {{#each refColumns}}
155
+ <th class="whitespace-nowrap text-right font-semibold" title="Rows in {{this.table}} where {{this.from}} references {{this.to}}">
156
+ <span class="font-mono normal-case">{{this.table}}</span>
157
+ </th>
158
+ {{/each}}
130
159
  </tr>
131
160
  </thead>
132
161
  <tbody>
133
162
  {{#each rows}}
134
- <tr class="group hover:bg-primary/[0.04]">
163
+ <tr class="group hover:bg-primary/[0.04]" data-table="{{../table}}" data-pk="{{this.pkEncoded}}">
164
+ {{#if ../hasPk}}
165
+ <td class="w-10 text-center align-middle">
166
+ {{#if this.pkEncoded}}
167
+ <input type="checkbox" class="row-checkbox checkbox checkbox-primary checkbox-xs" value="{{this.pkEncoded}}" aria-label="Select this row" />
168
+ {{/if}}
169
+ </td>
170
+ {{/if}}
135
171
  {{#each this.cells}}
136
- <td class="max-w-xs align-middle">
172
+ <td class="max-w-xs align-middle cell-editable {{#unless ../../readonly}}js-inline-cell{{/unless}}"
173
+ data-col="{{this.name}}" data-null="{{this.isNull}}" data-value="{{this.display}}">
137
174
  {{#if this.isNull}}
138
- <span class="chip chip-null">NULL</span>
175
+ <span class="chip chip-null js-cell-value">NULL</span>
139
176
  {{else}}
140
- <span class="block max-w-xs truncate font-mono text-[13px]" title="{{this.display}}">{{truncate this.display 120}}</span>
177
+ <span class="js-cell-value block max-w-xs truncate font-mono text-[13px]" title="{{this.display}}">{{truncate this.display 120}}</span>
141
178
  {{/if}}
142
179
  </td>
143
180
  {{/each}}
@@ -160,6 +197,21 @@
160
197
  {{/if}}
161
198
  {{/unless}}
162
199
  </td>
200
+ {{#each this.refs}}
201
+ <td class="whitespace-nowrap text-right align-middle">
202
+ {{#if this.value}}
203
+ <button type="button" class="js-ref-toggle inline-flex items-center gap-1.5 rounded-lg border border-base-300 bg-base-100 px-2 py-1 text-xs font-medium text-base-content/70 shadow-soft transition hover:border-primary/50 hover:text-primary {{#unless this.count}}opacity-50{{/unless}}"
204
+ data-table="{{../../table}}" data-pk="{{../pkEncoded}}" data-ref-table="{{this.table}}"
205
+ aria-expanded="false" title="View {{this.table}} rows where {{this.from}} = {{this.value}}">
206
+ <span class="font-mono normal-case">{{this.table}}</span>
207
+ <span class="rounded-md bg-primary/10 px-1.5 py-0.5 font-semibold tabular-nums text-primary">{{this.count}}</span>
208
+ <svg xmlns="http://www.w3.org/2000/svg" class="js-ref-chevron h-3 w-3 transition-transform" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M19 9l-7 7-7-7" /></svg>
209
+ </button>
210
+ {{else}}
211
+ <span class="text-xs text-base-content/35">&mdash;</span>
212
+ {{/if}}
213
+ </td>
214
+ {{/each}}
163
215
  </tr>
164
216
  {{/each}}
165
217
  </tbody>
@@ -200,4 +252,5 @@
200
252
  </div>
201
253
  {{/if}}
202
254
  </div>
255
+ <script type="application/json" id="browse-config">{{{json browseConfig}}}</script>
203
256
  <script src="{{basePath}}/js/browse.js" defer></script>
@@ -55,18 +55,6 @@
55
55
  Query editor
56
56
  </a>
57
57
  </li>
58
- <li>
59
- <a href="{{basePath}}/views" class="side-link {{#if (startsWith currentPath '/views')}}active{{/if}}">
60
- <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" /><path stroke-linecap="round" stroke-linejoin="round" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" /></svg>
61
- Views
62
- </a>
63
- </li>
64
- <li>
65
- <a href="{{basePath}}/triggers" class="side-link {{#if (startsWith currentPath '/triggers')}}active{{/if}}">
66
- <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M13 10V3L4 14h7v7l9-11h-7z" /></svg>
67
- Triggers
68
- </a>
69
- </li>
70
58
  {{#unless readonly}}
71
59
  <li>
72
60
  <a href="{{basePath}}/designer" class="side-link {{#if (startsWith currentPath '/designer')}}active{{/if}}">
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "admindb",
3
- "version": "1.0.3",
3
+ "version": "1.2.0",
4
4
  "description": "Browser-based SQLite database administration tool — embed it in your Express app or run it standalone.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -1,155 +0,0 @@
1
- /* Triggers page: create / drop triggers with a live SQL preview. */
2
- (function () {
3
- 'use strict';
4
- const cfgEl = document.getElementById('triggers-config');
5
- if (!cfgEl) return;
6
-
7
- function escapeHtml(s) {
8
- return (window.UI && UI.escapeHtml) ? UI.escapeHtml(s) : String(s == null ? '' : s);
9
- }
10
-
11
- function showError(el, msg) {
12
- el.classList.remove('hidden');
13
- el.innerHTML = '<div class="flex items-start gap-2.5 rounded-xl border border-error/40 bg-error/10 px-4 py-3 text-sm font-medium text-error"><span>' + escapeHtml(msg) + '</span></div>';
14
- }
15
-
16
- // ---- Editor gutter ------------------------------------------------------
17
-
18
- const editor = document.getElementById('trigger-body');
19
- const gutter = document.getElementById('trigger-gutter');
20
- function updateGutter() {
21
- if (!gutter || !editor) return;
22
- const lines = editor.value.split('\n').length;
23
- let html = '';
24
- for (let i = 1; i <= lines; i++) html += i + '\n';
25
- gutter.textContent = html;
26
- gutter.scrollTop = editor.scrollTop;
27
- }
28
- if (editor && gutter) {
29
- editor.addEventListener('scroll', () => { gutter.scrollTop = editor.scrollTop; });
30
- editor.addEventListener('input', updateGutter);
31
- updateGutter();
32
- }
33
-
34
- // ---- New trigger form ---------------------------------------------------
35
-
36
- const newBtn = document.getElementById('new-trigger');
37
- const panel = document.getElementById('new-trigger-panel');
38
- const nameInput = document.getElementById('trigger-name');
39
- const tableInput = document.getElementById('trigger-table');
40
- const timingInput = document.getElementById('trigger-timing');
41
- const eventInput = document.getElementById('trigger-event');
42
- const whenInput = document.getElementById('trigger-when');
43
- const bodyInput = document.getElementById('trigger-body');
44
- const errEl = document.getElementById('trigger-error');
45
- const createBtn = document.getElementById('create-trigger');
46
- const cancelBtn = document.getElementById('cancel-trigger');
47
- const preview = document.getElementById('trigger-preview');
48
- const previewCode = document.getElementById('trigger-preview-code');
49
- const previewHint = document.getElementById('trigger-preview-hint');
50
- let previewTimer = null;
51
-
52
- function currentEvent() {
53
- return eventInput ? eventInput.value : 'INSERT';
54
- }
55
-
56
- function renderPreview() {
57
- clearTimeout(previewTimer);
58
- const name = (nameInput.value || '').trim();
59
- const table = tableInput.value || '';
60
- const event = currentEvent();
61
- if (!name && !table && !(bodyInput.value || '').trim()) {
62
- preview.classList.add('hidden');
63
- previewCode.textContent = '';
64
- if (previewHint) previewHint.classList.remove('hidden');
65
- return;
66
- }
67
- previewTimer = setTimeout(async () => {
68
- try {
69
- const data = await Api.post('/api/triggers/generate', {
70
- name,
71
- table,
72
- timing: timingInput.value,
73
- event,
74
- when: (whenInput.value || '').trim() || undefined,
75
- body: bodyInput.value,
76
- });
77
- previewCode.textContent = data.sql;
78
- preview.classList.remove('hidden');
79
- if (previewHint) previewHint.classList.add('hidden');
80
- } catch (e) {
81
- preview.classList.add('hidden');
82
- previewCode.textContent = '';
83
- if (previewHint) previewHint.classList.remove('hidden');
84
- }
85
- }, 250);
86
- }
87
-
88
- if (newBtn && panel) {
89
- newBtn.addEventListener('click', () => {
90
- panel.classList.toggle('hidden');
91
- if (!panel.classList.contains('hidden')) {
92
- errEl.classList.add('hidden');
93
- nameInput.focus();
94
- renderPreview();
95
- }
96
- });
97
- }
98
- [nameInput, tableInput, timingInput, eventInput, whenInput, bodyInput].forEach((el) => {
99
- if (el) el.addEventListener('input', renderPreview);
100
- });
101
-
102
- if (cancelBtn) {
103
- cancelBtn.addEventListener('click', () => {
104
- panel.classList.add('hidden');
105
- errEl.classList.add('hidden');
106
- });
107
- }
108
-
109
- if (createBtn) {
110
- createBtn.addEventListener('click', async () => {
111
- errEl.classList.add('hidden');
112
- const name = (nameInput.value || '').trim();
113
- const table = tableInput.value || '';
114
- const body = (bodyInput.value || '').trim();
115
- const event = currentEvent();
116
- if (!name) return showError(errEl, 'Trigger name is required.');
117
- if (!table) return showError(errEl, 'Select a table for the trigger.');
118
- if (!body) return showError(errEl, 'Trigger body is required.');
119
- createBtn.disabled = true;
120
- try {
121
- const data = await Api.post('/api/triggers', {
122
- name,
123
- table,
124
- timing: timingInput.value,
125
- event,
126
- when: (whenInput.value || '').trim() || undefined,
127
- body,
128
- });
129
- UI.showToast(data.message || 'Trigger created.', 'success');
130
- window.location.reload();
131
- } catch (e) {
132
- showError(errEl, e.message);
133
- } finally {
134
- createBtn.disabled = false;
135
- }
136
- });
137
- }
138
-
139
- // ---- Drop trigger -------------------------------------------------------
140
-
141
- document.querySelectorAll('.trigger-drop').forEach((btn) => {
142
- btn.addEventListener('click', async () => {
143
- const name = btn.dataset.trigger;
144
- if (!name) return;
145
- if (!window.confirm('Drop trigger "' + name + '" permanently?')) return;
146
- try {
147
- const data = await Api.del('/api/triggers/' + encodeURIComponent(name));
148
- UI.showToast(data.message || 'Trigger dropped.', 'success');
149
- window.location.reload();
150
- } catch (e) {
151
- UI.showError(e.message);
152
- }
153
- });
154
- });
155
- })();