@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/README.md +111 -49
- package/dist/src/connection.d.ts +7 -1
- package/dist/src/connection.js +117 -12
- package/dist/src/migrations.js +131 -0
- package/dist/src/response-data.js +1 -0
- package/dist/src/stateql.d.ts +2 -0
- package/dist/src/stateql.js +158 -56
- package/dist/src/store.d.ts +10 -1
- package/dist/src/store.js +118 -66
- package/dist/src/types.d.ts +15 -1
- package/package.json +1 -1
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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.
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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.
|
|
283
|
-
.
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
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.
|
|
345
|
-
.
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
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.
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
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.
|
|
584
|
-
.
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
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.
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
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
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
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(
|
|
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)
|
package/dist/src/types.d.ts
CHANGED
|
@@ -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 {
|