@fadhilp/stateql 0.10.1 → 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
@@ -53,14 +53,22 @@ export class StateStore {
53
53
  this.closed = true;
54
54
  this.db.close();
55
55
  }
56
- nextId(prefix) {
57
- this.db
58
- .prepare("INSERT OR IGNORE INTO counters(prefix, value) VALUES (?, 0)")
59
- .run(prefix);
60
- const row = this.db
61
- .prepare("UPDATE counters SET value = value + 1 WHERE prefix = ? RETURNING value")
62
- .get(prefix);
63
- 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.`);
64
72
  }
65
73
  ensureSession(name = "default") {
66
74
  const existing = this.getSessionByName(name);
@@ -87,12 +95,13 @@ export class StateStore {
87
95
  .get(name);
88
96
  const created = !row;
89
97
  if (!row) {
90
- const id = this.nextId("s");
91
- this.db
92
- .prepare(`INSERT INTO sessions
93
- (id, name, status, created_at, updated_at)
94
- VALUES (?, ?, 'active', ?, ?)`)
95
- .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
+ });
96
105
  row = { id, status: "active" };
97
106
  }
98
107
  else if (row.status !== "active") {
@@ -238,15 +247,15 @@ export class StateStore {
238
247
  const timestamp = this.now().toISOString();
239
248
  this.db
240
249
  .prepare(`INSERT INTO profiles
241
- (name, target, secret_env, credential_ref, read_only, created_at, updated_at)
242
- VALUES (?, ?, ?, ?, ?, ?, ?)`)
243
- .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);
244
253
  return this.getProfile(input.name);
245
254
  }
246
255
  updateProfile(input) {
247
256
  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);
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);
250
259
  return Number(result.changes) === 1 ? this.getProfile(input.name) : undefined;
251
260
  }
252
261
  getProfile(name) {
@@ -278,14 +287,15 @@ export class StateStore {
278
287
  this.db.exec("COMMIT");
279
288
  return undefined;
280
289
  }
281
- const id = this.nextId("conn");
282
290
  const timestamp = this.now().toISOString();
283
- this.db
284
- .prepare(`INSERT INTO connections
285
- (id, session_id, name, driver, database_name, source, secret_env,
286
- credential_ref, read_only, version, created_at)
287
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 0, ?)`)
288
- .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
+ });
289
299
  this.allocateConnectionAlias(id);
290
300
  this.db
291
301
  .prepare(`UPDATE sessions
@@ -362,16 +372,17 @@ export class StateStore {
362
372
  return `sv_${row.version}`;
363
373
  }
364
374
  saveResult(input) {
365
- const id = this.nextId("q");
366
375
  this.db.exec("BEGIN IMMEDIATE");
367
376
  try {
368
- this.db
369
- .prepare(`INSERT INTO results
370
- (id, session_id, connection_id, fingerprint, sql, parameters,
371
- rows_json, columns_json, row_count, state_version, state_signature,
372
- state_confidence, expires_at, created_at)
373
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
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());
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
+ });
375
386
  this.enforceResultQuota(id);
376
387
  this.allocateGeneratedAlias(input.sessionId, id);
377
388
  const result = this.getResult(id);
@@ -466,14 +477,15 @@ export class StateStore {
466
477
  }
467
478
  }
468
479
  saveOperation(input) {
469
- const id = this.nextId("op");
470
- this.db
471
- .prepare(`INSERT INTO operations
472
- (id, session_id, actor_id, connection_id, fingerprint, sql, parameters,
473
- statement_type, affected_rows, status, transaction_id, replay_of,
474
- idempotency_key, state_version_before, state_version_after, created_at)
475
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
476
- .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
+ });
477
489
  return this.getOperation(id);
478
490
  }
