datagrok-tools 6.5.7 → 6.5.8
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/CHANGELOG.md +31 -0
- package/CLAUDE.md +25 -10
- package/Core.json +1027 -0
- package/GROK_S.md +511 -27
- package/bin/commands/api.js +121 -70
- package/bin/commands/help.js +3 -75
- package/bin/commands/server-domains.js +468 -0
- package/bin/commands/server-migrate.js +392 -0
- package/bin/commands/server.js +208 -72
- package/bin/grok.js +14 -5
- package/bin/utils/migrate/bundle.js +223 -0
- package/bin/utils/migrate/bundle.ts +222 -0
- package/bin/utils/migrate/parts.js +83 -0
- package/bin/utils/migrate/parts.ts +72 -0
- package/bin/utils/migrate/pool.js +17 -0
- package/bin/utils/migrate/pool.ts +13 -0
- package/bin/utils/migrate/pusher.js +980 -0
- package/bin/utils/migrate/pusher.ts +829 -0
- package/bin/utils/migrate/registry.js +349 -0
- package/bin/utils/migrate/registry.ts +255 -0
- package/bin/utils/migrate/rewriter.js +59 -0
- package/bin/utils/migrate/rewriter.ts +59 -0
- package/bin/utils/migrate/walker.js +571 -0
- package/bin/utils/migrate/walker.ts +509 -0
- package/bin/utils/node-dapi.js +692 -141
- package/bin/utils/playwright-runner.js +3 -1
- package/bin/utils/server-client.js +15 -2
- package/bin/utils/server-output.js +65 -4
- package/bin/utils/test-utils.js +1 -1
- package/domain-schema.schema.json +57 -6
- package/package.json +6 -1
- /package/{vitest.config.ts → vitest.config.mts} +0 -0
|
@@ -0,0 +1,468 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.applyBody = applyBody;
|
|
7
|
+
exports.columnRows = columnRows;
|
|
8
|
+
exports.handleDomains = handleDomains;
|
|
9
|
+
exports.parseKeyValues = parseKeyValues;
|
|
10
|
+
exports.parseMeasures = parseMeasures;
|
|
11
|
+
exports.printBatchReport = printBatchReport;
|
|
12
|
+
exports.querySpec = querySpec;
|
|
13
|
+
exports.rowsToCsv = rowsToCsv;
|
|
14
|
+
exports.schemaRow = schemaRow;
|
|
15
|
+
exports.tableRow = tableRow;
|
|
16
|
+
var fs = _interopRequireWildcard(require("fs"));
|
|
17
|
+
var path = _interopRequireWildcard(require("path"));
|
|
18
|
+
var _nodeDapi = require("../utils/node-dapi");
|
|
19
|
+
var _serverOutput = require("../utils/server-output");
|
|
20
|
+
function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f; })(e, t); }
|
|
21
|
+
/// `grok s domains ...` — entity-mapped domain schemas and their rows (DomainsRouter, `/domains/`).
|
|
22
|
+
/// Every verb addresses a schema as `<schema>` or a table as `<schema>.<table>`.
|
|
23
|
+
|
|
24
|
+
const APPLY_KEYS = ['tables', 'extend', 'propertySchemas', 'dropTables', 'ifVersion', 'confirmDestructive'];
|
|
25
|
+
const PERMISSIONS = ['View', 'Edit', 'Delete', 'Share', 'Extend'];
|
|
26
|
+
const USAGE = `Usage: grok s domains <verb> [args]
|
|
27
|
+
list [<schema>] [--filter <text>] Schemas, or the tables of one schema
|
|
28
|
+
get <schema> | <schema.table> [<row-id>] Manifest, a table's columns, or one row
|
|
29
|
+
query <schema.table> [--filter <expr>] [--columns a,b] [--sort 'a,!b'] [--expand x] [--limit n] [--offset n]
|
|
30
|
+
count <schema.table> [--filter <expr>]
|
|
31
|
+
insert <schema.table> --json rows.json | col=value ... [--error-on-duplicate]
|
|
32
|
+
update <schema.table> <row-id> --json values.json | col=value ... [--version n]
|
|
33
|
+
delete <schema.table> <row-id> | --filter <expr> [--limit n]
|
|
34
|
+
delete <schema> --force Purge a user-managed schema (data included)
|
|
35
|
+
upload <schema.table> <file.csv|.d42|.json> [--upsert] [--no-all-or-nothing] [--error-on-duplicate]
|
|
36
|
+
download <schema.table> [-O out.csv|out.d42] [--filter <expr>] [--columns a,b] [--sort 'a,!b'] [--limit n]
|
|
37
|
+
aggregate <schema.table> --measures 'count,sum(x) as total' [--group-by a,b] [--filter <expr>] [--sort s] [--limit n]
|
|
38
|
+
aggregate <schema.table> --json spec.json
|
|
39
|
+
transaction <schema> --json ops.json
|
|
40
|
+
audit <schema> | <schema.table> [<row-id>] [--limit n]
|
|
41
|
+
capabilities <schema.table>
|
|
42
|
+
grants <schema> | <schema.table>
|
|
43
|
+
grant <schema> | <schema.table> <group>[,<group>...] [--access View|Edit|Delete|Share|Extend]
|
|
44
|
+
revoke <schema> | <schema.table> <group>[,<group>...] [--access <permission>]
|
|
45
|
+
create <name> [--friendly-name <text>] [--description <text>]
|
|
46
|
+
apply <schema> --json manifest.json [--dry-run] [--confirm-destructive] [--if-version <v>]`;
|
|
47
|
+
async function handleDomains(dapi, verb, rest, argv, output) {
|
|
48
|
+
const args = rest.map(String);
|
|
49
|
+
const domains = dapi.domains;
|
|
50
|
+
const filter = argv.filter ?? argv.f ?? '';
|
|
51
|
+
const limit = argv.limit ?? argv.l;
|
|
52
|
+
const offset = Number(argv.offset ?? 0);
|
|
53
|
+
switch (verb) {
|
|
54
|
+
case 'list':
|
|
55
|
+
{
|
|
56
|
+
if (!args[0]) {
|
|
57
|
+
const schemas = await domains.schemas(filter);
|
|
58
|
+
(0, _serverOutput.printOutput)(schemas.map(schemaRow), output);
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
const schema = await domains.schema((0, _nodeDapi.parseDomainAddress)(args[0], {
|
|
62
|
+
table: false
|
|
63
|
+
}).schema);
|
|
64
|
+
(0, _serverOutput.printOutput)((schema.tables ?? []).map(tableRow), output);
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
case 'get':
|
|
68
|
+
{
|
|
69
|
+
if (!args[0]) return usage('get <schema> | <schema.table> [<row-id>]');
|
|
70
|
+
const a = (0, _nodeDapi.parseDomainAddress)(args[0]);
|
|
71
|
+
if (!a.table) {
|
|
72
|
+
printJson(await domains.manifest(a.schema), output, m => m?.name);
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
if (args[1]) {
|
|
76
|
+
const row = await domains.getRow(a.schema, a.table, args[1]);
|
|
77
|
+
if (!row) throw new Error(`Row ${args[1]} not found in ${args[0]}`);
|
|
78
|
+
(0, _serverOutput.printOutput)(row, output);
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
const manifest = await domains.manifest(a.schema);
|
|
82
|
+
const table = manifest?.tables?.[a.table];
|
|
83
|
+
if (!table) throw new Error(`Domain table '${args[0]}' not found`);
|
|
84
|
+
if (output === 'json') (0, _serverOutput.printOutput)(table, output);else (0, _serverOutput.printOutput)(columnRows(table), output);
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
case 'query':
|
|
88
|
+
{
|
|
89
|
+
if (!args[0]) return usage('query <schema.table> [--filter <expr>] [--columns a,b] [--sort s] [--limit n]');
|
|
90
|
+
const a = (0, _nodeDapi.parseDomainAddress)(args[0], {
|
|
91
|
+
table: true
|
|
92
|
+
});
|
|
93
|
+
const rows = await domains.query(a.schema, a.table, querySpec(argv, filter, limit ?? 50, offset));
|
|
94
|
+
(0, _serverOutput.printOutput)(rows, output);
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
97
|
+
case 'count':
|
|
98
|
+
{
|
|
99
|
+
if (!args[0]) return usage('count <schema.table> [--filter <expr>]');
|
|
100
|
+
const a = (0, _nodeDapi.parseDomainAddress)(args[0], {
|
|
101
|
+
table: true
|
|
102
|
+
});
|
|
103
|
+
const count = await domains.count(a.schema, a.table, filter || undefined);
|
|
104
|
+
if (output === 'json' || output === 'csv') (0, _serverOutput.printOutput)({
|
|
105
|
+
count
|
|
106
|
+
}, output);else console.log(count);
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
case 'insert':
|
|
110
|
+
{
|
|
111
|
+
if (!args[0]) return usage('insert <schema.table> --json rows.json | col=value ...');
|
|
112
|
+
const a = (0, _nodeDapi.parseDomainAddress)(args[0], {
|
|
113
|
+
table: true
|
|
114
|
+
});
|
|
115
|
+
const rows = argv.json ? readJson(argv.json) : parseKeyValues(args.slice(1));
|
|
116
|
+
if (!argv.json && !Object.keys(rows).length) return usage('insert <schema.table> --json rows.json | col=value ...');
|
|
117
|
+
(0, _serverOutput.printOutput)(await domains.insert(a.schema, a.table, rows, argv['error-on-duplicate'] === true), output);
|
|
118
|
+
return true;
|
|
119
|
+
}
|
|
120
|
+
case 'update':
|
|
121
|
+
{
|
|
122
|
+
if (!args[0] || !args[1]) return usage('update <schema.table> <row-id> --json values.json | col=value ... [--version n]');
|
|
123
|
+
const a = (0, _nodeDapi.parseDomainAddress)(args[0], {
|
|
124
|
+
table: true
|
|
125
|
+
});
|
|
126
|
+
const values = argv.json ? readJson(argv.json) : parseKeyValues(args.slice(2));
|
|
127
|
+
if (!argv.json && !Object.keys(values).length) return usage('update <schema.table> <row-id> --json values.json | col=value ...');
|
|
128
|
+
const version = argv.version !== undefined ? Number(argv.version) : undefined;
|
|
129
|
+
(0, _serverOutput.printOutput)(await domains.update(a.schema, a.table, args[1], values, version), output);
|
|
130
|
+
return true;
|
|
131
|
+
}
|
|
132
|
+
case 'delete':
|
|
133
|
+
{
|
|
134
|
+
if (!args[0]) return usage('delete <schema.table> <row-id> | delete <schema.table> --filter <expr> | delete <schema> --force');
|
|
135
|
+
const a = (0, _nodeDapi.parseDomainAddress)(args[0]);
|
|
136
|
+
if (!a.table) {
|
|
137
|
+
if (argv.force !== true) throw new Error(`Deleting schema '${a.schema}' purges every table and row in it; re-run with --force to confirm`);
|
|
138
|
+
await domains.deleteSchema(a.schema);
|
|
139
|
+
if (output !== 'quiet') console.log(`Deleted schema ${a.schema}`);
|
|
140
|
+
return true;
|
|
141
|
+
}
|
|
142
|
+
if (args[1]) {
|
|
143
|
+
await domains.deleteRow(a.schema, a.table, args[1]);
|
|
144
|
+
if (output !== 'quiet') console.log(`Deleted ${args[0]}/${args[1]}`);
|
|
145
|
+
return true;
|
|
146
|
+
}
|
|
147
|
+
if (!filter) return usage('delete <schema.table> <row-id> | delete <schema.table> --filter <expr> [--limit n]');
|
|
148
|
+
const report = await domains.deleteWhere(a.schema, a.table, filter, limit);
|
|
149
|
+
if (output === 'json' || output === 'csv') (0, _serverOutput.printOutput)(report, output);else if (output !== 'quiet') console.log(`Deleted ${report.deleted} rows${report.hasMore ? ' (more remain — re-run to continue)' : ''}`);
|
|
150
|
+
return true;
|
|
151
|
+
}
|
|
152
|
+
case 'upload':
|
|
153
|
+
return await handleUpload(dapi, args, argv, output);
|
|
154
|
+
case 'download':
|
|
155
|
+
return await handleDownload(dapi, args, argv, filter, limit, offset, output);
|
|
156
|
+
case 'aggregate':
|
|
157
|
+
{
|
|
158
|
+
if (!args[0]) return usage('aggregate <schema.table> --measures <m> [--group-by a,b] | --json spec.json');
|
|
159
|
+
const a = (0, _nodeDapi.parseDomainAddress)(args[0], {
|
|
160
|
+
table: true
|
|
161
|
+
});
|
|
162
|
+
const spec = argv.json ? readJson(argv.json) : aggregateSpec(argv, filter, limit);
|
|
163
|
+
if (!spec.measures?.length && !argv.json) return usage('aggregate <schema.table> --measures \'count,sum(x) as total\' [--group-by a,b]');
|
|
164
|
+
(0, _serverOutput.printOutput)(await domains.aggregate(a.schema, a.table, spec), output);
|
|
165
|
+
return true;
|
|
166
|
+
}
|
|
167
|
+
case 'transaction':
|
|
168
|
+
{
|
|
169
|
+
if (!args[0] || !argv.json) return usage('transaction <schema> --json ops.json');
|
|
170
|
+
const a = (0, _nodeDapi.parseDomainAddress)(args[0], {
|
|
171
|
+
table: false
|
|
172
|
+
});
|
|
173
|
+
const ops = readJson(argv.json);
|
|
174
|
+
(0, _serverOutput.printOutput)(await domains.transaction(a.schema, Array.isArray(ops) ? ops : ops.ops), output);
|
|
175
|
+
return true;
|
|
176
|
+
}
|
|
177
|
+
case 'audit':
|
|
178
|
+
{
|
|
179
|
+
if (!args[0]) return usage('audit <schema> | <schema.table> [<row-id>] [--limit n]');
|
|
180
|
+
const a = (0, _nodeDapi.parseDomainAddress)(args[0]);
|
|
181
|
+
const entries = !a.table ? await domains.schemaAudit(a.schema, limit) : args[1] ? await domains.rowAudit(a.schema, a.table, args[1]) : await domains.tableAudit(a.schema, a.table, limit);
|
|
182
|
+
(0, _serverOutput.printOutput)(entries, output);
|
|
183
|
+
return true;
|
|
184
|
+
}
|
|
185
|
+
case 'capabilities':
|
|
186
|
+
{
|
|
187
|
+
if (!args[0]) return usage('capabilities <schema.table>');
|
|
188
|
+
const a = (0, _nodeDapi.parseDomainAddress)(args[0], {
|
|
189
|
+
table: true
|
|
190
|
+
});
|
|
191
|
+
(0, _serverOutput.printOutput)(flatten(await domains.capabilities(a.schema, a.table)), output);
|
|
192
|
+
return true;
|
|
193
|
+
}
|
|
194
|
+
case 'grants':
|
|
195
|
+
{
|
|
196
|
+
if (!args[0]) return usage('grants <schema> | <schema.table>');
|
|
197
|
+
const grants = await domains.grants(await domains.entityId((0, _nodeDapi.parseDomainAddress)(args[0])));
|
|
198
|
+
(0, _serverOutput.printOutput)((grants ?? []).map(grantRow), output);
|
|
199
|
+
return true;
|
|
200
|
+
}
|
|
201
|
+
case 'grant':
|
|
202
|
+
case 'revoke':
|
|
203
|
+
return await handleGrant(dapi, verb, args, argv, output);
|
|
204
|
+
case 'create':
|
|
205
|
+
{
|
|
206
|
+
if (!args[0]) return usage('create <name> [--friendly-name <text>] [--description <text>]');
|
|
207
|
+
const res = await domains.createSchema(args[0], optString(argv['friendly-name']), optString(argv.description));
|
|
208
|
+
(0, _serverOutput.printOutput)(res, output);
|
|
209
|
+
return true;
|
|
210
|
+
}
|
|
211
|
+
case 'apply':
|
|
212
|
+
{
|
|
213
|
+
if (!args[0] || !argv.json) return usage('apply <schema> --json manifest.json [--dry-run] [--confirm-destructive] [--if-version <v>]');
|
|
214
|
+
const a = (0, _nodeDapi.parseDomainAddress)(args[0], {
|
|
215
|
+
table: false
|
|
216
|
+
});
|
|
217
|
+
const body = applyBody(readJson(argv.json), argv);
|
|
218
|
+
printJson(await domains.applySchema(a.schema, body, argv['dry-run'] === true), output, r => r?.version);
|
|
219
|
+
return true;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
(0, _serverOutput.printError)(new Error(USAGE));
|
|
223
|
+
return false;
|
|
224
|
+
}
|
|
225
|
+
function usage(line) {
|
|
226
|
+
(0, _serverOutput.printError)(new Error(`Usage: grok s domains ${line}`));
|
|
227
|
+
return false;
|
|
228
|
+
}
|
|
229
|
+
function optString(v) {
|
|
230
|
+
return v === undefined || v === true ? undefined : String(v);
|
|
231
|
+
}
|
|
232
|
+
function readJson(file) {
|
|
233
|
+
try {
|
|
234
|
+
return JSON.parse(fs.readFileSync(String(file), 'utf8'));
|
|
235
|
+
} catch (err) {
|
|
236
|
+
throw new Error(`Cannot read JSON file '${file}': ${err.message}`);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/** A nested result (manifest, apply plan) is printed as JSON in every format but quiet. */
|
|
241
|
+
function printJson(value, output, quietKey) {
|
|
242
|
+
if (output === 'quiet') console.log(quietKey?.(value) ?? '');else (0, _serverOutput.printOutput)(value, 'json');
|
|
243
|
+
}
|
|
244
|
+
function listFlag(v) {
|
|
245
|
+
if (v === undefined || v === true) return undefined;
|
|
246
|
+
const parts = (Array.isArray(v) ? v : [v]).flatMap(x => String(x).split(',')).map(s => s.trim()).filter(Boolean);
|
|
247
|
+
return parts.length ? parts : undefined;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/** Row values from `col=value` arguments; a value that parses as JSON is sent typed, anything else as a string. */
|
|
251
|
+
function parseKeyValues(args) {
|
|
252
|
+
const values = {};
|
|
253
|
+
for (const a of args) {
|
|
254
|
+
const s = String(a);
|
|
255
|
+
const eq = s.indexOf('=');
|
|
256
|
+
if (eq <= 0) throw new Error(`Expected <column>=<value>, got '${s}'`);
|
|
257
|
+
const raw = s.slice(eq + 1);
|
|
258
|
+
let v = raw;
|
|
259
|
+
try {
|
|
260
|
+
v = JSON.parse(raw);
|
|
261
|
+
} catch {/* a bare string */}
|
|
262
|
+
values[s.slice(0, eq)] = v;
|
|
263
|
+
}
|
|
264
|
+
return values;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/** `'count, sum(amount) as total, avg(amount)'` → aggregate measures. */
|
|
268
|
+
function parseMeasures(s) {
|
|
269
|
+
return String(s).split(',').map(m => m.trim()).filter(Boolean).map(m => {
|
|
270
|
+
const match = /^(\w+)\s*(?:\(\s*([\w.]*)\s*\))?(?:\s+as\s+(\w+))?$/i.exec(m);
|
|
271
|
+
if (!match) throw new Error(`Cannot parse measure '${m}' (expected 'count', 'sum(amount)' or 'avg(amount) as mean')`);
|
|
272
|
+
const [, fn, column, as] = match;
|
|
273
|
+
return {
|
|
274
|
+
fn: fn.toLowerCase(),
|
|
275
|
+
...(column ? {
|
|
276
|
+
column
|
|
277
|
+
} : {}),
|
|
278
|
+
...(as ? {
|
|
279
|
+
as
|
|
280
|
+
} : {})
|
|
281
|
+
};
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
function querySpec(argv, filter, limit, offset) {
|
|
285
|
+
const spec = {};
|
|
286
|
+
if (filter) spec.filter = filter;
|
|
287
|
+
if (argv.sort) spec.sort = String(argv.sort);
|
|
288
|
+
const columns = listFlag(argv.columns);
|
|
289
|
+
if (columns) spec.columns = columns;
|
|
290
|
+
const expand = listFlag(argv.expand);
|
|
291
|
+
if (expand) spec.expand = expand;
|
|
292
|
+
if (limit !== undefined) spec.limit = Number(limit);
|
|
293
|
+
if (offset > 0) spec.offset = offset;
|
|
294
|
+
return spec;
|
|
295
|
+
}
|
|
296
|
+
function aggregateSpec(argv, filter, limit) {
|
|
297
|
+
const spec = {
|
|
298
|
+
measures: argv.measures ? parseMeasures(argv.measures) : []
|
|
299
|
+
};
|
|
300
|
+
const groupBy = listFlag(argv['group-by']);
|
|
301
|
+
if (groupBy) spec.groupBy = groupBy;
|
|
302
|
+
if (filter) spec.filter = filter;
|
|
303
|
+
if (argv.sort) spec.sort = String(argv.sort);
|
|
304
|
+
if (limit !== undefined) spec.limit = Number(limit);
|
|
305
|
+
return spec;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/** The apply payload from a schema.json (or a partial body): only the keys the server accepts, flags folded in. */
|
|
309
|
+
function applyBody(json, argv) {
|
|
310
|
+
const body = {};
|
|
311
|
+
for (const k of APPLY_KEYS) if (json?.[k] !== undefined) body[k] = json[k];
|
|
312
|
+
if (argv['confirm-destructive'] === true) body.confirmDestructive = true;
|
|
313
|
+
if (argv['if-version'] !== undefined) body.ifVersion = String(argv['if-version']);
|
|
314
|
+
return body;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/** Serializer omits defaults, so the table view fills them back in. */
|
|
318
|
+
function schemaRow(s) {
|
|
319
|
+
return {
|
|
320
|
+
name: s?.name ?? '',
|
|
321
|
+
friendlyName: s?.friendlyName ?? '',
|
|
322
|
+
managedBy: s?.managedBy ?? 'package',
|
|
323
|
+
version: s?.version ?? '',
|
|
324
|
+
tables: Array.isArray(s?.tables) ? s.tables.length : 0,
|
|
325
|
+
id: s?.id ?? ''
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
function tableRow(t) {
|
|
329
|
+
return {
|
|
330
|
+
name: t?.name ?? '',
|
|
331
|
+
securityMode: t?.securityMode ?? 'table',
|
|
332
|
+
businessKey: Array.isArray(t?.businessKey) ? t.businessKey.join(',') : '',
|
|
333
|
+
nameColumn: t?.nameColumn ?? '',
|
|
334
|
+
origin: t?.origin ?? 'package',
|
|
335
|
+
readOnly: t?.readOnly ?? false,
|
|
336
|
+
description: t?.description ?? '',
|
|
337
|
+
id: t?.id ?? ''
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
function columnRows(table) {
|
|
341
|
+
return Object.entries(table?.columns ?? {}).map(([name, c]) => ({
|
|
342
|
+
column: name,
|
|
343
|
+
type: c?.type ?? '',
|
|
344
|
+
ref: c?.ref ?? '',
|
|
345
|
+
required: !!c?.required,
|
|
346
|
+
unique: !!c?.unique,
|
|
347
|
+
default: c?.default ?? '',
|
|
348
|
+
choices: Array.isArray(c?.choices) ? c.choices.join('|') : '',
|
|
349
|
+
description: c?.description ?? ''
|
|
350
|
+
}));
|
|
351
|
+
}
|
|
352
|
+
function grantRow(g) {
|
|
353
|
+
return {
|
|
354
|
+
group: g?.group?.friendlyName ?? g?.group?.id ?? '',
|
|
355
|
+
permission: g?.permission ?? '',
|
|
356
|
+
personal: g?.group?.personal ?? false,
|
|
357
|
+
groupId: g?.group?.id ?? ''
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
function flatten(o) {
|
|
361
|
+
const out = {};
|
|
362
|
+
for (const [k, v] of Object.entries(o ?? {})) out[k] = Array.isArray(v) ? v.join(',') : v;
|
|
363
|
+
return out;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/** Full-width CSV for exports: every key of every row, objects as JSON (the table printer truncates both). */
|
|
367
|
+
function rowsToCsv(rows) {
|
|
368
|
+
if (!rows.length) return '';
|
|
369
|
+
const keys = [];
|
|
370
|
+
const seen = new Set();
|
|
371
|
+
for (const r of rows) for (const k of Object.keys(r ?? {})) if (!seen.has(k)) {
|
|
372
|
+
seen.add(k);
|
|
373
|
+
keys.push(k);
|
|
374
|
+
}
|
|
375
|
+
const cell = v => v === null || v === undefined ? '' : typeof v === 'object' ? JSON.stringify(v) : String(v);
|
|
376
|
+
const lines = [keys.map(_serverOutput.csvCell).join(',')];
|
|
377
|
+
for (const r of rows) lines.push(keys.map(k => (0, _serverOutput.csvCell)(cell(r?.[k]))).join(','));
|
|
378
|
+
return lines.join('\n') + '\n';
|
|
379
|
+
}
|
|
380
|
+
function contentTypeFor(file) {
|
|
381
|
+
const ext = path.extname(file).toLowerCase();
|
|
382
|
+
if (ext === '.csv') return 'text/csv';
|
|
383
|
+
if (ext === '.d42') return 'application/octet-stream';
|
|
384
|
+
if (ext === '.json') return 'application/json';
|
|
385
|
+
throw new Error(`Unsupported upload format '${ext || file}': use .csv, .d42 or .json`);
|
|
386
|
+
}
|
|
387
|
+
async function handleUpload(dapi, args, argv, output) {
|
|
388
|
+
if (!args[0] || !args[1]) return usage('upload <schema.table> <file.csv|.d42|.json> [--upsert] [--no-all-or-nothing] [--error-on-duplicate]');
|
|
389
|
+
const a = (0, _nodeDapi.parseDomainAddress)(args[0], {
|
|
390
|
+
table: true
|
|
391
|
+
});
|
|
392
|
+
if (!fs.existsSync(args[1])) throw new Error(`Local file not found: ${args[1]}`);
|
|
393
|
+
const report = await dapi.domains.batch(a.schema, a.table, fs.readFileSync(args[1]), contentTypeFor(args[1]), {
|
|
394
|
+
mode: argv.upsert === true ? 'upsert' : 'insert',
|
|
395
|
+
allOrNothing: argv['all-or-nothing'] !== false,
|
|
396
|
+
errorOnDuplicate: argv['error-on-duplicate'] === true
|
|
397
|
+
});
|
|
398
|
+
printBatchReport(report, output);
|
|
399
|
+
return true;
|
|
400
|
+
}
|
|
401
|
+
function printBatchReport(report, output) {
|
|
402
|
+
const rows = Array.isArray(report?.rows) ? report.rows : [];
|
|
403
|
+
const failed = (report?.errorCount ?? 0) > 0 || !!report?.error;
|
|
404
|
+
if (output === 'json') (0, _serverOutput.printOutput)(report, output);else if (output === 'csv') (0, _serverOutput.printOutput)(rows, output);else if (output === 'quiet') {
|
|
405
|
+
for (const r of rows) if (r?.id) console.log(r.id);
|
|
406
|
+
} else {
|
|
407
|
+
console.log(`inserted: ${report?.inserted ?? 0} updated: ${report?.updated ?? 0} skipped: ${report?.skipped ?? 0} errors: ${report?.errorCount ?? 0}${report?.error ? ` (${report.error})` : ''}`);
|
|
408
|
+
const errors = rows.filter(r => r?.status === 'error' || r?.status === 'duplicate');
|
|
409
|
+
if (errors.length) (0, _serverOutput.printOutput)(errors.map(r => ({
|
|
410
|
+
index: r.index,
|
|
411
|
+
status: r.status,
|
|
412
|
+
id: r.id ?? r.existingId ?? '',
|
|
413
|
+
detail: (r.errors ?? []).map(e => `${e.column ? e.column + ': ' : ''}${e.message ?? e.code}`).join('; ')
|
|
414
|
+
})), 'table');
|
|
415
|
+
}
|
|
416
|
+
if (failed) process.exitCode = 1;
|
|
417
|
+
}
|
|
418
|
+
async function handleDownload(dapi, args, argv, filter, limit, offset, output) {
|
|
419
|
+
if (!args[0]) return usage('download <schema.table> [-O out.csv|out.d42] [--filter <expr>] [--columns a,b] [--sort s] [--limit n]');
|
|
420
|
+
const a = (0, _nodeDapi.parseDomainAddress)(args[0], {
|
|
421
|
+
table: true
|
|
422
|
+
});
|
|
423
|
+
const outFile = argv['output-file'] ?? argv.O;
|
|
424
|
+
const spec = querySpec(argv, filter, limit, offset);
|
|
425
|
+
if (outFile && path.extname(String(outFile)).toLowerCase() === '.d42') {
|
|
426
|
+
const bytes = await dapi.domains.queryD42(a.schema, a.table, spec);
|
|
427
|
+
fs.writeFileSync(String(outFile), bytes);
|
|
428
|
+
if (output !== 'quiet') console.log(`Wrote ${bytes.length} bytes to ${outFile}`);
|
|
429
|
+
return true;
|
|
430
|
+
}
|
|
431
|
+
const csv = rowsToCsv(await dapi.domains.query(a.schema, a.table, spec));
|
|
432
|
+
if (outFile) {
|
|
433
|
+
fs.writeFileSync(String(outFile), csv);
|
|
434
|
+
if (output !== 'quiet') console.log(`Wrote ${csv.length} bytes to ${outFile}`);
|
|
435
|
+
} else process.stdout.write(csv);
|
|
436
|
+
return true;
|
|
437
|
+
}
|
|
438
|
+
async function handleGrant(dapi, verb, args, argv, output) {
|
|
439
|
+
const [address, ...groupArgs] = args;
|
|
440
|
+
if (!address || !groupArgs.length) return usage(`${verb} <schema> | <schema.table> <group>[,<group>...] [--access ${PERMISSIONS.join('|')}]`);
|
|
441
|
+
const access = argv.access === undefined ? verb === 'grant' ? 'View' : undefined : String(argv.access);
|
|
442
|
+
if (access !== undefined && !PERMISSIONS.includes(access)) throw new Error(`Invalid --access '${access}'. Use one of ${PERMISSIONS.join(', ')}.`);
|
|
443
|
+
const target = (0, _nodeDapi.parseDomainAddress)(address);
|
|
444
|
+
const entityId = await dapi.domains.entityId(target);
|
|
445
|
+
const groups = groupArgs.flatMap(g => g.split(',')).map(g => g.trim()).filter(Boolean);
|
|
446
|
+
const results = [];
|
|
447
|
+
for (const g of groups) {
|
|
448
|
+
try {
|
|
449
|
+
const group = await dapi.groups.resolve(g);
|
|
450
|
+
if (verb === 'grant') await dapi.domains.grant(entityId, group.id, access);else await dapi.domains.revoke(entityId, group.id, access);
|
|
451
|
+
results.push({
|
|
452
|
+
group: g,
|
|
453
|
+
permission: access ?? 'all',
|
|
454
|
+
status: verb === 'grant' ? 'granted' : 'revoked'
|
|
455
|
+
});
|
|
456
|
+
} catch (err) {
|
|
457
|
+
results.push({
|
|
458
|
+
group: g,
|
|
459
|
+
permission: access ?? 'all',
|
|
460
|
+
status: 'error',
|
|
461
|
+
error: err?.apiError?.error ?? err?.message ?? String(err)
|
|
462
|
+
});
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
(0, _serverOutput.printOutput)(results, output);
|
|
466
|
+
if (results.some(r => r.status === 'error')) process.exitCode = 1;
|
|
467
|
+
return true;
|
|
468
|
+
}
|