@bakery-framework/plugin-db-explorer 2.0.0-alpha.9 → 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.
- package/package.json +5 -5
- package/src/access.ts +53 -17
- package/src/client/api.ts +48 -6
- package/src/client/bulk.ts +31 -7
- package/src/client/cell.ts +5 -5
- package/src/client/confirm.ts +11 -12
- package/src/client/csv-commit.ts +25 -5
- package/src/client/csv-map.ts +7 -7
- package/src/client/csv-model.ts +61 -8
- package/src/client/csv-pick.ts +1 -1
- package/src/client/csv-preview.ts +2 -2
- package/src/client/csv.ts +2 -2
- package/src/client/dom.ts +3 -5
- package/src/client/edit-session.ts +7 -7
- package/src/client/editors.ts +5 -5
- package/src/client/filter-builder.ts +6 -6
- package/src/client/fk.ts +9 -9
- package/src/client/grid-body.ts +4 -4
- package/src/client/grid-header.ts +2 -2
- package/src/client/grid-rowbar.ts +1 -1
- package/src/client/grid.ts +8 -9
- package/src/client/meta.ts +6 -6
- package/src/client/page.ts +4 -4
- package/src/client/panel.ts +8 -8
- package/src/client/relations.ts +4 -4
- package/src/client/save.ts +4 -4
- package/src/client/sidebar.ts +6 -6
- package/src/client/state.ts +4 -4
- package/src/client/statusbar.ts +6 -6
- package/src/client/structure.ts +8 -8
- package/src/client/tabs.ts +10 -10
- package/src/client/tabstrip.ts +3 -3
- package/src/client.ts +17 -17
- package/src/endpoints/common.ts +25 -6
- package/src/endpoints/graph.ts +5 -5
- package/src/endpoints/import.ts +5 -5
- package/src/endpoints/read.ts +70 -10
- package/src/endpoints/rows.ts +219 -14
- package/src/identity.ts +122 -52
- package/src/index.ts +1 -1
- package/src/policy.ts +4 -4
- package/src/preview.ts +5 -5
- package/src/setup.ts +16 -10
- package/src/shared/coerce.ts +21 -22
- package/src/shared/csv.ts +5 -5
- package/src/shared/filters.ts +6 -6
- package/src/shared/plan.ts +7 -7
- package/src/shell.ts +3 -3
- package/src/validate.ts +10 -10
package/src/shared/csv.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* CSV, RFC 4180-ish, with the delimiter sniffed rather than assumed.
|
|
3
3
|
*
|
|
4
|
-
* **Pure
|
|
4
|
+
* **Pure**. See the note at the top of `coerce.ts`, and, unlike the rest of
|
|
5
5
|
* `shared/`, **browser-only**. It was written when the server re-parsed the
|
|
6
6
|
* text it was sent; `endpoints/import.ts` takes records that are already mapped
|
|
7
7
|
* and coerced, so no CSV text ever reaches it. `client/csv-model.ts` is the
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
* `if (c === '"') inQuotes = true` with no check that the field is empty.
|
|
20
20
|
* RFC 4180 only gives `"` meaning at the start of a field.
|
|
21
21
|
* 2. **No BOM handling.** A file saved by Excel begins ``, so the first
|
|
22
|
-
* header comes back as `id` and matches no column
|
|
22
|
+
* header comes back as `id` and matches no column: the single most
|
|
23
23
|
* common import failure there is.
|
|
24
24
|
* 3. Comma only. A European export is `;`-delimited and a database dump is
|
|
25
25
|
* often tab-delimited; both parse as one column per row and then fail with
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
* (`row.length === 1 && row[0] === ''`), which also drops a legitimate
|
|
29
29
|
* single-column row whose value is empty.
|
|
30
30
|
*
|
|
31
|
-
* Fixing it in place would change `importCSV`'s
|
|
31
|
+
* Fixing it in place would change `importCSV`'s behavior for every existing
|
|
32
32
|
* caller of the ORM, which is a separate decision from adding an importer to
|
|
33
33
|
* the explorer. This parser is the explorer's; if the ORM's is ever replaced,
|
|
34
34
|
* this is the implementation to move.
|
|
@@ -112,7 +112,7 @@ function countPerLine(text: string, delimiter: string, max: number): number[] {
|
|
|
112
112
|
* No trimming and no type guessing: `"01"` and `01` are both the three-, then
|
|
113
113
|
* two-character strings they look like, and turning either into a number is
|
|
114
114
|
* `coerce.ts`'s job once a column is known. A field is only ever `''` when the
|
|
115
|
-
* file said so
|
|
115
|
+
* file said so: this parser has no concept of NULL.
|
|
116
116
|
*/
|
|
117
117
|
export function parseCSVRows(text: string, delimiter = ','): string[][] {
|
|
118
118
|
const csv = stripBOM(text)
|
|
@@ -137,7 +137,7 @@ export function parseCSVRows(text: string, delimiter = ','): string[][] {
|
|
|
137
137
|
endField()
|
|
138
138
|
// A blank line is not a row: it is what a trailing newline, or a stray one
|
|
139
139
|
// in the middle of a file, leaves behind. One field, empty, and never
|
|
140
|
-
// quoted is the only shape it can take
|
|
140
|
+
// quoted is the only shape it can take: a line reading `""` is a real row
|
|
141
141
|
// holding one empty value, which is why `rowQuoted` is tracked.
|
|
142
142
|
const blank = row.length === 1 && row[0] === '' && !rowQuoted
|
|
143
143
|
if (!blank) rows.push(row)
|
package/src/shared/filters.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The filter vocabulary, written once and read by both halves.
|
|
3
3
|
*
|
|
4
|
-
* **Pure, and deliberately in `shared
|
|
4
|
+
* **Pure, and deliberately in `shared/`**: the client builds filters and the
|
|
5
5
|
* endpoint validates them, and a vocabulary that lived on only one side would
|
|
6
6
|
* drift. Like the rest of `shared/`, this is compiled into the browser bundle,
|
|
7
7
|
* so it imports nothing from `@bakery-framework/*` (see `client/safety.test.ts`).
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
*
|
|
15
15
|
* This is that caller. Validating matters more than it looks: a *dropped*
|
|
16
16
|
* filter widens the result set, so a typo'd operator would silently show more
|
|
17
|
-
* rows than were asked for
|
|
17
|
+
* rows than were asked for, and the explorer's Delete acts on a selection made
|
|
18
18
|
* from exactly that view. Failing the request is the only safe direction
|
|
19
19
|
* (convention 2).
|
|
20
20
|
*/
|
|
@@ -51,7 +51,7 @@ export const FILTER_OPS: readonly FilterOp[] = [
|
|
|
51
51
|
* The two operators that bind nothing.
|
|
52
52
|
*
|
|
53
53
|
* `IS NULL` has no parameter, which is the whole reason a filter cannot be
|
|
54
|
-
*
|
|
54
|
+
* modeled as a plain column/value pair: the value input is *hidden* for
|
|
55
55
|
* these rather than ignored, because an input whose contents do nothing is a
|
|
56
56
|
* lie about what the query will do.
|
|
57
57
|
*/
|
|
@@ -107,7 +107,7 @@ export function filter(
|
|
|
107
107
|
/**
|
|
108
108
|
* Filters as `getData` wants them: keyed by column.
|
|
109
109
|
*
|
|
110
|
-
* **The wire shape is a record, so a column can carry only one filter
|
|
110
|
+
* **The wire shape is a record, so a column can carry only one filter**: the
|
|
111
111
|
* ORM iterates `Object.entries(options.filters)`. Two chips on one column
|
|
112
112
|
* therefore cannot both be sent, and the last one wins rather than the first,
|
|
113
113
|
* because the last is the one the user just touched. `duplicateColumns` exists
|
|
@@ -132,7 +132,7 @@ export function toWire(filters: readonly Filter[]): Record<string, WireFilter> {
|
|
|
132
132
|
return wire
|
|
133
133
|
}
|
|
134
134
|
|
|
135
|
-
/** Columns named by more than one filter
|
|
135
|
+
/** Columns named by more than one filter: the ones the wire cannot carry. */
|
|
136
136
|
export function duplicateColumns(filters: readonly Filter[]): string[] {
|
|
137
137
|
const seen = new Set<string>()
|
|
138
138
|
const twice = new Set<string>()
|
|
@@ -189,7 +189,7 @@ function parseOne(
|
|
|
189
189
|
const { op, value: operand } = value as { op?: unknown; value?: unknown }
|
|
190
190
|
if (!isFilterOp(op)) {
|
|
191
191
|
return {
|
|
192
|
-
error: `unknown filter operator ${JSON.stringify(op)} on ${column}
|
|
192
|
+
error: `unknown filter operator ${JSON.stringify(op)} on ${column}: expected one of ${FILTER_OPS.join(', ')}`,
|
|
193
193
|
}
|
|
194
194
|
}
|
|
195
195
|
if (!opTakesValue(op)) return { filter: { op } }
|
package/src/shared/plan.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Turning a spreadsheet into statements: which CSV column feeds which database
|
|
3
3
|
* column, what stops an import before it starts, and what one row edit changes.
|
|
4
4
|
*
|
|
5
|
-
* **Pure
|
|
5
|
+
* **Pure**. See the note at the top of `coerce.ts`. The import dialog runs
|
|
6
6
|
* `autoMap` and `blockingIssues` in the browser to show the mapping before
|
|
7
7
|
* anything is sent, and the server runs the same two functions on what arrives,
|
|
8
8
|
* because a client-side check is a convenience and never a guarantee.
|
|
@@ -27,7 +27,7 @@ export type ColumnMapping = Record<string, string | null>
|
|
|
27
27
|
* Nothing fuzzier: a near-miss that silently loads the wrong column is worse
|
|
28
28
|
* than an unmapped one the dialog can ask about.
|
|
29
29
|
*
|
|
30
|
-
* A database column is claimed at most once. Two headers that
|
|
30
|
+
* A database column is claimed at most once. Two headers that normalize to the
|
|
31
31
|
* same name would otherwise both map to it, and the second would quietly win.
|
|
32
32
|
*/
|
|
33
33
|
export function autoMap(
|
|
@@ -42,7 +42,7 @@ export function autoMap(
|
|
|
42
42
|
for (const c of dbColumns) {
|
|
43
43
|
const key = normalize(c)
|
|
44
44
|
// First declaration wins, so the mapping does not depend on column order
|
|
45
|
-
// among columns that
|
|
45
|
+
// among columns that normalize alike.
|
|
46
46
|
if (!byNormal.has(key)) byNormal.set(key, c)
|
|
47
47
|
}
|
|
48
48
|
|
|
@@ -76,7 +76,7 @@ export interface PlanIssue {
|
|
|
76
76
|
/**
|
|
77
77
|
* What makes this mapping unrunnable, as opposed to merely lossy.
|
|
78
78
|
*
|
|
79
|
-
* A column left out of the mapping is fine when the database can fill it in
|
|
79
|
+
* A column left out of the mapping is fine when the database can fill it in:
|
|
80
80
|
* an auto-increment key, a default, or a nullable column. A NOT NULL column
|
|
81
81
|
* with no default and no mapping is not: every row would fail at the database,
|
|
82
82
|
* one at a time, after the import had already started.
|
|
@@ -123,7 +123,7 @@ export function blockingIssues(
|
|
|
123
123
|
export interface UpdatePlanInput {
|
|
124
124
|
/** The row as the editor last saw it, keyed by raw column name. */
|
|
125
125
|
original: Record<string, unknown>
|
|
126
|
-
/** Edited values. **An absent key means unchanged
|
|
126
|
+
/** Edited values. **An absent key means unchanged**. See `coerce.ts`. */
|
|
127
127
|
edits: Record<string, unknown>
|
|
128
128
|
/** The identity columns, from `describeIdentity`. */
|
|
129
129
|
identity: readonly string[]
|
|
@@ -136,7 +136,7 @@ export interface UpdatePlanResult {
|
|
|
136
136
|
where: Record<string, unknown>
|
|
137
137
|
/** Edited keys that matched what was already there. */
|
|
138
138
|
unchanged: string[]
|
|
139
|
-
/** Identity columns `original` does not carry
|
|
139
|
+
/** Identity columns `original` does not carry: the plan is unusable. */
|
|
140
140
|
missingIdentity: string[]
|
|
141
141
|
}
|
|
142
142
|
|
|
@@ -148,7 +148,7 @@ export interface UpdatePlanResult {
|
|
|
148
148
|
* and set the new one, and a plan built from the edited row would look for a
|
|
149
149
|
* row that does not exist yet.
|
|
150
150
|
*
|
|
151
|
-
* Unchanged columns are dropped from `set`. That is not only economy
|
|
151
|
+
* Unchanged columns are dropped from `set`. That is not only economy: a `set`
|
|
152
152
|
* carrying every column makes two people editing different columns of the same
|
|
153
153
|
* row collide, and the whole reason to send a narrow statement is that they
|
|
154
154
|
* should not.
|
package/src/shell.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* stylesheet in the middle of it is a route table nobody reads. `setup.ts`
|
|
7
7
|
* keeps the routing and the CSRF policy; this keeps the paint.
|
|
8
8
|
*
|
|
9
|
-
* There is **no markup generated from data here
|
|
9
|
+
* There is **no markup generated from data here**: the body is an empty
|
|
10
10
|
* `#app` and a module script. Every value the user sees is written by
|
|
11
11
|
* `client/dom.ts` through `textContent`, which is why this plugin has no XSS
|
|
12
12
|
* surface even though it renders arbitrary row contents.
|
|
@@ -53,7 +53,7 @@ const CSS = `
|
|
|
53
53
|
.tab-new { font: inherit; background: none; border: 0; color: var(--dim); cursor: pointer; padding: .3rem .8rem; }
|
|
54
54
|
.tab-new:hover { color: var(--accent-text); }
|
|
55
55
|
|
|
56
|
-
/* Data / Structure / Relations
|
|
56
|
+
/* Data / Structure / Relations: one level of nesting, and only one. */
|
|
57
57
|
.viewtabs { display: flex; gap: .2rem; padding: .3rem .8rem 0; border-bottom: 1px solid var(--line); background: var(--bg); }
|
|
58
58
|
.viewtab { font: inherit; font-size: .8rem; background: none; border: 0; border-bottom: 2px solid transparent; color: var(--dim); padding: .3rem .7rem; cursor: pointer; }
|
|
59
59
|
.viewtab:hover { color: var(--text); }
|
|
@@ -140,7 +140,7 @@ const CSS = `
|
|
|
140
140
|
dialog h4 { margin: .8rem 0 .3rem; font-size: .85rem; color: var(--dim); }
|
|
141
141
|
.sel { font: inherit; background: var(--bg); color: var(--text); border: 1px solid var(--line); border-radius: 4px; padding: .15rem .3rem; }
|
|
142
142
|
|
|
143
|
-
/* The row side panel
|
|
143
|
+
/* The row side panel: where JSON, long text and both graph directions live. */
|
|
144
144
|
.panel { position: fixed; top: 0; right: 0; width: min(38rem, 100vw); height: 100vh; overflow-y: auto; background: var(--panel); border-left: 1px solid var(--line); padding: 1rem; z-index: 30; }
|
|
145
145
|
.panel-head { display: flex; justify-content: space-between; align-items: flex-start; gap: 1rem; }
|
|
146
146
|
.panel-title h3 { margin: 0; font-family: ui-monospace, monospace; }
|
package/src/validate.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Two rules run through every function here.
|
|
6
6
|
*
|
|
7
|
-
* **Errors accumulate.** A 400 names every bad field, not the first one
|
|
7
|
+
* **Errors accumulate.** A 400 names every bad field, not the first one: a
|
|
8
8
|
* fifty-row paste fixed one error per round trip is fifty round trips, and the
|
|
9
9
|
* user cannot see the shape of their mistake.
|
|
10
10
|
*
|
|
@@ -55,7 +55,7 @@ const error = (
|
|
|
55
55
|
/**
|
|
56
56
|
* Resolve a wire column name against the table.
|
|
57
57
|
*
|
|
58
|
-
* Accepts the raw database name, and the camel spelling as a courtesy
|
|
58
|
+
* Accepts the raw database name, and the camel spelling as a courtesy: the
|
|
59
59
|
* grid renders raw names, but a script written against a typed schema has camel
|
|
60
60
|
* ones and the two are the same column. Anything else is `unknown_column`.
|
|
61
61
|
*/
|
|
@@ -85,7 +85,7 @@ function coerceInto(
|
|
|
85
85
|
* is fine when the database can fill it in (auto-increment, a default, or
|
|
86
86
|
* nullable) and is a `required` error otherwise. Refusing here rather than at
|
|
87
87
|
* the database is what keeps a 1,000-row insert from failing on row 700 with
|
|
88
|
-
* 699 rows already written
|
|
88
|
+
* 699 rows already written: the whole statement is known to be well-formed
|
|
89
89
|
* before any of it runs.
|
|
90
90
|
*/
|
|
91
91
|
export function validateInsertRow(
|
|
@@ -135,8 +135,8 @@ export interface PartialOptions {
|
|
|
135
135
|
* Whether a `json` or `buffer` column may appear.
|
|
136
136
|
*
|
|
137
137
|
* `false` for `expect`, where the answer is not "be careful" but "this cannot
|
|
138
|
-
* be expressed": those two kinds have no portable equality predicate
|
|
139
|
-
* `comparableKind`
|
|
138
|
+
* be expressed": those two kinds have no portable equality predicate (see
|
|
139
|
+
* `comparableKind`), so an `expect` over one is a condition that never
|
|
140
140
|
* matches, which reads to the caller as a permanent conflict.
|
|
141
141
|
*/
|
|
142
142
|
allowUncomparable: boolean
|
|
@@ -145,7 +145,7 @@ export interface PartialOptions {
|
|
|
145
145
|
}
|
|
146
146
|
|
|
147
147
|
/**
|
|
148
|
-
* A partial row
|
|
148
|
+
* A partial row: the `set` of an update, or the `expect` it is guarded by.
|
|
149
149
|
*
|
|
150
150
|
* Absent keys are absent, and that is the point: this function has no concept
|
|
151
151
|
* of a default, because "the caller did not mention this column" means "leave
|
|
@@ -197,7 +197,7 @@ export function validatePartial(
|
|
|
197
197
|
* The columns of `set` that may only be written with `force`.
|
|
198
198
|
*
|
|
199
199
|
* A `json` or `buffer` column cannot appear in `expect`, so an edit to one is
|
|
200
|
-
* unguarded by construction
|
|
200
|
+
* unguarded by construction: it overwrites whatever is there, including
|
|
201
201
|
* whatever somebody else wrote a moment ago. `force` is how the caller says
|
|
202
202
|
* they know that. It is not a permission; it is an acknowledgement.
|
|
203
203
|
*/
|
|
@@ -220,15 +220,15 @@ export interface ValidatedKey {
|
|
|
220
220
|
/**
|
|
221
221
|
* A wire key, against the table's identity.
|
|
222
222
|
*
|
|
223
|
-
* **The column set must be exactly equal
|
|
223
|
+
* **The column set must be exactly equal**, not a subset, not a superset. A
|
|
224
224
|
* subset is a predicate that matches more than one row, which for a composite
|
|
225
225
|
* key is precisely the dashboard's bug (its MySQL path addresses rows by the
|
|
226
226
|
* *first* primary-key column, so one edit rewrites every row sharing it). A
|
|
227
227
|
* superset is a caller who believes the key contains something it does not, and
|
|
228
228
|
* silently ignoring the extra column would confirm that belief.
|
|
229
229
|
*
|
|
230
|
-
* The key is transparent by design
|
|
231
|
-
* token
|
|
230
|
+
* The key is transparent by design (`{ col: value }` rather than an opaque
|
|
231
|
+
* token), so there is no server-side key cache to keep, and nothing that can go
|
|
232
232
|
* stale between rendering a page and saving a row (convention 6).
|
|
233
233
|
*/
|
|
234
234
|
export function validateKey(
|