479
491
  getOperation(id) {
@@ -584,7 +596,6 @@ export class StateStore {
584
596
  .run(operationId);
585
597
  }
586
598
  createTransaction(input) {
587
- const id = this.nextId("tx");
588
599
  const timestamp = this.now().toISOString();
589
600
  this.db.exec("BEGIN IMMEDIATE");
590
601
  try {
@@ -604,12 +615,14 @@ export class StateStore {
604
615
  this.db.exec("COMMIT");
605
616
  return undefined;
606
617
  }
607
- this.db
608
- .prepare(`INSERT INTO transactions
609
- (id, session_id, owner_actor_id, connection_id, state,
610
- isolation_level, start_version, created_at)
611
- VALUES (?, ?, ?, ?, 'active', ?, ?, ?)`)
612
- .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
+ });
613
626
  this.db
614
627
  .prepare(`UPDATE sessions
615
628
  SET active_transaction_id = ?, updated_at = ?
@@ -630,7 +643,7 @@ export class StateStore {
630
643
  }
631
644
  transactionOperations(transactionId) {
632
645
  return this.db
633
- .prepare("SELECT * FROM operations WHERE transaction_id = ? ORDER BY created_at")
646
+ .prepare("SELECT * FROM operations WHERE transaction_id = ? ORDER BY created_at, rowid")
634
647
  .all(transactionId);
635
648
  }
636
649
  validatedTransactionOperations(transactionId) {
@@ -804,14 +817,15 @@ export class StateStore {
804
817
  }
805
818
  }
806
819
  savePlan(input) {
807
- const id = this.nextId("p");
808
- this.db
809
- .prepare(`INSERT INTO plans
810
- (id, session_id, owner_actor_id, connection_id, sql, parameters,
811
- statement_type, state_version, state_signature, destructive,
812
- allow_unbounded, allow_destructive, expires_at, created_at)
813
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
814
- .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
+ });
815
829
  return this.getPlan(id);
816
830
  }
817
831
  getPlan(id) {
@@ -876,13 +890,17 @@ export class StateStore {
876
890
  }
877
891
  }
878
892
  addHistory(input) {
879
- const id = input.id ?? this.nextId("cmd");
880
- this.db
881
- .prepare(`INSERT INTO history
882
- (id, timestamp, session_id, actor_id, origin, category, internal, command, sql, target, handle,
883
- executed, cached, success, error_code)
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);
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);
886
904
  this.db
887
905
  .prepare(`DELETE FROM history
888
906
  WHERE rowid IN (
@@ -1120,8 +1138,18 @@ function outcomeJson(outcome) {
1120
1138
  return outcome === undefined ? null : JSON.stringify(toJsonSafe(outcome));
1121
1139
  }
1122
1140
  function randomBase32Alias() {
1141
+ return randomBase32(10);
1142
+ }
1143
+ function randomBase32Id() {
1144
+ return randomBase32(26);
1145
+ }
1146
+ function randomBase32(length) {
1123
1147
  const alphabet = "abcdefghijklmnopqrstuvwxyz234567";
1124
- 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`);
1125
1153
  }
1126
1154
  function boundedHistorySql(sql) {
1127
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";
@@ -300,16 +303,19 @@ export interface ConnectOptions extends ExecutionOptions {
300
303
  secretEnv?: string;
301
304
  profile?: string;
302
305
  credentialRef?: string;
306
+ passwordRef?: string;
303
307
  }
304
308
  export interface ProfileOptions {
305
309
  readOnly?: boolean;
306
310
  secretEnv?: string;
307
311
  credentialRef?: string;
312
+ passwordRef?: string;
308
313
  }
309
314
  export interface ProfileUpdateOptions {
310
315
  target?: string | null;
311
316
  secretEnv?: string | null;
312
317
  credentialRef?: string | null;
318
+ passwordRef?: string | null;
313
319
  readOnly?: boolean;
314
320
  }
315
321
  export interface RedisQueryOptions extends ExecutionOptions {
@@ -353,6 +359,7 @@ export interface BatchCommand {
353
359
  read_only?: boolean;
354
360
  secret_env?: string;
355
361
  credential_ref?: string;
362
+ password_ref?: string | null;
356
363
  profile?: string;
357
364
  replay?: boolean;
358
365
  idempotency_key?: string;
@@ -398,6 +405,7 @@ export interface ProfileData {
398
405
  target: string | null;
399
406
  secret_env: string | null;
400
407
  credential_ref: string | null;
408
+ password_ref: string | null;
401
409
  read_only: boolean;
402
410
  }
403
411
  export interface ProfilesData {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fadhilp/stateql",
3
- "version": "0.10.1",
3
+ "version": "0.11.0",
4
4
  "description": "Stateful, agent-oriented database CLI for safe result reuse",
5
5
  "repository": {
6
6
  "type": "git",