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,273 @@
1
+ /**
2
+ * A view, written down.
3
+ *
4
+ * The composer builds views by clicking, and a view built by clicking is UI
5
+ * state: it can be saved and loaded, and that is all. Everything else in
6
+ * tablewalk holds that the *query text* is the single source of truth — it is
7
+ * why filters compose into the bar, why sorting edits the query, why a link
8
+ * carries the whole walk. A view that cannot be written down cannot be
9
+ * hand-edited, diffed in a config file, pasted into a conversation, or
10
+ * explained to someone who does not have the UI open.
11
+ *
12
+ * So a view has a text form, and it is the same language:
13
+ *
14
+ * work_order
15
+ * show reference, total_cost, customer_id.name as customer
16
+ * count work_order_part via work_order_id as parts
17
+ * sum work_order_part.quantity via work_order_id as units
18
+ * where customer_id.country_code = AU and total_cost > 500
19
+ * sort total_cost desc
20
+ * limit 20
21
+ *
22
+ * Only two things here are new: the base table on the front, and the
23
+ * aggregate clauses. Paths, conditions, sort and limit are parsed by
24
+ * `parseViewQuery`, which the composer's filter box already uses — one
25
+ * grammar, not two that drift.
26
+ *
27
+ * Aggregates name the child table and the foreign key column rather than the
28
+ * constraint, because a constraint name is a database artefact nobody reads
29
+ * (`work_order_part_work_order_id_fkey`) and `work_order_part via
30
+ * work_order_id` says the same thing in words that appear in the schema. The
31
+ * pair is unambiguous: two foreign keys from one child to one parent must sit
32
+ * on different columns.
33
+ */
34
+ import { backrefsTo, parseViewQuery, tableAtPrefix, } from './view.js';
35
+ import { findTable } from './schema.js';
36
+ const AGGREGATE_FNS = ['count', 'sum', 'min', 'max', 'avg'];
37
+ export function viewToText(view, nameConstraint) {
38
+ const lines = [view.base];
39
+ if (view.columns.length) {
40
+ const shown = view.columns.map((c) => (c.alias && c.alias !== c.path ? `${c.path} as ${c.alias}` : c.path));
41
+ lines.push(` show ${shown.join(', ')}`);
42
+ }
43
+ for (const aggregate of view.aggregates ?? []) {
44
+ lines.push(` ${aggregateToText(aggregate, nameConstraint)}`);
45
+ }
46
+ const where = filterToText(view);
47
+ if (where)
48
+ lines.push(` where ${where}`);
49
+ if (view.orderBy?.length) {
50
+ const parts = view.orderBy.map((o) => (o.direction === 'desc' ? `${o.path} desc` : o.path));
51
+ lines.push(` sort ${parts.join(', ')}`);
52
+ }
53
+ if (view.limit !== undefined)
54
+ lines.push(` limit ${view.limit}`);
55
+ return lines.join('\n');
56
+ }
57
+ function aggregateToText(aggregate, nameConstraint) {
58
+ /* `via` on the model is a constraint name; the text form names the child
59
+ table and its foreign key column instead. The name is recoverable from
60
+ the pair, and the pair is what a person can read.
61
+
62
+ With no way to resolve it, the constraint name is emitted as-is rather
63
+ than a table and column being invented — wrong-but-readable is worse
64
+ than opaque-but-true. */
65
+ const [table, column] = nameConstraint?.(aggregate.via) ?? [aggregate.via, ''];
66
+ const target = aggregate.fn === 'count' || !aggregate.column ? table : `${table}.${aggregate.column}`;
67
+ const on = aggregate.on ? ` on ${aggregate.on}` : '';
68
+ const alias = aggregate.alias ? ` as ${aggregate.alias}` : '';
69
+ return `${aggregate.fn} ${target} via ${column}${on}${alias}`;
70
+ }
71
+ /** A namer backed by a schema: the form callers with a connection should use. */
72
+ export function constraintNamer(schema, view) {
73
+ return (via) => {
74
+ /* An aggregate can hang off a path, not just the base, so every prefix
75
+ the view mentions is a candidate parent. */
76
+ const prefixes = new Set(['', ...(view.aggregates ?? []).map((a) => a.on ?? '')]);
77
+ for (const prefix of prefixes) {
78
+ const parent = tableAtPrefix(schema, view.base, prefix);
79
+ if (!parent)
80
+ continue;
81
+ const hit = backrefsTo(schema, parent.id).find((b) => b.fk.name === via);
82
+ if (hit)
83
+ return [hit.fk.from.table, hit.fk.from.columns[0]];
84
+ }
85
+ return undefined;
86
+ };
87
+ }
88
+ /** Render with a schema, so aggregates name real tables and columns. */
89
+ export function viewToTextWithSchema(schema, view) {
90
+ return viewToText(view, constraintNamer(schema, view));
91
+ }
92
+ function filterToText(view) {
93
+ const groups = view.filter?.groups ?? [];
94
+ if (!groups.length)
95
+ return '';
96
+ const rendered = groups.map((group) => group.map(conditionToText).join(' and '));
97
+ // A single group needs no parentheses; several are OR-ed and each is
98
+ // bracketed, matching how the parser reads them back.
99
+ return rendered.length === 1 ? rendered[0] : rendered.map((g) => `(${g})`).join(' or ');
100
+ }
101
+ function conditionToText(condition) {
102
+ const { path, op, value } = condition;
103
+ switch (op) {
104
+ case 'isNull':
105
+ return `${path} is empty`;
106
+ case 'isNotNull':
107
+ return `${path} is not empty`;
108
+ case 'contains':
109
+ return `${path} contains ${quote(value)}`;
110
+ case 'startsWith':
111
+ return `${path} startswith ${quote(value)}`;
112
+ case 'endsWith':
113
+ return `${path} endswith ${quote(value)}`;
114
+ case 'in': {
115
+ const list = Array.isArray(value) ? value : [value];
116
+ return `${path} in (${list.map(quote).join(', ')})`;
117
+ }
118
+ default:
119
+ return `${path} ${op} ${quote(value)}`;
120
+ }
121
+ }
122
+ /** Quote only when the grammar needs it, so the common case stays readable. */
123
+ function quote(value) {
124
+ if (value === null || value === undefined)
125
+ return '""';
126
+ const text = String(value);
127
+ if (typeof value === 'number' || typeof value === 'boolean')
128
+ return text;
129
+ return /^[\w.-]+$/.test(text) ? text : `"${text.replace(/"/g, '')}"`;
130
+ }
131
+ /* ---------- reading ---------- */
132
+ /**
133
+ * An aggregate clause: `count work_order_part via work_order_id as parts`.
134
+ *
135
+ * The function word is only treated as a clause when it is followed by
136
+ * something-then-`via`. A table with a column called `count` or `sum` is not
137
+ * unusual, and eating the word would turn `sum = 3` into a broken aggregate
138
+ * rather than a filter.
139
+ */
140
+ const AGGREGATE_RE = new RegExp(String.raw `\b(${AGGREGATE_FNS.join('|')})\s+([\w.$]+)\s+via\s+([\w$]+)` +
141
+ String.raw `(?:\s+on\s+([\w.$]+))?(?:\s+as\s+([\w$]+))?`, 'gi');
142
+ /**
143
+ * Parse a written view.
144
+ *
145
+ * The base table and the aggregates are handled here; everything else is
146
+ * handed to `parseViewQuery`, which the composer's filter box already uses.
147
+ * Two parsers for one language is how a language starts disagreeing with
148
+ * itself.
149
+ */
150
+ export function textToView(schema, text, options = {}) {
151
+ const errors = [];
152
+ const body = String(text ?? '').trim();
153
+ if (!body)
154
+ return { errors: [{ message: 'Start with a table name.' }] };
155
+ const baseName = body.split(/\s+/)[0];
156
+ const base = findTable(schema, baseName) ?? schema.tables.find((t) => t.name === baseName);
157
+ if (!base) {
158
+ return { errors: [{ message: `No table called "${baseName}" in this database.`, at: 0 }] };
159
+ }
160
+ const rest = body.slice(baseName.length);
161
+ /* Aggregates are lifted out before the rest is parsed, because
162
+ `parseViewQuery` knows nothing about them and would read `count` as the
163
+ start of a condition. */
164
+ const aggregates = [];
165
+ const remainder = rest.replace(AGGREGATE_RE, (_match, fn, target, viaColumn, on, alias) => {
166
+ /* `app.invoice_line` is one qualified table; `work_order_part.quantity`
167
+ is a table and a column; `app.invoice_line.quantity` is both. A dot
168
+ cannot tell them apart, so the schema does: if the whole target names a
169
+ table there is no column, otherwise the last dot separates them.
170
+
171
+ Splitting on the first dot — the obvious implementation — turned
172
+ `app.invoice_line` into table `app`, and the error it produced named a
173
+ table that does not exist. Postgres schemas are the common case for
174
+ this, which is exactly why it survived until the compiler was pointed
175
+ at a real one. */
176
+ const targetText = String(target);
177
+ const namesTable = (id) => Boolean(findTable(schema, id)) || schema.tables.some((t) => t.name === id);
178
+ let childTable = targetText;
179
+ let childColumn;
180
+ if (!namesTable(targetText)) {
181
+ const lastDot = targetText.lastIndexOf('.');
182
+ if (lastDot > 0) {
183
+ childTable = targetText.slice(0, lastDot);
184
+ childColumn = targetText.slice(lastDot + 1);
185
+ }
186
+ }
187
+ const parent = tableAtPrefix(schema, base.id, on ?? '');
188
+ if (!parent) {
189
+ errors.push({ message: `"${on}" is not a reference path on ${base.name}.` });
190
+ return '';
191
+ }
192
+ const backref = backrefsTo(schema, parent.id).find((b) => b.fk.from.table === childTable && b.fk.from.columns.includes(String(viaColumn)));
193
+ if (!backref) {
194
+ errors.push({
195
+ message: `Nothing in ${childTable}.${viaColumn} points at ${parent.name}. ` +
196
+ `Try: ${describeBackrefs(schema, parent.id)}`,
197
+ });
198
+ return '';
199
+ }
200
+ if (fn !== 'count' && !childColumn) {
201
+ errors.push({ message: `${fn} needs a column, e.g. ${fn} ${childTable}.amount via ${viaColumn}` });
202
+ return '';
203
+ }
204
+ if (childColumn) {
205
+ const childTableMeta = findTable(schema, backref.fk.from.table);
206
+ if (childTableMeta && !childTableMeta.columns.some((c) => c.name === childColumn)) {
207
+ errors.push({ message: `"${childColumn}" is not a column on ${childTable}.` });
208
+ return '';
209
+ }
210
+ }
211
+ aggregates.push({
212
+ fn: fn.toLowerCase(),
213
+ via: backref.fk.name,
214
+ on: on || undefined,
215
+ column: childColumn,
216
+ alias: alias || undefined,
217
+ });
218
+ return '';
219
+ });
220
+ /* Aliases are lifted out of the `show` clause before it is parsed.
221
+ `parseViewQuery` validates each entry as a path, and
222
+ `customer_id.name as customer` is not one — the alias is presentation,
223
+ not addressing, and only this layer knows about it. */
224
+ const { text: withoutAliases, aliases } = stripAliases(remainder);
225
+ const parsed = parseViewQuery(schema, base.id, withoutAliases, options.now, {
226
+ /* So `sort invoices desc` parses after `… as invoices` — the alias joins
227
+ the synthetic table for the sort clause, and only the sort clause. */
228
+ aggregateAliases: aggregates.map((a) => a.alias ?? '').filter(Boolean),
229
+ });
230
+ errors.push(...parsed.errors);
231
+ const view = {
232
+ id: options.id ?? 'text',
233
+ name: options.name ?? '',
234
+ base: base.id,
235
+ columns: parsed.columns.map((path) => {
236
+ const alias = aliases.get(path);
237
+ return alias ? { path, alias } : { path };
238
+ }),
239
+ aggregates: aggregates.length ? aggregates : undefined,
240
+ filter: parsed.filter.groups.length ? parsed.filter : undefined,
241
+ orderBy: parsed.orderBy.length ? parsed.orderBy : undefined,
242
+ limit: parsed.limit,
243
+ };
244
+ return { view, errors };
245
+ }
246
+ /**
247
+ * Remove ` as <alias>` from every entry of a `show` clause, returning the
248
+ * cleaned text and a path → alias map.
249
+ *
250
+ * Only the show clause: `as` means something else nowhere in this grammar
251
+ * today, but rewriting the whole string would break the moment it does.
252
+ */
253
+ function stripAliases(text) {
254
+ const aliases = new Map();
255
+ const cleaned = text.replace(/\b(show|select)\s+([^]*?)(?=\s+\b(?:where|filter|sort|order\s+by|limit)\b|$)/i, (_match, keyword, list) => {
256
+ const entries = list.split(',').map((entry) => {
257
+ const aliased = entry.trim().match(/^(.*?)\s+as\s+([\w$]+)$/i);
258
+ if (!aliased)
259
+ return entry.trim();
260
+ aliases.set(aliased[1].trim(), aliased[2]);
261
+ return aliased[1].trim();
262
+ });
263
+ return `${keyword} ${entries.join(', ')}`;
264
+ });
265
+ return { text: cleaned, aliases };
266
+ }
267
+ /** A short list of what *would* have worked, for the error to name. */
268
+ function describeBackrefs(schema, parentId) {
269
+ const options = backrefsTo(schema, parentId)
270
+ .slice(0, 3)
271
+ .map((b) => `${b.fk.from.table} via ${b.fk.from.columns[0]}`);
272
+ return options.length ? options.join(', ') : 'nothing points at this table';
273
+ }
@@ -0,0 +1,164 @@
1
+ /**
2
+ * The values a column is allowed to hold, when the database says so.
3
+ *
4
+ * A column called `status` typed `text` tells you nothing. A `CHECK (status
5
+ * IN ('draft','open','paid'))` tells you everything — the states the system
6
+ * has, which is often the single most useful fact about a table and the one
7
+ * an agent is most likely to guess wrong. Guessing produces `status =
8
+ * 'OPEN'`, which returns no rows and looks like an empty table rather than a
9
+ * spelling mistake.
10
+ *
11
+ * Every dialect writes the same constraint differently, so the parsing lives
12
+ * here and the adapters bring their own constraint text. This is deliberately
13
+ * a *recogniser*, not a SQL expression parser: it understands the handful of
14
+ * shapes that mean "one of these" and returns nothing for anything else. A
15
+ * partial understanding of `CHECK (total > 0 AND status IN (...))` would be
16
+ * worse than none, because the vocabulary it reported would be presented as
17
+ * the whole rule.
18
+ */
19
+ /** A quoted or bare identifier, as any dialect might write the column. */
20
+ function sameColumn(written, column) {
21
+ const bare = written.trim().replace(/^["`[]|["`\]]$/g, '');
22
+ return bare.toLowerCase() === column.toLowerCase();
23
+ }
24
+ /** `'a'`, `'it''s'`, `'a'::text` — the string literals inside a list. */
25
+ function literals(list) {
26
+ const values = [];
27
+ const pattern = /'((?:[^']|'')*)'(?:::[A-Za-z_" ]+(?:\[\])?)?\s*(,|$)/gy;
28
+ pattern.lastIndex = 0;
29
+ let rest = list.trim();
30
+ let at = 0;
31
+ while (at < rest.length) {
32
+ pattern.lastIndex = at;
33
+ const m = pattern.exec(rest);
34
+ if (!m)
35
+ return undefined;
36
+ values.push(m[1].replace(/''/g, "'"));
37
+ at = pattern.lastIndex;
38
+ while (rest[at] === ' ')
39
+ at += 1;
40
+ }
41
+ return values.length ? values : undefined;
42
+ }
43
+ /**
44
+ * Remove `::type` casts, which Postgres sprays through every stored
45
+ * expression — `(status)::text = ANY ((ARRAY['a'::character varying])::text[])`
46
+ * is `status = ANY (ARRAY['a'])` with six casts in it. Quoted strings are
47
+ * stepped over, so a value that happens to contain `::` survives.
48
+ */
49
+ function stripCasts(text) {
50
+ let out = '';
51
+ let quoted = null;
52
+ for (let i = 0; i < text.length; i += 1) {
53
+ const ch = text[i];
54
+ if (quoted) {
55
+ out += ch;
56
+ if (ch === quoted)
57
+ quoted = null;
58
+ continue;
59
+ }
60
+ if (ch === "'" || ch === '"') {
61
+ quoted = ch;
62
+ out += ch;
63
+ continue;
64
+ }
65
+ if (ch === ':' && text[i + 1] === ':') {
66
+ i += 2;
67
+ while (i < text.length && /[A-Za-z0-9_ "]/.test(text[i]))
68
+ i += 1;
69
+ if (text[i] === '[' && text[i + 1] === ']')
70
+ i += 1;
71
+ else
72
+ i -= 1;
73
+ continue;
74
+ }
75
+ out += ch;
76
+ }
77
+ return out;
78
+ }
79
+ /**
80
+ * The vocabulary a single CHECK expression pins for one column.
81
+ *
82
+ * Handles the two shapes that mean the same thing:
83
+ * `status IN ('a','b')` — every dialect
84
+ * `((status)::text = ANY ((ARRAY['a'::text, 'b'::text])))` — what Postgres
85
+ * stores instead
86
+ */
87
+ export function checkVocabulary(expression, column) {
88
+ const text = stripCasts(expression).trim();
89
+ /* Postgres rewrites `IN` to `= ANY (ARRAY[...])` before storing it, so the
90
+ constraint you wrote is never the constraint you read back. */
91
+ const any = /^[(\s]*([^\s()]+)[)\s]*=\s*ANY\s*[(\s]*ARRAY\s*\[(.*)\][\s)]*$/is.exec(text);
92
+ if (any && sameColumn(any[1], column))
93
+ return literals(any[2]);
94
+ const list = /^[(\s]*([^\s()]+)\s+IN\s*\((.*)\)[\s)]*$/is.exec(text);
95
+ if (list && sameColumn(list[1], column))
96
+ return literals(list[2]);
97
+ return undefined;
98
+ }
99
+ /**
100
+ * The vocabulary written into a column's type, as MySQL does it:
101
+ * `enum('draft','open')`, `set('a','b')`.
102
+ */
103
+ export function typeVocabulary(type) {
104
+ const m = /^\s*(?:enum|set)\s*\((.*)\)\s*$/is.exec(type);
105
+ return m ? literals(m[1]) : undefined;
106
+ }
107
+ /**
108
+ * Every CHECK clause in a `CREATE TABLE` statement, for dialects that keep
109
+ * the statement rather than the constraints — SQLite.
110
+ *
111
+ * Scanned with a depth counter rather than a regex because a check body
112
+ * contains parentheses of its own, and matching to the first `)` truncates
113
+ * `CHECK (status IN ('a','b'))` to `status IN ('a'`.
114
+ */
115
+ export function checksIn(ddl) {
116
+ const found = [];
117
+ const pattern = /\bCHECK\s*\(/gi;
118
+ let match;
119
+ while ((match = pattern.exec(ddl))) {
120
+ let depth = 1;
121
+ let i = match.index + match[0].length;
122
+ let quoted = null;
123
+ const start = i;
124
+ for (; i < ddl.length && depth > 0; i += 1) {
125
+ const ch = ddl[i];
126
+ if (quoted) {
127
+ if (ch === quoted)
128
+ quoted = null;
129
+ continue;
130
+ }
131
+ if (ch === "'" || ch === '"')
132
+ quoted = ch;
133
+ else if (ch === '(')
134
+ depth += 1;
135
+ else if (ch === ')')
136
+ depth -= 1;
137
+ }
138
+ if (depth === 0)
139
+ found.push(ddl.slice(start, i - 1));
140
+ pattern.lastIndex = i;
141
+ }
142
+ return found;
143
+ }
144
+ /**
145
+ * The allowed values for each column of a table, read out of its DDL.
146
+ *
147
+ * Column-level (`status TEXT CHECK (status IN (...))`) and table-level
148
+ * (`CHECK (status IN (...))`) checks are the same text by the time they get
149
+ * here, which is why this asks each check about each column rather than
150
+ * trying to work out which column a check was attached to.
151
+ */
152
+ export function vocabularyFromDDL(ddl, columns) {
153
+ const found = new Map();
154
+ for (const check of checksIn(ddl)) {
155
+ for (const column of columns) {
156
+ if (found.has(column))
157
+ continue;
158
+ const values = checkVocabulary(check, column);
159
+ if (values)
160
+ found.set(column, values);
161
+ }
162
+ }
163
+ return found;
164
+ }
package/package.json ADDED
@@ -0,0 +1,57 @@
1
+ {
2
+ "name": "tablewalk",
3
+ "version": "0.0.1",
4
+ "description": "Follow the data. A database browser built around the walk: a foreign key is a place you can go.",
5
+ "license": "MIT",
6
+ "author": "Baljinder Randhawa",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/rbaljinder/tablewalk.git"
10
+ },
11
+ "homepage": "https://github.com/rbaljinder/tablewalk#readme",
12
+ "bugs": {
13
+ "url": "https://github.com/rbaljinder/tablewalk/issues"
14
+ },
15
+ "type": "module",
16
+ "engines": {
17
+ "node": ">=24.0.0"
18
+ },
19
+ "bin": {
20
+ "tablewalk": "dist/server/index.js"
21
+ },
22
+ "files": [
23
+ "dist",
24
+ "README.md",
25
+ "LICENSE"
26
+ ],
27
+ "keywords": [
28
+ "database",
29
+ "sqlite",
30
+ "postgres",
31
+ "schema",
32
+ "browser",
33
+ "cli"
34
+ ],
35
+ "scripts": {
36
+ "dev": "tsx watch src/server/index.ts",
37
+ "start": "tsx src/server/index.ts",
38
+ "test": "tsc --noEmit && node test/check-client-imports.mjs && node test/check-client-types.mjs && tsx --test --test-timeout=120000 test/run.ts 'test/*.test.ts'",
39
+ "typecheck": "tsc --noEmit",
40
+ "build": "rm -rf dist && tsc -p tsconfig.build.json && cp -R src/client/. dist/client/ && test -f dist/client/index.html",
41
+ "prepack": "npm run build",
42
+ "prepublishOnly": "npm test",
43
+ "demo": "tsx test/make-demo-db.ts .demo/demo.db --scale 3000 && tsx src/server/index.ts --db .demo/demo.db --open",
44
+ "e2e": "playwright test",
45
+ "seed:live": "tsx test/seed-live.ts"
46
+ },
47
+ "optionalDependencies": {
48
+ "mysql2": "^3.23.4",
49
+ "pg": "^8.23.0"
50
+ },
51
+ "devDependencies": {
52
+ "@playwright/test": "^1.62.1",
53
+ "@types/node": "^22.10.0",
54
+ "tsx": "^4.23.12",
55
+ "typescript": "^5.7.3"
56
+ }
57
+ }