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.
Files changed (97) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +553 -0
  3. package/dist/adapters/adapter.js +372 -0
  4. package/dist/adapters/connect.js +33 -0
  5. package/dist/adapters/mysql.js +951 -0
  6. package/dist/adapters/postgres.js +1000 -0
  7. package/dist/adapters/sqlite.js +781 -0
  8. package/dist/client/agent.js +262 -0
  9. package/dist/client/app.js +973 -0
  10. package/dist/client/arrange.js +254 -0
  11. package/dist/client/ask.js +133 -0
  12. package/dist/client/breakdown.js +317 -0
  13. package/dist/client/clauses.js +390 -0
  14. package/dist/client/columns.js +98 -0
  15. package/dist/client/complete.js +437 -0
  16. package/dist/client/compose.js +166 -0
  17. package/dist/client/composer.css +495 -0
  18. package/dist/client/composer.js +1972 -0
  19. package/dist/client/connections.js +234 -0
  20. package/dist/client/connmanager.js +962 -0
  21. package/dist/client/connurl.js +188 -0
  22. package/dist/client/core.js +893 -0
  23. package/dist/client/deeplink.js +270 -0
  24. package/dist/client/delete.js +144 -0
  25. package/dist/client/diagram.js +885 -0
  26. package/dist/client/dropdown.js +279 -0
  27. package/dist/client/export.js +456 -0
  28. package/dist/client/features.css +524 -0
  29. package/dist/client/findvalue.js +169 -0
  30. package/dist/client/grid.js +205 -0
  31. package/dist/client/handoff.js +153 -0
  32. package/dist/client/help.css +145 -0
  33. package/dist/client/help.js +881 -0
  34. package/dist/client/history.js +222 -0
  35. package/dist/client/index.html +116 -0
  36. package/dist/client/insert.js +151 -0
  37. package/dist/client/menu.js +160 -0
  38. package/dist/client/nested.js +255 -0
  39. package/dist/client/page.css +713 -0
  40. package/dist/client/page.js +1345 -0
  41. package/dist/client/pagebuilder.js +1222 -0
  42. package/dist/client/pagemarks.js +95 -0
  43. package/dist/client/palette.js +374 -0
  44. package/dist/client/peek.js +254 -0
  45. package/dist/client/picker.js +139 -0
  46. package/dist/client/pins.js +140 -0
  47. package/dist/client/prompt.js +129 -0
  48. package/dist/client/record.js +707 -0
  49. package/dist/client/schemaexport.js +242 -0
  50. package/dist/client/schematext.js +125 -0
  51. package/dist/client/shape.js +178 -0
  52. package/dist/client/shapecheck.js +129 -0
  53. package/dist/client/skeleton.js +139 -0
  54. package/dist/client/sql.css +126 -0
  55. package/dist/client/sql.js +398 -0
  56. package/dist/client/sqlcomplete.js +163 -0
  57. package/dist/client/sqlsaved.js +107 -0
  58. package/dist/client/style.css +2711 -0
  59. package/dist/client/summary.js +259 -0
  60. package/dist/client/table.js +1035 -0
  61. package/dist/client/template.js +539 -0
  62. package/dist/client/theme.js +74 -0
  63. package/dist/client/tour.js +324 -0
  64. package/dist/client/undo.js +105 -0
  65. package/dist/client/url.js +166 -0
  66. package/dist/client/value.js +223 -0
  67. package/dist/client/views.js +215 -0
  68. package/dist/client/virtual.js +176 -0
  69. package/dist/client/welcome.js +170 -0
  70. package/dist/client/write.js +414 -0
  71. package/dist/server/changeimpact.js +195 -0
  72. package/dist/server/connections.js +615 -0
  73. package/dist/server/constraints.js +62 -0
  74. package/dist/server/credentials.js +230 -0
  75. package/dist/server/fixture.js +199 -0
  76. package/dist/server/graph.js +194 -0
  77. package/dist/server/impact.js +48 -0
  78. package/dist/server/index.js +2204 -0
  79. package/dist/server/journal.js +173 -0
  80. package/dist/server/layouts.js +128 -0
  81. package/dist/server/mcp.js +2840 -0
  82. package/dist/server/shapeonly.js +91 -0
  83. package/dist/shared/breakdown.js +231 -0
  84. package/dist/shared/breakdowntext.js +257 -0
  85. package/dist/shared/diff.js +130 -0
  86. package/dist/shared/like.js +29 -0
  87. package/dist/shared/lint.js +149 -0
  88. package/dist/shared/order.js +133 -0
  89. package/dist/shared/page.js +932 -0
  90. package/dist/shared/query.js +831 -0
  91. package/dist/shared/recordview.js +343 -0
  92. package/dist/shared/schema.js +377 -0
  93. package/dist/shared/sqlsaved.js +67 -0
  94. package/dist/shared/view.js +981 -0
  95. package/dist/shared/viewtext.js +273 -0
  96. package/dist/shared/vocabulary.js +164 -0
  97. 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
+ }