@fadhilp/stateql 0.8.1 → 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.
- package/README.md +140 -5
- package/dist/src/adapters.d.ts +8 -3
- package/dist/src/adapters.js +205 -5
- package/dist/src/cli.js +66 -3
- package/dist/src/connection.d.ts +1 -0
- package/dist/src/connection.js +18 -2
- package/dist/src/index.d.ts +2 -1
- package/dist/src/migrations.js +38 -0
- package/dist/src/mongodb.d.ts +5 -3
- package/dist/src/mongodb.js +66 -4
- package/dist/src/redis.d.ts +44 -0
- package/dist/src/redis.js +395 -0
- package/dist/src/sqlite-process.js +49 -1
- package/dist/src/stateql.d.ts +56 -4
- package/dist/src/stateql.js +858 -63
- package/dist/src/store.d.ts +24 -2
- package/dist/src/store.js +92 -12
- package/dist/src/table-editor.d.ts +41 -0
- package/dist/src/table-editor.js +137 -0
- package/dist/src/types.d.ts +75 -3
- package/package.json +4 -2
package/dist/src/connection.js
CHANGED
|
@@ -16,8 +16,10 @@ export function detectDriver(target) {
|
|
|
16
16
|
return "mysql";
|
|
17
17
|
if (/^mongodb(?:\+srv)?:\/\//i.test(target))
|
|
18
18
|
return "mongodb";
|
|
19
|
+
if (/^rediss?:\/\//i.test(target))
|
|
20
|
+
return "redis";
|
|
19
21
|
if (/^[a-z][a-z\d+.-]*:\/\//i.test(target)) {
|
|
20
|
-
throw new StateQLError("UNSUPPORTED_DRIVER", "Only MongoDB, MySQL, PostgreSQL, and SQLite are supported.");
|
|
22
|
+
throw new StateQLError("UNSUPPORTED_DRIVER", "Only MongoDB, MySQL, PostgreSQL, Redis, and SQLite are supported.");
|
|
21
23
|
}
|
|
22
24
|
return "sqlite";
|
|
23
25
|
}
|
|
@@ -44,6 +46,20 @@ export function mongoDatabaseName(target) {
|
|
|
44
46
|
}
|
|
45
47
|
throw new StateQLError("INVALID_COMMAND", "MongoDB URL must include an explicit database name.");
|
|
46
48
|
}
|
|
49
|
+
export function redisDatabaseName(target) {
|
|
50
|
+
try {
|
|
51
|
+
const url = new URL(target);
|
|
52
|
+
if (!url.hostname || !["redis:", "rediss:"].includes(url.protocol.toLowerCase()))
|
|
53
|
+
throw new Error();
|
|
54
|
+
const path = url.pathname.replace(/^\//, "");
|
|
55
|
+
if (path && !/^\d+$/.test(path))
|
|
56
|
+
throw new Error();
|
|
57
|
+
return `db${path || "0"}`;
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
throw new StateQLError("INVALID_COMMAND", "Invalid Redis URL or database number.");
|
|
61
|
+
}
|
|
62
|
+
}
|
|
47
63
|
export function credentialSource(value, expectedDriver, referenceSource = "secret_env") {
|
|
48
64
|
const sourceLabel = referenceSource === "credential_ref"
|
|
49
65
|
? "Credential reference"
|
|
@@ -51,7 +67,7 @@ export function credentialSource(value, expectedDriver, referenceSource = "secre
|
|
|
51
67
|
const explicitSqlite = /^sqlite:(?!\/\/)/i.test(value);
|
|
52
68
|
const driver = explicitSqlite ? "sqlite" : detectDriver(value);
|
|
53
69
|
if (driver === "sqlite" && (!explicitSqlite || value.length === 7)) {
|
|
54
|
-
throw new StateQLError("INVALID_COMMAND", `${sourceLabel} must contain a complete PostgreSQL/MySQL URL or an explicit sqlite: source; MongoDB URLs are also supported.`, {
|
|
70
|
+
throw new StateQLError("INVALID_COMMAND", `${sourceLabel} must contain a complete PostgreSQL/MySQL/Redis URL or an explicit sqlite: source; MongoDB URLs are also supported.`, {
|
|
55
71
|
suggestedAction: "Store the full database URL, or prefix an SQLite path with sqlite:.",
|
|
56
72
|
});
|
|
57
73
|
}
|
package/dist/src/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { StateQL } from "./stateql.js";
|
|
2
2
|
export { CredentialResolutionError, StateQLError, exitCodeFor, } from "./errors.js";
|
|
3
3
|
export type { CredentialResolutionFailure } from "./errors.js";
|
|
4
|
-
export type {
|
|
4
|
+
export type { TableChange, TableIdentity, TableUpdate } from "./table-editor.js";
|
|
5
|
+
export type { ActorData, ActorLinkData, ActorResolutionData, ActorsData, ActorUnlinkData, AliasData, ApplyData, BatchCommand, BatchCommandName, BatchOptions, CatalogObject, CatalogObjectKind, DescribeObjectData, ListObjectsData, ListObjectsFilter, CapabilitiesData, CommandExecutionContext, CommandOrigin, CloseSessionData, Column, ColumnsData, CommitTransactionData, ConnectOptions, ConnectionData, CountData, CredentialAccess, CredentialOperation, CredentialRequest, CredentialSource, CredentialResolver, DisconnectData, DoctorData, Driver, ExecData, ExecOptions, ExecutionOptions, ExportData, Failure, FilterOptions, HistoryData, HistoryEntry, HistoryCategory, HistoryOptions, MongoAggregateOptions, MongoDocument, MongoFindOptions, MongoMutationOptions, MongoExecOptions, MongoPlanOptions, MongoQueryOptions, MongoReadCommand, MongoWriteCommand, MongoWriteOutcome, OperationData, PlanData, PlanOptions, ProfileData, ProfileOptions, ProfileUpdateOptions, ProfilesData, PurgeData, RedisCommand, RedisExecOptions, RedisPlanOptions, RedisQueryOptions, RedisWriteOutcome, QueryOptions, RecentOperationData, RecentResultData, RemovedProfileData, Response, ResultData, RollbackTransactionData, Row, RowsData, RowsOptions, SqlParameters, SqlDriver, StateQLActorOptions, StateQLOptions, StateConfidence, StateQLSnapshot, StateQLSnapshotOptions, StatusConnectionData, StatusData, Success, SessionData, SessionListItem, SessionsData, SessionSummaryData, TransactionData, TransactionReferenceData, Warning, } from "./types.js";
|
package/dist/src/migrations.js
CHANGED
|
@@ -80,6 +80,41 @@ const MIGRATIONS = [
|
|
|
80
80
|
apply: migrateCredentialRefs,
|
|
81
81
|
validate: validateCredentialRefs,
|
|
82
82
|
},
|
|
83
|
+
{
|
|
84
|
+
name: "history_target_v1",
|
|
85
|
+
apply(db) { addColumn(db, "history", "target", "TEXT"); },
|
|
86
|
+
validate(db) { requireColumns(db, "history", ["target"]); },
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
name: "generated_aliases_v1",
|
|
90
|
+
apply(db) {
|
|
91
|
+
addColumn(db, "aliases", "generated", "INTEGER NOT NULL DEFAULT 0");
|
|
92
|
+
db.exec("CREATE UNIQUE INDEX IF NOT EXISTS aliases_generated_result ON aliases(result_id) WHERE generated = 1");
|
|
93
|
+
},
|
|
94
|
+
validate(db) {
|
|
95
|
+
requireColumns(db, "aliases", ["generated"]);
|
|
96
|
+
requireIndexes(db, ["aliases_generated_result"]);
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
name: "history_classification_v1",
|
|
101
|
+
apply(db) {
|
|
102
|
+
addColumn(db, "history", "category", "TEXT NOT NULL DEFAULT 'management'");
|
|
103
|
+
addColumn(db, "history", "internal", "INTEGER NOT NULL DEFAULT 0");
|
|
104
|
+
db.exec(`
|
|
105
|
+
UPDATE history SET category = 'statement'
|
|
106
|
+
WHERE category = 'management' AND command IN
|
|
107
|
+
('query','exec','plan','apply','filter','mongo.query','mongo.exec','mongo.plan','redis.query','redis.exec','redis.plan');
|
|
108
|
+
UPDATE history SET category = 'introspection'
|
|
109
|
+
WHERE category = 'management' AND (command LIKE 'inspect.%' OR command IN ('objects.list','object.describe','table.read'));
|
|
110
|
+
`);
|
|
111
|
+
db.exec("CREATE INDEX IF NOT EXISTS history_session_category ON history(session_id, category, internal)");
|
|
112
|
+
},
|
|
113
|
+
validate(db) {
|
|
114
|
+
requireColumns(db, "history", ["category", "internal"]);
|
|
115
|
+
requireIndexes(db, ["history_session_category"]);
|
|
116
|
+
},
|
|
117
|
+
},
|
|
83
118
|
];
|
|
84
119
|
export function runMigrations(db, now) {
|
|
85
120
|
db.exec(`
|
|
@@ -181,6 +216,7 @@ function createInitialSchema(db) {
|
|
|
181
216
|
session_id TEXT NOT NULL,
|
|
182
217
|
name TEXT NOT NULL,
|
|
183
218
|
result_id TEXT NOT NULL,
|
|
219
|
+
generated INTEGER NOT NULL DEFAULT 0,
|
|
184
220
|
PRIMARY KEY(session_id, name),
|
|
185
221
|
FOREIGN KEY(result_id) REFERENCES results(id)
|
|
186
222
|
);
|
|
@@ -244,6 +280,8 @@ function createInitialSchema(db) {
|
|
|
244
280
|
actor_id TEXT NOT NULL,
|
|
245
281
|
command TEXT NOT NULL,
|
|
246
282
|
origin TEXT NOT NULL DEFAULT 'legacy',
|
|
283
|
+
category TEXT NOT NULL DEFAULT 'management',
|
|
284
|
+
internal INTEGER NOT NULL DEFAULT 0,
|
|
247
285
|
sql TEXT,
|
|
248
286
|
handle TEXT,
|
|
249
287
|
executed INTEGER NOT NULL,
|
package/dist/src/mongodb.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type AdapterContext } from "./adapters.js";
|
|
2
2
|
import type { ConnectionRecord } from "./store.js";
|
|
3
|
-
import type { Column, MongoReadCommand, MongoWriteCommand, MongoWriteOutcome, Row } from "./types.js";
|
|
3
|
+
import type { CatalogObject, Column, DescribeObjectData, ListObjectsData, ListObjectsFilter, MongoReadCommand, MongoWriteCommand, MongoWriteOutcome, Row } from "./types.js";
|
|
4
4
|
interface MongoReadResult {
|
|
5
5
|
rows: Row[];
|
|
6
6
|
columns: Column[];
|
|
@@ -34,9 +34,11 @@ export declare class MongoAdapter {
|
|
|
34
34
|
ping(): Promise<void>;
|
|
35
35
|
signature(): Promise<string>;
|
|
36
36
|
read(command: MongoReadCommand, maxRows: number): Promise<MongoReadResult>;
|
|
37
|
-
write(command: MongoWriteCommand): Promise<MongoWriteResult>;
|
|
38
|
-
writeBatch(commands: MongoWriteCommand[], isolation: string): Promise<MongoWriteResult[]>;
|
|
37
|
+
write(command: MongoWriteCommand, expectedRows?: 1): Promise<MongoWriteResult>;
|
|
38
|
+
writeBatch(commands: MongoWriteCommand[], isolation: string, expectedRows?: boolean): Promise<MongoWriteResult[]>;
|
|
39
39
|
inspect(kind: string, name?: string): Promise<unknown>;
|
|
40
|
+
listObjects(filter: ListObjectsFilter): Promise<ListObjectsData>;
|
|
41
|
+
describeObject(object: CatalogObject): Promise<DescribeObjectData>;
|
|
40
42
|
close(): Promise<void>;
|
|
41
43
|
private connect;
|
|
42
44
|
private executeWrite;
|
package/dist/src/mongodb.js
CHANGED
|
@@ -221,7 +221,7 @@ export class MongoAdapter {
|
|
|
221
221
|
await cursor?.close().catch(() => undefined);
|
|
222
222
|
}
|
|
223
223
|
}
|
|
224
|
-
async write(command) {
|
|
224
|
+
async write(command, expectedRows) {
|
|
225
225
|
let value;
|
|
226
226
|
try {
|
|
227
227
|
value = validateMongoWriteCommand(command);
|
|
@@ -240,17 +240,24 @@ export class MongoAdapter {
|
|
|
240
240
|
throw error;
|
|
241
241
|
throw new AdapterWriteError(errorText(error), false);
|
|
242
242
|
}
|
|
243
|
+
if (expectedRows === 1 && (value.operation !== "updateOne" || value.options?.upsert))
|
|
244
|
+
throw new AdapterWriteError("Conditional edits require updateOne without upsert.", false);
|
|
243
245
|
try {
|
|
244
|
-
|
|
246
|
+
const result = await withContext(this.executeWrite(value), this.context, () => this.stop(), true);
|
|
247
|
+
if (expectedRows === 1 && result.outcome.matched_count !== 1)
|
|
248
|
+
throw new AdapterWriteError("ROW_CONFLICT: The document changed or was removed.", false);
|
|
249
|
+
return result;
|
|
245
250
|
}
|
|
246
251
|
catch (error) {
|
|
252
|
+
if (error instanceof AdapterWriteError)
|
|
253
|
+
throw error;
|
|
247
254
|
const stopped = writeStoppedError(error, this.context, true);
|
|
248
255
|
if (stopped)
|
|
249
256
|
throw stopped;
|
|
250
257
|
throw new AdapterWriteError(errorText(error), knownNoWrite(error) ? false : true);
|
|
251
258
|
}
|
|
252
259
|
}
|
|
253
|
-
async writeBatch(commands, isolation) {
|
|
260
|
+
async writeBatch(commands, isolation, expectedRows = false) {
|
|
254
261
|
if (isolation.toLowerCase() !== "snapshot") {
|
|
255
262
|
throw new BatchWriteError(`Unsupported MongoDB isolation level "${isolation}".`, false);
|
|
256
263
|
}
|
|
@@ -288,7 +295,10 @@ export class MongoAdapter {
|
|
|
288
295
|
for (const command of values) {
|
|
289
296
|
throwIfStopped(this.context, dispatched);
|
|
290
297
|
dispatched = true;
|
|
291
|
-
|
|
298
|
+
const result = await withContext(this.executeWrite(command, session), this.context, () => this.stop(), true);
|
|
299
|
+
if (expectedRows && result.outcome.matched_count !== 1)
|
|
300
|
+
throw new Error("ROW_CONFLICT: A document changed or was removed.");
|
|
301
|
+
results.push(result);
|
|
292
302
|
}
|
|
293
303
|
throwIfStopped(this.context, dispatched);
|
|
294
304
|
committing = true;
|
|
@@ -342,6 +352,10 @@ export class MongoAdapter {
|
|
|
342
352
|
if (kind === "constraints") {
|
|
343
353
|
return { collection: collectionName, constraints: [] };
|
|
344
354
|
}
|
|
355
|
+
if (kind === "editable") {
|
|
356
|
+
const objects = await this.client.db(this.databaseName).listCollections({ name: collectionName }, { nameOnly: true, signal: operationSignal(this.context), maxTimeMS: remainingMilliseconds(this.context) }).toArray();
|
|
357
|
+
return { writable: objects[0]?.type === "collection", columns: [] };
|
|
358
|
+
}
|
|
345
359
|
const columns = await this.sampleColumns(collectionName);
|
|
346
360
|
if (kind === "columns")
|
|
347
361
|
return { collection: collectionName, columns };
|
|
@@ -362,6 +376,51 @@ export class MongoAdapter {
|
|
|
362
376
|
throw readError(error, this.context);
|
|
363
377
|
}
|
|
364
378
|
}
|
|
379
|
+
async listObjects(filter) {
|
|
380
|
+
if (filter.kind !== undefined && filter.kind !== "collection" && filter.kind !== "view")
|
|
381
|
+
throw new Error(`MongoDB does not support catalog kind "${filter.kind}".`);
|
|
382
|
+
if (filter.schema !== undefined)
|
|
383
|
+
throw new Error("MongoDB collections do not use schemas.");
|
|
384
|
+
const offset = filter.offset ?? 0;
|
|
385
|
+
const limit = filter.limit ?? 50;
|
|
386
|
+
if (typeof offset !== "number" || !Number.isSafeInteger(offset) || offset < 0 || offset > 1_000_000 || !Number.isSafeInteger(limit) || limit < 1 || limit > 200)
|
|
387
|
+
throw new Error("Invalid MongoDB catalog page bounds.");
|
|
388
|
+
if (filter.search !== undefined && (!filter.search || filter.search.length > 200 || filter.search.includes("\0")))
|
|
389
|
+
throw new Error("Invalid MongoDB catalog search.");
|
|
390
|
+
await this.connect();
|
|
391
|
+
const query = {};
|
|
392
|
+
if (filter.kind === "collection")
|
|
393
|
+
query.type = "collection";
|
|
394
|
+
if (filter.kind === "view")
|
|
395
|
+
query.type = "view";
|
|
396
|
+
const cursor = this.client.db(this.databaseName).listCollections(query, {
|
|
397
|
+
nameOnly: true,
|
|
398
|
+
signal: operationSignal(this.context),
|
|
399
|
+
maxTimeMS: remainingMilliseconds(this.context),
|
|
400
|
+
timeoutMS: remainingMilliseconds(this.context),
|
|
401
|
+
batchSize: Math.min(limit + 1, 201),
|
|
402
|
+
});
|
|
403
|
+
const fetched = await collectCursor(cursor, offset + limit + 1, this.context);
|
|
404
|
+
const rows = fetched.slice(offset);
|
|
405
|
+
const more = rows.length > limit;
|
|
406
|
+
return {
|
|
407
|
+
objects: rows.slice(0, limit).map((row) => ({ kind: row.type === "view" ? "view" : "collection", name: String(row.name), identity: String(row.name) })),
|
|
408
|
+
next_offset: more ? offset + limit : null,
|
|
409
|
+
supported_kinds: ["collection", "view"],
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
async describeObject(object) {
|
|
413
|
+
if ((object.kind !== "collection" && object.kind !== "view") || object.schema !== undefined)
|
|
414
|
+
throw new Error(`MongoDB does not support catalog kind "${object.kind}".`);
|
|
415
|
+
const rows = await collectCursor(this.client.db(this.databaseName).listCollections({ name: object.name }, { nameOnly: false, signal: operationSignal(this.context), maxTimeMS: remainingMilliseconds(this.context), timeoutMS: remainingMilliseconds(this.context), batchSize: 1 }), 1, this.context);
|
|
416
|
+
const found = rows[0];
|
|
417
|
+
if (!found)
|
|
418
|
+
throw new Error("Catalog object was not found.");
|
|
419
|
+
return ejsonSafe({
|
|
420
|
+
object: { kind: found.type === "view" ? "view" : "collection", name: found.name, identity: found.name },
|
|
421
|
+
definition: { type: found.type, options: found.options ?? {} },
|
|
422
|
+
});
|
|
423
|
+
}
|
|
365
424
|
async close() {
|
|
366
425
|
if (this.closed)
|
|
367
426
|
return;
|
|
@@ -775,6 +834,9 @@ function ejsonSafe(value) {
|
|
|
775
834
|
return null;
|
|
776
835
|
return JSON.parse(BSON.EJSON.stringify(value, { relaxed: false }));
|
|
777
836
|
}
|
|
837
|
+
function escapeRegex(value) {
|
|
838
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
839
|
+
}
|
|
778
840
|
function remainingMilliseconds(context) {
|
|
779
841
|
return Math.max(1, Math.min(2_147_483_647, Math.ceil(context.deadline - Date.now())));
|
|
780
842
|
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { type AdapterContext } from "./adapters.js";
|
|
2
|
+
import type { CatalogObject, DescribeObjectData, ListObjectsData, ListObjectsFilter, RedisCommand, RedisWriteOutcome, Row, Column } from "./types.js";
|
|
3
|
+
export interface RedisReadResult {
|
|
4
|
+
rows: Row[];
|
|
5
|
+
columns: Column[];
|
|
6
|
+
nextCursor?: string | null;
|
|
7
|
+
}
|
|
8
|
+
export interface RedisPrecondition {
|
|
9
|
+
key: string;
|
|
10
|
+
fingerprint: string;
|
|
11
|
+
/** Absolute expiry in epoch milliseconds, or Redis -1/-2 sentinel. */
|
|
12
|
+
expiresAt: number;
|
|
13
|
+
}
|
|
14
|
+
export interface RedisWriteResult {
|
|
15
|
+
affectedRows: number;
|
|
16
|
+
outcome: RedisWriteOutcome;
|
|
17
|
+
}
|
|
18
|
+
export declare function validateRedisReadCommand(value: unknown): RedisCommand;
|
|
19
|
+
export declare function validateRedisWriteCommand(value: unknown): RedisCommand;
|
|
20
|
+
export declare function serializeRedisCommand(command: RedisCommand): string;
|
|
21
|
+
export declare function deserializeRedisCommand(value: string): RedisCommand;
|
|
22
|
+
export declare class RedisAdapter {
|
|
23
|
+
private readonly readOnly;
|
|
24
|
+
private readonly context;
|
|
25
|
+
readonly confidence: "ttl_based";
|
|
26
|
+
private readonly client;
|
|
27
|
+
private connected;
|
|
28
|
+
private closed;
|
|
29
|
+
constructor(source: string, readOnly: boolean, context: AdapterContext);
|
|
30
|
+
ping(): Promise<void>;
|
|
31
|
+
signature(): Promise<string>;
|
|
32
|
+
read(input: RedisCommand): Promise<RedisReadResult>;
|
|
33
|
+
precondition(input: RedisCommand): Promise<RedisPrecondition>;
|
|
34
|
+
write(input: RedisCommand, precondition?: RedisPrecondition): Promise<RedisWriteResult>;
|
|
35
|
+
listObjects(filter: ListObjectsFilter): Promise<ListObjectsData>;
|
|
36
|
+
describeObject(object: CatalogObject): Promise<DescribeObjectData>;
|
|
37
|
+
close(): Promise<void>;
|
|
38
|
+
private scanValue;
|
|
39
|
+
private expirationIdentity;
|
|
40
|
+
private keyFingerprint;
|
|
41
|
+
private connect;
|
|
42
|
+
private execute;
|
|
43
|
+
private stop;
|
|
44
|
+
}
|