@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.
@@ -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, origin?: CommandOrigin): HistoryRecord[];
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
- return this.db
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
- return this.db
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, origin) {
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 session_id = ?${origin === undefined ? "" : " AND origin = ?"}
918
+ WHERE ${predicates.join(" AND ")}
821
919
  ORDER BY rowid DESC
822
- LIMIT ?`)
823
- .all(...(origin === undefined ? [sessionId, limit] : [sessionId, origin, limit]));
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[];
@@ -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
+ }
@@ -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.9.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",