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,343 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A record template: one row, with its references pulled in around it.
|
|
3
|
+
*
|
|
4
|
+
* The view composer as built answers "give me a table of X with columns from
|
|
5
|
+
* the things X points at" — one row per base record, joins flattened into
|
|
6
|
+
* columns. That is a useful shape and the wrong one for the question tablewalk
|
|
7
|
+
* actually exists to answer, which is "show me *this* work order, properly".
|
|
8
|
+
*
|
|
9
|
+
* That question wants a page: the work order's own fields, the customer's
|
|
10
|
+
* details inline because a work order without its customer's name is a
|
|
11
|
+
* riddle, and its parts and notes as tables underneath because there are
|
|
12
|
+
* several of each and a count is not enough.
|
|
13
|
+
*
|
|
14
|
+
* The difference from a list view is not cosmetic — it is what the thing is
|
|
15
|
+
* bound to. A list view is a *query*: it produces rows and you read them. A
|
|
16
|
+
* record template is bound to a **table**, not to a row, and applies to every
|
|
17
|
+
* row of it. Configure `work_order` once and every work order you ever walk
|
|
18
|
+
* to is laid out that way. That is what makes it worth configuring at all.
|
|
19
|
+
*
|
|
20
|
+
* Two consequences fall out, and both are why this shape earns its place:
|
|
21
|
+
*
|
|
22
|
+
* - A has-many relationship is finally answerable. In a list view it either
|
|
23
|
+
* multiplies rows or collapses to a count; here it is simply a table on
|
|
24
|
+
* the page, which is what anyone asking for "the order and its lines"
|
|
25
|
+
* meant in the first place.
|
|
26
|
+
* - Views stop being a dead end. A list view is somewhere you look; a
|
|
27
|
+
* record template is where a walk *arrives*, so views become part of the
|
|
28
|
+
* navigation rather than a parallel product beside it.
|
|
29
|
+
*/
|
|
30
|
+
import { findTable, labelColumn, looksLikeDate, primaryKey, referencesFrom, referencesTo, } from './schema.js';
|
|
31
|
+
/** Rows to show in an embedded list before it needs its own page. */
|
|
32
|
+
export const DEFAULT_LIST_LIMIT = 10;
|
|
33
|
+
/**
|
|
34
|
+
* Below this many columns, grouping costs more than it saves.
|
|
35
|
+
*
|
|
36
|
+
* A four-column table split into "Identity" and "Details" is two headings and
|
|
37
|
+
* a divider to organise four fields — more ceremony than content, and slower
|
|
38
|
+
* to read than the flat list it replaced.
|
|
39
|
+
*/
|
|
40
|
+
export const GROUP_THRESHOLD = 12;
|
|
41
|
+
const AUDIT_NAME = /^(sys_|created|updated|modified|deleted|inserted)|_(at|by)$|^(version|etag|revision)$/i;
|
|
42
|
+
/**
|
|
43
|
+
* How a table's own columns divide into blocks.
|
|
44
|
+
*
|
|
45
|
+
* Derived from what the schema already says rather than from a stored layout:
|
|
46
|
+
* keys and the label column identify, foreign keys link, dates measure, and
|
|
47
|
+
* anything named like an audit column is bookkeeping. Ordering is by what
|
|
48
|
+
* gets looked at first, not by declaration order.
|
|
49
|
+
*
|
|
50
|
+
* Lives here rather than in the renderer because it *is* the default
|
|
51
|
+
* template. Having the derivation in one place and the stored layout in
|
|
52
|
+
* another is how the two start disagreeing about what a record looks like.
|
|
53
|
+
*/
|
|
54
|
+
export function groupOwnColumns(table) {
|
|
55
|
+
const columns = table.columns;
|
|
56
|
+
if (columns.length < GROUP_THRESHOLD)
|
|
57
|
+
return [[null, columns]];
|
|
58
|
+
const label = labelColumn(table);
|
|
59
|
+
const groups = new Map([
|
|
60
|
+
['Identity', []],
|
|
61
|
+
['Bookkeeping', []],
|
|
62
|
+
['References', []],
|
|
63
|
+
['Details', []],
|
|
64
|
+
['Dates', []],
|
|
65
|
+
]);
|
|
66
|
+
for (const col of columns) {
|
|
67
|
+
if (col.primaryKey || col.name === label)
|
|
68
|
+
groups.get('Identity').push(col);
|
|
69
|
+
else if (col.references)
|
|
70
|
+
groups.get('References').push(col);
|
|
71
|
+
else if (AUDIT_NAME.test(col.name))
|
|
72
|
+
groups.get('Bookkeeping').push(col);
|
|
73
|
+
else if (looksLikeDate(col))
|
|
74
|
+
groups.get('Dates').push(col);
|
|
75
|
+
else
|
|
76
|
+
groups.get('Details').push(col);
|
|
77
|
+
}
|
|
78
|
+
// A section with nothing in it is a heading that means nothing.
|
|
79
|
+
return [...groups].filter(([, cols]) => cols.length);
|
|
80
|
+
}
|
|
81
|
+
/* ---------- deriving a starting template ---------- */
|
|
82
|
+
/**
|
|
83
|
+
* The template a table gets before anyone configures one.
|
|
84
|
+
*
|
|
85
|
+
* Derived rather than blank, because a blank record page is a worse starting
|
|
86
|
+
* point than the default layout the record view already produces, and because
|
|
87
|
+
* the fastest way to explain what a template *is* is to show someone one made
|
|
88
|
+
* of their own data.
|
|
89
|
+
*
|
|
90
|
+
* The rules are the ones the record view already uses, made explicit: the
|
|
91
|
+
* record's own fields first, then each to-one reference as its own labelled
|
|
92
|
+
* block, then each incoming relationship as a table.
|
|
93
|
+
*/
|
|
94
|
+
export function deriveTemplate(schema, tableId) {
|
|
95
|
+
const table = findTable(schema, tableId);
|
|
96
|
+
if (!table)
|
|
97
|
+
return undefined;
|
|
98
|
+
const sections = groupOwnColumns(table).map(([title, columns]) => ({
|
|
99
|
+
kind: 'fields',
|
|
100
|
+
title: title ?? undefined,
|
|
101
|
+
paths: columns.map((c) => c.name),
|
|
102
|
+
}));
|
|
103
|
+
/* One block per outgoing reference, holding a few readable columns of the
|
|
104
|
+
row it points at. Not every column: pulling a customer's forty fields
|
|
105
|
+
into a work order buries the work order. */
|
|
106
|
+
for (const fk of referencesFrom(schema, tableId)) {
|
|
107
|
+
if (fk.to.table === tableId)
|
|
108
|
+
continue; // a self-reference adds no context
|
|
109
|
+
const target = findTable(schema, fk.to.table);
|
|
110
|
+
if (!target)
|
|
111
|
+
continue;
|
|
112
|
+
const via = fk.from.columns.join('+');
|
|
113
|
+
sections.push({
|
|
114
|
+
kind: 'fields',
|
|
115
|
+
title: `${target.name} · via ${fk.from.columns.join(', ')}`,
|
|
116
|
+
paths: summaryColumns(target).map((c) => `${via}.${c}`),
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
for (const fk of referencesTo(schema, tableId)) {
|
|
120
|
+
const child = findTable(schema, fk.from.table);
|
|
121
|
+
if (!child)
|
|
122
|
+
continue;
|
|
123
|
+
sections.push({
|
|
124
|
+
kind: 'list',
|
|
125
|
+
title: `${child.name} · via ${fk.from.columns.join(', ')}`,
|
|
126
|
+
via: fk.name,
|
|
127
|
+
columns: listColumns(child, fk.from.columns),
|
|
128
|
+
limit: DEFAULT_LIST_LIMIT,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
return { id: `derived:${tableId}`, name: table.name, base: tableId, sections };
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Every column of a child table worth showing, in declaration order.
|
|
135
|
+
*
|
|
136
|
+
* All of them, not a summary. A summary is the right shape for a reference
|
|
137
|
+
* pulled *into* a record — a customer's forty fields would bury the work order
|
|
138
|
+
* they are attached to — and the wrong shape for a table of children, which is
|
|
139
|
+
* the thing you opened the record to read. `work_order_part` summarised down
|
|
140
|
+
* to one column of quantities is a list of numbers with nothing to attach them
|
|
141
|
+
* to.
|
|
142
|
+
*
|
|
143
|
+
* The join columns are still dropped: every row here carries the same value
|
|
144
|
+
* for them, so a column of one repeated value is width spent on nothing. The
|
|
145
|
+
* key stays, because it is what distinguishes the rows.
|
|
146
|
+
*
|
|
147
|
+
* Anyone who wants fewer can say so in the layout builder, which is the right
|
|
148
|
+
* order for a default — start from everything and subtract, rather than start
|
|
149
|
+
* from a guess and hunt for what is missing.
|
|
150
|
+
*/
|
|
151
|
+
export function listColumns(table, exclude = []) {
|
|
152
|
+
const skip = new Set(exclude);
|
|
153
|
+
const chosen = table.columns.filter((c) => !skip.has(c.name)).map((c) => c.name);
|
|
154
|
+
// Everything was part of the join: the key is better than an empty table.
|
|
155
|
+
return chosen.length ? chosen : primaryKey(table).slice(0, 1);
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* A few columns that say what a row *is*.
|
|
159
|
+
*
|
|
160
|
+
* Keys and the columns forming the join are dropped: in a table of a work
|
|
161
|
+
* order's parts, every row has the same `work_order_id`, so a column of it is
|
|
162
|
+
* a column of one repeated value. What is left is what distinguishes the rows
|
|
163
|
+
* from each other, which is the only thing the section is there to show.
|
|
164
|
+
*/
|
|
165
|
+
export function summaryColumns(table, exclude = [], max = 5) {
|
|
166
|
+
const skip = new Set(exclude);
|
|
167
|
+
const chosen = [];
|
|
168
|
+
for (const column of table.columns) {
|
|
169
|
+
if (skip.has(column.name))
|
|
170
|
+
continue;
|
|
171
|
+
if (column.primaryKey && table.columns.some((c) => !c.primaryKey && !skip.has(c.name)))
|
|
172
|
+
continue;
|
|
173
|
+
chosen.push(column.name);
|
|
174
|
+
if (chosen.length >= max)
|
|
175
|
+
break;
|
|
176
|
+
}
|
|
177
|
+
// Everything was a key or excluded: the key is better than nothing.
|
|
178
|
+
return chosen.length ? chosen : primaryKey(table).slice(0, 1);
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Match each list section to the foreign key it names.
|
|
182
|
+
*
|
|
183
|
+
* A section whose constraint has since been dropped is returned as an error
|
|
184
|
+
* rather than skipped: a template that quietly stops showing a table looks
|
|
185
|
+
* exactly like a record that has no rows in it.
|
|
186
|
+
*/
|
|
187
|
+
export function resolveLists(schema, template) {
|
|
188
|
+
const lists = [];
|
|
189
|
+
const errors = [];
|
|
190
|
+
for (const section of template.sections) {
|
|
191
|
+
if (section.kind !== 'list')
|
|
192
|
+
continue;
|
|
193
|
+
const fk = referencesTo(schema, template.base).find((f) => f.name === section.via);
|
|
194
|
+
if (!fk) {
|
|
195
|
+
errors.push(`${template.name}: nothing called "${section.via}" points at ${template.base} any more. ` +
|
|
196
|
+
`The section was left in place so it can be removed or repointed.`);
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
const child = findTable(schema, fk.from.table);
|
|
200
|
+
if (!child) {
|
|
201
|
+
errors.push(`${template.name}: ${fk.from.table} is not in this schema.`);
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
lists.push({
|
|
205
|
+
section,
|
|
206
|
+
fk,
|
|
207
|
+
child,
|
|
208
|
+
join: fk.to.columns.map((parentColumn, i) => ({
|
|
209
|
+
parentColumn,
|
|
210
|
+
childColumn: fk.from.columns[i] ?? fk.from.columns[0],
|
|
211
|
+
})),
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
return { lists, errors };
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Every path the base row itself has to supply.
|
|
218
|
+
*
|
|
219
|
+
* Collected across field sections so the record can be fetched in one query
|
|
220
|
+
* rather than one per section — a page with six reference blocks should not
|
|
221
|
+
* be six round trips.
|
|
222
|
+
*/
|
|
223
|
+
export function fieldPaths(template) {
|
|
224
|
+
const seen = new Set();
|
|
225
|
+
for (const section of template.sections) {
|
|
226
|
+
if (section.kind !== 'fields')
|
|
227
|
+
continue;
|
|
228
|
+
for (const path of section.paths)
|
|
229
|
+
seen.add(path);
|
|
230
|
+
}
|
|
231
|
+
return [...seen];
|
|
232
|
+
}
|
|
233
|
+
/** Whether a template still says anything, after resolution. */
|
|
234
|
+
export function isEmpty(template) {
|
|
235
|
+
return !template.sections.some((s) => s.kind === 'list' ? true : s.paths.length > 0);
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* Read the `layouts` section of a tablewalk.json.
|
|
239
|
+
*
|
|
240
|
+
* Throws on anything malformed rather than skipping it, naming the entry and
|
|
241
|
+
* the fix — the same rule `parsePagesConfig` follows, for the same reason: a
|
|
242
|
+
* layout that silently did not appear is a bug report that starts "I
|
|
243
|
+
* definitely wrote it".
|
|
244
|
+
*
|
|
245
|
+
* Structure only. Whether `customer_id` is a real column, or `invoice` a real
|
|
246
|
+
* table, needs a schema this runs long before — those are answered when the
|
|
247
|
+
* layout is served, and reported on the layout itself.
|
|
248
|
+
*/
|
|
249
|
+
export function parseLayoutsConfig(raw, where) {
|
|
250
|
+
const layouts = raw?.layouts;
|
|
251
|
+
if (layouts === undefined)
|
|
252
|
+
return [];
|
|
253
|
+
if (!Array.isArray(layouts))
|
|
254
|
+
throw new Error(`${where}: "layouts" must be an array.`);
|
|
255
|
+
const seen = new Set();
|
|
256
|
+
return layouts.map((entry, i) => {
|
|
257
|
+
const at = `${where}: layout ${i + 1}`;
|
|
258
|
+
if (!entry || typeof entry !== 'object' || Array.isArray(entry)) {
|
|
259
|
+
throw new Error(`${at} is not an object.`);
|
|
260
|
+
}
|
|
261
|
+
const l = entry;
|
|
262
|
+
if (typeof l.table !== 'string' || !l.table.trim()) {
|
|
263
|
+
throw new Error(`${at} has no "table" — a layout lays out one table's records.`);
|
|
264
|
+
}
|
|
265
|
+
const table = l.table.trim();
|
|
266
|
+
if (l.connection !== undefined && typeof l.connection !== 'string') {
|
|
267
|
+
throw new Error(`${at} ("${table}") has a "connection" that is not a string.`);
|
|
268
|
+
}
|
|
269
|
+
if (l.name !== undefined && typeof l.name !== 'string') {
|
|
270
|
+
throw new Error(`${at} ("${table}") has a "name" that is not a string.`);
|
|
271
|
+
}
|
|
272
|
+
/* One per table per connection: the store this fills is keyed that way,
|
|
273
|
+
so a second would silently win over the first. */
|
|
274
|
+
const key = `${l.connection ?? ''}::${table}`;
|
|
275
|
+
if (seen.has(key))
|
|
276
|
+
throw new Error(`${at} repeats a layout for "${table}".`);
|
|
277
|
+
seen.add(key);
|
|
278
|
+
if (!Array.isArray(l.sections) || !l.sections.length) {
|
|
279
|
+
throw new Error(`${at} ("${table}") has no "sections".`);
|
|
280
|
+
}
|
|
281
|
+
const sections = l.sections.map((raw2, j) => {
|
|
282
|
+
const sat = `${at} ("${table}"), section ${j + 1}`;
|
|
283
|
+
if (!raw2 || typeof raw2 !== 'object' || Array.isArray(raw2)) {
|
|
284
|
+
throw new Error(`${sat} is not an object.`);
|
|
285
|
+
}
|
|
286
|
+
const s = raw2;
|
|
287
|
+
if (s.kind !== 'fields' && s.kind !== 'list') {
|
|
288
|
+
throw new Error(`${sat} has "kind": ${JSON.stringify(s.kind)} — it must be "fields" or "list".`);
|
|
289
|
+
}
|
|
290
|
+
if (s.title !== undefined && typeof s.title !== 'string') {
|
|
291
|
+
throw new Error(`${sat} has a "title" that is not a string.`);
|
|
292
|
+
}
|
|
293
|
+
if (s.kind === 'fields') {
|
|
294
|
+
if (!Array.isArray(s.paths) || !s.paths.every((p) => typeof p === 'string' && p.trim())) {
|
|
295
|
+
throw new Error(`${sat} needs "paths": a list of column names or paths like customer_id.name.`);
|
|
296
|
+
}
|
|
297
|
+
return { kind: 'fields', ...(s.title ? { title: s.title } : {}), paths: s.paths.map((p) => String(p).trim()) };
|
|
298
|
+
}
|
|
299
|
+
/* A list says which relationship it shows, one way or the other. */
|
|
300
|
+
const hasVia = typeof s.via === 'string' && s.via.trim();
|
|
301
|
+
const hasPair = typeof s.from === 'string' && s.from.trim() && typeof s.path === 'string' && s.path.trim();
|
|
302
|
+
if (!hasVia && !hasPair) {
|
|
303
|
+
throw new Error(`${sat} needs either "via" (a foreign key constraint name) or "from" and "path" `
|
|
304
|
+
+ `— the child table and the column of it that points back here.`);
|
|
305
|
+
}
|
|
306
|
+
if (s.columns !== undefined
|
|
307
|
+
&& (!Array.isArray(s.columns) || !s.columns.every((c) => typeof c === 'string'))) {
|
|
308
|
+
throw new Error(`${sat} has "columns" that is not a list of column names.`);
|
|
309
|
+
}
|
|
310
|
+
if (s.limit !== undefined && (typeof s.limit !== 'number' || !Number.isInteger(s.limit) || s.limit < 1)) {
|
|
311
|
+
throw new Error(`${sat} has a "limit" that is not a whole number of rows.`);
|
|
312
|
+
}
|
|
313
|
+
if (s.orderBy !== undefined) {
|
|
314
|
+
if (!Array.isArray(s.orderBy))
|
|
315
|
+
throw new Error(`${sat} has an "orderBy" that is not an array.`);
|
|
316
|
+
for (const o of s.orderBy) {
|
|
317
|
+
const entry2 = o;
|
|
318
|
+
if (!entry2 || typeof entry2.column !== 'string') {
|
|
319
|
+
throw new Error(`${sat} has an "orderBy" entry with no "column".`);
|
|
320
|
+
}
|
|
321
|
+
if (entry2.direction !== undefined && entry2.direction !== 'asc' && entry2.direction !== 'desc') {
|
|
322
|
+
throw new Error(`${sat} has an "orderBy" direction that is not "asc" or "desc".`);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
return {
|
|
327
|
+
kind: 'list',
|
|
328
|
+
...(s.title ? { title: s.title } : {}),
|
|
329
|
+
...(hasVia ? { via: String(s.via).trim() } : {}),
|
|
330
|
+
...(hasPair ? { from: String(s.from).trim(), path: String(s.path).trim() } : {}),
|
|
331
|
+
columns: Array.isArray(s.columns) ? s.columns.map((c) => String(c)) : [],
|
|
332
|
+
...(s.limit !== undefined ? { limit: s.limit } : {}),
|
|
333
|
+
...(s.orderBy !== undefined ? { orderBy: s.orderBy } : {}),
|
|
334
|
+
};
|
|
335
|
+
});
|
|
336
|
+
return {
|
|
337
|
+
table,
|
|
338
|
+
...(l.connection ? { connection: String(l.connection) } : {}),
|
|
339
|
+
...(l.name ? { name: String(l.name) } : {}),
|
|
340
|
+
sections,
|
|
341
|
+
};
|
|
342
|
+
});
|
|
343
|
+
}
|