@fadhilp/stateql 0.9.0 → 0.10.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/README.md +149 -5
- package/dist/src/adapters.d.ts +7 -2
- package/dist/src/adapters.js +167 -0
- package/dist/src/cli.js +66 -3
- package/dist/src/connection.d.ts +1 -0
- package/dist/src/connection.js +18 -2
- package/dist/src/index.d.ts +2 -1
- package/dist/src/migrations.js +44 -0
- package/dist/src/mongodb.d.ts +4 -2
- package/dist/src/mongodb.js +53 -2
- package/dist/src/redis.d.ts +44 -0
- package/dist/src/redis.js +395 -0
- package/dist/src/sqlite-process.js +41 -0
- package/dist/src/stateql.d.ts +18 -4
- package/dist/src/stateql.js +650 -62
- package/dist/src/store.d.ts +25 -2
- package/dist/src/store.js +116 -12
- package/dist/src/table-editor.d.ts +5 -0
- package/dist/src/table-editor.js +22 -0
- package/dist/src/types.d.ts +80 -3
- package/package.json +4 -2
package/dist/src/store.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { DatabaseSync } from "node:sqlite";
|
|
2
|
-
import type { Column, CommandOrigin, Driver, MongoWriteOutcome, Row, SqlParameters, StateConfidence } from "./types.js";
|
|
2
|
+
import type { Column, CommandOrigin, Driver, HistoryCategory, MongoWriteOutcome, Row, SqlParameters, StateConfidence } from "./types.js";
|
|
3
3
|
export interface SessionRecord {
|
|
4
4
|
id: string;
|
|
5
5
|
name: string;
|
|
@@ -11,6 +11,7 @@ export interface SessionRecord {
|
|
|
11
11
|
}
|
|
12
12
|
export interface ConnectionRecord {
|
|
13
13
|
id: string;
|
|
14
|
+
alias?: string;
|
|
14
15
|
session_id: string;
|
|
15
16
|
name: string;
|
|
16
17
|
driver: Driver;
|
|
@@ -46,6 +47,7 @@ export interface ResultRecord {
|
|
|
46
47
|
state_confidence: StateConfidence;
|
|
47
48
|
expires_at: string;
|
|
48
49
|
created_at: string;
|
|
50
|
+
alias?: string;
|
|
49
51
|
}
|
|
50
52
|
export interface OperationRecord {
|
|
51
53
|
id: string;
|
|
@@ -101,6 +103,8 @@ export interface HistoryRecord {
|
|
|
101
103
|
session_id: string;
|
|
102
104
|
actor_id: string;
|
|
103
105
|
origin: CommandOrigin;
|
|
106
|
+
category: HistoryCategory;
|
|
107
|
+
internal: number;
|
|
104
108
|
command: string;
|
|
105
109
|
sql: string | null;
|
|
106
110
|
target: string | null;
|
|
@@ -142,6 +146,13 @@ export declare class StateStore {
|
|
|
142
146
|
credentialRef?: string;
|
|
143
147
|
readOnly: boolean;
|
|
144
148
|
}): ProfileRecord;
|
|
149
|
+
updateProfile(input: {
|
|
150
|
+
name: string;
|
|
151
|
+
target: string | null;
|
|
152
|
+
secretEnv: string | null;
|
|
153
|
+
credentialRef: string | null;
|
|
154
|
+
readOnly: boolean;
|
|
155
|
+
}): ProfileRecord | undefined;
|
|
145
156
|
getProfile(name: string): ProfileRecord | undefined;
|
|
146
157
|
listProfiles(): ProfileRecord[];
|
|
147
158
|
removeProfile(name: string): boolean;
|
|
@@ -156,6 +167,8 @@ export declare class StateStore {
|
|
|
156
167
|
credentialRef?: string;
|
|
157
168
|
readOnly: boolean;
|
|
158
169
|
}): ConnectionRecord | undefined;
|
|
170
|
+
private allocateConnectionAlias;
|
|
171
|
+
private backfillConnectionAliases;
|
|
159
172
|
getConnection(id: string): ConnectionRecord | undefined;
|
|
160
173
|
activeConnection(session: SessionRecord): ConnectionRecord | undefined;
|
|
161
174
|
disconnect(sessionId: string, actorId: string): boolean;
|
|
@@ -178,6 +191,9 @@ export declare class StateStore {
|
|
|
178
191
|
resultRows(result: ResultRecord): Row[];
|
|
179
192
|
resultColumns(result: ResultRecord): Column[];
|
|
180
193
|
setAlias(sessionId: string, name: string, resultId: string): void;
|
|
194
|
+
generatedAlias(resultId: string): string;
|
|
195
|
+
private allocateGeneratedAlias;
|
|
196
|
+
private backfillGeneratedAliases;
|
|
181
197
|
saveOperation(input: {
|
|
182
198
|
sessionId: string;
|
|
183
199
|
actorId: string;
|
|
@@ -277,6 +293,8 @@ export declare class StateStore {
|
|
|
277
293
|
sessionId: string;
|
|
278
294
|
actorId: string;
|
|
279
295
|
origin?: CommandOrigin;
|
|
296
|
+
category?: HistoryCategory;
|
|
297
|
+
internal?: boolean;
|
|
280
298
|
command: string;
|
|
281
299
|
sql?: string;
|
|
282
300
|
target?: string;
|
|
@@ -287,7 +305,12 @@ export declare class StateStore {
|
|
|
287
305
|
errorCode?: string;
|
|
288
306
|
id?: string;
|
|
289
307
|
}): HistoryRecord;
|
|
290
|
-
history(sessionId: string, limit: number,
|
|
308
|
+
history(sessionId: string, limit: number, input?: CommandOrigin | {
|
|
309
|
+
origin?: CommandOrigin;
|
|
310
|
+
category?: HistoryCategory;
|
|
311
|
+
internal?: boolean;
|
|
312
|
+
offset?: number;
|
|
313
|
+
}): HistoryRecord[];
|
|
291
314
|
recentOperations(sessionId: string, limit: number): OperationRecord[];
|
|
292
315
|
knownResults(sessionId: string, limit: number): Array<ResultRecord & {
|
|
293
316
|
alias: string | null;
|
package/dist/src/store.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
1
2
|
import { chmodSync, existsSync, mkdirSync, statSync } from "node:fs";
|
|
2
3
|
import { DatabaseSync } from "node:sqlite";
|
|
3
4
|
import { join } from "node:path";
|
|
@@ -35,6 +36,8 @@ export class StateStore {
|
|
|
35
36
|
this.db.exec("PRAGMA busy_timeout = 5000");
|
|
36
37
|
this.db.exec("PRAGMA foreign_keys = ON");
|
|
37
38
|
runMigrations(this.db, this.now);
|
|
39
|
+
this.backfillConnectionAliases();
|
|
40
|
+
this.backfillGeneratedAliases();
|
|
38
41
|
this.recoverStaleCommittingTransactions();
|
|
39
42
|
this.deleteExpiredData();
|
|
40
43
|
}
|
|
@@ -240,6 +243,12 @@ export class StateStore {
|
|
|
240
243
|
.run(input.name, input.target ?? null, input.secretEnv ?? null, input.credentialRef ?? null, input.readOnly ? 1 : 0, timestamp, timestamp);
|
|
241
244
|
return this.getProfile(input.name);
|
|
242
245
|
}
|
|
246
|
+
updateProfile(input) {
|
|
247
|
+
const result = this.db.prepare(`UPDATE profiles
|
|
248
|
+
SET target = ?, secret_env = ?, credential_ref = ?, read_only = ?, updated_at = ?
|
|
249
|
+
WHERE name = ?`).run(input.target, input.secretEnv, input.credentialRef, input.readOnly ? 1 : 0, this.now().toISOString(), input.name);
|
|
250
|
+
return Number(result.changes) === 1 ? this.getProfile(input.name) : undefined;
|
|
251
|
+
}
|
|
243
252
|
getProfile(name) {
|
|
244
253
|
return this.db
|
|
245
254
|
.prepare("SELECT * FROM profiles WHERE name = ?")
|
|
@@ -277,6 +286,7 @@ export class StateStore {
|
|
|
277
286
|
credential_ref, read_only, version, created_at)
|
|
278
287
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 0, ?)`)
|
|
279
288
|
.run(id, input.sessionId, input.name, input.driver, input.databaseName, input.source, input.secretEnv ?? null, input.credentialRef ?? null, input.readOnly ? 1 : 0, timestamp);
|
|
289
|
+
this.allocateConnectionAlias(id);
|
|
280
290
|
this.db
|
|
281
291
|
.prepare(`UPDATE sessions
|
|
282
292
|
SET active_connection_id = ?, updated_at = ?
|
|
@@ -290,6 +300,28 @@ export class StateStore {
|
|
|
290
300
|
throw error;
|
|
291
301
|
}
|
|
292
302
|
}
|
|
303
|
+
allocateConnectionAlias(connectionId) {
|
|
304
|
+
for (let attempt = 0; attempt < 64; attempt++) {
|
|
305
|
+
this.db.prepare("UPDATE OR IGNORE connections SET alias = ? WHERE id = ? AND alias IS NULL").run(randomBase32Alias(), connectionId);
|
|
306
|
+
const connection = this.getConnection(connectionId);
|
|
307
|
+
if (connection?.alias)
|
|
308
|
+
return connection.alias;
|
|
309
|
+
}
|
|
310
|
+
throw new Error("Could not allocate a unique connection alias.");
|
|
311
|
+
}
|
|
312
|
+
backfillConnectionAliases() {
|
|
313
|
+
this.db.exec("BEGIN IMMEDIATE");
|
|
314
|
+
try {
|
|
315
|
+
const connections = this.db.prepare("SELECT id FROM connections WHERE alias IS NULL").all();
|
|
316
|
+
for (const connection of connections)
|
|
317
|
+
this.allocateConnectionAlias(connection.id);
|
|
318
|
+
this.db.exec("COMMIT");
|
|
319
|
+
}
|
|
320
|
+
catch (error) {
|
|
321
|
+
this.db.exec("ROLLBACK");
|
|
322
|
+
throw error;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
293
325
|
getConnection(id) {
|
|
294
326
|
return this.db
|
|
295
327
|
.prepare("SELECT * FROM connections WHERE id = ?")
|
|
@@ -341,6 +373,7 @@ export class StateStore {
|
|
|
341
373
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
342
374
|
.run(id, input.sessionId, input.connectionId, input.fingerprint, input.sql, JSON.stringify(toJsonSafe(input.parameters)), JSON.stringify(toJsonSafe(input.rows)), JSON.stringify(input.columns), input.rows.length, input.stateVersion, input.stateSignature, input.stateConfidence, input.expiresAt, this.now().toISOString());
|
|
343
375
|
this.enforceResultQuota(id);
|
|
376
|
+
this.allocateGeneratedAlias(input.sessionId, id);
|
|
344
377
|
const result = this.getResult(id);
|
|
345
378
|
this.db.exec("COMMIT");
|
|
346
379
|
return result;
|
|
@@ -351,15 +384,18 @@ export class StateStore {
|
|
|
351
384
|
}
|
|
352
385
|
}
|
|
353
386
|
findResult(fingerprint) {
|
|
354
|
-
|
|
387
|
+
const result = this.db
|
|
355
388
|
.prepare(`SELECT * FROM results
|
|
356
389
|
WHERE fingerprint = ?
|
|
357
390
|
ORDER BY created_at DESC
|
|
358
391
|
LIMIT 1`)
|
|
359
392
|
.get(fingerprint);
|
|
393
|
+
if (result)
|
|
394
|
+
result.alias = this.generatedAlias(result.id);
|
|
395
|
+
return result;
|
|
360
396
|
}
|
|
361
397
|
getResult(idOrAlias, sessionId) {
|
|
362
|
-
|
|
398
|
+
const result = this.db
|
|
363
399
|
.prepare(`SELECT results.*
|
|
364
400
|
FROM results
|
|
365
401
|
LEFT JOIN aliases
|
|
@@ -369,6 +405,9 @@ export class StateStore {
|
|
|
369
405
|
AND (? IS NULL OR results.session_id = ?)
|
|
370
406
|
LIMIT 1`)
|
|
371
407
|
.get(idOrAlias, idOrAlias, sessionId ?? null, sessionId ?? null);
|
|
408
|
+
if (result)
|
|
409
|
+
result.alias = this.generatedAlias(result.id);
|
|
410
|
+
return result;
|
|
372
411
|
}
|
|
373
412
|
resultRows(result) {
|
|
374
413
|
return parseJson(result.rows_json, `result "${result.id}" rows`, isRows);
|
|
@@ -377,12 +416,55 @@ export class StateStore {
|
|
|
377
416
|
return parseJson(result.columns_json, `result "${result.id}" columns`, isColumns);
|
|
378
417
|
}
|
|
379
418
|
setAlias(sessionId, name, resultId) {
|
|
419
|
+
const existing = this.db.prepare("SELECT result_id, generated FROM aliases WHERE session_id = ? AND name = ?").get(sessionId, name);
|
|
420
|
+
if (existing?.generated) {
|
|
421
|
+
if (existing.result_id === resultId)
|
|
422
|
+
return;
|
|
423
|
+
throw new StateQLError("INVALID_COMMAND", "Generated result aliases cannot be reassigned.");
|
|
424
|
+
}
|
|
380
425
|
this.db
|
|
381
|
-
.prepare(`INSERT INTO aliases(session_id, name, result_id)
|
|
382
|
-
VALUES (?, ?,
|
|
426
|
+
.prepare(`INSERT INTO aliases(session_id, name, result_id, generated)
|
|
427
|
+
VALUES (?, ?, ?, 0)
|
|
383
428
|
ON CONFLICT(session_id, name) DO UPDATE SET result_id = excluded.result_id`)
|
|
384
429
|
.run(sessionId, name, resultId);
|
|
385
430
|
}
|
|
431
|
+
generatedAlias(resultId) {
|
|
432
|
+
const row = this.db.prepare("SELECT name FROM aliases WHERE result_id = ? AND generated = 1").get(resultId);
|
|
433
|
+
if (!row)
|
|
434
|
+
throw new Error(`Result "${resultId}" has no generated alias.`);
|
|
435
|
+
return row.name;
|
|
436
|
+
}
|
|
437
|
+
allocateGeneratedAlias(sessionId, resultId) {
|
|
438
|
+
const existing = this.db.prepare("SELECT name FROM aliases WHERE result_id = ? AND generated = 1").get(resultId);
|
|
439
|
+
if (existing)
|
|
440
|
+
return existing.name;
|
|
441
|
+
for (let attempt = 0; attempt < 64; attempt++) {
|
|
442
|
+
const name = randomBase32Alias();
|
|
443
|
+
this.db.prepare("INSERT OR IGNORE INTO aliases(session_id, name, result_id, generated) VALUES (?, ?, ?, 1)").run(sessionId, name, resultId);
|
|
444
|
+
const allocated = this.db.prepare("SELECT name FROM aliases WHERE result_id = ? AND generated = 1").get(resultId);
|
|
445
|
+
if (allocated)
|
|
446
|
+
return allocated.name;
|
|
447
|
+
}
|
|
448
|
+
throw new Error("Could not allocate a unique result alias.");
|
|
449
|
+
}
|
|
450
|
+
backfillGeneratedAliases() {
|
|
451
|
+
const results = this.db.prepare(`SELECT results.id, results.session_id
|
|
452
|
+
FROM results
|
|
453
|
+
LEFT JOIN aliases ON aliases.result_id = results.id AND aliases.generated = 1
|
|
454
|
+
WHERE aliases.result_id IS NULL`).all();
|
|
455
|
+
if (!results.length)
|
|
456
|
+
return;
|
|
457
|
+
this.db.exec("BEGIN IMMEDIATE");
|
|
458
|
+
try {
|
|
459
|
+
for (const result of results)
|
|
460
|
+
this.allocateGeneratedAlias(result.session_id, result.id);
|
|
461
|
+
this.db.exec("COMMIT");
|
|
462
|
+
}
|
|
463
|
+
catch (error) {
|
|
464
|
+
this.db.exec("ROLLBACK");
|
|
465
|
+
throw error;
|
|
466
|
+
}
|
|
467
|
+
}
|
|
386
468
|
saveOperation(input) {
|
|
387
469
|
const id = this.nextId("op");
|
|
388
470
|
this.db
|
|
@@ -797,10 +879,10 @@ export class StateStore {
|
|
|
797
879
|
const id = input.id ?? this.nextId("cmd");
|
|
798
880
|
this.db
|
|
799
881
|
.prepare(`INSERT INTO history
|
|
800
|
-
(id, timestamp, session_id, actor_id, origin, command, sql, target, handle,
|
|
882
|
+
(id, timestamp, session_id, actor_id, origin, category, internal, command, sql, target, handle,
|
|
801
883
|
executed, cached, success, error_code)
|
|
802
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
803
|
-
.run(id, this.now().toISOString(), input.sessionId, input.actorId, input.origin ?? "legacy", input.command, boundedHistorySql(input.sql), input.target?.slice(0, 1024) ?? null, input.handle ?? null, input.executed ? 1 : 0, input.cached ? 1 : 0, input.success ? 1 : 0, input.errorCode ?? null);
|
|
884
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
885
|
+
.run(id, this.now().toISOString(), input.sessionId, input.actorId, input.origin ?? "legacy", input.category ?? "management", input.internal ? 1 : 0, input.command, boundedHistorySql(input.sql), input.target?.slice(0, 1024) ?? null, input.handle ?? null, input.executed ? 1 : 0, input.cached ? 1 : 0, input.success ? 1 : 0, input.errorCode ?? null);
|
|
804
886
|
this.db
|
|
805
887
|
.prepare(`DELETE FROM history
|
|
806
888
|
WHERE rowid IN (
|
|
@@ -814,13 +896,29 @@ export class StateStore {
|
|
|
814
896
|
.prepare("SELECT * FROM history WHERE id = ?")
|
|
815
897
|
.get(id);
|
|
816
898
|
}
|
|
817
|
-
history(sessionId, limit,
|
|
899
|
+
history(sessionId, limit, input = {}) {
|
|
900
|
+
const options = typeof input === "string" ? { origin: input } : input;
|
|
901
|
+
const predicates = ["session_id = ?"];
|
|
902
|
+
const values = [sessionId];
|
|
903
|
+
if (options.origin !== undefined) {
|
|
904
|
+
predicates.push("origin = ?");
|
|
905
|
+
values.push(options.origin);
|
|
906
|
+
}
|
|
907
|
+
if (options.category !== undefined) {
|
|
908
|
+
predicates.push("category = ?");
|
|
909
|
+
values.push(options.category);
|
|
910
|
+
}
|
|
911
|
+
if (options.internal !== undefined) {
|
|
912
|
+
predicates.push("internal = ?");
|
|
913
|
+
values.push(options.internal ? 1 : 0);
|
|
914
|
+
}
|
|
915
|
+
values.push(limit, options.offset ?? 0);
|
|
818
916
|
return this.db
|
|
819
917
|
.prepare(`SELECT * FROM history
|
|
820
|
-
WHERE
|
|
918
|
+
WHERE ${predicates.join(" AND ")}
|
|
821
919
|
ORDER BY rowid DESC
|
|
822
|
-
LIMIT ?`)
|
|
823
|
-
.all(...
|
|
920
|
+
LIMIT ? OFFSET ?`)
|
|
921
|
+
.all(...values);
|
|
824
922
|
}
|
|
825
923
|
recentOperations(sessionId, limit) {
|
|
826
924
|
return this.db
|
|
@@ -837,6 +935,7 @@ export class StateStore {
|
|
|
837
935
|
LEFT JOIN aliases
|
|
838
936
|
ON aliases.result_id = results.id
|
|
839
937
|
AND aliases.session_id = results.session_id
|
|
938
|
+
AND aliases.generated = 1
|
|
840
939
|
WHERE results.session_id = ?
|
|
841
940
|
ORDER BY results.created_at DESC
|
|
842
941
|
LIMIT ?`)
|
|
@@ -946,12 +1045,13 @@ export class StateStore {
|
|
|
946
1045
|
while (this.resultBytes() > this.maxStateBytes) {
|
|
947
1046
|
const candidate = this.db.prepare(`SELECT id FROM results
|
|
948
1047
|
WHERE id <> ? AND NOT EXISTS (
|
|
949
|
-
SELECT 1 FROM aliases WHERE aliases.result_id = results.id
|
|
1048
|
+
SELECT 1 FROM aliases WHERE aliases.result_id = results.id AND aliases.generated = 0
|
|
950
1049
|
)
|
|
951
1050
|
ORDER BY created_at, rowid LIMIT 1`).get(protectedId);
|
|
952
1051
|
if (!candidate) {
|
|
953
1052
|
throw new StateQLError("STATE_QUOTA_EXCEEDED", `Stored results exceed the ${this.maxStateBytes}-byte state quota.`, { suggestedAction: "Purge results or increase maxStateBytes." });
|
|
954
1053
|
}
|
|
1054
|
+
this.db.prepare("DELETE FROM aliases WHERE result_id = ? AND generated = 1").run(candidate.id);
|
|
955
1055
|
this.db.prepare("DELETE FROM results WHERE id = ?").run(candidate.id);
|
|
956
1056
|
}
|
|
957
1057
|
}
|
|
@@ -1019,6 +1119,10 @@ export class StateStore {
|
|
|
1019
1119
|
function outcomeJson(outcome) {
|
|
1020
1120
|
return outcome === undefined ? null : JSON.stringify(toJsonSafe(outcome));
|
|
1021
1121
|
}
|
|
1122
|
+
function randomBase32Alias() {
|
|
1123
|
+
const alphabet = "abcdefghijklmnopqrstuvwxyz234567";
|
|
1124
|
+
return [...randomBytes(10)].map((value) => alphabet[value & 31]).join("");
|
|
1125
|
+
}
|
|
1022
1126
|
function boundedHistorySql(sql) {
|
|
1023
1127
|
if (sql === undefined)
|
|
1024
1128
|
return null;
|
|
@@ -26,6 +26,10 @@ export interface TableUpdate {
|
|
|
26
26
|
original: Row;
|
|
27
27
|
changes: TableChange;
|
|
28
28
|
}
|
|
29
|
+
export interface TableUpdateBatch {
|
|
30
|
+
version: 1;
|
|
31
|
+
updates: TableUpdate[];
|
|
32
|
+
}
|
|
29
33
|
export declare function quoteIdentifier(name: string, driver: Driver): string;
|
|
30
34
|
export declare function editableRow(metadata: EditableTable, row: Row): boolean;
|
|
31
35
|
export declare function compileTableUpdate(update: TableUpdate): {
|
|
@@ -34,3 +38,4 @@ export declare function compileTableUpdate(update: TableUpdate): {
|
|
|
34
38
|
mongo?: MongoWriteCommand;
|
|
35
39
|
};
|
|
36
40
|
export declare function parseTableUpdate(parameters: string): TableUpdate;
|
|
41
|
+
export declare function parseTableUpdates(parameters: string): TableUpdate[];
|
package/dist/src/table-editor.js
CHANGED
|
@@ -113,3 +113,25 @@ export function parseTableUpdate(parameters) {
|
|
|
113
113
|
throw new StateQLError("STALE_PLAN", "The stored table update is invalid. Reload the row and plan again.");
|
|
114
114
|
}
|
|
115
115
|
}
|
|
116
|
+
export function parseTableUpdates(parameters) {
|
|
117
|
+
try {
|
|
118
|
+
const outer = JSON.parse(parameters);
|
|
119
|
+
if (!Array.isArray(outer) || outer.length !== 1 || typeof outer[0] !== "string" || outer[0].length > 512 * 1024)
|
|
120
|
+
throw new Error();
|
|
121
|
+
const batch = JSON.parse(outer[0]);
|
|
122
|
+
if (!batch || batch.version !== 1 || !Array.isArray(batch.updates) || batch.updates.length < 1 || batch.updates.length > 100)
|
|
123
|
+
throw new Error();
|
|
124
|
+
for (const update of batch.updates) {
|
|
125
|
+
if (!update?.metadata || !["sqlite", "postgres", "mysql", "mongodb"].includes(update.metadata.driver) ||
|
|
126
|
+
!Array.isArray(update.metadata.columns) || update.metadata.columns.length > 100 ||
|
|
127
|
+
!update.metadata.columns.every(column => typeof column.name === "string" && typeof column.type === "string" && typeof column.nullable === "boolean" && typeof column.generated === "boolean" && Number.isSafeInteger(column.key)) ||
|
|
128
|
+
!update.original || !update.changes || typeof update.changes !== "object")
|
|
129
|
+
throw new Error();
|
|
130
|
+
compileTableUpdate(update);
|
|
131
|
+
}
|
|
132
|
+
return batch.updates;
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
throw new StateQLError("STALE_PLAN", "The stored table update batch is invalid. Reload the rows and plan again.");
|
|
136
|
+
}
|
|
137
|
+
}
|
package/dist/src/types.d.ts
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
export type SqlDriver = "sqlite" | "postgres" | "mysql";
|
|
2
|
-
export type Driver = SqlDriver | "mongodb";
|
|
2
|
+
export type Driver = SqlDriver | "mongodb" | "redis";
|
|
3
3
|
export type CommandOrigin = "legacy" | "user" | "model" | "system" | "api";
|
|
4
|
+
export type HistoryCategory = "statement" | "introspection" | "management";
|
|
4
5
|
/** Trusted host metadata for one executeCommand call; never part of BatchCommand input. */
|
|
5
6
|
export interface CommandExecutionContext {
|
|
6
7
|
signal?: AbortSignal;
|
|
7
8
|
origin?: CommandOrigin;
|
|
9
|
+
/** Marks host-generated setup or introspection separately from user statements. */
|
|
10
|
+
internal?: boolean;
|
|
8
11
|
}
|
|
9
12
|
export type CredentialAccess = "read" | "write";
|
|
10
13
|
export type CredentialOperation = "connect" | "query" | "inspect" | "plan" | "exec" | "apply" | "transaction.commit";
|
|
@@ -78,6 +81,8 @@ export interface HistoryEntry {
|
|
|
78
81
|
session_id: string;
|
|
79
82
|
actor_id: string;
|
|
80
83
|
origin: CommandOrigin;
|
|
84
|
+
category: HistoryCategory;
|
|
85
|
+
internal: boolean;
|
|
81
86
|
command: string;
|
|
82
87
|
sql: string | null;
|
|
83
88
|
target?: string | null;
|
|
@@ -87,6 +92,11 @@ export interface HistoryEntry {
|
|
|
87
92
|
success: boolean;
|
|
88
93
|
error_code: string | null;
|
|
89
94
|
}
|
|
95
|
+
export interface StateQLSnapshotOptions {
|
|
96
|
+
historyLimit?: number;
|
|
97
|
+
historyCategory?: HistoryCategory;
|
|
98
|
+
historyInternal?: boolean;
|
|
99
|
+
}
|
|
90
100
|
export interface StateQLSnapshot {
|
|
91
101
|
session: {
|
|
92
102
|
session_id: string;
|
|
@@ -96,6 +106,9 @@ export interface StateQLSnapshot {
|
|
|
96
106
|
actor_id: string;
|
|
97
107
|
connection: {
|
|
98
108
|
connection_id: string;
|
|
109
|
+
/** Generated display identity; optional for older snapshot producers. */
|
|
110
|
+
alias?: string;
|
|
111
|
+
display_alias?: string;
|
|
99
112
|
name: string;
|
|
100
113
|
status: "connected";
|
|
101
114
|
driver: Driver;
|
|
@@ -195,12 +208,49 @@ export interface MongoWriteOutcome {
|
|
|
195
208
|
upserted_id?: unknown;
|
|
196
209
|
deleted_count?: number;
|
|
197
210
|
}
|
|
211
|
+
export interface RedisCommand {
|
|
212
|
+
command: string;
|
|
213
|
+
args?: string[];
|
|
214
|
+
}
|
|
215
|
+
export interface RedisWriteOutcome extends MongoWriteOutcome {
|
|
216
|
+
result: string | number | null;
|
|
217
|
+
}
|
|
218
|
+
export type CatalogObjectKind = "table" | "view" | "collection" | "function" | "trigger" | "enum" | "key";
|
|
219
|
+
export interface CatalogObject {
|
|
220
|
+
kind: CatalogObjectKind;
|
|
221
|
+
schema?: string;
|
|
222
|
+
name: string;
|
|
223
|
+
/** Stable database-native overload/object identity when name alone is ambiguous. */
|
|
224
|
+
identity?: string;
|
|
225
|
+
[key: string]: unknown;
|
|
226
|
+
}
|
|
227
|
+
export interface ListObjectsFilter {
|
|
228
|
+
kind?: CatalogObjectKind;
|
|
229
|
+
schema?: string;
|
|
230
|
+
search?: string;
|
|
231
|
+
/** Numeric for SQL/MongoDB; Redis uses its opaque SCAN cursor string. */
|
|
232
|
+
offset?: number | string;
|
|
233
|
+
limit?: number;
|
|
234
|
+
}
|
|
235
|
+
export interface ListObjectsData {
|
|
236
|
+
objects: CatalogObject[];
|
|
237
|
+
next_offset: number | string | null;
|
|
238
|
+
supported_kinds: CatalogObjectKind[];
|
|
239
|
+
}
|
|
240
|
+
export interface DescribeObjectData {
|
|
241
|
+
object: CatalogObject;
|
|
242
|
+
definition?: string | Record<string, unknown> | unknown[] | null;
|
|
243
|
+
[key: string]: unknown;
|
|
244
|
+
}
|
|
198
245
|
export interface ExecutionOptions {
|
|
199
246
|
timeoutMs?: number;
|
|
200
247
|
signal?: AbortSignal;
|
|
201
248
|
}
|
|
202
249
|
export interface HistoryOptions {
|
|
203
250
|
origin?: CommandOrigin;
|
|
251
|
+
category?: HistoryCategory;
|
|
252
|
+
internal?: boolean;
|
|
253
|
+
offset?: number;
|
|
204
254
|
}
|
|
205
255
|
export interface StateQLOptions extends ExecutionOptions {
|
|
206
256
|
home?: string;
|
|
@@ -256,6 +306,21 @@ export interface ProfileOptions {
|
|
|
256
306
|
secretEnv?: string;
|
|
257
307
|
credentialRef?: string;
|
|
258
308
|
}
|
|
309
|
+
export interface ProfileUpdateOptions {
|
|
310
|
+
target?: string | null;
|
|
311
|
+
secretEnv?: string | null;
|
|
312
|
+
credentialRef?: string | null;
|
|
313
|
+
readOnly?: boolean;
|
|
314
|
+
}
|
|
315
|
+
export interface RedisQueryOptions extends ExecutionOptions {
|
|
316
|
+
cache?: "auto" | "bypass" | "require";
|
|
317
|
+
}
|
|
318
|
+
export interface RedisExecOptions extends ExecutionOptions {
|
|
319
|
+
replay?: boolean;
|
|
320
|
+
idempotencyKey?: string;
|
|
321
|
+
}
|
|
322
|
+
export interface RedisPlanOptions extends ExecutionOptions {
|
|
323
|
+
}
|
|
259
324
|
export interface RowsOptions {
|
|
260
325
|
offset?: number;
|
|
261
326
|
limit?: number;
|
|
@@ -269,7 +334,7 @@ export interface MongoPlanOptions extends ExecutionOptions {
|
|
|
269
334
|
allowUnbounded?: boolean;
|
|
270
335
|
allowDestructive?: boolean;
|
|
271
336
|
}
|
|
272
|
-
export type BatchCommandName = "connect" | "disconnect" | "status" | "profile.add" | "profile.list" | "profile.show" | "profile.remove" | "session.start" | "session.list" | "session.show" | "session.summary" | "session.close" | "query" | "filter" | "exec" | "show" | "rows" | "count" | "columns" | "alias.set" | "inspect" | "transaction.begin" | "transaction.status" | "transaction.commit" | "transaction.rollback" | "plan" | "mongo.query" | "mongo.exec" | "mongo.plan" | "apply" | "history" | "receipt" | "doctor" | "purge" | "capabilities";
|
|
337
|
+
export type BatchCommandName = "connect" | "disconnect" | "status" | "profile.add" | "profile.list" | "profile.show" | "profile.remove" | "profile.update" | "session.start" | "session.list" | "session.show" | "session.summary" | "session.close" | "query" | "filter" | "exec" | "show" | "rows" | "count" | "columns" | "alias.set" | "inspect" | "transaction.begin" | "transaction.status" | "transaction.commit" | "transaction.rollback" | "plan" | "mongo.query" | "mongo.exec" | "mongo.plan" | "redis.query" | "redis.exec" | "redis.plan" | "objects.list" | "object.describe" | "apply" | "history" | "receipt" | "doctor" | "purge" | "capabilities";
|
|
273
338
|
export interface BatchCommand {
|
|
274
339
|
command: BatchCommandName;
|
|
275
340
|
target?: string;
|
|
@@ -282,6 +347,8 @@ export interface BatchCommand {
|
|
|
282
347
|
table?: string;
|
|
283
348
|
params?: SqlParameters;
|
|
284
349
|
mongo?: MongoReadCommand | MongoWriteCommand;
|
|
350
|
+
redis?: RedisCommand;
|
|
351
|
+
object?: CatalogObject;
|
|
285
352
|
cache?: "auto" | "bypass" | "require";
|
|
286
353
|
read_only?: boolean;
|
|
287
354
|
secret_env?: string;
|
|
@@ -292,11 +359,14 @@ export interface BatchCommand {
|
|
|
292
359
|
allow_unbounded?: boolean;
|
|
293
360
|
allow_destructive?: boolean;
|
|
294
361
|
offset?: number;
|
|
362
|
+
cursor?: string;
|
|
295
363
|
limit?: number;
|
|
296
364
|
isolation?: string;
|
|
297
365
|
timeout_ms?: number;
|
|
298
366
|
/** Retrieval filter for the history command; does not attribute this command. */
|
|
299
367
|
history_origin?: CommandOrigin;
|
|
368
|
+
history_category?: HistoryCategory;
|
|
369
|
+
history_internal?: boolean;
|
|
300
370
|
scope?: "expired" | "results" | "history" | "all";
|
|
301
371
|
}
|
|
302
372
|
export interface BatchOptions {
|
|
@@ -312,6 +382,9 @@ export type Row = Record<string, unknown>;
|
|
|
312
382
|
/** Data returned by the stable, non-dynamic StateQL public methods. */
|
|
313
383
|
export interface ConnectionData {
|
|
314
384
|
connection_id: string;
|
|
385
|
+
/** Persistent generated display identity; connection_id remains canonical. */
|
|
386
|
+
alias: string;
|
|
387
|
+
display_alias: string;
|
|
315
388
|
driver: Driver;
|
|
316
389
|
database: string;
|
|
317
390
|
name: string;
|
|
@@ -416,6 +489,9 @@ export interface SessionSummaryData {
|
|
|
416
489
|
}
|
|
417
490
|
export interface ResultData {
|
|
418
491
|
result_id: string;
|
|
492
|
+
alias: string;
|
|
493
|
+
/** Canonical generated alias; remains stable even when alias is an explicit caller alias. */
|
|
494
|
+
display_alias: string;
|
|
419
495
|
rows: number;
|
|
420
496
|
columns: Column[];
|
|
421
497
|
preview: Row[];
|
|
@@ -428,6 +504,7 @@ export interface ResultData {
|
|
|
428
504
|
mode: string;
|
|
429
505
|
expires_at: string;
|
|
430
506
|
};
|
|
507
|
+
next_cursor?: string | null;
|
|
431
508
|
}
|
|
432
509
|
export interface RowsData {
|
|
433
510
|
result_id: string;
|
|
@@ -468,7 +545,7 @@ export interface OperationData {
|
|
|
468
545
|
state_version_before: string;
|
|
469
546
|
state_version_after: string | null;
|
|
470
547
|
replay_of?: string;
|
|
471
|
-
outcome?: MongoWriteOutcome;
|
|
548
|
+
outcome?: MongoWriteOutcome | RedisWriteOutcome;
|
|
472
549
|
}
|
|
473
550
|
export interface ExecData extends OperationData {
|
|
474
551
|
duplicate?: boolean;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fadhilp/stateql",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.1",
|
|
4
4
|
"description": "Stateful, agent-oriented database CLI for safe result reuse",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
],
|
|
26
26
|
"scripts": {
|
|
27
27
|
"build": "tsc -p tsconfig.json",
|
|
28
|
-
"test": "npm run build && node --test dist/test/cli.test.js dist/test/credential.test.js dist/test/mongodb.test.js dist/test/mysql.test.js dist/test/postgres.test.js dist/test/query.test.js dist/test/sqlite.test.js dist/test/store.test.js dist/test/terminal.test.js dist/test/transaction.test.js dist/test/write.test.js dist/test/panel.test.js",
|
|
28
|
+
"test": "npm run build && node --test dist/test/cli.test.js dist/test/credential.test.js dist/test/mongodb.test.js dist/test/mysql.test.js dist/test/postgres.test.js dist/test/query.test.js dist/test/redis.test.js dist/test/sqlite.test.js dist/test/store.test.js dist/test/terminal.test.js dist/test/transaction.test.js dist/test/write.test.js dist/test/panel.test.js",
|
|
29
29
|
"release": "npm version",
|
|
30
30
|
"version": "npm install",
|
|
31
31
|
"prepack": "npm test"
|
|
@@ -37,6 +37,7 @@
|
|
|
37
37
|
"postgresql",
|
|
38
38
|
"cli",
|
|
39
39
|
"agents",
|
|
40
|
+
"redis",
|
|
40
41
|
"sql"
|
|
41
42
|
],
|
|
42
43
|
"license": "MIT",
|
|
@@ -44,6 +45,7 @@
|
|
|
44
45
|
"node": ">=22.16"
|
|
45
46
|
},
|
|
46
47
|
"dependencies": {
|
|
48
|
+
"@redis/client": "^5.12.1",
|
|
47
49
|
"mongodb": "^6.21.0",
|
|
48
50
|
"mysql2": "^3.23.1",
|
|
49
51
|
"node-sql-parser": "^5.4.0",
|