@fadhilp/stateql 0.10.0 → 0.11.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.
package/dist/src/store.js CHANGED
@@ -36,6 +36,7 @@ export class StateStore {
36
36
  this.db.exec("PRAGMA busy_timeout = 5000");
37
37
  this.db.exec("PRAGMA foreign_keys = ON");
38
38
  runMigrations(this.db, this.now);
39
+ this.backfillConnectionAliases();
39
40
  this.backfillGeneratedAliases();
40
41
  this.recoverStaleCommittingTransactions();
41
42
  this.deleteExpiredData();
@@ -52,14 +53,22 @@ export class StateStore {
52
53
  this.closed = true;
53
54
  this.db.close();
54
55
  }
55
- nextId(prefix) {
56
- this.db
57
- .prepare("INSERT OR IGNORE INTO counters(prefix, value) VALUES (?, 0)")
58
- .run(prefix);
59
- const row = this.db
60
- .prepare("UPDATE counters SET value = value + 1 WHERE prefix = ? RETURNING value")
61
- .get(prefix);
62
- return `${prefix}_${row.value}`;
56
+ randomId(prefix) {
57
+ return `${prefix}_${randomBase32Id()}`;
58
+ }
59
+ insertWithRandomId(prefix, table, insert) {
60
+ for (let attempt = 0; attempt < 64; attempt++) {
61
+ const id = this.randomId(prefix);
62
+ try {
63
+ insert(id);
64
+ return id;
65
+ }
66
+ catch (error) {
67
+ if (!isIdCollision(error, table))
68
+ throw error;
69
+ }
70
+ }
71
+ throw new Error(`Could not allocate a unique ${prefix} ID.`);
63
72
  }
64
73
  ensureSession(name = "default") {
65
74
  const existing = this.getSessionByName(name);
@@ -86,12 +95,13 @@ export class StateStore {
86
95
  .get(name);
87
96
  const created = !row;
88
97
  if (!row) {
89
- const id = this.nextId("s");
90
- this.db
91
- .prepare(`INSERT INTO sessions
92
- (id, name, status, created_at, updated_at)
93
- VALUES (?, ?, 'active', ?, ?)`)
94
- .run(id, name, timestamp, timestamp);
98
+ const id = this.insertWithRandomId("s", "sessions", (candidate) => {
99
+ this.db
100
+ .prepare(`INSERT INTO sessions
101
+ (id, name, status, created_at, updated_at)
102
+ VALUES (?, ?, 'active', ?, ?)`)
103
+ .run(candidate, name, timestamp, timestamp);
104
+ });
95
105
  row = { id, status: "active" };
96
106
  }
97
107
  else if (row.status !== "active") {
@@ -237,15 +247,15 @@ export class StateStore {
237
247
  const timestamp = this.now().toISOString();
238
248
  this.db
239
249
  .prepare(`INSERT INTO profiles
240
- (name, target, secret_env, credential_ref, read_only, created_at, updated_at)
241
- VALUES (?, ?, ?, ?, ?, ?, ?)`)
242
- .run(input.name, input.target ?? null, input.secretEnv ?? null, input.credentialRef ?? null, input.readOnly ? 1 : 0, timestamp, timestamp);
250
+ (name, target, secret_env, credential_ref, password_ref, read_only, created_at, updated_at)
251
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`)
252
+ .run(input.name, input.target ?? null, input.secretEnv ?? null, input.credentialRef ?? null, input.passwordRef ?? null, input.readOnly ? 1 : 0, timestamp, timestamp);
243
253
  return this.getProfile(input.name);
244
254
  }
245
255
  updateProfile(input) {
246
256
  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);
257
+ SET target = ?, secret_env = ?, credential_ref = ?, password_ref = ?, read_only = ?, updated_at = ?
258
+ WHERE name = ?`).run(input.target, input.secretEnv, input.credentialRef, input.passwordRef, input.readOnly ? 1 : 0, this.now().toISOString(), input.name);
249
259
  return Number(result.changes) === 1 ? this.getProfile(input.name) : undefined;
250
260
  }
251
261
  getProfile(name) {
@@ -277,14 +287,16 @@ export class StateStore {
277
287
  this.db.exec("COMMIT");
278
288
  return undefined;
279
289
  }
280
- const id = this.nextId("conn");
281
290
  const timestamp = this.now().toISOString();
282
- this.db
283
- .prepare(`INSERT INTO connections
284
- (id, session_id, name, driver, database_name, source, secret_env,
285
- credential_ref, read_only, version, created_at)
286
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 0, ?)`)
287
- .run(id, input.sessionId, input.name, input.driver, input.databaseName, input.source, input.secretEnv ?? null, input.credentialRef ?? null, input.readOnly ? 1 : 0, timestamp);
291
+ const id = this.insertWithRandomId("conn", "connections", (candidate) => {
292
+ this.db
293
+ .prepare(`INSERT INTO connections
294
+ (id, session_id, name, driver, database_name, source, secret_env,
295
+ credential_ref, password_ref, read_only, version, created_at)
296
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0, ?)`)
297
+ .run(candidate, input.sessionId, input.name, input.driver, input.databaseName, input.source, input.secretEnv ?? null, input.credentialRef ?? null, input.passwordRef ?? null, input.readOnly ? 1 : 0, timestamp);
298
+ });
299
+ this.allocateConnectionAlias(id);
288
300
  this.db
289
301
  .prepare(`UPDATE sessions
290
302
  SET active_connection_id = ?, updated_at = ?
@@ -298,6 +310,28 @@ export class StateStore {
298
310
  throw error;
299
311
  }
300
312
  }
313
+ allocateConnectionAlias(connectionId) {
314
+ for (let attempt = 0; attempt < 64; attempt++) {
315
+ this.db.prepare("UPDATE OR IGNORE connections SET alias = ? WHERE id = ? AND alias IS NULL").run(randomBase32Alias(), connectionId);
316
+ const connection = this.getConnection(connectionId);
317
+ if (connection?.alias)
318
+ return connection.alias;
319
+ }
320
+ throw new Error("Could not allocate a unique connection alias.");
321
+ }
322
+ backfillConnectionAliases() {
323
+ this.db.exec("BEGIN IMMEDIATE");
324
+ try {
325
+ const connections = this.db.prepare("SELECT id FROM connections WHERE alias IS NULL").all();
326
+ for (const connection of connections)
327
+ this.allocateConnectionAlias(connection.id);
328
+ this.db.exec("COMMIT");
329
+ }
330
+ catch (error) {
331
+ this.db.exec("ROLLBACK");
332
+ throw error;
333
+ }
334
+ }
301
335
  getConnection(id) {
302
336
  return this.db
303
337
  .prepare("SELECT * FROM connections WHERE id = ?")
@@ -338,16 +372,17 @@ export class StateStore {
338
372
  return `sv_${row.version}`;
339
373
  }
340
374
  saveResult(input) {
341
- const id = this.nextId("q");
342
375
  this.db.exec("BEGIN IMMEDIATE");
343
376
  try {
344
- this.db
345
- .prepare(`INSERT INTO results
346
- (id, session_id, connection_id, fingerprint, sql, parameters,
347
- rows_json, columns_json, row_count, state_version, state_signature,
348
- state_confidence, expires_at, created_at)
349
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
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());
377
+ const id = this.insertWithRandomId("q", "results", (candidate) => {
378
+ this.db
379
+ .prepare(`INSERT INTO results
380
+ (id, session_id, connection_id, fingerprint, sql, parameters,
381
+ rows_json, columns_json, row_count, state_version, state_signature,
382
+ state_confidence, expires_at, created_at)
383
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
384
+ .run(candidate, 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());
385
+ });
351
386
  this.enforceResultQuota(id);
352
387
  this.allocateGeneratedAlias(input.sessionId, id);
353
388
  const result = this.getResult(id);
@@ -442,14 +477,15 @@ export class StateStore {
442
477
  }
443
478
  }
444
479
  saveOperation(input) {
445
- const id = this.nextId("op");
446
- this.db
447
- .prepare(`INSERT INTO operations
448
- (id, session_id, actor_id, connection_id, fingerprint, sql, parameters,
449
- statement_type, affected_rows, status, transaction_id, replay_of,
450
- idempotency_key, state_version_before, state_version_after, created_at)
451
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
452
- .run(id, input.sessionId, input.actorId, input.connectionId, input.fingerprint, input.sql, JSON.stringify(toJsonSafe(input.parameters)), input.statementType, input.affectedRows ?? null, input.status, input.transactionId ?? null, input.replayOf ?? null, input.idempotencyKey ?? null, input.stateVersionBefore, input.stateVersionAfter ?? null, this.now().toISOString());
480
+ const id = this.insertWithRandomId("op", "operations", (candidate) => {
481
+ this.db
482
+ .prepare(`INSERT INTO operations
483
+ (id, session_id, actor_id, connection_id, fingerprint, sql, parameters,
484
+ statement_type, affected_rows, status, transaction_id, replay_of,
485
+ idempotency_key, state_version_before, state_version_after, created_at)
486
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
487
+ .run(candidate, input.sessionId, input.actorId, input.connectionId, input.fingerprint, input.sql, JSON.stringify(toJsonSafe(input.parameters)), input.statementType, input.affectedRows ?? null, input.status, input.transactionId ?? null, input.replayOf ?? null, input.idempotencyKey ?? null, input.stateVersionBefore, input.stateVersionAfter ?? null, this.now().toISOString());
488
+ });
453
489
  return this.getOperation(id);
454
490
  }
455
491
  getOperation(id) {
@@ -560,7 +596,6 @@ export class StateStore {
560
596
  .run(operationId);
561
597
  }
562
598
  createTransaction(input) {
563
- const id = this.nextId("tx");
564
599
  const timestamp = this.now().toISOString();
565
600
  this.db.exec("BEGIN IMMEDIATE");
566
601
  try {
@@ -580,12 +615,14 @@ export class StateStore {
580
615
  this.db.exec("COMMIT");
581
616
  return undefined;
582
617
  }
583
- this.db
584
- .prepare(`INSERT INTO transactions
585
- (id, session_id, owner_actor_id, connection_id, state,
586
- isolation_level, start_version, created_at)
587
- VALUES (?, ?, ?, ?, 'active', ?, ?, ?)`)
588
- .run(id, input.sessionId, input.actorId, input.connectionId, input.isolation, `sv_${eligible.version}`, timestamp);
618
+ const id = this.insertWithRandomId("tx", "transactions", (candidate) => {
619
+ this.db
620
+ .prepare(`INSERT INTO transactions
621
+ (id, session_id, owner_actor_id, connection_id, state,
622
+ isolation_level, start_version, created_at)
623
+ VALUES (?, ?, ?, ?, 'active', ?, ?, ?)`)
624
+ .run(candidate, input.sessionId, input.actorId, input.connectionId, input.isolation, `sv_${eligible.version}`, timestamp);
625
+ });
589
626
  this.db
590
627
  .prepare(`UPDATE sessions
591
628
  SET active_transaction_id = ?, updated_at = ?
@@ -606,7 +643,7 @@ export class StateStore {
606
643
  }
607
644
  transactionOperations(transactionId) {
608
645
  return this.db
609
- .prepare("SELECT * FROM operations WHERE transaction_id = ? ORDER BY created_at")
646
+ .prepare("SELECT * FROM operations WHERE transaction_id = ? ORDER BY created_at, rowid")
610
647
  .all(transactionId);
611
648
  }
612
649
  validatedTransactionOperations(transactionId) {
@@ -780,14 +817,15 @@ export class StateStore {
780
817
  }
781
818
  }
782
819
  savePlan(input) {
783
- const id = this.nextId("p");
784
- this.db
785
- .prepare(`INSERT INTO plans
786
- (id, session_id, owner_actor_id, connection_id, sql, parameters,
787
- statement_type, state_version, state_signature, destructive,
788
- allow_unbounded, allow_destructive, expires_at, created_at)
789
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
790
- .run(id, input.sessionId, input.ownerActorId, input.connectionId, input.sql, JSON.stringify(toJsonSafe(input.parameters)), input.statementType, input.stateVersion, input.stateSignature, input.destructive ? 1 : 0, input.allowUnbounded ? 1 : 0, input.allowDestructive ? 1 : 0, input.expiresAt, this.now().toISOString());
820
+ const id = this.insertWithRandomId("p", "plans", (candidate) => {
821
+ this.db
822
+ .prepare(`INSERT INTO plans
823
+ (id, session_id, owner_actor_id, connection_id, sql, parameters,
824
+ statement_type, state_version, state_signature, destructive,
825
+ allow_unbounded, allow_destructive, expires_at, created_at)
826
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
827
+ .run(candidate, input.sessionId, input.ownerActorId, input.connectionId, input.sql, JSON.stringify(toJsonSafe(input.parameters)), input.statementType, input.stateVersion, input.stateSignature, input.destructive ? 1 : 0, input.allowUnbounded ? 1 : 0, input.allowDestructive ? 1 : 0, input.expiresAt, this.now().toISOString());
828
+ });
791
829
  return this.getPlan(id);
792
830
  }
793
831
  getPlan(id) {
@@ -852,13 +890,17 @@ export class StateStore {
852
890
  }
853
891
  }
854
892
  addHistory(input) {
855
- const id = input.id ?? this.nextId("cmd");
856
- this.db
857
- .prepare(`INSERT INTO history
858
- (id, timestamp, session_id, actor_id, origin, category, internal, command, sql, target, handle,
859
- executed, cached, success, error_code)
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);
893
+ const insert = (id) => {
894
+ this.db
895
+ .prepare(`INSERT INTO history
896
+ (id, timestamp, session_id, actor_id, origin, category, internal, command, sql, target, handle,
897
+ executed, cached, success, error_code)
898
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
899
+ .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);
900
+ };
901
+ const id = input.id ?? this.insertWithRandomId("cmd", "history", insert);
902
+ if (input.id !== undefined)
903
+ insert(id);
862
904
  this.db
863
905
  .prepare(`DELETE FROM history
864
906
  WHERE rowid IN (
@@ -1096,8 +1138,18 @@ function outcomeJson(outcome) {
1096
1138
  return outcome === undefined ? null : JSON.stringify(toJsonSafe(outcome));
1097
1139
  }
1098
1140
  function randomBase32Alias() {
1141
+ return randomBase32(10);
1142
+ }
1143
+ function randomBase32Id() {
1144
+ return randomBase32(26);
1145
+ }
1146
+ function randomBase32(length) {
1099
1147
  const alphabet = "abcdefghijklmnopqrstuvwxyz234567";
1100
- return [...randomBytes(10)].map((value) => alphabet[value & 31]).join("");
1148
+ return [...randomBytes(length)].map((value) => alphabet[value & 31]).join("");
1149
+ }
1150
+ function isIdCollision(error, table) {
1151
+ return error instanceof Error &&
1152
+ error.message.includes(`UNIQUE constraint failed: ${table}.id`);
1101
1153
  }
1102
1154
  function boundedHistorySql(sql) {
1103
1155
  if (sql === undefined)
@@ -11,7 +11,7 @@ export interface CommandExecutionContext {
11
11
  }
12
12
  export type CredentialAccess = "read" | "write";
13
13
  export type CredentialOperation = "connect" | "query" | "inspect" | "plan" | "exec" | "apply" | "transaction.commit";
14
- export type CredentialSource = "secret_env" | "credential_ref";
14
+ export type CredentialSource = "secret_env" | "credential_ref" | "password_ref";
15
15
  interface CredentialRequestBase {
16
16
  reference: string;
17
17
  actorId: string;
@@ -39,6 +39,9 @@ export type CredentialRequest = CredentialRequestBase & ({
39
39
  source?: "secret_env";
40
40
  } | {
41
41
  source: "credential_ref";
42
+ } | {
43
+ source: "password_ref";
44
+ target: string;
42
45
  });
43
46
  export type CredentialResolver = (request: CredentialRequest) => string | undefined | Promise<string | undefined>;
44
47
  export type StateConfidence = "authoritative" | "transaction_snapshot" | "database_reported" | "local" | "ttl_based" | "unknown";
@@ -106,6 +109,9 @@ export interface StateQLSnapshot {
106
109
  actor_id: string;
107
110
  connection: {
108
111
  connection_id: string;
112
+ /** Generated display identity; optional for older snapshot producers. */
113
+ alias?: string;
114
+ display_alias?: string;
109
115
  name: string;
110
116
  status: "connected";
111
117
  driver: Driver;
@@ -297,16 +303,19 @@ export interface ConnectOptions extends ExecutionOptions {
297
303
  secretEnv?: string;
298
304
  profile?: string;
299
305
  credentialRef?: string;
306
+ passwordRef?: string;
300
307
  }
301
308
  export interface ProfileOptions {
302
309
  readOnly?: boolean;
303
310
  secretEnv?: string;
304
311
  credentialRef?: string;
312
+ passwordRef?: string;
305
313
  }
306
314
  export interface ProfileUpdateOptions {
307
315
  target?: string | null;
308
316
  secretEnv?: string | null;
309
317
  credentialRef?: string | null;
318
+ passwordRef?: string | null;
310
319
  readOnly?: boolean;
311
320
  }
312
321
  export interface RedisQueryOptions extends ExecutionOptions {
@@ -350,6 +359,7 @@ export interface BatchCommand {
350
359
  read_only?: boolean;
351
360
  secret_env?: string;
352
361
  credential_ref?: string;
362
+ password_ref?: string | null;
353
363
  profile?: string;
354
364
  replay?: boolean;
355
365
  idempotency_key?: string;
@@ -379,6 +389,9 @@ export type Row = Record<string, unknown>;
379
389
  /** Data returned by the stable, non-dynamic StateQL public methods. */
380
390
  export interface ConnectionData {
381
391
  connection_id: string;
392
+ /** Persistent generated display identity; connection_id remains canonical. */
393
+ alias: string;
394
+ display_alias: string;
382
395
  driver: Driver;
383
396
  database: string;
384
397
  name: string;
@@ -392,6 +405,7 @@ export interface ProfileData {
392
405
  target: string | null;
393
406
  secret_env: string | null;
394
407
  credential_ref: string | null;
408
+ password_ref: string | null;
395
409
  read_only: boolean;
396
410
  }
397
411
  export interface ProfilesData {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fadhilp/stateql",
3
- "version": "0.10.0",
3
+ "version": "0.11.0",
4
4
  "description": "Stateful, agent-oriented database CLI for safe result reuse",
5
5
  "repository": {
6
6
  "type": "git",