tablewalk 0.0.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/LICENSE +21 -0
- package/README.md +553 -0
- package/dist/adapters/adapter.js +372 -0
- package/dist/adapters/connect.js +33 -0
- package/dist/adapters/mysql.js +951 -0
- package/dist/adapters/postgres.js +1000 -0
- package/dist/adapters/sqlite.js +781 -0
- package/dist/client/agent.js +262 -0
- package/dist/client/app.js +973 -0
- package/dist/client/arrange.js +254 -0
- package/dist/client/ask.js +133 -0
- package/dist/client/breakdown.js +317 -0
- package/dist/client/clauses.js +390 -0
- package/dist/client/columns.js +98 -0
- package/dist/client/complete.js +437 -0
- package/dist/client/compose.js +166 -0
- package/dist/client/composer.css +495 -0
- package/dist/client/composer.js +1972 -0
- package/dist/client/connections.js +234 -0
- package/dist/client/connmanager.js +962 -0
- package/dist/client/connurl.js +188 -0
- package/dist/client/core.js +893 -0
- package/dist/client/deeplink.js +270 -0
- package/dist/client/delete.js +144 -0
- package/dist/client/diagram.js +885 -0
- package/dist/client/dropdown.js +279 -0
- package/dist/client/export.js +456 -0
- package/dist/client/features.css +524 -0
- package/dist/client/findvalue.js +169 -0
- package/dist/client/grid.js +205 -0
- package/dist/client/handoff.js +153 -0
- package/dist/client/help.css +145 -0
- package/dist/client/help.js +881 -0
- package/dist/client/history.js +222 -0
- package/dist/client/index.html +116 -0
- package/dist/client/insert.js +151 -0
- package/dist/client/menu.js +160 -0
- package/dist/client/nested.js +255 -0
- package/dist/client/page.css +713 -0
- package/dist/client/page.js +1345 -0
- package/dist/client/pagebuilder.js +1222 -0
- package/dist/client/pagemarks.js +95 -0
- package/dist/client/palette.js +374 -0
- package/dist/client/peek.js +254 -0
- package/dist/client/picker.js +139 -0
- package/dist/client/pins.js +140 -0
- package/dist/client/prompt.js +129 -0
- package/dist/client/record.js +707 -0
- package/dist/client/schemaexport.js +242 -0
- package/dist/client/schematext.js +125 -0
- package/dist/client/shape.js +178 -0
- package/dist/client/shapecheck.js +129 -0
- package/dist/client/skeleton.js +139 -0
- package/dist/client/sql.css +126 -0
- package/dist/client/sql.js +398 -0
- package/dist/client/sqlcomplete.js +163 -0
- package/dist/client/sqlsaved.js +107 -0
- package/dist/client/style.css +2711 -0
- package/dist/client/summary.js +259 -0
- package/dist/client/table.js +1035 -0
- package/dist/client/template.js +539 -0
- package/dist/client/theme.js +74 -0
- package/dist/client/tour.js +324 -0
- package/dist/client/undo.js +105 -0
- package/dist/client/url.js +166 -0
- package/dist/client/value.js +223 -0
- package/dist/client/views.js +215 -0
- package/dist/client/virtual.js +176 -0
- package/dist/client/welcome.js +170 -0
- package/dist/client/write.js +414 -0
- package/dist/server/changeimpact.js +195 -0
- package/dist/server/connections.js +615 -0
- package/dist/server/constraints.js +62 -0
- package/dist/server/credentials.js +230 -0
- package/dist/server/fixture.js +199 -0
- package/dist/server/graph.js +194 -0
- package/dist/server/impact.js +48 -0
- package/dist/server/index.js +2204 -0
- package/dist/server/journal.js +173 -0
- package/dist/server/layouts.js +128 -0
- package/dist/server/mcp.js +2840 -0
- package/dist/server/shapeonly.js +91 -0
- package/dist/shared/breakdown.js +231 -0
- package/dist/shared/breakdowntext.js +257 -0
- package/dist/shared/diff.js +130 -0
- package/dist/shared/like.js +29 -0
- package/dist/shared/lint.js +149 -0
- package/dist/shared/order.js +133 -0
- package/dist/shared/page.js +932 -0
- package/dist/shared/query.js +831 -0
- package/dist/shared/recordview.js +343 -0
- package/dist/shared/schema.js +377 -0
- package/dist/shared/sqlsaved.js +67 -0
- package/dist/shared/view.js +981 -0
- package/dist/shared/viewtext.js +273 -0
- package/dist/shared/vocabulary.js +164 -0
- package/package.json +57 -0
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The schema, in a paragraph you can click.
|
|
3
|
+
*
|
|
4
|
+
* The diagram beneath this shows every table at once, which answers "what is in
|
|
5
|
+
* here" only for someone who already knows. This says it: where the domain
|
|
6
|
+
* centres, which tables are the ones you will actually query, how deep the
|
|
7
|
+
* references go, and what is sitting over there connected to nothing.
|
|
8
|
+
*
|
|
9
|
+
* Everything is a link, because a summary you cannot act on is a paragraph to
|
|
10
|
+
* read and then leave. Naming `customer` and not letting someone open it is the
|
|
11
|
+
* landing page describing the database instead of opening it.
|
|
12
|
+
*
|
|
13
|
+
* The analysis is in shape.js and is pure; this file only decides how to say
|
|
14
|
+
* it. Keeping those apart is what lets the hard part be tested without a
|
|
15
|
+
* browser.
|
|
16
|
+
*/
|
|
17
|
+
import { el, go, loadJson, saveJson, state, tableView } from './core.js';
|
|
18
|
+
import { describeSchema } from './shape.js';
|
|
19
|
+
import { openHelp } from './help.js';
|
|
20
|
+
|
|
21
|
+
/* Collapsed or not, remembered.
|
|
22
|
+
|
|
23
|
+
The summary earns its space on a first visit and stops earning it on the
|
|
24
|
+
twentieth, and which visit this is is not something the page can work out.
|
|
25
|
+
So it asks once and remembers the answer — per database, because arriving
|
|
26
|
+
at an unfamiliar one is the case the summary exists for. */
|
|
27
|
+
const OPEN_KEY = 'tablewalk.summary.open';
|
|
28
|
+
const isOpen = (id) => loadJson(OPEN_KEY, {})[id] !== false;
|
|
29
|
+
const setOpen = (id, open) => saveJson(OPEN_KEY, { ...loadJson(OPEN_KEY, {}), [id]: open });
|
|
30
|
+
|
|
31
|
+
/** Names shown before a list becomes "and n more". */
|
|
32
|
+
const NAMED = 5;
|
|
33
|
+
|
|
34
|
+
const link = (table) =>
|
|
35
|
+
el('button', {
|
|
36
|
+
type: 'button',
|
|
37
|
+
class: 'shape-link',
|
|
38
|
+
title: `${table.columns.length} column${table.columns.length === 1 ? '' : 's'}`,
|
|
39
|
+
text: table.name,
|
|
40
|
+
onclick: () => go(tableView(table.id), 'reset'),
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* A list of tables, capped.
|
|
45
|
+
*
|
|
46
|
+
* Twenty-three leaf tables listed in full is a wall that says less than five
|
|
47
|
+
* and a count — the reader is looking for the shape, and past a handful the
|
|
48
|
+
* names stop carrying it.
|
|
49
|
+
*/
|
|
50
|
+
function tableList(tables) {
|
|
51
|
+
const shown = tables.slice(0, NAMED);
|
|
52
|
+
const rest = tables.length - shown.length;
|
|
53
|
+
const out = [];
|
|
54
|
+
shown.forEach((t, i) => {
|
|
55
|
+
if (i) out.push(document.createTextNode(i === shown.length - 1 && !rest ? ' and ' : ', '));
|
|
56
|
+
out.push(link(t));
|
|
57
|
+
});
|
|
58
|
+
if (rest) out.push(document.createTextNode(`, and ${rest} more`));
|
|
59
|
+
return out;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function line(label, ...content) {
|
|
63
|
+
return el('li', { class: 'shape-line' }, [
|
|
64
|
+
el('span', { class: 'shape-label', text: label }),
|
|
65
|
+
el('span', { class: 'shape-says' }, content.flat()),
|
|
66
|
+
]);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Where to start, in this database rather than in general.
|
|
71
|
+
*
|
|
72
|
+
* What stood here was three synthetic queries — `work_order is_urgent =
|
|
73
|
+
* false` and two more — teaching the syntax to someone who does not yet know
|
|
74
|
+
* what is in the database. That is the wrong first question. The paragraph
|
|
75
|
+
* directly above already answers the right one, and these are the same
|
|
76
|
+
* tables it names, with their sizes, as places to go.
|
|
77
|
+
*
|
|
78
|
+
* The language is still taught, just later and by example: clicking one puts
|
|
79
|
+
* `work_order` in the query bar and runs it, so the first thing anybody sees
|
|
80
|
+
* is that the bar takes a table name — which is the whole first lesson.
|
|
81
|
+
*/
|
|
82
|
+
function startingPoints(shape) {
|
|
83
|
+
const seen = new Set();
|
|
84
|
+
const picks = [];
|
|
85
|
+
const add = (table, why) => {
|
|
86
|
+
/* Four, not six. The card is capped at a share of the window, and a
|
|
87
|
+
fifth chip buys a row that gets clipped rather than read — the sixth
|
|
88
|
+
most-connected table in a schema is not where anyone starts. */
|
|
89
|
+
if (!table || seen.has(table.id) || picks.length >= 4) return;
|
|
90
|
+
seen.add(table.id);
|
|
91
|
+
picks.push({ table, why });
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
/* Hubs first — what the domain is built around — then the tables that
|
|
95
|
+
record what happened, which is usually what someone came to look at. */
|
|
96
|
+
for (const hub of shape.hubs) {
|
|
97
|
+
add(hub.table, `${hub.incoming} tables point at it`);
|
|
98
|
+
}
|
|
99
|
+
for (const leaf of shape.leaves) add(leaf, 'records events');
|
|
100
|
+
/* Then simply the biggest, for a schema with no clear shape: on a database
|
|
101
|
+
of forty flat tables, size is the only honest hint. */
|
|
102
|
+
for (const table of [...(state.schema?.tables ?? [])]
|
|
103
|
+
.filter((t) => !t.isView && typeof t.approxRows === 'number')
|
|
104
|
+
.sort((a, b) => (b.approxRows ?? 0) - (a.approxRows ?? 0))) {
|
|
105
|
+
add(table, 'the largest table here');
|
|
106
|
+
}
|
|
107
|
+
if (!picks.length) return null;
|
|
108
|
+
|
|
109
|
+
return el('section', { class: 'landing-starts' }, [
|
|
110
|
+
el('div', { class: 'landing-starts-head' }, [
|
|
111
|
+
el('h3', { class: 'shape-label', text: 'Start here' }),
|
|
112
|
+
el('button', {
|
|
113
|
+
type: 'button',
|
|
114
|
+
class: 'ghost help-more',
|
|
115
|
+
text: 'Query language (?)',
|
|
116
|
+
onclick: openHelp,
|
|
117
|
+
}),
|
|
118
|
+
]),
|
|
119
|
+
el('div', { class: 'landing-chips' }, picks.map(({ table, why }) => el('button', {
|
|
120
|
+
type: 'button',
|
|
121
|
+
class: 'landing-chip start-chip',
|
|
122
|
+
title: `Open ${table.id}`,
|
|
123
|
+
onclick: () => go(tableView(table.id), 'reset'),
|
|
124
|
+
}, [
|
|
125
|
+
el('span', { class: 'landing-chip-name', text: table.name }),
|
|
126
|
+
el('span', { class: 'landing-chip-table' }, [
|
|
127
|
+
typeof table.approxRows === 'number'
|
|
128
|
+
? el('span', { class: 'start-rows', text: `${table.approxRows.toLocaleString()} rows` })
|
|
129
|
+
: null,
|
|
130
|
+
el('span', { text: why }),
|
|
131
|
+
].filter(Boolean)),
|
|
132
|
+
]))),
|
|
133
|
+
]);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function schemaSummary() {
|
|
137
|
+
if (!state.schema) return null;
|
|
138
|
+
const shape = describeSchema(state.schema);
|
|
139
|
+
const { counts } = shape;
|
|
140
|
+
if (!counts.tables) return null;
|
|
141
|
+
|
|
142
|
+
const lines = [];
|
|
143
|
+
|
|
144
|
+
if (shape.hubs.length) {
|
|
145
|
+
/* First, because it is the answer to "what is this database about". A
|
|
146
|
+
table that nine others point at is the thing the domain is built
|
|
147
|
+
around, whatever it happens to be called. */
|
|
148
|
+
const [first] = shape.hubs;
|
|
149
|
+
lines.push(line(
|
|
150
|
+
'Centres on',
|
|
151
|
+
link(first.table),
|
|
152
|
+
document.createTextNode(`, referenced by ${first.incoming} other tables`),
|
|
153
|
+
shape.hubs.length > 1
|
|
154
|
+
? [document.createTextNode('. Then '), ...tableList(shape.hubs.slice(1).map((h) => h.table))]
|
|
155
|
+
: [],
|
|
156
|
+
));
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (shape.leaves.length) {
|
|
160
|
+
/* Leaves point outwards and are pointed at by nothing — the transaction
|
|
161
|
+
and event tables, which is usually what someone came to query and is
|
|
162
|
+
what a diagram buries at its edges. */
|
|
163
|
+
lines.push(line('Records events in', tableList(shape.leaves)));
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (shape.joins.length) {
|
|
167
|
+
lines.push(line(
|
|
168
|
+
'Connected through',
|
|
169
|
+
tableList(shape.joins.map((j) => j.table)),
|
|
170
|
+
document.createTextNode(shape.joins.length === 1 ? ', a join table' : ', which are join tables'),
|
|
171
|
+
));
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (shape.chain.length > 2) {
|
|
175
|
+
// The domain written out. "Five levels deep" says less than the path.
|
|
176
|
+
const path = [];
|
|
177
|
+
shape.chain.forEach((t, i) => {
|
|
178
|
+
if (i) path.push(el('span', { class: 'shape-arrow', text: ' → ' }));
|
|
179
|
+
path.push(link(t));
|
|
180
|
+
});
|
|
181
|
+
lines.push(line('Deepest walk', path));
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (shape.islands.length) {
|
|
185
|
+
/* Worth saying out loud. A table connected to nothing is either
|
|
186
|
+
configuration or something whose relationships were never declared, and
|
|
187
|
+
both are things to know before trusting the diagram. */
|
|
188
|
+
lines.push(line(
|
|
189
|
+
'Standing alone',
|
|
190
|
+
tableList(shape.islands),
|
|
191
|
+
document.createTextNode(shape.islands.length === 1 ? ' — no foreign keys' : ' — no foreign keys at all'),
|
|
192
|
+
));
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (!lines.length) return null;
|
|
196
|
+
|
|
197
|
+
const id = state.schema.connection ?? state.schema.label ?? '';
|
|
198
|
+
let open = isOpen(id);
|
|
199
|
+
|
|
200
|
+
/* The examples live inside this card rather than in a section of their own.
|
|
201
|
+
They are the same kind of thing — "here is what this database is, here is
|
|
202
|
+
what to type at it" — and two headed sections stacked above the diagram
|
|
203
|
+
spent more of the page on framing than on the schema. */
|
|
204
|
+
const body = el('div', { class: 'shape-body' }, [
|
|
205
|
+
el('ul', { class: 'shape-lines' }, lines),
|
|
206
|
+
startingPoints(shape),
|
|
207
|
+
].filter(Boolean));
|
|
208
|
+
|
|
209
|
+
/* The one line that survives collapsing. Whatever else the summary said,
|
|
210
|
+
the table the domain is built around is the part worth keeping visible,
|
|
211
|
+
so folding the card away costs a reminder rather than the answer. */
|
|
212
|
+
const gist = shape.hubs.length
|
|
213
|
+
? el('span', { class: 'shape-gist', text: `centres on ${shape.hubs[0].table.name}` })
|
|
214
|
+
: null;
|
|
215
|
+
|
|
216
|
+
const toggle = el('button', {
|
|
217
|
+
type: 'button',
|
|
218
|
+
class: 'shape-toggle',
|
|
219
|
+
'aria-expanded': String(open),
|
|
220
|
+
title: 'Show or hide the summary',
|
|
221
|
+
text: state.schema.label,
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
const section = el('section', { class: `shape${open ? '' : ' shape-closed'}` }, [
|
|
225
|
+
el('div', { class: 'shape-head' }, [
|
|
226
|
+
toggle,
|
|
227
|
+
el('span', {
|
|
228
|
+
class: 'shape-counts',
|
|
229
|
+
/* How many tables is a size, and a view is a different kind of
|
|
230
|
+
thing, so both are worth a word. A count of foreign keys is
|
|
231
|
+
neither: nobody decides anything from "20", and the lines
|
|
232
|
+
underneath say what the keys actually do — which is the only part
|
|
233
|
+
of the graph that was ever worth reading. */
|
|
234
|
+
text: `${counts.tables} table${counts.tables === 1 ? '' : 's'}`
|
|
235
|
+
+ (counts.views ? `, ${counts.views} view${counts.views === 1 ? '' : 's'}` : ''),
|
|
236
|
+
}),
|
|
237
|
+
gist,
|
|
238
|
+
shape.start
|
|
239
|
+
? el('button', {
|
|
240
|
+
type: 'button',
|
|
241
|
+
class: 'ghost shape-start',
|
|
242
|
+
title: 'The table that connects to the most, with names you can recognise rows by',
|
|
243
|
+
text: `Start at ${shape.start.name} →`,
|
|
244
|
+
onclick: () => go(tableView(shape.start.id), 'reset'),
|
|
245
|
+
})
|
|
246
|
+
: null,
|
|
247
|
+
].filter(Boolean)),
|
|
248
|
+
body,
|
|
249
|
+
]);
|
|
250
|
+
|
|
251
|
+
toggle.addEventListener('click', () => {
|
|
252
|
+
open = !open;
|
|
253
|
+
section.classList.toggle('shape-closed', !open);
|
|
254
|
+
toggle.setAttribute('aria-expanded', String(open));
|
|
255
|
+
setOpen(id, open);
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
return section;
|
|
259
|
+
}
|