@bakery-framework/plugin-db-explorer 2.0.0-alpha.8 → 2.0.0-rc.1

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 (49) hide show
  1. package/package.json +5 -5
  2. package/src/access.ts +53 -17
  3. package/src/client/api.ts +48 -6
  4. package/src/client/bulk.ts +31 -7
  5. package/src/client/cell.ts +5 -5
  6. package/src/client/confirm.ts +11 -12
  7. package/src/client/csv-commit.ts +25 -5
  8. package/src/client/csv-map.ts +7 -7
  9. package/src/client/csv-model.ts +61 -8
  10. package/src/client/csv-pick.ts +1 -1
  11. package/src/client/csv-preview.ts +2 -2
  12. package/src/client/csv.ts +2 -2
  13. package/src/client/dom.ts +3 -5
  14. package/src/client/edit-session.ts +7 -7
  15. package/src/client/editors.ts +5 -5
  16. package/src/client/filter-builder.ts +6 -6
  17. package/src/client/fk.ts +9 -9
  18. package/src/client/grid-body.ts +4 -4
  19. package/src/client/grid-header.ts +2 -2
  20. package/src/client/grid-rowbar.ts +1 -1
  21. package/src/client/grid.ts +8 -9
  22. package/src/client/meta.ts +6 -6
  23. package/src/client/page.ts +4 -4
  24. package/src/client/panel.ts +8 -8
  25. package/src/client/relations.ts +4 -4
  26. package/src/client/save.ts +4 -4
  27. package/src/client/sidebar.ts +6 -6
  28. package/src/client/state.ts +4 -4
  29. package/src/client/statusbar.ts +6 -6
  30. package/src/client/structure.ts +8 -8
  31. package/src/client/tabs.ts +10 -10
  32. package/src/client/tabstrip.ts +3 -3
  33. package/src/client.ts +17 -17
  34. package/src/endpoints/common.ts +25 -6
  35. package/src/endpoints/graph.ts +5 -5
  36. package/src/endpoints/import.ts +5 -5
  37. package/src/endpoints/read.ts +70 -10
  38. package/src/endpoints/rows.ts +219 -14
  39. package/src/identity.ts +122 -52
  40. package/src/index.ts +1 -1
  41. package/src/policy.ts +4 -4
  42. package/src/preview.ts +5 -5
  43. package/src/setup.ts +16 -10
  44. package/src/shared/coerce.ts +21 -22
  45. package/src/shared/csv.ts +5 -5
  46. package/src/shared/filters.ts +6 -6
  47. package/src/shared/plan.ts +7 -7
  48. package/src/shell.ts +3 -3
  49. package/src/validate.ts +10 -10
@@ -1,5 +1,5 @@
1
1
  /**
2
- * The row side panel — one row, as a form.
2
+ * The row side panel: one row, as a form.
3
3
  *
4
4
  * Promoted from `drawer.ts`, which did the same job with less of it. The grid
5
5
  * is the right shape for scanning and the wrong shape for a forty-column row:
@@ -14,11 +14,11 @@
14
14
  * beats a cell for those two.
15
15
  * - **Both directions of the graph.** "References" jumps to the row this one
16
16
  * points at; "Referenced by" lists the rows pointing here.
17
- * - **Exact counts.** They used to be labelled approximate and were: the only
17
+ * - **Exact counts.** They used to be labeled approximate and were: the only
18
18
  * filter available was a substring `LIKE`, so a row with id `1` counted `11`
19
19
  * and `21` too. With `eq` the number is the number.
20
20
  *
21
- * "Referenced by" is still **lazy** — one request per referencing table, issued
21
+ * "Referenced by" is still **lazy**: one request per referencing table, issued
22
22
  * when the section is opened. Doing that for every visible row would be the
23
23
  * per-cell `fetch` that `fk.ts` exists to avoid, in a different costume.
24
24
  */
