@fadhilp/stateql 0.4.0 → 0.4.1
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 +13 -4
- package/dist/src/connection.d.ts +4 -0
- package/dist/src/connection.js +27 -1
- package/dist/src/stateql.js +14 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -64,7 +64,9 @@ Example first page:
|
|
|
64
64
|
Running the same normalized query with the same parameters reuses `q_1` while
|
|
65
65
|
its cache is valid. Use `--cache bypass` when a fresh read is required.
|
|
66
66
|
|
|
67
|
-
PostgreSQL and MySQL credentials should come from environment variables
|
|
67
|
+
PostgreSQL and MySQL credentials should come from environment variables. The
|
|
68
|
+
variable must contain the complete connection URL, not only its password.
|
|
69
|
+
Environment-backed SQLite paths require an explicit `sqlite:` prefix.
|
|
68
70
|
|
|
69
71
|
```bash
|
|
70
72
|
export APP_DATABASE_URL='postgres://user:password@host/app'
|
|
@@ -72,15 +74,20 @@ stql connect --env APP_DATABASE_URL --name app --read-only
|
|
|
72
74
|
|
|
73
75
|
export MYSQL_DATABASE_URL='mysql://user:password@host/app'
|
|
74
76
|
stql connect --env MYSQL_DATABASE_URL --name mysql-app --read-only
|
|
77
|
+
|
|
78
|
+
export SQLITE_DATABASE='sqlite:./app.sqlite'
|
|
79
|
+
stql connect --env SQLITE_DATABASE --name local --read-only
|
|
75
80
|
```
|
|
76
81
|
|
|
82
|
+
A connection accepts exactly one direct target, `--env`, or `--profile` source.
|
|
77
83
|
MySQL uses positional `?` parameters. MariaDB compatibility is not currently
|
|
78
84
|
claimed.
|
|
79
85
|
|
|
80
86
|
## Commands
|
|
81
87
|
|
|
82
88
|
```text
|
|
83
|
-
stql connect <sqlite-path|postgres-url|mysql-url> [--name NAME] [--
|
|
89
|
+
stql connect <sqlite-path|postgres-url|mysql-url> [--name NAME] [--read-write]
|
|
90
|
+
stql connect --env ENV [--name NAME] [--read-write]
|
|
84
91
|
stql connect --profile NAME
|
|
85
92
|
stql status
|
|
86
93
|
stql profile add|list|show|remove
|
|
@@ -301,8 +308,10 @@ secret-free failures. Unknown resolver errors are replaced with a generic
|
|
|
301
308
|
StateQL calls the resolver only immediately before database access, after SQL
|
|
302
309
|
safety and duplicate checks. Requests contain actor/session identity, the
|
|
303
310
|
operation's effective read/write access, an abort signal, and sanitized
|
|
304
|
-
connection metadata. Returned values
|
|
305
|
-
|
|
311
|
+
connection metadata. Returned values must be complete PostgreSQL/MySQL URLs or
|
|
312
|
+
explicit `sqlite:` sources. StateQL validates the source and its stored driver
|
|
313
|
+
before adapter construction, and normalizes SQLite paths. Credential-bearing
|
|
314
|
+
PostgreSQL and MySQL URLs are redacted before connection
|
|
306
315
|
metadata is persisted and never enter history, snapshots, cache keys, or
|
|
307
316
|
responses. SQLite paths remain persisted connection metadata, as they are for
|
|
308
317
|
direct SQLite connections. Harnesses remain responsible for approval policy,
|
package/dist/src/connection.d.ts
CHANGED
|
@@ -2,6 +2,10 @@ import type { ConnectionRecord } from "./store.js";
|
|
|
2
2
|
import type { Driver, StateConfidence } from "./types.js";
|
|
3
3
|
export declare function databaseIdentity(connection: ConnectionRecord): unknown;
|
|
4
4
|
export declare function detectDriver(target: string): Driver;
|
|
5
|
+
export declare function credentialSource(value: string, expectedDriver?: Driver): {
|
|
6
|
+
driver: Driver;
|
|
7
|
+
source: string;
|
|
8
|
+
};
|
|
5
9
|
export declare function normalizeSqliteSource(target: string): string;
|
|
6
10
|
export declare function databaseUrlHasSecret(target: string): boolean;
|
|
7
11
|
export declare function version(connection: ConnectionRecord): string;
|
package/dist/src/connection.js
CHANGED
|
@@ -18,8 +18,34 @@ export function detectDriver(target) {
|
|
|
18
18
|
}
|
|
19
19
|
return "sqlite";
|
|
20
20
|
}
|
|
21
|
+
export function credentialSource(value, expectedDriver) {
|
|
22
|
+
const explicitSqlite = /^sqlite:(?!\/\/)/i.test(value);
|
|
23
|
+
const driver = explicitSqlite ? "sqlite" : detectDriver(value);
|
|
24
|
+
if (driver === "sqlite" && (!explicitSqlite || value.length === 7)) {
|
|
25
|
+
throw new StateQLError("INVALID_COMMAND", "Secret environment variable must contain a complete PostgreSQL/MySQL URL or an explicit sqlite: source.", {
|
|
26
|
+
suggestedAction: "Store the full database URL, or prefix an SQLite path with sqlite:.",
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
if (driver !== "sqlite") {
|
|
30
|
+
try {
|
|
31
|
+
const url = new URL(value);
|
|
32
|
+
if (!url.hostname && !url.pathname.replaceAll("/", ""))
|
|
33
|
+
throw new Error();
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
throw new StateQLError("INVALID_COMMAND", "Secret environment variable must contain a valid database URL.");
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
if (expectedDriver && driver !== expectedDriver) {
|
|
40
|
+
throw new StateQLError("INVALID_COMMAND", "Resolved credential driver does not match the selected database connection.");
|
|
41
|
+
}
|
|
42
|
+
return {
|
|
43
|
+
driver,
|
|
44
|
+
source: driver === "sqlite" ? normalizeSqliteSource(value) : value,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
21
47
|
export function normalizeSqliteSource(target) {
|
|
22
|
-
const source = target.
|
|
48
|
+
const source = target.replace(/^sqlite:/i, "");
|
|
23
49
|
if (source === ":memory:") {
|
|
24
50
|
throw new StateQLError("INVALID_COMMAND", "SQLite :memory: databases cannot persist across StateQL commands.");
|
|
25
51
|
}
|
package/dist/src/stateql.js
CHANGED
|
@@ -2,7 +2,7 @@ import { writeFileSync } from "node:fs";
|
|
|
2
2
|
import { basename, resolve } from "node:path";
|
|
3
3
|
import { env } from "node:process";
|
|
4
4
|
import { AdapterExecutionError, AdapterWriteError, BatchWriteError, createAdapter, createAdapterContext, } from "./adapters.js";
|
|
5
|
-
import { confidence, databaseIdentity, databaseUrlHasSecret, detectDriver, isEnvironmentName, normalizeSqliteSource, validateProfileName, version, } from "./connection.js";
|
|
5
|
+
import { confidence, credentialSource, databaseIdentity, databaseUrlHasSecret, detectDriver, isEnvironmentName, normalizeSqliteSource, validateProfileName, version, } from "./connection.js";
|
|
6
6
|
import { asStateQLError, CredentialResolutionError, StateQLError, } from "./errors.js";
|
|
7
7
|
import { filterMaterializedRows, prepareFilterStatement, validateFilterParameters, } from "./filter.js";
|
|
8
8
|
import { operationData, paginationWarnings, profileData, rowsToCsv, sessionData, transactionData, } from "./response-data.js";
|
|
@@ -72,8 +72,10 @@ export class StateQL {
|
|
|
72
72
|
if (session.active_transaction_id) {
|
|
73
73
|
throw new StateQLError("TRANSACTION_FAILED", "Commit or roll back the active transaction before connecting again.");
|
|
74
74
|
}
|
|
75
|
-
|
|
76
|
-
|
|
75
|
+
const sourceCount = [target, options.profile, options.secretEnv]
|
|
76
|
+
.filter((value) => value !== undefined).length;
|
|
77
|
+
if (sourceCount > 1) {
|
|
78
|
+
throw new StateQLError("INVALID_COMMAND", "Use exactly one connection target, profile, or secret environment variable.");
|
|
77
79
|
}
|
|
78
80
|
const implicitProfile = !options.profile && !options.secretEnv && target
|
|
79
81
|
? this.store.getProfile(target)
|
|
@@ -101,7 +103,10 @@ export class StateQL {
|
|
|
101
103
|
if (!secret) {
|
|
102
104
|
throw new StateQLError("INVALID_COMMAND", "Connection target is required.");
|
|
103
105
|
}
|
|
104
|
-
const
|
|
106
|
+
const resolvedSource = secretEnv
|
|
107
|
+
? credentialSource(secret)
|
|
108
|
+
: { driver: detectDriver(secret), source: secret };
|
|
109
|
+
const { driver } = resolvedSource;
|
|
105
110
|
if (driver !== "sqlite" &&
|
|
106
111
|
!secretEnv &&
|
|
107
112
|
databaseUrlHasSecret(secret)) {
|
|
@@ -109,7 +114,9 @@ export class StateQL {
|
|
|
109
114
|
suggestedAction: "Set the URL in an environment variable and reconnect with --env NAME.",
|
|
110
115
|
});
|
|
111
116
|
}
|
|
112
|
-
const adapterSource =
|
|
117
|
+
const adapterSource = secretEnv
|
|
118
|
+
? resolvedSource.source
|
|
119
|
+
: driver === "sqlite" ? normalizeSqliteSource(secret) : secret;
|
|
113
120
|
const source = driver === "sqlite"
|
|
114
121
|
? adapterSource
|
|
115
122
|
: secretEnv
|
|
@@ -1508,7 +1515,7 @@ export class StateQL {
|
|
|
1508
1515
|
async resolveConnectionSource(connection, session, operation, access, context) {
|
|
1509
1516
|
if (!connection.secret_env)
|
|
1510
1517
|
return connection.source;
|
|
1511
|
-
|
|
1518
|
+
const value = await this.resolveCredential(connection.secret_env, session, operation, access, context, {
|
|
1512
1519
|
connection: {
|
|
1513
1520
|
id: connection.id,
|
|
1514
1521
|
name: connection.name,
|
|
@@ -1517,6 +1524,7 @@ export class StateQL {
|
|
|
1517
1524
|
readOnly: Boolean(connection.read_only),
|
|
1518
1525
|
},
|
|
1519
1526
|
});
|
|
1527
|
+
return credentialSource(value, connection.driver).source;
|
|
1520
1528
|
}
|
|
1521
1529
|
async resolveCredential(reference, session, operation, access, context, details = {}) {
|
|
1522
1530
|
const resolver = this.credentialResolver;
|