@fadhilp/stateql 0.9.0 → 0.10.0

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;
@@ -46,6 +46,7 @@ export interface ResultRecord {
46
46
  state_confidence: StateConfidence;
47
47
  expires_at: string;
48
48
  created_at: string;
49
+ alias?: string;
49
50
  }
50
51
  export interface OperationRecord {
51
52
  id: string;
@@ -101,6 +102,8 @@ export interface HistoryRecord {
101
102
  session_id: string;
102
103
  actor_id: string;
103
104
  origin: CommandOrigin;
105
+ category: HistoryCategory;
106
+ internal: number;
104
107
  command: string;
105
108
  sql: string | null;
106
109
  target: string | null;
@@ -142,6 +145,13 @@ export declare class StateStore {
142
145
  credentialRef?: string;
143
146
  readOnly: boolean;
144
147
  }): ProfileRecord;
148
+ updateProfile(input: {
149
+ name: string;
150
+ target: string | null;
151
+ secretEnv: string | null;
152
+ credentialRef: string | null;
153
+ readOnly: boolean;
154
+ }): ProfileRecord | undefined;
145
155
  getProfile(name: string): ProfileRecord | undefined;
146
156
  listProfiles(): ProfileRecord[];
147
157
  removeProfile(name: string): boolean;
@@ -178,6 +188,9 @@ export declare class StateStore {
178
188
  resultRows(result: ResultRecord): Row[];
179
189
  resultColumns(result: ResultRecord): Column[];
180
190
  setAlias(sessionId: string, name: string, resultId: string): void;
191
+ generatedAlias(resultId: string): string;
192
+ private allocateGeneratedAlias;
193
+ private backfillGeneratedAliases;
181
194
  saveOperation(input: {
182
195
  sessionId: string;
183
196
  actorId: string;
@@ -277,6 +290,8 @@ export declare class StateStore {
277
290
  sessionId: string;
278
291
  actorId: string;
279
292
  origin?: CommandOrigin;
293
+ category?: HistoryCategory;
294
+ internal?: boolean;
280
295
  command: string;
281
296
  sql?: string;
282
297
  target?: string;
@@ -287,7 +302,12 @@ export declare class StateStore {
287
302
  errorCode?: string;
288
303
  id?: string;
289
304
  }): HistoryRecord;
290
- history(sessionId: string, limit: number, origin?: CommandOrigin): HistoryRecord[];
305
+ history(sessionId: string, limit: number, input?: CommandOrigin | {
306
+ origin?: CommandOrigin;
307
+ category?: HistoryCategory;
308
+ internal?: boolean;
309
+ offset?: number;
310
+ }): HistoryRecord[];
291
311
  recentOperations(sessionId: string, limit: number): OperationRecord[];
292
312
  knownResults(sessionId: string, limit: number): Array<ResultRecord & {
293
313
  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,7 @@ 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.backfillGeneratedAliases();
38
40
  this.recoverStaleCommittingTransactions();
39
41
  this.deleteExpiredData();
40
42
  }
@@ -240,6 +242,12 @@ export class StateStore {
240
242
  .run(input.name, input.target ?? null, input.secretEnv ?? null, input.credentialRef ?? null, input.readOnly ? 1 : 0, timestamp, timestamp);
241
243
  return this.getProfile(input.name);
242
244
  }
245
+ updateProfile(input) {
246
+ const result = this.db.prepare(`UPDATE profiles
247
+ SET target = ?, secret_env = ?, credential_ref = ?, read_only = ?, updated_at = ?
248
+ WHERE name = ?`).run(input.target, input.secretEnv, input.credentialRef, input.readOnly ? 1 : 0, this.now().toISOString(), input.name);
249
+ return Number(result.changes) === 1 ? this.getProfile(input.name) : undefined;
250
+ }
243
251
  getProfile(name) {
244
252
  return this.db
245
253
  .prepare("SELECT * FROM profiles WHERE name = ?")
@@ -341,6 +349,7 @@ export class StateStore {
341
349
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
342
350
  .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
351
  this.enforceResultQuota(id);
352
+ this.allocateGeneratedAlias(input.sessionId, id);
344
353
  const result = this.getResult(id);
345
354
  this.db.exec("COMMIT");
346
355
  return result;
@@ -351,15 +360,18 @@ export class StateStore {
351
360
  }
352
361
  }
353
362
  findResult(fingerprint) {
354
- return this.db
363
+ const result = this.db
355
364
  .prepare(`SELECT * FROM results
356
365
  WHERE fingerprint = ?
357
366
  ORDER BY created_at DESC
358
367
  LIMIT 1`)
359
368
  .get(fingerprint);
369
+ if (result)
370
+ result.alias = this.generatedAlias(result.id);
371
+ return result;
360
372
  }
361
373
  getResult(idOrAlias, sessionId) {
362
- return this.db
374
+ const result = this.db
363
375
  .prepare(`SELECT results.*
364
376
  FROM results
365
377
  LEFT JOIN aliases
@@ -369,6 +381,9 @@ export class StateStore {
369
381
  AND (? IS NULL OR results.session_id = ?)
370
382
  LIMIT 1`)
371
383
  .get(idOrAlias, idOrAlias, sessionId ?? null, sessionId ?? null);
384
+ if (result)
385
+ result.alias = this.generatedAlias(result.id);
386
+ return result;
372
387
  }
373
388
  resultRows(result) {
374
389
  return parseJson(result.rows_json, `result "${result.id}" rows`, isRows);
@@ -377,12 +392,55 @@ export class StateStore {
377
392
  return parseJson(result.columns_json, `result "${result.id}" columns`, isColumns);
378
393
  }
379
394
  setAlias(sessionId, name, resultId) {
395
+ const existing = this.db.prepare("SELECT result_id, generated FROM aliases WHERE session_id = ? AND name = ?").get(sessionId, name);
396
+ if (existing?.generated) {
397
+ if (existing.result_id === resultId)
398
+ return;
399
+ throw new StateQLError("INVALID_COMMAND", "Generated result aliases cannot be reassigned.");
400
+ }
380
401
  this.db
381
- .prepare(`INSERT INTO aliases(session_id, name, result_id)
382
- VALUES (?, ?, ?)
402
+ .prepare(`INSERT INTO aliases(session_id, name, result_id, generated)
403
+ VALUES (?, ?, ?, 0)
383
404
  ON CONFLICT(session_id, name) DO UPDATE SET result_id = excluded.result_id`)
384
405
  .run(sessionId, name, resultId);
385
406
  }
407
+ generatedAlias(resultId) {
408
+ const row = this.db.prepare("SELECT name FROM aliases WHERE result_id = ? AND generated = 1").get(resultId);
409
+ if (!row)
410
+ throw new Error(`Result "${resultId}" has no generated alias.`);
411
+ return row.name;
412
+ }
413
+ allocateGeneratedAlias(sessionId, resultId) {
414
+ const existing = this.db.prepare("SELECT name FROM aliases WHERE result_id = ? AND generated = 1").get(resultId);
415
+ if (existing)
416
+ return existing.name;
417
+ for (let attempt = 0; attempt < 64; attempt++) {
418
+ const name = randomBase32Alias();
419
+ this.db.prepare("INSERT OR IGNORE INTO aliases(session_id, name, result_id, generated) VALUES (?, ?, ?, 1)").run(sessionId, name, resultId);
420
+ const allocated = this.db.prepare("SELECT name FROM aliases WHERE result_id = ? AND generated = 1").get(resultId);
421
+ if (allocated)
422
+ return allocated.name;
423
+ }
424
+ throw new Error("Could not allocate a unique result alias.");
425
+ }
426
+ backfillGeneratedAliases() {
427
+ const results = this.db.prepare(`SELECT results.id, results.session_id
428
+ FROM results
429
+ LEFT JOIN aliases ON aliases.result_id = results.id AND aliases.generated = 1
430
+ WHERE aliases.result_id IS NULL`).all();
431
+ if (!results.length)
432
+ return;
433
+ this.db.exec("BEGIN IMMEDIATE");
434
+ try {
435
+ for (const result of results)
436
+ this.allocateGeneratedAlias(result.session_id, result.id);
437
+ this.db.exec("COMMIT");
438
+ }
439
+ catch (error) {
440
+ this.db.exec("ROLLBACK");
441
+ throw error;
442
+ }
443
+ }
386
444
  saveOperation(input) {
387
445
  const id = this.nextId("op");
388
446
  this.db
@@ -797,10 +855,10 @@ export class StateStore {
797
855
  const id = input.id ?? this.nextId("cmd");
798
856
  this.db
799
857
  .prepare(`INSERT INTO history
800
- (id, timestamp, session_id, actor_id, origin, command, sql, target, handle,
858
+ (id, timestamp, session_id, actor_id, origin, category, internal, command, sql, target, handle,
801
859
  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);
860
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
861
+ .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
862
  this.db
805
863
  .prepare(`DELETE FROM history
806
864
  WHERE rowid IN (
@@ -814,13 +872,29 @@ export class StateStore {
814
872
  .prepare("SELECT * FROM history WHERE id = ?")
815
873
  .get(id);
816
874
  }
817
- history(sessionId, limit, origin) {
875
+ history(sessionId, limit, input = {}) {
876
+ const options = typeof input === "string" ? { origin: input } : input;
877
+ const predicates = ["session_id = ?"];
878
+ const values = [sessionId];
879
+ if (options.origin !== undefined) {
880
+ predicates.push("origin = ?");
881
+ values.push(options.origin);
882
+ }
883
+ if (options.category !== undefined) {
884
+ predicates.push("category = ?");
885
+ values.push(options.category);
886
+ }
887
+ if (options.internal !== undefined) {
888
+ predicates.push("internal = ?");
889
+ values.push(options.internal ? 1 : 0);
890
+ }
891
+ values.push(limit, options.offset ?? 0);
818
892
  return this.db
819
893
  .prepare(`SELECT * FROM history
820
- WHERE session_id = ?${origin === undefined ? "" : " AND origin = ?"}
894
+ WHERE ${predicates.join(" AND ")}
821
895
  ORDER BY rowid DESC
822
- LIMIT ?`)
823
- .all(...(origin === undefined ? [sessionId, limit] : [sessionId, origin, limit]));
896
+ LIMIT ? OFFSET ?`)
897
+ .all(...values);
824
898
  }
825
899
  recentOperations(sessionId, limit) {
826
900
  return this.db
@@ -837,6 +911,7 @@ export class StateStore {
837
911
  LEFT JOIN aliases
838
912
  ON aliases.result_id = results.id
839
913
  AND aliases.session_id = results.session_id
914
+ AND aliases.generated = 1
840
915
  WHERE results.session_id = ?
841
916
  ORDER BY results.created_at DESC
842
917
  LIMIT ?`)
@@ -946,12 +1021,13 @@ export class StateStore {
946
1021
  while (this.resultBytes() > this.maxStateBytes) {
947
1022
  const candidate = this.db.prepare(`SELECT id FROM results
948
1023
  WHERE id <> ? AND NOT EXISTS (
949
- SELECT 1 FROM aliases WHERE aliases.result_id = results.id
1024
+ SELECT 1 FROM aliases WHERE aliases.result_id = results.id AND aliases.generated = 0
950
1025
  )
951
1026
  ORDER BY created_at, rowid LIMIT 1`).get(protectedId);
952
1027
  if (!candidate) {
953
1028
  throw new StateQLError("STATE_QUOTA_EXCEEDED", `Stored results exceed the ${this.maxStateBytes}-byte state quota.`, { suggestedAction: "Purge results or increase maxStateBytes." });
954
1029
  }
1030
+ this.db.prepare("DELETE FROM aliases WHERE result_id = ? AND generated = 1").run(candidate.id);
955
1031
  this.db.prepare("DELETE FROM results WHERE id = ?").run(candidate.id);
956
1032
  }
957
1033
  }
@@ -1019,6 +1095,10 @@ export class StateStore {
1019
1095
  function outcomeJson(outcome) {
1020
1096
  return outcome === undefined ? null : JSON.stringify(toJsonSafe(outcome));
1021
1097
  }
1098
+ function randomBase32Alias() {
1099
+ const alphabet = "abcdefghijklmnopqrstuvwxyz234567";
1100
+ return [...randomBytes(10)].map((value) => alphabet[value & 31]).join("");
1101
+ }
1022
1102
  function boundedHistorySql(sql) {
1023
1103
  if (sql === undefined)
1024
1104
  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;
@@ -195,12 +205,49 @@ export interface MongoWriteOutcome {
195
205
  upserted_id?: unknown;
196
206
  deleted_count?: number;
197
207
  }
208
+ export interface RedisCommand {
209
+ command: string;
210
+ args?: string[];
211
+ }
212
+ export interface RedisWriteOutcome extends MongoWriteOutcome {
213
+ result: string | number | null;
214
+ }
215
+ export type CatalogObjectKind = "table" | "view" | "collection" | "function" | "trigger" | "enum" | "key";
216
+ export interface CatalogObject {
217
+ kind: CatalogObjectKind;
218
+ schema?: string;
219
+ name: string;
220
+ /** Stable database-native overload/object identity when name alone is ambiguous. */
221
+ identity?: string;
222
+ [key: string]: unknown;
223
+ }
224
+ export interface ListObjectsFilter {
225
+ kind?: CatalogObjectKind;
226
+ schema?: string;
227
+ search?: string;
228
+ /** Numeric for SQL/MongoDB; Redis uses its opaque SCAN cursor string. */
229
+ offset?: number | string;
230
+ limit?: number;
231
+ }
232
+ export interface ListObjectsData {
233
+ objects: CatalogObject[];
234
+ next_offset: number | string | null;
235
+ supported_kinds: CatalogObjectKind[];
236
+ }
237
+ export interface DescribeObjectData {
238
+ object: CatalogObject;
239
+ definition?: string | Record<string, unknown> | unknown[] | null;
240
+ [key: string]: unknown;
241
+ }
198
242
  export interface ExecutionOptions {
199
243
  timeoutMs?: number;
200
244
  signal?: AbortSignal;
201
245
  }
202
246
  export interface HistoryOptions {
203
247
  origin?: CommandOrigin;
248
+ category?: HistoryCategory;
249
+ internal?: boolean;
250
+ offset?: number;
204
251
  }
205
252
  export interface StateQLOptions extends ExecutionOptions {
206
253
  home?: string;
@@ -256,6 +303,21 @@ export interface ProfileOptions {
256
303
  secretEnv?: string;
257
304
  credentialRef?: string;
258
305
  }
306
+ export interface ProfileUpdateOptions {
307
+ target?: string | null;
308
+ secretEnv?: string | null;
309
+ credentialRef?: string | null;
310
+ readOnly?: boolean;
311
+ }
312
+ export interface RedisQueryOptions extends ExecutionOptions {
313
+ cache?: "auto" | "bypass" | "require";
314
+ }
315
+ export interface RedisExecOptions extends ExecutionOptions {
316
+ replay?: boolean;
317
+ idempotencyKey?: string;
318
+ }
319
+ export interface RedisPlanOptions extends ExecutionOptions {
320
+ }
259
321
  export interface RowsOptions {
260
322
  offset?: number;
261
323
  limit?: number;
@@ -269,7 +331,7 @@ export interface MongoPlanOptions extends ExecutionOptions {
269
331
  allowUnbounded?: boolean;
270
332
  allowDestructive?: boolean;
271
333
  }
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";
334
+ 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
335
  export interface BatchCommand {
274
336
  command: BatchCommandName;
275
337
  target?: string;
@@ -282,6 +344,8 @@ export interface BatchCommand {
282
344
  table?: string;
283
345
  params?: SqlParameters;
284
346
  mongo?: MongoReadCommand | MongoWriteCommand;
347
+ redis?: RedisCommand;
348
+ object?: CatalogObject;
285
349
  cache?: "auto" | "bypass" | "require";
286
350
  read_only?: boolean;
287
351
  secret_env?: string;
@@ -292,11 +356,14 @@ export interface BatchCommand {
292
356
  allow_unbounded?: boolean;
293
357
  allow_destructive?: boolean;
294
358
  offset?: number;
359
+ cursor?: string;
295
360
  limit?: number;
296
361
  isolation?: string;
297
362
  timeout_ms?: number;
298
363
  /** Retrieval filter for the history command; does not attribute this command. */
299
364
  history_origin?: CommandOrigin;
365
+ history_category?: HistoryCategory;
366
+ history_internal?: boolean;
300
367
  scope?: "expired" | "results" | "history" | "all";
301
368
  }
302
369
  export interface BatchOptions {
@@ -416,6 +483,9 @@ export interface SessionSummaryData {
416
483
  }
417
484
  export interface ResultData {
418
485
  result_id: string;
486
+ alias: string;
487
+ /** Canonical generated alias; remains stable even when alias is an explicit caller alias. */
488
+ display_alias: string;
419
489
  rows: number;
420
490
  columns: Column[];
421
491
  preview: Row[];
@@ -428,6 +498,7 @@ export interface ResultData {
428
498
  mode: string;
429
499
  expires_at: string;
430
500
  };
501
+ next_cursor?: string | null;
431
502
  }
432
503
  export interface RowsData {
433
504
  result_id: string;
@@ -468,7 +539,7 @@ export interface OperationData {
468
539
  state_version_before: string;
469
540
  state_version_after: string | null;
470
541
  replay_of?: string;
471
- outcome?: MongoWriteOutcome;
542
+ outcome?: MongoWriteOutcome | RedisWriteOutcome;
472
543
  }
473
544
  export interface ExecData extends OperationData {
474
545
  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.0",
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",