@@ -103,7 +103,7 @@ function fields(ctx: PanelContext, id: string | null): HTMLElement {
103
103
  /**
104
104
  * One column: its name, its declared type, and an editor or a read-only value.
105
105
  *
106
- * The same `createEditor` the grid opens in a cell — including the null toggle,
106
+ * The same `createEditor` the grid opens in a cell, including the null toggle,
107
107
  * which is the whole reason a 40-column row is editable here at all: several of
108
108
  * those columns are nullable, and a form of plain text inputs cannot say so.
109
109
  * `multiline` is the one difference, and it is what makes this the place long
@@ -136,7 +136,7 @@ function field(
136
136
  current,
137
137
  {
138
138
  // In the panel there is no cursor to move, so every change stages
139
- // immediately. Same buffer, same Save — the difference is only that the
139
+ // immediately. Same buffer, same Save: the difference is only that the
140
140
  // grid needs a commit key to know where to go next and this does not.
141
141
  onInput: value => {
142
142
  ctx.session.stage(id, ctx.row, column.name, value)
@@ -178,7 +178,7 @@ function footer(
178
178
  ctx.session.drop(id)
179
179
  ctx.onDirtyChange()
180
180
  message.textContent =
181
- 'reverted — reopen the row to see the stored values'
181
+ 'reverted: reopen the row to see the stored values'
182
182
  },
183
183
  { class: 'btn' },
184
184
  ),
@@ -187,7 +187,7 @@ function footer(
187
187
  }
188
188
 
189
189
  /**
190
- * The rows *this* row points at — one link per outgoing foreign key.
190
+ * The rows *this* row points at: one link per outgoing foreign key.
191
191
  *
192
192
  * Not lazy, because it costs nothing: the key is already in the row, so the
193
193
  * link is built from data in hand and only the click spends a request. A key
@@ -235,7 +235,7 @@ function referenceRow(ctx: PanelContext, relation: Relation): HTMLElement {
235
235
  /**
236
236
  * Tables that point at this row, resolved when the section is opened.
237
237
  *
238
- * The counts are **exact**. They used to be labelled approximate and were —
238
+ * The counts are **exact**. They used to be labeled approximate and were:
239
239
  * the only filter the table-data endpoint offered was `col LIKE '%value%'`, so
240
240
  * a row with id `1` matched `11` and `21` too. `eq` removed the caveat along
241
241
  * with the reason for it.
@@ -20,7 +20,7 @@ import type { ForeignKeyInfo, SchemaGraph } from './meta'
20
20
  import { sameTable } from './meta'
21
21
 
22
22
  export interface Relation {
23
- /** The columns on the near side — this table's, for `outgoing`. */
23
+ /** The columns on the near side: this table's, for `outgoing`. */
24
24
  cols: string[]
25
25
  /** The table at the other end. */
26
26
  table: string
@@ -43,10 +43,10 @@ export interface Relations {
43
43
  * programming: `getForeignKeys()` reports whatever the database calls a table
44
44
  * while `getConstraints()` is camel-keyed, and the graph endpoint passes both
45
45
  * through untouched. Comparing literally makes every foreign key on a
46
- * `snake_case` schema invisible — the whole feature silently absent, which is
46
+ * `snake_case` schema invisible: the whole feature silently absent, which is
47
47
  * exactly what `meta.ts`'s own note on `sameTable` records.
48
48
  *
49
- * A self-reference — a `manager_id` pointing at the same table's `id` — appears
49
+ * A self-reference (a `manager_id` pointing at the same table's `id`) appears
50
50
  * in **both** lists, which is correct: it is genuinely a key this table
51
51
  * declares and genuinely a key that points here.
52
52
  */
@@ -189,7 +189,7 @@ function incomingRow(ctx: RelationsContext, relation: Relation): HTMLElement {
189
189
  * The filters that open the rows of `relation.table` pointing at one row.
190
190
  *
191
191
  * Exported for the row panel, which is where "referenced by *this row*" lives.
192
- * `eq` per column, so the count and the page are exact — this used to be a
192
+ * `eq` per column, so the count and the page are exact: this used to be a
193
193
  * substring `LIKE` and the panel had to label its counts "approximate".
194
194
  */
195
195
  export function filtersForIncoming(
@@ -43,7 +43,7 @@ function kindLookup(
43
43
  *
44
44
  * The pre-image goes as `expect`, so the statement is `identity ∧ expect` and a
45
45
  * concurrent edit is a 409 rather than a silent overwrite. `force` is sent only
46
- * when a changed column is `json` or `buffer` — kinds SQL cannot compare, so
46
+ * when a changed column is `json` or `buffer`: kinds SQL cannot compare, so
47
47
  * there is no predicate to guard them with.
48
48
  */
49
49
  export async function saveRow(
@@ -113,8 +113,8 @@ async function reportFailure(
113
113
  /**
114
114
  * Keep mine / Take theirs, with the server's copy of the row in hand.
115
115
  *
116
- * The 409 body carries the row as it now stands — that is why the endpoint
117
- * attaches it — so this is a real choice rather than a prompt to guess.
116
+ * The 409 body carries the row as it now stands (that is why the endpoint
117
+ * attaches it), so this is a real choice rather than a prompt to guess.
118
118
  */
119
119
  function offerResolution(
120
120
  table: SchemaTable,
@@ -172,7 +172,7 @@ function offerResolution(
172
172
  /**
173
173
  * Keep mine: the same `set`, with `expect` taken from *their* row.
174
174
  *
175
- * Not `expect: {}` — that is last-write-wins, which the endpoint refuses to
175
+ * Not `expect: {}`. That is last-write-wins, which the endpoint refuses to
176
176
  * default to for exactly this reason. Rebasing on the row that was just shown
177
177
  * means a *third* edit arriving between the 409 and this retry is a 409 again,
178
178
  * which is the correct answer rather than an inconvenience.
@@ -6,7 +6,7 @@
6
6
  * everything else and had no business sharing a module with the grid's chrome.
7
7
  *
8
8
  * A **single click opens a preview tab** and a **double click makes it
9
- * permanent** — the two are wired here and defined in `tabs.ts`.
9
+ * permanent**: the two are wired here and defined in `tabs.ts`.
10
10
  */
11
11
 
12
12
  import { append, box, button, each, el, on } from './dom'
@@ -46,7 +46,7 @@ export function renderSidebar(ctx: SidebarContext): HTMLElement {
46
46
  * The system-tables checkbox, present only when there is something to reveal.
47
47
  *
48
48
  * `__bakery_schema` is the ORM's sync ledger and is not the user's data, so it
49
- * is hidden by default — but every real client offers the toggle rather than
49
+ * is hidden by default, but every real client offers the toggle rather than
50
50
  * hiding such tables outright, because a ledger row is occasionally exactly
51
51
  * what someone needs to see. The count is in the label so the checkbox says
52
52
  * what it would do before it is clicked.
@@ -65,7 +65,7 @@ function systemToggle(ctx: SidebarContext): HTMLElement | null {
65
65
  const label = el('label', {
66
66
  class: 'note system-toggle',
67
67
  text: ` show system tables (${hidden})`,
68
- title: "the framework's own bookkeeping — the ORM sync ledger",
68
+ title: "the framework's own bookkeeping: the ORM sync ledger",
69
69
  })
70
70
  label.prepend(check)
71
71
  return label
@@ -76,8 +76,8 @@ function tableButton(ctx: SidebarContext, table: SchemaTable): HTMLElement {
76
76
  class: 'table-btn',
77
77
  })
78
78
  // `dblclick` fires *after* its two `click`s, so the single click has already
79
- // opened the preview tab and this only promotes it — which is exactly VS
80
- // Code's behaviour and needs no suppression of the first click.
79
+ // opened the preview tab and this only promotes it, which is exactly VS
80
+ // Code's behavior and needs no suppression of the first click.
81
81
  on(node, 'dblclick', () => ctx.onOpen(table.name))
82
82
 
83
83
  if (table.name === ctx.activeTable) node.classList.add('active')
@@ -96,7 +96,7 @@ function badgeFor(table: SchemaTable): HTMLElement | null {
96
96
  return el('span', {
97
97
  class: 'ro',
98
98
  text: '⊞',
99
- title: 'a view — it has no rows of its own to address',
99
+ title: 'a view: it has no rows of its own to address',
100
100
  })
101
101
  }
102
102
  if (!table.writable) {
@@ -9,7 +9,7 @@
9
9
  * The view lives in the **hash** rather than the query string on purpose. The
10
10
  * explorer is reached with `?db-key=…` on a first visit, the client scrubs that
11
11
  * parameter out of the URL immediately, and a view state sharing the query
12
- * string would be scrubbed along with it — or worse, would have to be
12
+ * string would be scrubbed along with it, or worse, would have to be
13
13
  * reconstructed by an edit to the same `URLSearchParams` the credential was
14
14
  * just removed from. A hash also never reaches the server, which is the right
15
15
  * place for "which row am I on".
@@ -34,7 +34,7 @@ export interface ViewState {
34
34
  sortBy: string | null
35
35
  sortOrder: 'ASC' | 'DESC'
36
36
  /**
37
- * Column, operator and operand — several, each removable.
37
+ * Column, operator and operand: several, each removable.
38
38
  *
39
39
  * A **list** rather than a record, because the builder lets a filter exist
40
40
  * before it has a column or a value and a record cannot hold a half-built
@@ -62,7 +62,7 @@ export function defaultView(table = ''): ViewState {
62
62
  * omitted rather than written, so the common case is `t=parcels`.
63
63
  *
64
64
  * There is no `r` any more. It carried a row identity because a filter could
65
- * not name a row — `id=1` matched `11` under a substring `LIKE` — and with `eq`
65
+ * not name a row (`id=1` matched `11` under a substring `LIKE`), and with `eq`
66
66
  * available a foreign-key jump is just a filter. An old link with `r=` decodes
67
67
  * to the same page it always did, minus the highlight.
68
68
  */
@@ -159,7 +159,7 @@ export function tableOf(
159
159
  *
160
160
  * Matched against the **raw** database name, which is what the schema report
161
161
  * carries: `introspect()` builds from `getSchema()`, and that speaks raw names.
162
- * (`getConstraints()` camel-cases, so the same table is `bakerySchema` there —
162
+ * (`getConstraints()` camel-cases, so the same table is `bakerySchema` there:
163
163
  * that spelling never reaches this client, and matching it here would risk
164
164
  * hiding a user table that happens to be called `bakerySchema`.)
165
165
  */
@@ -5,7 +5,7 @@
5
5
  * Every database client has one, and it earns its space by answering three
6
6
  * questions that otherwise need a round trip through the developer tools: how
7
7
  * many rows there really are, how long the server took, and whether this
8
- * session can write at all. The last one is the reason it exists here — the
8
+ * session can write at all. The last one is the reason it exists here: the
9
9
  * access level used to be a line of sidebar text that scrolled away.
10
10
  *
11
11
  * `statusParts` is **pure** and is where the wording lives, so the segments are
@@ -30,7 +30,7 @@ export interface StatusFacts {
30
30
  /**
31
31
  * The segments, in order, already worded.
32
32
  *
33
- * Absent facts are **omitted rather than rendered as a placeholder** — a
33
+ * Absent facts are **omitted rather than rendered as a placeholder**: a
34
34
  * `page 1 / ?` teaches nobody anything, and `getData` genuinely does not always
35
35
  * return a total. An empty array is the honest answer for "nothing is open".
36
36
  */
@@ -70,7 +70,7 @@ function formatCount(count: number): string {
70
70
  * Sub-millisecond timings keep one decimal.
71
71
  *
72
72
  * `getElapsed` returns a float, and a fast query rounding to `0 ms` reads as
73
- * "not measured" rather than "fast" — which is the opposite of what it means.
73
+ * "not measured" rather than "fast", which is the opposite of what it means.
74
74
  */
75
75
  function formatMs(ms: number): string {
76
76
  return ms < 10 ? ms.toFixed(1) : String(Math.round(ms))
@@ -89,7 +89,7 @@ export class StatusBar {
89
89
  * The last facts painted.
90
90
  *
91
91
  * Kept so `bumpDirty` can repaint one segment without the caller having to
92
- * reconstruct the row count, the page and the timing — which it cannot,
92
+ * reconstruct the row count, the page and the timing, which it cannot,
93
93
  * since a staged edit does not re-fetch and the alternative would be showing
94
94
  * a stale count or none.
95
95
  */
@@ -122,8 +122,8 @@ export class StatusBar {
122
122
 
123
123
  private render(): void {
124
124
  this.line.textContent = statusParts(this.facts).join(' · ')
125
- // The dirty count is the one segment worth colouring, and it is always
126
- // last — so the class goes on the bar rather than on a span nobody can see
125
+ // The dirty count is the one segment worth coloring, and it is always
126
+ // last, so the class goes on the bar rather than on a span nobody can see
127
127
  // when the bar is empty.
128
128
  this.node.classList.toggle('dirty', this.facts.dirtyRows > 0)
129
129
  }
@@ -1,13 +1,13 @@
1
1
  /**
2
2
  * The Structure view: what the table *is*, as opposed to what is in it.
3
3
  *
4
- * Every serious client — DBeaver, DataGrip, Devart — splits a table into Data
4
+ * Every serious client (DBeaver, DataGrip, Devart) splits a table into Data
5
5
  * and its structure, and keeps them apart. This one was the single biggest
6
6
  * omission: `GET /api/_db/schema` has always returned the type, nullability,
7
7
  * default, enum values, auto-increment flag and resolved row identity of every
8
8
  * column, and the client used all of it *only* to pick which editor widget to
9
9
  * open. None of it was ever on screen. The reason a table is read-only was the
10
- * worst of it — it existed, in the server's own words, and appeared nowhere
10
+ * worst of it: it existed, in the server's own words, and appeared nowhere
11
11
  * except a tooltip on a padlock.
12
12
  *
13
13
  * `structureRows` is **pure** and is the whole derivation, so what each column
@@ -22,7 +22,7 @@ export interface StructureRow {
22
22
  name: string
23
23
  type: string
24
24
  nullable: string
25
- /** `AUTO`, a literal `DEFAULT`, or empty — never a guess. */
25
+ /** `AUTO`, a literal `DEFAULT`, or empty, never a guess. */
26
26
  default: string
27
27
  /** `PK`, `unique`, or empty. */
28
28
  key: string
@@ -40,7 +40,7 @@ export interface StructureRow {
40
40
  *
41
41
  * - **`hasDefault` and `autoIncrement` are different facts**, and an
42
42
  * auto-increment column is reported as such rather than as "has a default".
43
- * They differ in the one place it matters — `omittableOnInsert` — and
43
+ * They differ in the one place it matters (`omittableOnInsert`), and
44
44
  * conflating them on screen would teach the reader the wrong model.
45
45
  * - **`notnull` is not the negation of `nullable` here.** The server sends
46
46
  * both, from two different introspection calls (`getConstraints()` when it
@@ -114,8 +114,8 @@ const INDEX_HEADINGS = ['index', 'type', 'columns'] as const
114
114
  /**
115
115
  * A scrolling table of plain text cells.
116
116
  *
117
- * Columns and indexes are the same table twice — headings, then a `<tr>` of
118
- * `<td class="cell">` per row — so they are one builder and two `string[][]`.
117
+ * Columns and indexes are the same table twice (headings, then a `<tr>` of
118
+ * `<td class="cell">` per row), so they are one builder and two `string[][]`.
119
119
  */
120
120
  function dataTable(
121
121
  headings: readonly string[],
@@ -150,7 +150,7 @@ export function renderStructure(ctx: StructureContext): HTMLElement {
150
150
  }
151
151
 
152
152
  /**
153
- * How a row of this table is named — and, when it cannot be, why.
153
+ * How a row of this table is named, and, when it cannot be, why.
154
154
  *
155
155
  * First rather than last, and a banner rather than a footnote. "This table is
156
156
  * read-only" with no reason is the message that generates support questions;
@@ -165,7 +165,7 @@ function identitySection(ctx: StructureContext): HTMLElement {
165
165
  section.appendChild(
166
166
  el('p', {
167
167
  class: 'banner warn',
168
- text: `no row of this table can be named — ${ctx.reason}`,
168
+ text: `no row of this table can be named: ${ctx.reason}`,
169
169
  }),
170
170
  )
171
171
  return section
@@ -1,19 +1,19 @@
1
1
  /**
2
2
  * Open tables as tabs, with VS Code's preview semantics.
3
3
  *
4
- * **Pure — no DOM.** Every transition is a function from a state to a state, so
4
+ * **Pure, no DOM.** Every transition is a function from a state to a state, so
5
5
  * the rules below are asserted in `tabs.test.ts` rather than inferred from a
6
6
  * click handler. `tabstrip.ts` is the view over this.
7
7
  *
8
- * The behaviour is Supabase Studio's, which is VS Code's:
8
+ * The behavior is Supabase Studio's, which is VS Code's:
9
9
  *
10
- * - A **single click** opens a *preview* tab — rendered italic, and **replaced
10
+ * - A **single click** opens a *preview* tab: rendered italic, and **replaced
11
11
  * in place** by the next single click. Browsing twelve tables therefore
12
12
  * leaves one tab open, not twelve, which is the whole point.
13
13
  * - **Double-clicking**, or **editing**, makes it permanent. Investment is what
14
14
  * promotes a tab; merely looking at it is not.
15
15
  * - A table that is already open is *selected* rather than reopened, and its
16
- * own view state — page, sort, filters — is left exactly as it was. That is
16
+ * own view state (page, sort, filters) is left exactly as it was. That is
17
17
  * the property that makes tabs worth having: switching back restores.
18
18
  *
19
19
  * There is at most one preview tab, which is what makes "replace the preview"
@@ -58,8 +58,8 @@ function previewIndex(state: TabsState): number {
58
58
  /**
59
59
  * Single click: open as preview.
60
60
  *
61
- * Three cases in order, and the order is the behaviour. An already-open table
62
- * wins over everything — reopening it would throw away the page and filters the
61
+ * Three cases in order, and the order is the behavior. An already-open table
62
+ * wins over everything: reopening it would throw away the page and filters the
63
63
  * user left it on, which is precisely what tabs exist to preserve.
64
64
  */
65
65
  export function openPreview(state: TabsState, view: ViewState): TabsState {
@@ -109,8 +109,8 @@ export function selectTab(state: TabsState, index: number): TabsState {
109
109
  /**
110
110
  * Close one tab.
111
111
  *
112
- * Closing the active tab activates whatever slid into its slot — the tab to the
113
- * right — and the last tab when there is nothing to the right. Closing anything
112
+ * Closing the active tab activates whatever slid into its slot (the tab to the
113
+ * right), and the last tab when there is nothing to the right. Closing anything
114
114
  * left of the active one shifts the active index down so the *same* tab stays
115
115
  * selected, which is the bug the naive version has.
116
116
  */
@@ -172,7 +172,7 @@ export function pruneTabs(
172
172
  /**
173
173
  * The whole strip in a hash.
174
174
  *
175
- * `x` repeats, one per tab, each holding a whole encoded view — `URLSearchParams`
175
+ * `x` repeats, one per tab, each holding a whole encoded view: `URLSearchParams`
176
176
  * escapes the inner `&` on the way in and unescapes it on the way out, so the
177
177
  * nesting needs no encoding of its own. `a` is the active index and `w` the
178
178
  * preview one; both are indexes rather than flags because there is at most one
@@ -191,7 +191,7 @@ export function decodeTabs(hash: string): TabsState {
191
191
  const params = new URLSearchParams(hash.replace(/^#/, ''))
192
192
  const encoded = params.getAll('x')
193
193
 
194
- // A pre-tabs link — `#t=parcels&p=2` — is one permanent tab. Those links are
194
+ // A pre-tabs link (`#t=parcels&p=2`) is one permanent tab. Those links are
195
195
  // in chat logs and bookmarks, and silently rendering nothing for them would
196
196
  // be the most visible possible regression.
197
197
  if (!encoded.length) {
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * The tab strip, and the view switcher under it.
3
3
  *
4
- * Two rows, and **exactly two** — the table tabs, then Data / Structure /
4
+ * Two rows, and **exactly two**: the table tabs, then Data / Structure /
5
5
  * Relations for whichever table is active. Beekeeper sells itself on not having
6
6
  * "tabs within tabs", and it is right: one level of nesting is navigable and
7
7
  * two is a maze. Nothing below this line gets its own strip.
@@ -19,7 +19,7 @@ export interface TabStripContext {
19
19
  /** Double-click on a preview tab: keep it. */
20
20
  onPromote: (index: number) => void
21
21
  onClose: (index: number) => void
22
- /** The `+` button — back to the table picker. */
22
+ /** The `+` button: back to the table picker. */
23
23
  onNew: () => void
24
24
  }
25
25
 
@@ -50,7 +50,7 @@ function tabNode(ctx: TabStripContext, index: number): HTMLElement {
50
50
 
51
51
  const label = button(tab.view.table, () => ctx.onSelect(index), {
52
52
  class: 'tab-label',
53
- title: tab.preview ? `${tab.view.table} — preview` : tab.view.table,
53
+ title: tab.preview ? `${tab.view.table}: preview` : tab.view.table,
54
54
  })
55
55
  on(label, 'dblclick', () => ctx.onPromote(index))
56
56
  node.appendChild(label)
package/src/client.ts CHANGED
@@ -1,23 +1,23 @@
1
1
  /**
2
2
  * The explorer's browser entry: boot, then wiring. Nothing else.
3
3
  *
4
- * This file used to be the whole client — 197 lines, one `renderTable` that
4
+ * This file used to be the whole client: 197 lines, one `renderTable` that
5
5
  * fetched, built a header, built a body and built a pager, and scored **34**
6
6
  * against biome's ceiling of 25. Growing that into a data editor would have
7
7
  * meant growing that one function, so the shape changed first. The pieces live
8
- * under `client/`, each obeying two mechanical rules — **no function both
9
- * fetches and renders**, and **every loop body is a named function** — and what
8
+ * under `client/`, each obeying two mechanical rules (**no function both
9
+ * fetches and renders**, and **every loop body is a named function**), and what
10
10
  * is left here is the composition: state, routing, and which callback goes
11
11
  * where.
12
12
  *
13
13
  * The layout is the one a database client is expected to have: a table list, a
14
14
  * strip of table tabs with VS Code preview semantics, Data / Structure /
15
- * Relations under it — **one level of nesting and no more** — and a status bar.
15
+ * Relations under it (**one level of nesting and no more**), and a status bar.
16
16
  * Tab state lives in `client/tabs.ts` and is pure; this module owns the hash.
17
17
  *
18
18
  * What is deliberately *not* here, and is not anywhere: a SQL console, an ER
19
- * diagram, and grid virtualisation. The first is refused by the plugin's
20
- * contract — no raw SQL, structurally — and the other two are not what a row
19
+ * diagram, and grid virtualization. The first is refused by the plugin's
20
+ * contract (no raw SQL, structurally), and the other two are not what a row
21
21
  * editor is for.
22
22
  */
23
23
 
@@ -119,7 +119,7 @@ function writeHash(): void {
119
119
  selfNavigation = true
120
120
  location.hash = encodeTabs(tabs)
121
121
  // `hashchange` fires as a task, so the flag has to survive at least until
122
- // the next one — a microtask would clear it before the listener ran.
122
+ // the next one: a microtask would clear it before the listener ran.
123
123
  setTimeout(() => {
124
124
  selfNavigation = false
125
125
  }, 0)
@@ -137,7 +137,7 @@ async function confirmDiscard(): Promise<boolean> {
137
137
  const ok = await confirmChoice({
138
138
  verb: 'discard',
139
139
  count: session.dirtyRows(),
140
- table: currentTable() || '—',
140
+ table: currentTable() || '-',
141
141
  detail: 'unsaved edits will be thrown away',
142
142
  })
143
143
  if (ok) session.clear()
@@ -202,8 +202,8 @@ function switchView(view: TableView): void {
202
202
  *
203
203
  * The slots matter: `renderChrome` repaints the sidebar and the strip in
204
204
  * place, and `#main` is not one of them. Rebuilding the whole shell on every
205
- * tab-strip change would tear down the grid — including any editor open in it
206
- * and the focus inside that editor — every time a staged edit promoted a
205
+ * tab-strip change would tear down the grid (including any editor open in it
206
+ * and the focus inside that editor) every time a staged edit promoted a
207
207
  * preview tab, which is precisely when it must not.
208
208
  */
209
209
  const sideSlot = el('div', { class: 'side-slot' })
@@ -263,7 +263,7 @@ async function renderAll(): Promise<void> {
263
263
  await renderMain()
264
264
  }
265
265
 
266
- /** Fetch, then render — the two never live in one function. */
266
+ /** Fetch, then render: the two never live in one function. */
267
267
  async function renderMain(): Promise<void> {
268
268
  const main = mainSlot
269
269
  const view = activeView(tabs)
@@ -282,7 +282,7 @@ async function renderMain(): Promise<void> {
282
282
  }
283
283
 
284
284
  // Structure and Relations render from the schema report the client already
285
- // holds — no request, so no loading state and no failure path.
285
+ // holds: no request, so no loading state and no failure path.
286
286
  if (view.view !== 'data') {
287
287
  page.paintMeta(main, view, table)
288
288
  return
@@ -318,7 +318,7 @@ function openRowPanel(
318
318
  handle.close()
319
319
  save(table, id)
320
320
  },
321
- // A real edit is what makes a preview tab permanent — VS Code's rule, and
321
+ // A real edit is what makes a preview tab permanent: VS Code's rule, and
322
322
  // the one that matters: nobody wants the tab they just typed into replaced
323
323
  // by the next single click in the sidebar.
324
324
  onDirtyChange: () => {
@@ -333,14 +333,14 @@ function openRowPanel(
333
333
  /**
334
334
  * Follow a foreign key.
335
335
  *
336
- * One `eq` filter per referenced column, which *is* the row identity — so the
336
+ * One `eq` filter per referenced column, which *is* the row identity, so the
337
337
  * destination page holds exactly the referenced row. This used to need a
338
338
  * second mechanism: `filters` was a substring `LIKE`, `id=1` also matched `11`,
339
339
  * so a link carried a separate `focus` identity and the grid highlighted it.
340
340
  * `eq` removed the need and `focus` went with it.
341
341
  *
342
342
  * The current view is pushed first, so Back returns to where the reference was
343
- * followed from — the breadcrumb, which stays.
343
+ * followed from: the breadcrumb, which stays.
344
344
  */
345
345
  async function followFk(
346
346
  target: FkTarget,
@@ -370,14 +370,14 @@ async function boot(): Promise<void> {
370
370
  adoptUrlKey()
371
371
  try {
372
372
  // `{access, tables}`: the client has to know its posture *before* it
373
- // renders, so it never draws an edit affordance it cannot honour.
373
+ // renders, so it never draws an edit affordance it cannot honor.
374
374
  state.report = await fetchSchema()
375
375
  } catch (error) {
376
376
  app.replaceChildren(el('p', { class: 'error', text: messageOf(error) }))
377
377
  return
378
378
  }
379
379
 
380
- // The graph is decoration — a schema with no declared foreign keys is
380
+ // The graph is decoration: a schema with no declared foreign keys is
381
381
  // ordinary, and a failure here must not cost anyone the grid.
382
382
  state.graph = await fetchGraph().catch(() => null)
383
383
 
@@ -2,11 +2,11 @@
2
2
  * The preamble every write endpoint shares, in the order the checks have to
3
3
  * happen.
4
4
  *
5
- * 1. **`currentCanWrite()`** — a `read` caller is refused before the body is
5
+ * 1. **`currentCanWrite()`**: a `read` caller is refused before the body is
6
6
  * even parsed, so nothing about the request can influence the answer.
7
- * 2. **The body** — JSON, an object, naming a table.
8
- * 3. **The table** — 404 if it is not there.
9
- * 4. **The identity** — 409 if the table has none. A table with no primary
7
+ * 2. **The body**: JSON, an object, naming a table.
8
+ * 3. **The table**: 404 if it is not there.
9
+ * 4. **The identity**: 409 if the table has none. A table with no primary
10
10
  * key and no all-NOT-NULL unique index is read-only for everyone,
11
11
  * including a `write` caller, because there is no way to name one of its
12
12
  * rows. See `identity.ts`.
@@ -15,6 +15,7 @@
15
15
  * are about the request's own shape rather than about who is asking.
16
16
  */
17
17
 
18
+ import { errorMsg, pluginLog } from '@bakery-framework/core/logger'
18
19
  import { Case, Try } from '@bakery-framework/core/utils'
19
20
  import type { JsonResponseData } from '@bakery-framework/core/utils/common'
20
21
  import { response } from '@bakery-framework/core/utils/http'
@@ -37,7 +38,7 @@ export function invalid(errors: FieldError[]): Envelope {
37
38
  * The request body as an object, or `null`.
38
39
  *
39
40
  * `req.json()` rather than core's `processBody`, which answers `{}` for a body
40
- * it could not parse — indistinguishable from an empty one, so a truncated
41
+ * it could not parse: indistinguishable from an empty one, so a truncated
41
42
  * upload would be reported as a missing `table` field.
42
43
  */
43
44
  export async function readBody(
@@ -55,7 +56,7 @@ export async function readBody(
55
56
  /**
56
57
  * Find a table by the name the caller used.
57
58
  *
58
- * Raw database name first — that is what `/api/_db/schema` renders and what the
59
+ * Raw database name first: that is what `/api/_db/schema` renders and what the
59
60
  * grid sends back. The camel spelling is accepted as well, because a script
60
61
  * written against a typed schema has `orderItems` where the database has
61
62
  * `order_items`, and refusing that would be refusing the ORM's own vocabulary.
@@ -120,3 +121,21 @@ export async function beginWrite(req: Request): Promise<WriteStart> {
120
121
 
121
122
  return { ok: true, table, body }
122
123
  }
124
+
125
+ /**
126
+ * Refuse a request without telling the caller what the database said.
127
+ *
128
+ * Every read endpoint used to answer `400` with `error.message` verbatim, so a
129
+ * malformed `page` came back as SQLite's "datatype mismatch" and a bad lookup
130
+ * key as a JavaScript `TypeError` naming an internal expression. Postgres is
131
+ * worse: its parse errors quote the statement. A caller holding only `read`
132
+ * cannot be handed the query text, and none of it helps a client that has
133
+ * already been told its request was invalid.
134
+ *
135
+ * The message still exists: it goes to the server log with the operation that
136
+ * produced it, which is where an operator can act on it.
137
+ */
138
+ export function refuse(op: string, error: unknown, status = 400): Envelope {
139
+ pluginLog.EXPLORER_QUERY_ERR({ op, error: errorMsg(error) })
140
+ return response.json.error(status, `${op} failed`)
141
+ }
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * `/api/_db/graph` is the map, fetched once: every declared foreign key, every
5
5
  * table's identity, and the column worth showing instead of an id.
6
- * `/api/_db/lookup` resolves actual references — **batched, one query per
6
+ * `/api/_db/lookup` resolves actual references: **batched, one query per
7
7
  * table**, because the shape this replaces is a `fetch` per visible cell, and a
8
8
  * fifty-row page with three foreign keys is a hundred and fifty round trips.
9
9
  */
@@ -15,7 +15,7 @@ import { connection } from '@bakery-framework/orm/connection'
15
15
  import { qId } from '@bakery-framework/orm/schema-util'
16
16
  import { type Identity, introspect, type TableFacts } from '../identity'
17
17
  import { overLimit } from '../policy'
18
- import { findTable, readBody } from './common'
18
+ import { findTable, readBody, refuse } from './common'
19
19
 
20
20
  export async function handleGraph(): Promise<JsonResponseData<unknown>> {
21
21
  return await Try.return(
@@ -23,7 +23,7 @@ export async function handleGraph(): Promise<JsonResponseData<unknown>> {
23
23
  const [tables, foreignKeys] = await Promise.all([
24
24
  introspect(),
25
25
  // Composites are already grouped by the adapter, keyed by the tuple
26
- // rather than by constraint name — SQLite reports no name at all.
26
+ // rather than by constraint name: SQLite reports no name at all.
27
27
  connection.getForeignKeys(),
28
28
  ])
29
29
 
@@ -111,7 +111,7 @@ export async function handleLookup(
111
111
  for (const index of indexes) {
112
112
  const key = parsed[index]!.key
113
113
  // A ref whose key does not name exactly the identity is skipped
114
- // rather than widened — a partial key is a predicate over more than
114
+ // rather than widened: a partial key is a predicate over more than
115
115
  // one row, which is the bug `validateKey` refuses for a write and
116
116
  // there is no reason to accept it for a read.
117
117
  if (cols.some(col => !(col in key))) continue
@@ -143,6 +143,6 @@ export async function handleLookup(
143
143
 
144
144
  return response.json.success('success', { rows: results })
145
145
  },
146
- (error: any) => response.json.error(400, error?.message ?? 'Lookup failed'),
146
+ error => refuse('lookup', error),
147
147
  )
148
148
  }