@3minapi/cli 0.0.0-stage → 0.22.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 +88 -2
- package/dist/bin.js +54 -0
- package/dist/cli.js +241 -0
- package/dist/commands/auth.js +573 -0
- package/dist/commands/common.js +48 -0
- package/dist/commands/dashboard.js +298 -0
- package/dist/commands/delete.js +167 -0
- package/dist/commands/doctor.js +326 -0
- package/dist/commands/guide.js +84 -0
- package/dist/commands/index.js +28 -0
- package/dist/commands/init.js +248 -0
- package/dist/commands/logs.js +154 -0
- package/dist/commands/new.js +109 -0
- package/dist/commands/paths.js +64 -0
- package/dist/commands/reload.js +32 -0
- package/dist/commands/results.js +445 -0
- package/dist/commands/start.js +443 -0
- package/dist/commands/stop.js +68 -0
- package/dist/commands/tasks.js +93 -0
- package/dist/commands/worker.js +178 -0
- package/dist/core/api.js +132 -0
- package/dist/core/config.js +188 -0
- package/dist/core/control-db.js +462 -0
- package/dist/core/credentials.js +75 -0
- package/dist/core/domain/constants.js +121 -0
- package/dist/core/domain/contract.js +102 -0
- package/dist/core/domain/errors.js +57 -0
- package/dist/core/domain/index.js +3 -0
- package/dist/core/domain/types.js +1 -0
- package/dist/core/error-report.js +103 -0
- package/dist/core/host/detach.js +49 -0
- package/dist/core/host/live-connection.js +270 -0
- package/dist/core/host/live-receiver.js +264 -0
- package/dist/core/host/module.js +1 -0
- package/dist/core/host/poller.js +113 -0
- package/dist/core/host/pull-step.js +54 -0
- package/dist/core/host/receiver.js +1 -0
- package/dist/core/host/reload.js +34 -0
- package/dist/core/host/runner-lock.js +245 -0
- package/dist/core/host/session.js +84 -0
- package/dist/core/host/usage.js +20 -0
- package/dist/core/log-read.js +59 -0
- package/dist/core/logger.js +206 -0
- package/dist/core/names.js +27 -0
- package/dist/core/network.js +10 -0
- package/dist/core/open-browser.js +26 -0
- package/dist/core/output.js +68 -0
- package/dist/core/paths.js +26 -0
- package/dist/core/project.js +85 -0
- package/dist/core/prompt.js +140 -0
- package/dist/core/redact.js +45 -0
- package/dist/core/secure-fs.js +50 -0
- package/dist/core/text-file.js +12 -0
- package/dist/core/text.js +27 -0
- package/dist/core/time.js +84 -0
- package/dist/core/update-notice.js +142 -0
- package/dist/core/version.js +16 -0
- package/dist/core/warnings.js +10 -0
- package/dist/help.js +144 -0
- package/dist/modules/dashboard/assets.js +411 -0
- package/dist/modules/dashboard/data.js +565 -0
- package/dist/modules/dashboard/guard.js +75 -0
- package/dist/modules/dashboard/lock.js +93 -0
- package/dist/modules/dashboard/server.js +295 -0
- package/dist/modules/dashboard/views/html.js +114 -0
- package/dist/modules/dashboard/views/layout.js +59 -0
- package/dist/modules/dashboard/views/logs.js +122 -0
- package/dist/modules/dashboard/views/overview.js +316 -0
- package/dist/modules/dashboard/views/runs.js +486 -0
- package/dist/modules/dashboard/views/workers.js +157 -0
- package/dist/modules/workers/builtin/host-identity.js +59 -0
- package/dist/modules/workers/builtin/results-worker.js +37 -0
- package/dist/modules/workers/local-server.js +244 -0
- package/dist/modules/workers/module.js +98 -0
- package/dist/modules/workers/reload-result.js +16 -0
- package/dist/modules/workers/repeat-scheduler.js +165 -0
- package/dist/modules/workers/results/missing-paths.js +66 -0
- package/dist/modules/workers/results/query.js +175 -0
- package/dist/modules/workers/results/reader-input.js +134 -0
- package/dist/modules/workers/results/reader.js +430 -0
- package/dist/modules/workers/results/results-child.js +64 -0
- package/dist/modules/workers/results/shape.js +212 -0
- package/dist/modules/workers/results/store.js +336 -0
- package/dist/modules/workers/results/substitute.js +30 -0
- package/dist/modules/workers/runtime/env.js +43 -0
- package/dist/modules/workers/runtime/error-chain.js +109 -0
- package/dist/modules/workers/runtime/executor.js +289 -0
- package/dist/modules/workers/runtime/inspect.js +53 -0
- package/dist/modules/workers/runtime/protocol.js +1 -0
- package/dist/modules/workers/runtime/resolve.js +30 -0
- package/dist/modules/workers/runtime/schema.js +85 -0
- package/dist/modules/workers/runtime/thread.js +117 -0
- package/dist/modules/workers/start.js +265 -0
- package/dist/modules/workers/sync.js +241 -0
- package/dist/modules/workers/task-runner.js +892 -0
- package/dist/modules/workers/template.js +100 -0
- package/dist/modules/workers/worker-set.js +122 -0
- package/dist/modules/workers/workers.js +66 -0
- package/package.json +48 -5
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
import { randomBytes } from 'node:crypto';
|
|
2
|
+
import { DESCRIBE_MAX_GROUPS, READ_MAX_BYTES, RESULTS_TABLE } from "../../../core/domain/index.js";
|
|
3
|
+
import { DESCRIBE_MAX_BYTES, describeTask, describeWorker, jsonBytes } from "./query.js";
|
|
4
|
+
export { replacer } from "./query.js";
|
|
5
|
+
import { MISSING_PATHS_MAX, missingPaths, missingPathsWarning } from "./missing-paths.js";
|
|
6
|
+
import { childPath } from "./shape.js";
|
|
7
|
+
import { ResultStore } from "./store.js";
|
|
8
|
+
export const REMOTE_SOURCES = ['pull', 'public'];
|
|
9
|
+
class Failure extends Error {
|
|
10
|
+
reason;
|
|
11
|
+
constructor(reason, message) {
|
|
12
|
+
super(message);
|
|
13
|
+
this.reason = reason;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
export function runReader(root, db, c, req, hostId) {
|
|
17
|
+
try {
|
|
18
|
+
switch (req.op) {
|
|
19
|
+
case 'describe':
|
|
20
|
+
return {
|
|
21
|
+
ok: true,
|
|
22
|
+
result: 'task_id' in req
|
|
23
|
+
? describeOne(db, req.task_id, hostId, req.run_no)
|
|
24
|
+
: describeMany(root, req, hostId)
|
|
25
|
+
};
|
|
26
|
+
case 'query':
|
|
27
|
+
return { ok: true, result: query(db, c, req, hostId) };
|
|
28
|
+
case 'read':
|
|
29
|
+
return { ok: true, result: read(db, req, hostId) };
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
catch (err) {
|
|
33
|
+
if (err instanceof Failure)
|
|
34
|
+
return { ok: false, reason: err.reason, message: err.message };
|
|
35
|
+
throw err;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
function findRow(db, taskId, hostId, withPayload = false, runNo) {
|
|
39
|
+
const row = db
|
|
40
|
+
.prepare(`SELECT task_id, worker, created_at, status, bytes, shape_hash, shape, run_no${withPayload ? ', payload' : ''}
|
|
41
|
+
FROM ${RESULTS_TABLE} WHERE task_id = ? AND host_id = ? AND source IN (${REMOTE_SOURCES.map(() => '?').join(', ')})
|
|
42
|
+
${runNo === undefined ? 'ORDER BY run_no DESC LIMIT 1' : 'AND run_no = ?'}`)
|
|
43
|
+
.get(taskId, hostId, ...REMOTE_SOURCES, ...(runNo === undefined ? [] : [runNo]));
|
|
44
|
+
if (!row) {
|
|
45
|
+
const runs = runNo === undefined ? null : runInfo(db, taskId, hostId);
|
|
46
|
+
throw new Failure('result_not_found', runs
|
|
47
|
+
? `no saved run ${runNo} for task ${taskId} — ${runs.count} run(s) are saved, the latest is run ${runs.last_run_no}`
|
|
48
|
+
: `no saved result for task ${taskId} on this host — it ran on another host, was pruned, or its worker has not finished`);
|
|
49
|
+
}
|
|
50
|
+
return row;
|
|
51
|
+
}
|
|
52
|
+
function runInfo(db, taskId, hostId) {
|
|
53
|
+
const r = db
|
|
54
|
+
.prepare(`SELECT count(*) AS n, max(run_no) AS last FROM ${RESULTS_TABLE}
|
|
55
|
+
WHERE task_id = ? AND host_id = ? AND source IN (${REMOTE_SOURCES.map(() => '?').join(', ')})`)
|
|
56
|
+
.get(taskId, hostId, ...REMOTE_SOURCES);
|
|
57
|
+
return Number(r.n) > 0 ? { count: Number(r.n), last_run_no: Number(r.last) } : null;
|
|
58
|
+
}
|
|
59
|
+
function runFields(db, row, hostId) {
|
|
60
|
+
const runs = runInfo(db, row.task_id, hostId);
|
|
61
|
+
if (!runs || (runs.last_run_no <= 1 && row.run_no <= 1))
|
|
62
|
+
return {};
|
|
63
|
+
return { run_no: row.run_no, runs };
|
|
64
|
+
}
|
|
65
|
+
function describeOne(db, taskId, hostId, runNo) {
|
|
66
|
+
const row = findRow(db, taskId, hostId, false, runNo);
|
|
67
|
+
const run = runFields(db, row, hostId);
|
|
68
|
+
if (row.status === 'failed' || row.shape === null) {
|
|
69
|
+
const error = JSON.parse(findRow(db, taskId, hostId, true, row.run_no).payload);
|
|
70
|
+
const out = {
|
|
71
|
+
task_id: row.task_id,
|
|
72
|
+
...run,
|
|
73
|
+
worker: row.worker,
|
|
74
|
+
created_at: row.created_at,
|
|
75
|
+
status: row.status,
|
|
76
|
+
error
|
|
77
|
+
};
|
|
78
|
+
return jsonBytes(out) <= DESCRIBE_MAX_BYTES
|
|
79
|
+
? out
|
|
80
|
+
: {
|
|
81
|
+
...out,
|
|
82
|
+
error: { message: 'the saved error is too large to show; read it with op "read"' }
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
return {
|
|
86
|
+
...run,
|
|
87
|
+
status: row.status,
|
|
88
|
+
bytes: row.bytes,
|
|
89
|
+
...describeTask({
|
|
90
|
+
task_id: row.task_id,
|
|
91
|
+
host_id: hostId,
|
|
92
|
+
worker: row.worker,
|
|
93
|
+
created_at: row.created_at,
|
|
94
|
+
shape_hash: row.shape_hash,
|
|
95
|
+
shape: JSON.parse(row.shape),
|
|
96
|
+
source: 'pull',
|
|
97
|
+
status: row.status,
|
|
98
|
+
reply: 'result',
|
|
99
|
+
bytes: row.bytes,
|
|
100
|
+
payload: ''
|
|
101
|
+
})
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
function describeMany(root, req, hostId) {
|
|
105
|
+
const store = ResultStore.openReadOnly(root);
|
|
106
|
+
if (!store)
|
|
107
|
+
throw new Failure('result_not_found', 'no results are saved on this host');
|
|
108
|
+
try {
|
|
109
|
+
const out = describeWorker(store, {
|
|
110
|
+
worker: req.worker,
|
|
111
|
+
...(req.since !== undefined ? { since: req.since } : {}),
|
|
112
|
+
sources: REMOTE_SOURCES,
|
|
113
|
+
hostId
|
|
114
|
+
});
|
|
115
|
+
if (out.total === 0) {
|
|
116
|
+
throw new Failure('result_not_found', `no completed results of worker "${req.worker}"${req.since ? ` since ${req.since}` : ''} are saved on this host`);
|
|
117
|
+
}
|
|
118
|
+
return out;
|
|
119
|
+
}
|
|
120
|
+
finally {
|
|
121
|
+
store.close();
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
export const RESULTS_VIEW_COLUMNS = [
|
|
125
|
+
'task_id',
|
|
126
|
+
'run_no',
|
|
127
|
+
'worker',
|
|
128
|
+
'created_at',
|
|
129
|
+
'status',
|
|
130
|
+
'bytes',
|
|
131
|
+
'shape_hash',
|
|
132
|
+
'payload'
|
|
133
|
+
];
|
|
134
|
+
function query(db, c, req, hostId) {
|
|
135
|
+
const inner = `_scope_${randomBytes(8).toString('hex')}`;
|
|
136
|
+
db.exec(`CREATE TEMP VIEW ${inner} AS SELECT ${RESULTS_VIEW_COLUMNS.join(', ')} FROM main.${RESULTS_TABLE} WHERE ${scopeSql(req.scope, hostId)};
|
|
137
|
+
CREATE TEMP VIEW results AS SELECT * FROM ${inner};`);
|
|
138
|
+
const groups = db
|
|
139
|
+
.prepare(`SELECT status, shape_hash, count(*) AS n FROM ${inner} GROUP BY status, shape_hash ORDER BY n DESC`)
|
|
140
|
+
.all();
|
|
141
|
+
if (groups.length === 0) {
|
|
142
|
+
throw new Failure('result_not_found', 'no saved results match this scope on this host (worker, task_id(s) and since are combined with AND)');
|
|
143
|
+
}
|
|
144
|
+
const shapes = groups
|
|
145
|
+
.filter((g) => g.status === 'completed' && g.shape_hash !== null)
|
|
146
|
+
.map((g) => ({ shape_hash: g.shape_hash, tasks: g.n }));
|
|
147
|
+
const failed = groups.filter((g) => g.status === 'failed').reduce((n, g) => n + g.n, 0);
|
|
148
|
+
const shown = shapes.slice(0, DESCRIBE_MAX_GROUPS);
|
|
149
|
+
const rest = shapes.slice(DESCRIBE_MAX_GROUPS);
|
|
150
|
+
const shapesMore = rest.length > 0 ? { groups: rest.length, tasks: rest.reduce((n, s) => n + s.tasks, 0) } : null;
|
|
151
|
+
if (req.expect_shape !== undefined) {
|
|
152
|
+
const other = shapes.filter((s) => s.shape_hash !== req.expect_shape);
|
|
153
|
+
if (other.length > 0 || shapes.length === 0) {
|
|
154
|
+
const listed = shown.map((s) => `${s.shape_hash}: ${s.tasks}`).join(', ') +
|
|
155
|
+
(shapesMore ? `, and ${shapesMore.groups} more` : '');
|
|
156
|
+
throw new Failure('shape_mismatch', `the scope has results of other shapes (${listed || 'none completed'}) ` +
|
|
157
|
+
`than expect_shape ${req.expect_shape} — the query did not run. Narrow the scope (task_ids or since), or describe the worker to see when the shape changed`);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
const missing = scopeMissingPaths(db, req, hostId);
|
|
161
|
+
db.setAuthorizer((action, p1, _p2, dbName, view) => {
|
|
162
|
+
if (action === c.SQLITE_SELECT ||
|
|
163
|
+
action === c.SQLITE_FUNCTION ||
|
|
164
|
+
action === c.SQLITE_RECURSIVE) {
|
|
165
|
+
return c.SQLITE_OK;
|
|
166
|
+
}
|
|
167
|
+
if (action === c.SQLITE_READ) {
|
|
168
|
+
if (dbName === 'main' && p1 === RESULTS_TABLE && view === inner)
|
|
169
|
+
return c.SQLITE_OK;
|
|
170
|
+
if (dbName === 'temp' && (p1 === 'results' || p1 === inner))
|
|
171
|
+
return c.SQLITE_OK;
|
|
172
|
+
if (p1 === 'json_each' || p1 === 'json_tree')
|
|
173
|
+
return c.SQLITE_OK;
|
|
174
|
+
if (dbName === null)
|
|
175
|
+
return c.SQLITE_OK;
|
|
176
|
+
}
|
|
177
|
+
return c.SQLITE_DENY;
|
|
178
|
+
});
|
|
179
|
+
let stmt;
|
|
180
|
+
try {
|
|
181
|
+
stmt = db.prepare(req.sql);
|
|
182
|
+
}
|
|
183
|
+
catch (err) {
|
|
184
|
+
throw sqlFailure(err);
|
|
185
|
+
}
|
|
186
|
+
const columns = stmt.columns().map((col) => col.name);
|
|
187
|
+
stmt.setReturnArrays(true);
|
|
188
|
+
const tasks = groups.reduce((n, g) => n + g.n, 0);
|
|
189
|
+
const warnings = [
|
|
190
|
+
...(shapes.length > 1
|
|
191
|
+
? [
|
|
192
|
+
`the scope mixes ${shapes.length} result shapes — a path that exists in one shape is NULL in another, ` +
|
|
193
|
+
'so sums and counts can be silently wrong. Check with describe, then pass expect_shape or narrow the scope'
|
|
194
|
+
]
|
|
195
|
+
: []),
|
|
196
|
+
...(missing.length > 0 ? [missingPathsWarning(missing)] : [])
|
|
197
|
+
];
|
|
198
|
+
const build = (rows, truncated) => ({
|
|
199
|
+
columns,
|
|
200
|
+
rows,
|
|
201
|
+
row_count: rows.length,
|
|
202
|
+
...(truncated
|
|
203
|
+
? {
|
|
204
|
+
truncated: true,
|
|
205
|
+
note: `stopped at ${rows.length} rows to stay within the reply limit — aggregate in SQL (count, sum, GROUP BY) or add LIMIT/OFFSET`
|
|
206
|
+
}
|
|
207
|
+
: {}),
|
|
208
|
+
scope: { tasks, ...(failed > 0 ? { failed } : {}) },
|
|
209
|
+
shapes: shown,
|
|
210
|
+
...(shapesMore ? { shapes_more: shapesMore } : {}),
|
|
211
|
+
...(missing.length > 0 ? { missing_paths: missing.slice(0, MISSING_PATHS_MAX) } : {}),
|
|
212
|
+
...(warnings.length > 0 ? { warning: warnings.join('. ') } : {})
|
|
213
|
+
});
|
|
214
|
+
const budget = DESCRIBE_MAX_BYTES - jsonBytes(build([], true)) - 16;
|
|
215
|
+
const rows = [];
|
|
216
|
+
let used = 0;
|
|
217
|
+
let truncated = false;
|
|
218
|
+
try {
|
|
219
|
+
for (const row of stmt.iterate()) {
|
|
220
|
+
const size = jsonBytes(row) + 1;
|
|
221
|
+
if (used + size > budget) {
|
|
222
|
+
truncated = true;
|
|
223
|
+
break;
|
|
224
|
+
}
|
|
225
|
+
used += size;
|
|
226
|
+
rows.push(row);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
catch (err) {
|
|
230
|
+
throw sqlFailure(err);
|
|
231
|
+
}
|
|
232
|
+
return build(rows, truncated);
|
|
233
|
+
}
|
|
234
|
+
function scopeMissingPaths(db, req, hostId) {
|
|
235
|
+
const rows = db
|
|
236
|
+
.prepare(`SELECT shape FROM main.${RESULTS_TABLE} WHERE ${scopeSql(req.scope, hostId)} AND status = 'completed' GROUP BY shape_hash`)
|
|
237
|
+
.all();
|
|
238
|
+
const known = new Set();
|
|
239
|
+
for (const row of rows) {
|
|
240
|
+
if (row.shape === null)
|
|
241
|
+
return [];
|
|
242
|
+
let shape;
|
|
243
|
+
try {
|
|
244
|
+
shape = JSON.parse(row.shape);
|
|
245
|
+
}
|
|
246
|
+
catch {
|
|
247
|
+
return [];
|
|
248
|
+
}
|
|
249
|
+
if (shape.truncated)
|
|
250
|
+
return [];
|
|
251
|
+
for (const path of Object.keys(shape.paths))
|
|
252
|
+
known.add(path);
|
|
253
|
+
}
|
|
254
|
+
if (known.size === 0)
|
|
255
|
+
return [];
|
|
256
|
+
return missingPaths(req.sql, known);
|
|
257
|
+
}
|
|
258
|
+
function scopeSql(scope, hostId) {
|
|
259
|
+
const lit = (v) => `'${v.replace(/'/g, "''")}'`;
|
|
260
|
+
const conds = [`source IN (${REMOTE_SOURCES.map(lit).join(', ')})`, `host_id = ${lit(hostId)}`];
|
|
261
|
+
if (scope.worker !== undefined)
|
|
262
|
+
conds.push(`worker = ${lit(scope.worker)}`);
|
|
263
|
+
if (scope.task_ids !== undefined)
|
|
264
|
+
conds.push(`task_id IN (${scope.task_ids.map(lit).join(', ')})`);
|
|
265
|
+
if (scope.since !== undefined)
|
|
266
|
+
conds.push(`created_at >= ${lit(scope.since)}`);
|
|
267
|
+
return conds.join(' AND ');
|
|
268
|
+
}
|
|
269
|
+
function sqlFailure(err) {
|
|
270
|
+
const e = err;
|
|
271
|
+
const message = e.message ?? String(err);
|
|
272
|
+
if (e.errcode === 23) {
|
|
273
|
+
return new Failure('query_denied', `${message} — queries may only read the view "results" (columns: ${RESULTS_VIEW_COLUMNS.join(', ')}) and use functions such as json_extract and json_each`);
|
|
274
|
+
}
|
|
275
|
+
return new Failure('query_error', message);
|
|
276
|
+
}
|
|
277
|
+
function read(db, req, hostId) {
|
|
278
|
+
const row = findRow(db, req.task_id, hostId, true, req.run_no);
|
|
279
|
+
return readNode(JSON.parse(row.payload), { task_id: req.task_id, ...runFields(db, row, hostId), status: row.status, path: req.path }, req);
|
|
280
|
+
}
|
|
281
|
+
export function readPayloadSlice(payload, at) {
|
|
282
|
+
try {
|
|
283
|
+
const result = readNode(JSON.parse(payload), { path: at.path }, at);
|
|
284
|
+
return { ok: true, result };
|
|
285
|
+
}
|
|
286
|
+
catch (err) {
|
|
287
|
+
if (err instanceof Failure || err instanceof SyntaxError) {
|
|
288
|
+
return { ok: false, message: err.message };
|
|
289
|
+
}
|
|
290
|
+
throw err;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
function readNode(root, base, req) {
|
|
294
|
+
const view = req.summarize ?? ((v) => v);
|
|
295
|
+
let node = root;
|
|
296
|
+
for (const step of parsePath(req.path)) {
|
|
297
|
+
if (typeof step === 'number') {
|
|
298
|
+
if (!Array.isArray(node) || step >= node.length)
|
|
299
|
+
throw missing(req.path);
|
|
300
|
+
node = node[step];
|
|
301
|
+
}
|
|
302
|
+
else {
|
|
303
|
+
if (node === null ||
|
|
304
|
+
typeof node !== 'object' ||
|
|
305
|
+
Array.isArray(node) ||
|
|
306
|
+
!Object.hasOwn(node, step)) {
|
|
307
|
+
throw missing(req.path);
|
|
308
|
+
}
|
|
309
|
+
node = node[step];
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
const limit = req.limit ?? Number.MAX_SAFE_INTEGER;
|
|
313
|
+
if (Array.isArray(node)) {
|
|
314
|
+
return slice(base, 'array', node.length, req.offset, limit, (i) => view(node[i]), (items) => ({ items }), req.path);
|
|
315
|
+
}
|
|
316
|
+
if (node !== null && typeof node === 'object') {
|
|
317
|
+
const keys = Object.keys(node);
|
|
318
|
+
const obj = node;
|
|
319
|
+
return slice(base, 'object', keys.length, req.offset, limit, (i) => [keys[i], view(obj[keys[i]])], (entries) => ({
|
|
320
|
+
items: Object.fromEntries(entries)
|
|
321
|
+
}), req.path);
|
|
322
|
+
}
|
|
323
|
+
if (typeof node === 'string')
|
|
324
|
+
return readText(base, node, req.offset, limit);
|
|
325
|
+
if (req.offset > 0)
|
|
326
|
+
throw new Failure('read_out_of_range', `${req.path} is a single value — read it with offset 0`);
|
|
327
|
+
return { ...base, kind: 'value', total: 1, offset: 0, count: 1, next: null, value: node };
|
|
328
|
+
}
|
|
329
|
+
function slice(base, kind, total, offset, limit, at, wrap, path) {
|
|
330
|
+
if (offset > total || (offset === total && total > 0)) {
|
|
331
|
+
throw new Failure('read_out_of_range', `offset ${offset} is past the end of ${path} (${total} ${kind === 'array' ? 'elements' : 'keys'})`);
|
|
332
|
+
}
|
|
333
|
+
const items = [];
|
|
334
|
+
let used = 0;
|
|
335
|
+
for (let i = offset; i < total && items.length < limit; i++) {
|
|
336
|
+
const v = at(i);
|
|
337
|
+
const size = jsonBytes(v) + 1;
|
|
338
|
+
if (used + size > READ_MAX_BYTES) {
|
|
339
|
+
if (items.length === 0) {
|
|
340
|
+
const sub = kind === 'array' ? `${path}[${i}]` : childPath(path, v[0]);
|
|
341
|
+
throw new Failure('read_out_of_range', `${sub} alone is ${size} bytes, more than one read (${READ_MAX_BYTES} bytes) — read a path inside it (for example ${sub}.<field>) or query it with SQL`);
|
|
342
|
+
}
|
|
343
|
+
break;
|
|
344
|
+
}
|
|
345
|
+
used += size;
|
|
346
|
+
items.push(v);
|
|
347
|
+
}
|
|
348
|
+
const end = offset + items.length;
|
|
349
|
+
return {
|
|
350
|
+
...base,
|
|
351
|
+
kind,
|
|
352
|
+
total,
|
|
353
|
+
offset,
|
|
354
|
+
count: items.length,
|
|
355
|
+
next: end < total ? end : null,
|
|
356
|
+
...wrap(items)
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
function readText(base, text, offset, limit) {
|
|
360
|
+
const chars = Array.from(text);
|
|
361
|
+
const total = chars.length;
|
|
362
|
+
if (offset > total || (offset === total && total > 0)) {
|
|
363
|
+
throw new Failure('read_out_of_range', `offset ${offset} is past the end of the text (${total} characters)`);
|
|
364
|
+
}
|
|
365
|
+
let used = 2;
|
|
366
|
+
let end = offset;
|
|
367
|
+
while (end < total && end - offset < limit) {
|
|
368
|
+
const size = jsonBytes(chars[end]) - 2;
|
|
369
|
+
if (used + size > READ_MAX_BYTES)
|
|
370
|
+
break;
|
|
371
|
+
used += size;
|
|
372
|
+
end++;
|
|
373
|
+
}
|
|
374
|
+
return {
|
|
375
|
+
...base,
|
|
376
|
+
kind: 'text',
|
|
377
|
+
total,
|
|
378
|
+
offset,
|
|
379
|
+
count: end - offset,
|
|
380
|
+
next: end < total ? end : null,
|
|
381
|
+
text: chars.slice(offset, end).join('')
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
function missing(path) {
|
|
385
|
+
return new Failure('read_out_of_range', `${path} does not exist in this result — check the paths with op "describe"`);
|
|
386
|
+
}
|
|
387
|
+
export function parsePath(path) {
|
|
388
|
+
const steps = [];
|
|
389
|
+
let i = 1;
|
|
390
|
+
const bad = () => new Failure('read_out_of_range', `cannot parse path ${path} — use $.key, $."a key" or $.list[3]`);
|
|
391
|
+
if (path[0] !== '$')
|
|
392
|
+
throw bad();
|
|
393
|
+
while (i < path.length) {
|
|
394
|
+
if (path[i] === '.') {
|
|
395
|
+
i++;
|
|
396
|
+
if (path[i] === '"') {
|
|
397
|
+
let j = i + 1;
|
|
398
|
+
while (j < path.length && path[j] !== '"')
|
|
399
|
+
j += path[j] === '\\' ? 2 : 1;
|
|
400
|
+
if (j >= path.length)
|
|
401
|
+
throw bad();
|
|
402
|
+
try {
|
|
403
|
+
steps.push(JSON.parse(path.slice(i, j + 1)));
|
|
404
|
+
}
|
|
405
|
+
catch {
|
|
406
|
+
throw bad();
|
|
407
|
+
}
|
|
408
|
+
i = j + 1;
|
|
409
|
+
}
|
|
410
|
+
else {
|
|
411
|
+
const m = /^[A-Za-z_][A-Za-z0-9_]*/.exec(path.slice(i));
|
|
412
|
+
if (!m)
|
|
413
|
+
throw bad();
|
|
414
|
+
steps.push(m[0]);
|
|
415
|
+
i += m[0].length;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
else if (path[i] === '[') {
|
|
419
|
+
const m = /^\[(\d+)\]/.exec(path.slice(i));
|
|
420
|
+
if (!m)
|
|
421
|
+
throw bad();
|
|
422
|
+
steps.push(Number(m[1]));
|
|
423
|
+
i += m[0].length;
|
|
424
|
+
}
|
|
425
|
+
else {
|
|
426
|
+
throw bad();
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
return steps;
|
|
430
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
|
|
2
|
+
if (typeof path === "string" && /^\.\.?\//.test(path)) {
|
|
3
|
+
return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
|
|
4
|
+
return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
|
|
5
|
+
});
|
|
6
|
+
}
|
|
7
|
+
return path;
|
|
8
|
+
};
|
|
9
|
+
import { suppressSqliteExperimentalWarning } from "../../../core/warnings.js";
|
|
10
|
+
suppressSqliteExperimentalWarning();
|
|
11
|
+
const { DatabaseSync, constants } = await import('node:sqlite');
|
|
12
|
+
const ALLOWED = new Set([
|
|
13
|
+
constants.SQLITE_SELECT,
|
|
14
|
+
constants.SQLITE_READ,
|
|
15
|
+
constants.SQLITE_FUNCTION,
|
|
16
|
+
constants.SQLITE_RECURSIVE
|
|
17
|
+
]);
|
|
18
|
+
const chunks = [];
|
|
19
|
+
for await (const chunk of process.stdin)
|
|
20
|
+
chunks.push(chunk);
|
|
21
|
+
const msg = JSON.parse(Buffer.concat(chunks).toString('utf8'));
|
|
22
|
+
const { replacer, runReader } = await import(__rewriteRelativeImportExtension(import.meta.url.endsWith('.ts') ? './reader.ts' : './reader.js'));
|
|
23
|
+
let result;
|
|
24
|
+
if (msg.op === 'sql') {
|
|
25
|
+
try {
|
|
26
|
+
const db = new DatabaseSync(msg.file, { readOnly: true, timeout: 5_000 });
|
|
27
|
+
try {
|
|
28
|
+
db.setAuthorizer((action) => ALLOWED.has(action) ? constants.SQLITE_OK : constants.SQLITE_DENY);
|
|
29
|
+
const stmt = db.prepare(msg.sql);
|
|
30
|
+
const columns = stmt.columns().map((c) => c.name);
|
|
31
|
+
stmt.setReturnArrays(true);
|
|
32
|
+
const rows = stmt.all();
|
|
33
|
+
result = { ok: true, columns, rows };
|
|
34
|
+
}
|
|
35
|
+
finally {
|
|
36
|
+
db.close();
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
catch (err) {
|
|
40
|
+
const e = err;
|
|
41
|
+
const message = e.message ?? String(err);
|
|
42
|
+
const kind = e.errcode === 23
|
|
43
|
+
? 'denied'
|
|
44
|
+
: /syntax error|incomplete input/.test(message)
|
|
45
|
+
? 'syntax'
|
|
46
|
+
: 'error';
|
|
47
|
+
result = { ok: false, kind, message };
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
try {
|
|
52
|
+
const db = new DatabaseSync(msg.file, { readOnly: true, timeout: 5_000 });
|
|
53
|
+
try {
|
|
54
|
+
result = runReader(msg.root, db, constants, msg.request, msg.hostId);
|
|
55
|
+
}
|
|
56
|
+
finally {
|
|
57
|
+
db.close();
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
catch (err) {
|
|
61
|
+
result = { ok: false, reason: 'query_error', message: err.message ?? String(err) };
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
process.stdout.write(JSON.stringify(result, replacer));
|