@fadhilp/stateql 0.1.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 +222 -0
- package/dist/src/adapters.d.ts +23 -0
- package/dist/src/adapters.js +346 -0
- package/dist/src/cli.d.ts +2 -0
- package/dist/src/cli.js +516 -0
- package/dist/src/errors.d.ts +12 -0
- package/dist/src/errors.js +45 -0
- package/dist/src/index.d.ts +3 -0
- package/dist/src/index.js +2 -0
- package/dist/src/sql.d.ts +13 -0
- package/dist/src/sql.js +91 -0
- package/dist/src/stateql.d.ts +55 -0
- package/dist/src/stateql.js +1655 -0
- package/dist/src/store.d.ts +245 -0
- package/dist/src/store.js +622 -0
- package/dist/src/types.d.ts +112 -0
- package/dist/src/types.js +1 -0
- package/dist/src/util.d.ts +8 -0
- package/dist/src/util.js +75 -0
- package/package.json +48 -0
package/dist/src/cli.js
ADDED
|
@@ -0,0 +1,516 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createReadStream, readFileSync } from "node:fs";
|
|
3
|
+
import { extname, resolve } from "node:path";
|
|
4
|
+
import { createInterface } from "node:readline";
|
|
5
|
+
import { parseArgs } from "node:util";
|
|
6
|
+
import { exitCodeFor } from "./errors.js";
|
|
7
|
+
import { StateQL } from "./stateql.js";
|
|
8
|
+
const parsed = parseArgs({
|
|
9
|
+
allowPositionals: true,
|
|
10
|
+
strict: true,
|
|
11
|
+
options: {
|
|
12
|
+
name: { type: "string" },
|
|
13
|
+
profile: { type: "string" },
|
|
14
|
+
env: { type: "string" },
|
|
15
|
+
"read-only": { type: "boolean" },
|
|
16
|
+
"read-write": { type: "boolean" },
|
|
17
|
+
params: { type: "string" },
|
|
18
|
+
param: { type: "string", multiple: true },
|
|
19
|
+
"params-file": { type: "string" },
|
|
20
|
+
cache: { type: "string" },
|
|
21
|
+
replay: { type: "boolean" },
|
|
22
|
+
"idempotency-key": { type: "string" },
|
|
23
|
+
"allow-unbounded": { type: "boolean" },
|
|
24
|
+
"allow-destructive": { type: "boolean" },
|
|
25
|
+
offset: { type: "string" },
|
|
26
|
+
limit: { type: "string" },
|
|
27
|
+
format: { type: "string" },
|
|
28
|
+
output: { type: "string" },
|
|
29
|
+
isolation: { type: "string" },
|
|
30
|
+
"continue-on-error": { type: "boolean" },
|
|
31
|
+
help: { type: "boolean", short: "h" },
|
|
32
|
+
version: { type: "boolean", short: "v" },
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
const [command, subcommand, ...rest] = parsed.positionals;
|
|
36
|
+
const values = parsed.values;
|
|
37
|
+
if (values.version) {
|
|
38
|
+
console.log("0.1.0");
|
|
39
|
+
process.exit(0);
|
|
40
|
+
}
|
|
41
|
+
if (values.help || !command) {
|
|
42
|
+
console.log(helpText());
|
|
43
|
+
process.exit(0);
|
|
44
|
+
}
|
|
45
|
+
const stateql = new StateQL();
|
|
46
|
+
try {
|
|
47
|
+
if (command === "batch" || command === "pipe") {
|
|
48
|
+
await runBatch();
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
await runSingle();
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
finally {
|
|
55
|
+
stateql.close();
|
|
56
|
+
}
|
|
57
|
+
async function runSingle() {
|
|
58
|
+
let response;
|
|
59
|
+
let mode = "agent";
|
|
60
|
+
try {
|
|
61
|
+
mode = outputMode(command ?? "", values.output);
|
|
62
|
+
response = await dispatch();
|
|
63
|
+
}
|
|
64
|
+
catch (error) {
|
|
65
|
+
response = cliFailure(error);
|
|
66
|
+
}
|
|
67
|
+
print(response, mode, command ?? "");
|
|
68
|
+
if (!response.ok)
|
|
69
|
+
process.exitCode = exitCodeFor(response.error.code);
|
|
70
|
+
}
|
|
71
|
+
async function runBatch() {
|
|
72
|
+
const mode = outputMode(command ?? "batch", values.output);
|
|
73
|
+
const jsonResponses = [];
|
|
74
|
+
const pendingCommands = [];
|
|
75
|
+
const commands = readBatchCommands(subcommand ?? "-");
|
|
76
|
+
const trackedCommands = (async function* () {
|
|
77
|
+
for await (const item of commands) {
|
|
78
|
+
pendingCommands.push(item.command);
|
|
79
|
+
yield item;
|
|
80
|
+
}
|
|
81
|
+
})();
|
|
82
|
+
try {
|
|
83
|
+
for await (const response of stateql.batch(trackedCommands, {
|
|
84
|
+
continueOnError: values["continue-on-error"] ?? false,
|
|
85
|
+
})) {
|
|
86
|
+
const responseCommand = pendingCommands.shift() ?? command ?? "batch";
|
|
87
|
+
if (mode === "json")
|
|
88
|
+
jsonResponses.push(response);
|
|
89
|
+
else
|
|
90
|
+
print(response, mode, responseCommand);
|
|
91
|
+
if (!response.ok && process.exitCode === undefined) {
|
|
92
|
+
process.exitCode = exitCodeFor(response.error.code);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
catch (error) {
|
|
97
|
+
const response = cliFailure(error);
|
|
98
|
+
if (mode === "json")
|
|
99
|
+
jsonResponses.push(response);
|
|
100
|
+
else
|
|
101
|
+
print(response, mode, pendingCommands.shift() ?? command ?? "batch");
|
|
102
|
+
process.exitCode = exitCodeFor(response.error.code);
|
|
103
|
+
}
|
|
104
|
+
if (mode === "json")
|
|
105
|
+
console.log(JSON.stringify(jsonResponses, null, 2));
|
|
106
|
+
}
|
|
107
|
+
async function dispatch() {
|
|
108
|
+
const params = parseParameters(values.params, values.param, values["params-file"]);
|
|
109
|
+
const sql = [subcommand, ...rest].filter(Boolean).join(" ");
|
|
110
|
+
switch (command) {
|
|
111
|
+
case "connect": {
|
|
112
|
+
if (values["read-only"] && values["read-write"]) {
|
|
113
|
+
throw new Error("--read-only and --read-write are mutually exclusive.");
|
|
114
|
+
}
|
|
115
|
+
if (values.profile && subcommand) {
|
|
116
|
+
throw new Error("Use either --profile or a connection target.");
|
|
117
|
+
}
|
|
118
|
+
return stateql.connect(subcommand, {
|
|
119
|
+
...(values.name ? { name: values.name } : {}),
|
|
120
|
+
...(values.env ? { secretEnv: values.env } : {}),
|
|
121
|
+
...(values.profile ? { profile: values.profile } : {}),
|
|
122
|
+
...(values["read-only"]
|
|
123
|
+
? { readOnly: true }
|
|
124
|
+
: values["read-write"]
|
|
125
|
+
? { readOnly: false }
|
|
126
|
+
: {}),
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
case "disconnect":
|
|
130
|
+
return stateql.disconnect();
|
|
131
|
+
case "status":
|
|
132
|
+
return stateql.status();
|
|
133
|
+
case "profile":
|
|
134
|
+
return dispatchProfile(subcommand, rest);
|
|
135
|
+
case "session":
|
|
136
|
+
return dispatchSession(subcommand, rest);
|
|
137
|
+
case "query":
|
|
138
|
+
return stateql.query(sql, {
|
|
139
|
+
params,
|
|
140
|
+
cache: cacheMode(values.cache),
|
|
141
|
+
});
|
|
142
|
+
case "filter":
|
|
143
|
+
return stateql.filter(requireValue(subcommand, "result handle"), requireValue(rest.join(" ").trim(), "filter predicate"), { params });
|
|
144
|
+
case "exec":
|
|
145
|
+
return stateql.exec(sql, {
|
|
146
|
+
params,
|
|
147
|
+
replay: values.replay ?? false,
|
|
148
|
+
...(values["idempotency-key"]
|
|
149
|
+
? { idempotencyKey: values["idempotency-key"] }
|
|
150
|
+
: {}),
|
|
151
|
+
allowUnbounded: values["allow-unbounded"] ?? false,
|
|
152
|
+
allowDestructive: values["allow-destructive"] ?? false,
|
|
153
|
+
});
|
|
154
|
+
case "show":
|
|
155
|
+
return stateql.show(requireValue(subcommand, "result handle"));
|
|
156
|
+
case "rows":
|
|
157
|
+
return stateql.rows(requireValue(subcommand, "result handle"), {
|
|
158
|
+
offset: numberOption(values.offset, 0),
|
|
159
|
+
limit: numberOption(values.limit, 20),
|
|
160
|
+
});
|
|
161
|
+
case "count":
|
|
162
|
+
return stateql.count(requireValue(subcommand, "result handle"));
|
|
163
|
+
case "columns":
|
|
164
|
+
return stateql.columns(requireValue(subcommand, "result handle"));
|
|
165
|
+
case "export":
|
|
166
|
+
return stateql.exportResult(requireValue(subcommand, "result handle"), requireValue(values.output, "--output"), exportFormat(values.format));
|
|
167
|
+
case "alias":
|
|
168
|
+
if (subcommand !== "set")
|
|
169
|
+
throw new Error("Expected: alias set NAME RESULT");
|
|
170
|
+
return stateql.setAlias(requireValue(rest[0], "alias"), requireValue(rest[1], "result handle"));
|
|
171
|
+
case "inspect":
|
|
172
|
+
return stateql.inspect(normalizeInspectKind(requireValue(subcommand, "inspection kind")), rest[0]);
|
|
173
|
+
case "transaction":
|
|
174
|
+
return dispatchTransaction(subcommand, rest[0]);
|
|
175
|
+
case "plan":
|
|
176
|
+
return stateql.plan(sql, {
|
|
177
|
+
params,
|
|
178
|
+
allowUnbounded: values["allow-unbounded"] ?? false,
|
|
179
|
+
...(values["allow-destructive"]
|
|
180
|
+
? { allowDestructive: true }
|
|
181
|
+
: {}),
|
|
182
|
+
});
|
|
183
|
+
case "apply":
|
|
184
|
+
return stateql.apply(requireValue(subcommand, "plan handle"));
|
|
185
|
+
case "history":
|
|
186
|
+
return stateql.history(numberOption(values.limit, 20));
|
|
187
|
+
case "receipt":
|
|
188
|
+
return stateql.receipt(requireValue(subcommand, "operation handle"));
|
|
189
|
+
case "capabilities":
|
|
190
|
+
return stateql.capabilities();
|
|
191
|
+
default:
|
|
192
|
+
throw new Error(`Unknown command "${command}".`);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
async function dispatchSession(action, args) {
|
|
196
|
+
switch (action) {
|
|
197
|
+
case "start":
|
|
198
|
+
return stateql.startSession(requireValue(values.name ?? args[0], "session name"));
|
|
199
|
+
case "list":
|
|
200
|
+
return stateql.listSessions();
|
|
201
|
+
case "show":
|
|
202
|
+
return stateql.showSession(args[0]);
|
|
203
|
+
case "summary":
|
|
204
|
+
return stateql.sessionSummary();
|
|
205
|
+
case "close":
|
|
206
|
+
return stateql.closeSession();
|
|
207
|
+
default:
|
|
208
|
+
throw new Error(`Unknown session command "${action ?? ""}".`);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
async function dispatchProfile(action, args) {
|
|
212
|
+
switch (action) {
|
|
213
|
+
case "add":
|
|
214
|
+
if (values["read-only"] && values["read-write"]) {
|
|
215
|
+
throw new Error("--read-only and --read-write are mutually exclusive.");
|
|
216
|
+
}
|
|
217
|
+
return stateql.addProfile(requireValue(args[0], "profile name"), args[1], {
|
|
218
|
+
...(values.env ? { secretEnv: values.env } : {}),
|
|
219
|
+
readOnly: !values["read-write"],
|
|
220
|
+
});
|
|
221
|
+
case "list":
|
|
222
|
+
return stateql.listProfiles();
|
|
223
|
+
case "show":
|
|
224
|
+
return stateql.showProfile(requireValue(args[0], "profile name"));
|
|
225
|
+
case "remove":
|
|
226
|
+
return stateql.removeProfile(requireValue(args[0], "profile name"));
|
|
227
|
+
default:
|
|
228
|
+
throw new Error(`Unknown profile command "${action ?? ""}".`);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
async function dispatchTransaction(action, id) {
|
|
232
|
+
switch (action) {
|
|
233
|
+
case "begin":
|
|
234
|
+
return stateql.beginTransaction(values.isolation);
|
|
235
|
+
case "status":
|
|
236
|
+
return stateql.transactionStatus(id);
|
|
237
|
+
case "commit":
|
|
238
|
+
return stateql.commitTransaction(id);
|
|
239
|
+
case "rollback":
|
|
240
|
+
return stateql.rollbackTransaction(id);
|
|
241
|
+
default:
|
|
242
|
+
throw new Error(`Unknown transaction command "${action ?? ""}".`);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
function parseParameters(value, values, file) {
|
|
246
|
+
const modes = [Boolean(value), Boolean(values?.length), Boolean(file)].filter(Boolean).length;
|
|
247
|
+
if (modes > 1) {
|
|
248
|
+
throw new Error("Use only one of --params, repeated --param, or --params-file.");
|
|
249
|
+
}
|
|
250
|
+
if (values?.length)
|
|
251
|
+
return values.map(parseParameter);
|
|
252
|
+
if (!value && !file)
|
|
253
|
+
return [];
|
|
254
|
+
const json = file
|
|
255
|
+
? readFileSync(file === "-" ? 0 : resolve(file), "utf8")
|
|
256
|
+
: value;
|
|
257
|
+
let parsedValue;
|
|
258
|
+
try {
|
|
259
|
+
parsedValue = JSON.parse(json);
|
|
260
|
+
}
|
|
261
|
+
catch {
|
|
262
|
+
throw new Error(`Invalid JSON from ${file ? "--params-file" : "--params"}. ` +
|
|
263
|
+
"In PowerShell, use repeated --param values.");
|
|
264
|
+
}
|
|
265
|
+
if (!Array.isArray(parsedValue) &&
|
|
266
|
+
(!parsedValue || typeof parsedValue !== "object")) {
|
|
267
|
+
throw new Error("--params must be a JSON array or object.");
|
|
268
|
+
}
|
|
269
|
+
return parsedValue;
|
|
270
|
+
}
|
|
271
|
+
function parseParameter(value) {
|
|
272
|
+
try {
|
|
273
|
+
return JSON.parse(value);
|
|
274
|
+
}
|
|
275
|
+
catch {
|
|
276
|
+
return value;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
async function* readBatchCommands(source) {
|
|
280
|
+
if (source !== "-" && extname(source).toLowerCase() === ".json") {
|
|
281
|
+
const parsedInput = parseBatchJson(readFileSync(resolve(source), "utf8"), source);
|
|
282
|
+
if (!Array.isArray(parsedInput)) {
|
|
283
|
+
throw new Error("Batch JSON file must contain an array.");
|
|
284
|
+
}
|
|
285
|
+
for (let index = 0; index < parsedInput.length; index += 1) {
|
|
286
|
+
yield parseBatchCommand(parsedInput[index], `item ${index + 1}`);
|
|
287
|
+
}
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
const input = source === "-" ? process.stdin : createReadStream(resolve(source));
|
|
291
|
+
const lines = createInterface({ input, crlfDelay: Infinity });
|
|
292
|
+
let lineNumber = 0;
|
|
293
|
+
for await (const line of lines) {
|
|
294
|
+
lineNumber += 1;
|
|
295
|
+
if (!line.trim())
|
|
296
|
+
continue;
|
|
297
|
+
yield parseBatchCommand(parseBatchJson(line, `line ${lineNumber}`), `line ${lineNumber}`);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
function parseBatchCommand(value, location) {
|
|
301
|
+
if (!value ||
|
|
302
|
+
typeof value !== "object" ||
|
|
303
|
+
typeof value.command !== "string") {
|
|
304
|
+
throw new Error(`Invalid batch command at ${location}.`);
|
|
305
|
+
}
|
|
306
|
+
return value;
|
|
307
|
+
}
|
|
308
|
+
function parseBatchJson(value, location) {
|
|
309
|
+
try {
|
|
310
|
+
return JSON.parse(value);
|
|
311
|
+
}
|
|
312
|
+
catch {
|
|
313
|
+
throw new Error(`Invalid JSON at ${location}.`);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
function numberOption(value, fallback) {
|
|
317
|
+
return value === undefined ? fallback : Number(value);
|
|
318
|
+
}
|
|
319
|
+
function cacheMode(value) {
|
|
320
|
+
if (!value || value === "auto")
|
|
321
|
+
return "auto";
|
|
322
|
+
if (value === "bypass" || value === "require")
|
|
323
|
+
return value;
|
|
324
|
+
throw new Error("--cache must be auto, bypass, or require.");
|
|
325
|
+
}
|
|
326
|
+
function exportFormat(value) {
|
|
327
|
+
if (!value || value === "csv")
|
|
328
|
+
return "csv";
|
|
329
|
+
if (value === "json" || value === "jsonl")
|
|
330
|
+
return value;
|
|
331
|
+
throw new Error("--format must be json, jsonl, or csv.");
|
|
332
|
+
}
|
|
333
|
+
function normalizeInspectKind(value) {
|
|
334
|
+
const aliases = {
|
|
335
|
+
index: "indexes",
|
|
336
|
+
constraint: "constraints",
|
|
337
|
+
};
|
|
338
|
+
return aliases[value] ?? value;
|
|
339
|
+
}
|
|
340
|
+
function requireValue(value, description) {
|
|
341
|
+
if (value)
|
|
342
|
+
return value;
|
|
343
|
+
throw new Error(`Missing ${description}.`);
|
|
344
|
+
}
|
|
345
|
+
function outputMode(currentCommand, value) {
|
|
346
|
+
const mode = currentCommand === "export"
|
|
347
|
+
? process.env.STQL_OUTPUT ?? "agent"
|
|
348
|
+
: value ?? process.env.STQL_OUTPUT ?? "agent";
|
|
349
|
+
if (mode === "agent" ||
|
|
350
|
+
mode === "json" ||
|
|
351
|
+
mode === "jsonl" ||
|
|
352
|
+
mode === "text" ||
|
|
353
|
+
mode === "silent") {
|
|
354
|
+
return mode;
|
|
355
|
+
}
|
|
356
|
+
throw new Error("--output must be agent, json, jsonl, text, or silent.");
|
|
357
|
+
}
|
|
358
|
+
function print(result, mode, currentCommand) {
|
|
359
|
+
if (mode === "silent") {
|
|
360
|
+
if (result.ok) {
|
|
361
|
+
const handle = extractHandle(result.data);
|
|
362
|
+
if (handle)
|
|
363
|
+
console.log(handle);
|
|
364
|
+
}
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
if (mode === "text") {
|
|
368
|
+
if (!result.ok) {
|
|
369
|
+
console.log(`${result.error.code}: ${result.error.message}`);
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
const handle = extractHandle(result.data);
|
|
373
|
+
console.log(handle ? `ok ${handle}` : "ok");
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
if (mode === "agent") {
|
|
377
|
+
console.log(JSON.stringify(toAgentResponse(result, currentCommand)));
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
console.log(mode === "json" ? JSON.stringify(result, null, 2) : JSON.stringify(result));
|
|
381
|
+
}
|
|
382
|
+
function toAgentResponse(result, currentCommand) {
|
|
383
|
+
if (!result.ok)
|
|
384
|
+
return { ok: false, error: result.error };
|
|
385
|
+
if (!result.data ||
|
|
386
|
+
typeof result.data !== "object" ||
|
|
387
|
+
Array.isArray(result.data)) {
|
|
388
|
+
return { ok: true, data: result.data };
|
|
389
|
+
}
|
|
390
|
+
const data = { ...result.data };
|
|
391
|
+
const handleKey = primaryHandleKey(currentCommand);
|
|
392
|
+
const handle = handleKey && typeof data[handleKey] === "string" && data[handleKey]
|
|
393
|
+
? data[handleKey]
|
|
394
|
+
: undefined;
|
|
395
|
+
if (handle && handleKey)
|
|
396
|
+
delete data[handleKey];
|
|
397
|
+
if ((currentCommand === "query" ||
|
|
398
|
+
currentCommand === "filter" ||
|
|
399
|
+
currentCommand === "show") &&
|
|
400
|
+
typeof data.rows === "number" &&
|
|
401
|
+
Array.isArray(data.preview)) {
|
|
402
|
+
const total = data.rows;
|
|
403
|
+
const rows = data.preview;
|
|
404
|
+
const truncated = Boolean(data.truncated);
|
|
405
|
+
data.rows = rows;
|
|
406
|
+
data.total = total;
|
|
407
|
+
data.next_offset = truncated ? rows.length : null;
|
|
408
|
+
delete data.preview;
|
|
409
|
+
delete data.preview_count;
|
|
410
|
+
delete data.columns;
|
|
411
|
+
delete data.storage;
|
|
412
|
+
delete data.state_version;
|
|
413
|
+
delete data.duplicate_of;
|
|
414
|
+
}
|
|
415
|
+
else if (currentCommand === "rows") {
|
|
416
|
+
delete data.offset;
|
|
417
|
+
delete data.limit;
|
|
418
|
+
delete data.returned;
|
|
419
|
+
}
|
|
420
|
+
else if (currentCommand === "count" && typeof data.rows === "number") {
|
|
421
|
+
data.total = data.rows;
|
|
422
|
+
delete data.rows;
|
|
423
|
+
}
|
|
424
|
+
for (const key of ["ok", "error", "handle", "warnings"]) {
|
|
425
|
+
if (!(key in data))
|
|
426
|
+
continue;
|
|
427
|
+
data[`data_${key}`] = data[key];
|
|
428
|
+
delete data[key];
|
|
429
|
+
}
|
|
430
|
+
return {
|
|
431
|
+
ok: true,
|
|
432
|
+
...(handle ? { handle } : {}),
|
|
433
|
+
...data,
|
|
434
|
+
...(result.warnings.length ? { warnings: result.warnings } : {}),
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
function primaryHandleKey(currentCommand) {
|
|
438
|
+
const keys = {
|
|
439
|
+
query: "result_id",
|
|
440
|
+
filter: "result_id",
|
|
441
|
+
show: "result_id",
|
|
442
|
+
rows: "result_id",
|
|
443
|
+
count: "result_id",
|
|
444
|
+
columns: "result_id",
|
|
445
|
+
export: "result_id",
|
|
446
|
+
alias: "result_id",
|
|
447
|
+
"alias.set": "result_id",
|
|
448
|
+
exec: "operation_id",
|
|
449
|
+
receipt: "operation_id",
|
|
450
|
+
apply: "operation_id",
|
|
451
|
+
plan: "plan_id",
|
|
452
|
+
connect: "connection_id",
|
|
453
|
+
transaction: "transaction_id",
|
|
454
|
+
"transaction.begin": "transaction_id",
|
|
455
|
+
"transaction.status": "transaction_id",
|
|
456
|
+
"transaction.commit": "transaction_id",
|
|
457
|
+
"transaction.rollback": "transaction_id",
|
|
458
|
+
session: "session_id",
|
|
459
|
+
"session.start": "session_id",
|
|
460
|
+
"session.show": "session_id",
|
|
461
|
+
"session.close": "session_id",
|
|
462
|
+
};
|
|
463
|
+
return keys[currentCommand];
|
|
464
|
+
}
|
|
465
|
+
function cliFailure(error) {
|
|
466
|
+
return {
|
|
467
|
+
ok: false,
|
|
468
|
+
command_id: "cmd_0",
|
|
469
|
+
session_id: process.env.STQL_SESSION ?? "default",
|
|
470
|
+
error: {
|
|
471
|
+
code: "INVALID_COMMAND",
|
|
472
|
+
message: error instanceof Error ? error.message : String(error),
|
|
473
|
+
retryable: false,
|
|
474
|
+
executed: false,
|
|
475
|
+
},
|
|
476
|
+
meta: { duration_ms: 0 },
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
function extractHandle(data) {
|
|
480
|
+
if (!data || typeof data !== "object")
|
|
481
|
+
return undefined;
|
|
482
|
+
const record = data;
|
|
483
|
+
for (const key of [
|
|
484
|
+
"result_id",
|
|
485
|
+
"operation_id",
|
|
486
|
+
"plan_id",
|
|
487
|
+
"transaction_id",
|
|
488
|
+
"connection_id",
|
|
489
|
+
"session_id",
|
|
490
|
+
]) {
|
|
491
|
+
if (typeof record[key] === "string")
|
|
492
|
+
return record[key];
|
|
493
|
+
}
|
|
494
|
+
return undefined;
|
|
495
|
+
}
|
|
496
|
+
function helpText() {
|
|
497
|
+
return `StateQL 0.1.0
|
|
498
|
+
|
|
499
|
+
Usage: stql <command> [arguments] [options]
|
|
500
|
+
|
|
501
|
+
Commands:
|
|
502
|
+
connect, disconnect, status
|
|
503
|
+
profile add|list|show|remove
|
|
504
|
+
session start|list|show|summary|close
|
|
505
|
+
query, filter, exec, show, rows, count, columns, export
|
|
506
|
+
alias set
|
|
507
|
+
inspect schema|table|columns|indexes|constraints
|
|
508
|
+
transaction begin|status|commit|rollback
|
|
509
|
+
plan, apply, history, receipt, capabilities
|
|
510
|
+
batch [file.json|file.jsonl|-]
|
|
511
|
+
pipe
|
|
512
|
+
|
|
513
|
+
SQL parameters: --params JSON, repeated --param VALUE, or --params-file FILE.
|
|
514
|
+
Output: --output agent|json|jsonl|text|silent (default: agent).
|
|
515
|
+
Batch/pipe accept JSON array files or JSONL streams. Stop on first error.`;
|
|
516
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { StateQLErrorShape } from "./types.js";
|
|
2
|
+
export declare class StateQLError extends Error {
|
|
3
|
+
readonly details: StateQLErrorShape;
|
|
4
|
+
constructor(code: string, message: string, options?: {
|
|
5
|
+
retryable?: boolean;
|
|
6
|
+
executed?: boolean;
|
|
7
|
+
suggestedAction?: string;
|
|
8
|
+
extra?: Record<string, unknown>;
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
export declare function exitCodeFor(code: string): number;
|
|
12
|
+
export declare function asStateQLError(error: unknown): StateQLError;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
export class StateQLError extends Error {
|
|
2
|
+
details;
|
|
3
|
+
constructor(code, message, options = {}) {
|
|
4
|
+
super(message);
|
|
5
|
+
this.name = "StateQLError";
|
|
6
|
+
this.details = {
|
|
7
|
+
code,
|
|
8
|
+
message,
|
|
9
|
+
retryable: options.retryable ?? false,
|
|
10
|
+
executed: options.executed ?? false,
|
|
11
|
+
...(options.suggestedAction
|
|
12
|
+
? { suggested_action: options.suggestedAction }
|
|
13
|
+
: {}),
|
|
14
|
+
...options.extra,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
export function exitCodeFor(code) {
|
|
19
|
+
if (code === "INVALID_COMMAND" || code === "INVALID_SQL")
|
|
20
|
+
return 2;
|
|
21
|
+
if (code.startsWith("CONNECTION_"))
|
|
22
|
+
return 3;
|
|
23
|
+
if (code === "QUERY_FAILED")
|
|
24
|
+
return 4;
|
|
25
|
+
if (code === "READ_ONLY_CONNECTION" ||
|
|
26
|
+
code === "UNBOUNDED_MUTATION" ||
|
|
27
|
+
code === "DESTRUCTIVE_OPERATION_BLOCKED") {
|
|
28
|
+
return 5;
|
|
29
|
+
}
|
|
30
|
+
if (code === "POTENTIAL_DUPLICATE_WRITE" || code === "OUTCOME_UNKNOWN")
|
|
31
|
+
return 6;
|
|
32
|
+
if (code.startsWith("STALE_") || code === "RESULT_EXPIRED")
|
|
33
|
+
return 7;
|
|
34
|
+
if (code === "PERMISSION_DENIED")
|
|
35
|
+
return 8;
|
|
36
|
+
if (code === "UNSUPPORTED_DRIVER")
|
|
37
|
+
return 9;
|
|
38
|
+
return 1;
|
|
39
|
+
}
|
|
40
|
+
export function asStateQLError(error) {
|
|
41
|
+
if (error instanceof StateQLError)
|
|
42
|
+
return error;
|
|
43
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
44
|
+
return new StateQLError("INTERNAL_ERROR", message);
|
|
45
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export { StateQL } from "./stateql.js";
|
|
2
|
+
export { StateQLError, exitCodeFor } from "./errors.js";
|
|
3
|
+
export type { BatchCommand, BatchCommandName, BatchOptions, ConnectOptions, ExecOptions, Failure, FilterOptions, PlanOptions, ProfileOptions, QueryOptions, Response, RowsOptions, SqlParameters, StateQLOptions, Success, } from "./types.js";
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { AST } from "node-sql-parser";
|
|
2
|
+
import type { Driver } from "./types.js";
|
|
3
|
+
export type StatementType = "select" | "insert" | "replace" | "update" | "delete" | "create" | "alter" | "drop" | "truncate";
|
|
4
|
+
export interface SqlAnalysis {
|
|
5
|
+
ast: AST;
|
|
6
|
+
normalized: string;
|
|
7
|
+
statementType: StatementType;
|
|
8
|
+
read: boolean;
|
|
9
|
+
unboundedMutation: boolean;
|
|
10
|
+
destructive: boolean;
|
|
11
|
+
ordered: boolean;
|
|
12
|
+
}
|
|
13
|
+
export declare function analyzeSql(sql: string, driver: Driver): SqlAnalysis;
|
package/dist/src/sql.js
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import { StateQLError } from "./errors.js";
|
|
3
|
+
const { Parser } = createRequire(import.meta.url)("node-sql-parser");
|
|
4
|
+
const parser = new Parser();
|
|
5
|
+
const SUPPORTED_STATEMENTS = new Set([
|
|
6
|
+
"select",
|
|
7
|
+
"insert",
|
|
8
|
+
"replace",
|
|
9
|
+
"update",
|
|
10
|
+
"delete",
|
|
11
|
+
"create",
|
|
12
|
+
"alter",
|
|
13
|
+
"drop",
|
|
14
|
+
"truncate",
|
|
15
|
+
]);
|
|
16
|
+
export function analyzeSql(sql, driver) {
|
|
17
|
+
const trimmed = sql.trim();
|
|
18
|
+
if (!trimmed)
|
|
19
|
+
throw new StateQLError("INVALID_SQL", "SQL is empty.");
|
|
20
|
+
try {
|
|
21
|
+
const database = driver === "postgres" ? "Postgresql" : "Sqlite";
|
|
22
|
+
const parsed = parser.astify(trimmed, { database });
|
|
23
|
+
if (Array.isArray(parsed)) {
|
|
24
|
+
if (parsed.length !== 1) {
|
|
25
|
+
throw new StateQLError("INVALID_SQL", "Exactly one SQL statement is required.");
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
const ast = (Array.isArray(parsed) ? parsed[0] : parsed);
|
|
29
|
+
if (!ast)
|
|
30
|
+
throw new StateQLError("INVALID_SQL", "SQL is empty.");
|
|
31
|
+
const rawType = String(ast.type);
|
|
32
|
+
if (!SUPPORTED_STATEMENTS.has(rawType)) {
|
|
33
|
+
throw new StateQLError("INVALID_SQL", `Unsupported SQL statement type "${rawType}".`);
|
|
34
|
+
}
|
|
35
|
+
const statementType = rawType;
|
|
36
|
+
if (statementType === "select" && selectContainsWrite(ast)) {
|
|
37
|
+
throw new StateQLError("INVALID_SQL", "Read statements cannot contain writes or SELECT INTO.");
|
|
38
|
+
}
|
|
39
|
+
const normalized = parser
|
|
40
|
+
.sqlify(ast, { database })
|
|
41
|
+
.replace(/;\s*$/, "")
|
|
42
|
+
.replace(/\s+/g, " ")
|
|
43
|
+
.trim();
|
|
44
|
+
const details = ast;
|
|
45
|
+
const read = statementType === "select";
|
|
46
|
+
const mutation = statementType === "update" ||
|
|
47
|
+
statementType === "delete" ||
|
|
48
|
+
statementType === "truncate";
|
|
49
|
+
const destructive = statementType === "drop" ||
|
|
50
|
+
statementType === "alter" ||
|
|
51
|
+
statementType === "delete" ||
|
|
52
|
+
statementType === "replace" ||
|
|
53
|
+
statementType === "truncate";
|
|
54
|
+
return {
|
|
55
|
+
ast,
|
|
56
|
+
normalized,
|
|
57
|
+
statementType,
|
|
58
|
+
read,
|
|
59
|
+
unboundedMutation: statementType === "truncate" || (mutation && !details.where),
|
|
60
|
+
destructive,
|
|
61
|
+
ordered: read && Boolean(details.orderby),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
catch (error) {
|
|
65
|
+
if (error instanceof StateQLError)
|
|
66
|
+
throw error;
|
|
67
|
+
const message = error instanceof Error ? error.message : "Invalid SQL.";
|
|
68
|
+
throw new StateQLError("INVALID_SQL", message);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
function selectContainsWrite(ast) {
|
|
72
|
+
const details = ast;
|
|
73
|
+
const into = details.into;
|
|
74
|
+
if (into?.type === "into" || into?.expr)
|
|
75
|
+
return true;
|
|
76
|
+
const withStatements = details.with;
|
|
77
|
+
if (!Array.isArray(withStatements))
|
|
78
|
+
return false;
|
|
79
|
+
return withStatements.some((entry) => {
|
|
80
|
+
if (!entry || typeof entry !== "object")
|
|
81
|
+
return false;
|
|
82
|
+
const statement = entry.stmt;
|
|
83
|
+
if (!statement || typeof statement !== "object")
|
|
84
|
+
return false;
|
|
85
|
+
const wrapper = statement;
|
|
86
|
+
const child = (wrapper.ast ?? statement);
|
|
87
|
+
if (child.type !== "select")
|
|
88
|
+
return true;
|
|
89
|
+
return selectContainsWrite(child);
|
|
90
|
+
});
|
|
91
|
+
}
|