taurusdb-core 0.1.0 → 0.3.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/auth/sql-profile-loader/env-source.js +15 -30
- package/dist/auth/sql-profile-loader/loader.js +8 -0
- package/dist/auth/sql-profile-loader/parsing.js +10 -9
- package/dist/auth/sql-profile-loader/runtime-override.d.ts +1 -0
- package/dist/auth/sql-profile-loader/runtime-override.js +21 -7
- package/dist/auth/sql-profile-loader/types.d.ts +5 -3
- package/dist/capability/types.d.ts +2 -0
- package/dist/capability/types.js +2 -0
- package/dist/config/env.js +0 -1
- package/dist/config/schema.d.ts +0 -3
- package/dist/config/schema.js +0 -1
- package/dist/engine/runtime.js +6 -3
- package/dist/engine/types.d.ts +0 -1
- package/dist/engine.js +0 -1
- package/dist/executor/connection-pool.d.ts +0 -2
- package/dist/executor/connection-pool.js +6 -31
- package/dist/executor/sql-executor.js +0 -1
- package/dist/executor/types.d.ts +0 -1
- package/dist/taurus/flashback.d.ts +1 -0
- package/dist/taurus/flashback.js +13 -2
- package/dist/utils/logger.js +8 -0
- package/package.json +1 -1
|
@@ -17,18 +17,18 @@ export function parseEnvProfile(env) {
|
|
|
17
17
|
let host;
|
|
18
18
|
let port;
|
|
19
19
|
let database;
|
|
20
|
-
let
|
|
21
|
-
let
|
|
20
|
+
let username;
|
|
21
|
+
let passwordRef;
|
|
22
22
|
if (dsn) {
|
|
23
23
|
const url = new URL(dsn);
|
|
24
24
|
engine = parseEngineFromDsnProtocol(url.protocol);
|
|
25
25
|
host = url.hostname;
|
|
26
26
|
port = url.port ? Number.parseInt(url.port, 10) : defaultPortForEngine(engine);
|
|
27
27
|
database = asString(url.pathname.replace(/^\//, ""));
|
|
28
|
-
|
|
28
|
+
username = asString(decodeURIComponent(url.username));
|
|
29
29
|
const dsnPassword = asString(decodeURIComponent(url.password));
|
|
30
30
|
if (dsnPassword) {
|
|
31
|
-
|
|
31
|
+
passwordRef = parseCredentialRef(dsnPassword, "TAURUSDB_SQL_DSN.password");
|
|
32
32
|
}
|
|
33
33
|
}
|
|
34
34
|
else {
|
|
@@ -38,41 +38,27 @@ export function parseEnvProfile(env) {
|
|
|
38
38
|
port = explicitPort ?? defaultPortForEngine(engine);
|
|
39
39
|
database = asString(env.TAURUSDB_SQL_DATABASE);
|
|
40
40
|
}
|
|
41
|
-
const mutationUserName = asString(env.TAURUSDB_SQL_MUTATION_USER);
|
|
42
|
-
const mutationPasswordRaw = asString(env.TAURUSDB_SQL_MUTATION_PASSWORD);
|
|
43
41
|
if (!dsn &&
|
|
44
42
|
!explicitHost &&
|
|
45
43
|
!asString(env.TAURUSDB_SQL_USER) &&
|
|
46
44
|
!asString(env.TAURUSDB_SQL_PASSWORD) &&
|
|
47
|
-
!database
|
|
48
|
-
!mutationUserName &&
|
|
49
|
-
!mutationPasswordRaw) {
|
|
45
|
+
!database) {
|
|
50
46
|
return undefined;
|
|
51
47
|
}
|
|
52
48
|
if (!port || !Number.isFinite(port) || port <= 0) {
|
|
53
49
|
throw new Error("Failed to resolve SQL port from environment.");
|
|
54
50
|
}
|
|
55
|
-
|
|
56
|
-
if (!
|
|
57
|
-
throw new Error("Missing
|
|
51
|
+
username = username ?? asString(env.TAURUSDB_SQL_USER);
|
|
52
|
+
if (!username) {
|
|
53
|
+
throw new Error("Missing SQL username in environment. Set TAURUSDB_SQL_USER or include it in DSN.");
|
|
58
54
|
}
|
|
59
|
-
|
|
60
|
-
|
|
55
|
+
passwordRef =
|
|
56
|
+
passwordRef ??
|
|
61
57
|
(Object.hasOwn(env, "TAURUSDB_SQL_PASSWORD")
|
|
62
58
|
? parseCredentialRef(env.TAURUSDB_SQL_PASSWORD, "TAURUSDB_SQL_PASSWORD")
|
|
63
59
|
: undefined);
|
|
64
|
-
if (!
|
|
65
|
-
throw new Error("Missing
|
|
66
|
-
}
|
|
67
|
-
let mutationUser;
|
|
68
|
-
if (mutationUserName || mutationPasswordRaw) {
|
|
69
|
-
if (!mutationUserName || !mutationPasswordRaw) {
|
|
70
|
-
throw new Error("Invalid mutation credentials in environment: TAURUSDB_SQL_MUTATION_USER and TAURUSDB_SQL_MUTATION_PASSWORD must be set together.");
|
|
71
|
-
}
|
|
72
|
-
mutationUser = {
|
|
73
|
-
username: mutationUserName,
|
|
74
|
-
password: parseCredentialRef(mutationPasswordRaw, "TAURUSDB_SQL_MUTATION_PASSWORD"),
|
|
75
|
-
};
|
|
60
|
+
if (!passwordRef) {
|
|
61
|
+
throw new Error("Missing SQL password in environment. Set TAURUSDB_SQL_PASSWORD or include it in DSN.");
|
|
76
62
|
}
|
|
77
63
|
const poolSize = asInteger(env.TAURUSDB_SQL_POOL_SIZE);
|
|
78
64
|
if (poolSize !== undefined && poolSize <= 0) {
|
|
@@ -84,11 +70,10 @@ export function parseEnvProfile(env) {
|
|
|
84
70
|
host,
|
|
85
71
|
port,
|
|
86
72
|
database,
|
|
87
|
-
|
|
88
|
-
username
|
|
89
|
-
password:
|
|
73
|
+
user: {
|
|
74
|
+
username,
|
|
75
|
+
password: passwordRef,
|
|
90
76
|
},
|
|
91
|
-
mutationUser,
|
|
92
77
|
poolSize,
|
|
93
78
|
});
|
|
94
79
|
}
|
|
@@ -3,6 +3,7 @@ import os from "node:os";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { parseEnvProfile } from "./env-source.js";
|
|
5
5
|
import { parseProfilesFile } from "./file-source.js";
|
|
6
|
+
import { withRedactedToString } from "./parsing.js";
|
|
6
7
|
export function resolveDefaultProfilePath(config) {
|
|
7
8
|
if (config.profilesPath) {
|
|
8
9
|
return config.profilesPath;
|
|
@@ -67,6 +68,13 @@ export class SqlProfileLoader {
|
|
|
67
68
|
mergedProfiles.set(name, profile);
|
|
68
69
|
}
|
|
69
70
|
}
|
|
71
|
+
if (mergedProfiles.size === 0) {
|
|
72
|
+
mergedProfiles.set("taurus_mcp", withRedactedToString({
|
|
73
|
+
name: "taurus_mcp",
|
|
74
|
+
engine: "mysql",
|
|
75
|
+
port: 3306,
|
|
76
|
+
}));
|
|
77
|
+
}
|
|
70
78
|
const defaultDatasource = this.config.defaultDatasource ??
|
|
71
79
|
fileDefaultDatasource ??
|
|
72
80
|
(mergedProfiles.size === 1 ? mergedProfiles.keys().next().value : undefined);
|
|
@@ -194,13 +194,15 @@ export function parseProfileRecord(name, value, context) {
|
|
|
194
194
|
if (poolSize !== undefined && poolSize <= 0) {
|
|
195
195
|
throw new Error(`Invalid datasource profile ${name} in ${context}: poolSize must be positive.`);
|
|
196
196
|
}
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
197
|
+
// Backward compatibility for older profile field names. New configs should use `user`.
|
|
198
|
+
const userRaw = value.user ??
|
|
199
|
+
value.readonlyUser ??
|
|
200
|
+
value.readonly ??
|
|
201
|
+
value.readOnlyUser;
|
|
202
|
+
if (userRaw === undefined) {
|
|
203
|
+
throw new Error(`Invalid datasource profile ${name} in ${context}: missing user.`);
|
|
204
|
+
}
|
|
205
|
+
const user = parseUserCredential(userRaw, `${context}.${name}.user`);
|
|
204
206
|
const tls = parseTlsOptions(value.tls, `${context}.${name}.tls`);
|
|
205
207
|
return withRedactedToString({
|
|
206
208
|
name,
|
|
@@ -208,8 +210,7 @@ export function parseProfileRecord(name, value, context) {
|
|
|
208
210
|
host,
|
|
209
211
|
port,
|
|
210
212
|
database,
|
|
211
|
-
|
|
212
|
-
mutationUser,
|
|
213
|
+
user,
|
|
213
214
|
tls,
|
|
214
215
|
poolSize,
|
|
215
216
|
});
|
|
@@ -5,6 +5,7 @@ export declare class RuntimeOverrideProfileLoader implements RuntimeTargetProfil
|
|
|
5
5
|
private readonly runtimeTargets;
|
|
6
6
|
constructor(base: ProfileLoader);
|
|
7
7
|
setRuntimeTarget(name: string, target: RuntimeDataSourceTarget): void;
|
|
8
|
+
clearRuntimeUser(name: string): void;
|
|
8
9
|
clearRuntimeTarget(name: string): void;
|
|
9
10
|
clearAllRuntimeTargets(): void;
|
|
10
11
|
getRuntimeTarget(name: string): RuntimeDataSourceTarget | undefined;
|
|
@@ -5,8 +5,10 @@ export function applyRuntimeTarget(profile, target) {
|
|
|
5
5
|
}
|
|
6
6
|
return withRedactedToString({
|
|
7
7
|
...profile,
|
|
8
|
-
host: target.host,
|
|
8
|
+
host: target.host ?? profile.host,
|
|
9
9
|
port: target.port ?? profile.port,
|
|
10
|
+
database: target.database ?? profile.database,
|
|
11
|
+
user: target.user ?? profile.user,
|
|
10
12
|
});
|
|
11
13
|
}
|
|
12
14
|
export class RuntimeOverrideProfileLoader {
|
|
@@ -16,12 +18,24 @@ export class RuntimeOverrideProfileLoader {
|
|
|
16
18
|
this.base = base;
|
|
17
19
|
}
|
|
18
20
|
setRuntimeTarget(name, target) {
|
|
19
|
-
this.runtimeTargets.
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
21
|
+
const current = this.runtimeTargets.get(name);
|
|
22
|
+
const next = {
|
|
23
|
+
host: target.host ?? current?.host,
|
|
24
|
+
port: target.port ?? current?.port,
|
|
25
|
+
database: target.database ?? current?.database,
|
|
26
|
+
user: target.user ?? current?.user,
|
|
27
|
+
instanceId: target.instanceId ?? current?.instanceId,
|
|
28
|
+
nodeId: target.nodeId ?? current?.nodeId,
|
|
29
|
+
};
|
|
30
|
+
this.runtimeTargets.set(name, next);
|
|
31
|
+
}
|
|
32
|
+
clearRuntimeUser(name) {
|
|
33
|
+
const current = this.runtimeTargets.get(name);
|
|
34
|
+
if (!current) {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
const { user: _user, ...next } = current;
|
|
38
|
+
this.runtimeTargets.set(name, next);
|
|
25
39
|
}
|
|
26
40
|
clearRuntimeTarget(name) {
|
|
27
41
|
this.runtimeTargets.delete(name);
|
|
@@ -31,8 +31,7 @@ export interface DataSourceProfile {
|
|
|
31
31
|
host?: string;
|
|
32
32
|
port: number;
|
|
33
33
|
database?: string;
|
|
34
|
-
|
|
35
|
-
mutationUser?: UserCredential;
|
|
34
|
+
user?: UserCredential;
|
|
36
35
|
tls?: TlsOptions;
|
|
37
36
|
poolSize?: number;
|
|
38
37
|
toString(): string;
|
|
@@ -43,13 +42,16 @@ export interface ProfileLoader {
|
|
|
43
42
|
get(name: string): Promise<DataSourceProfile | undefined>;
|
|
44
43
|
}
|
|
45
44
|
export interface RuntimeDataSourceTarget {
|
|
46
|
-
host
|
|
45
|
+
host?: string;
|
|
47
46
|
port?: number;
|
|
47
|
+
database?: string;
|
|
48
|
+
user?: UserCredential;
|
|
48
49
|
instanceId?: string;
|
|
49
50
|
nodeId?: string;
|
|
50
51
|
}
|
|
51
52
|
export interface RuntimeTargetProfileLoader extends ProfileLoader {
|
|
52
53
|
setRuntimeTarget(name: string, target: RuntimeDataSourceTarget): void;
|
|
54
|
+
clearRuntimeUser(name: string): void;
|
|
53
55
|
clearRuntimeTarget(name: string): void;
|
|
54
56
|
clearAllRuntimeTargets(): void;
|
|
55
57
|
getRuntimeTarget(name: string): RuntimeDataSourceTarget | undefined;
|
|
@@ -41,9 +41,11 @@ export declare class UnsupportedFeatureError extends Error {
|
|
|
41
41
|
readonly feature: TaurusFeatureName;
|
|
42
42
|
readonly requiredVersion?: string;
|
|
43
43
|
readonly currentVersion?: string;
|
|
44
|
+
readonly parameterHint?: string;
|
|
44
45
|
constructor(feature: TaurusFeatureName, message: string, options?: {
|
|
45
46
|
requiredVersion?: string;
|
|
46
47
|
currentVersion?: string;
|
|
48
|
+
parameterHint?: string;
|
|
47
49
|
cause?: unknown;
|
|
48
50
|
});
|
|
49
51
|
}
|
package/dist/capability/types.js
CHANGED
|
@@ -3,12 +3,14 @@ export class UnsupportedFeatureError extends Error {
|
|
|
3
3
|
feature;
|
|
4
4
|
requiredVersion;
|
|
5
5
|
currentVersion;
|
|
6
|
+
parameterHint;
|
|
6
7
|
constructor(feature, message, options = {}) {
|
|
7
8
|
super(message);
|
|
8
9
|
this.name = "UnsupportedFeatureError";
|
|
9
10
|
this.feature = feature;
|
|
10
11
|
this.requiredVersion = options.requiredVersion;
|
|
11
12
|
this.currentVersion = options.currentVersion;
|
|
13
|
+
this.parameterHint = options.parameterHint;
|
|
12
14
|
if (options.cause !== undefined) {
|
|
13
15
|
this.cause = options.cause;
|
|
14
16
|
}
|
package/dist/config/env.js
CHANGED
|
@@ -120,7 +120,6 @@ export function buildRawConfigFromEnv(env) {
|
|
|
120
120
|
return {
|
|
121
121
|
defaultDatasource: readString(env.TAURUSDB_DEFAULT_DATASOURCE) ?? inferredDatasourceName,
|
|
122
122
|
profilesPath: expandTildePath(readString(env.TAURUSDB_SQL_PROFILES)),
|
|
123
|
-
enableMutations: parseBoolean(env.TAURUSDB_MCP_ENABLE_MUTATIONS, "TAURUSDB_MCP_ENABLE_MUTATIONS"),
|
|
124
123
|
cloud: {
|
|
125
124
|
provider: "huaweicloud",
|
|
126
125
|
region: cloudRegion,
|
package/dist/config/schema.d.ts
CHANGED
|
@@ -2,7 +2,6 @@ import { z } from "zod";
|
|
|
2
2
|
export declare const ConfigSchema: z.ZodObject<{
|
|
3
3
|
defaultDatasource: z.ZodOptional<z.ZodString>;
|
|
4
4
|
profilesPath: z.ZodOptional<z.ZodString>;
|
|
5
|
-
enableMutations: z.ZodDefault<z.ZodBoolean>;
|
|
6
5
|
cloud: z.ZodDefault<z.ZodObject<{
|
|
7
6
|
provider: z.ZodDefault<z.ZodEnum<["huaweicloud"]>>;
|
|
8
7
|
region: z.ZodOptional<z.ZodString>;
|
|
@@ -270,7 +269,6 @@ export declare const ConfigSchema: z.ZodObject<{
|
|
|
270
269
|
} | undefined;
|
|
271
270
|
}>>;
|
|
272
271
|
}, "strip", z.ZodTypeAny, {
|
|
273
|
-
enableMutations: boolean;
|
|
274
272
|
cloud: {
|
|
275
273
|
provider: "huaweicloud";
|
|
276
274
|
domainSuffix: string;
|
|
@@ -344,7 +342,6 @@ export declare const ConfigSchema: z.ZodObject<{
|
|
|
344
342
|
}, {
|
|
345
343
|
defaultDatasource?: string | undefined;
|
|
346
344
|
profilesPath?: string | undefined;
|
|
347
|
-
enableMutations?: boolean | undefined;
|
|
348
345
|
cloud?: {
|
|
349
346
|
provider?: "huaweicloud" | undefined;
|
|
350
347
|
region?: string | undefined;
|
package/dist/config/schema.js
CHANGED
|
@@ -82,7 +82,6 @@ const CesMetricsSourceSchema = z
|
|
|
82
82
|
export const ConfigSchema = z.object({
|
|
83
83
|
defaultDatasource: z.string().min(1).optional(),
|
|
84
84
|
profilesPath: z.string().min(1).optional(),
|
|
85
|
-
enableMutations: z.boolean().default(true),
|
|
86
85
|
cloud: CloudSchema,
|
|
87
86
|
limits: LimitsSchema,
|
|
88
87
|
audit: AuditSchema,
|
package/dist/engine/runtime.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { UnsupportedFeatureError } from "../capability/types.js";
|
|
2
2
|
import { InMemoryConfirmationStore } from "../safety/confirmation-store.js";
|
|
3
|
-
import { buildFlashbackSql, FlashbackNoViewError, flashbackReadonlyOptions, formatTimestamp, resolveRelativeTimestampFromBase, resolveFlashbackTimestamp, } from "../taurus/flashback.js";
|
|
3
|
+
import { buildFlashbackSql, FlashbackNoViewError, flashbackReadonlyOptions, formatTimestamp, normalizeFlashbackWhereClause, resolveRelativeTimestampFromBase, resolveFlashbackTimestamp, } from "../taurus/flashback.js";
|
|
4
4
|
import { buildListRecycleBinSql, buildRestoreRecycleBinTableSql, recycleBinMutationOptions, recycleBinReadonlyOptions } from "../taurus/recycle-bin.js";
|
|
5
5
|
import { normalizeSql, sqlHash } from "../utils/hash.js";
|
|
6
6
|
function resolveConfirmationSql(input) {
|
|
@@ -90,7 +90,8 @@ async function buildFlashbackNoViewError(engine, ctx, input, database, requested
|
|
|
90
90
|
}
|
|
91
91
|
if (input.where?.trim()) {
|
|
92
92
|
try {
|
|
93
|
-
const
|
|
93
|
+
const where = normalizeFlashbackWhereClause(input.where);
|
|
94
|
+
const updatedAtResult = await engine.executor.executeReadonly(`SELECT ${quoteIdentifier("updated_at")} FROM ${quoteIdentifier(database)}.${quoteIdentifier(input.table)} WHERE (${where}) LIMIT 1`, ctx, { maxRows: 1, maxColumns: 1, maxFieldChars: 128 });
|
|
94
95
|
const updatedAtRow = firstRowAsObject(updatedAtResult);
|
|
95
96
|
if (typeof updatedAtRow?.updated_at === "string") {
|
|
96
97
|
details.current_row_updated_at = updatedAtRow.updated_at;
|
|
@@ -286,6 +287,7 @@ export async function flashbackQuery(engine, input, ctx, opts) {
|
|
|
286
287
|
throw new UnsupportedFeatureError("flashback_query", flashbackFeature.reason ??
|
|
287
288
|
`Flashback query requires kernel version >= ${flashbackFeature.minVersion ?? "unknown"}.`, {
|
|
288
289
|
requiredVersion: flashbackFeature.minVersion,
|
|
290
|
+
parameterHint: flashbackFeature.param,
|
|
289
291
|
currentVersion: (await engine.capabilityProbe.getKernelInfo(ctx))
|
|
290
292
|
.kernelVersion,
|
|
291
293
|
});
|
|
@@ -318,6 +320,7 @@ export async function listRecycleBin(engine, ctx, opts) {
|
|
|
318
320
|
throw new UnsupportedFeatureError("recycle_bin", recycleBinFeature.reason ??
|
|
319
321
|
`Recycle bin requires kernel version >= ${recycleBinFeature.minVersion ?? "unknown"}.`, {
|
|
320
322
|
requiredVersion: recycleBinFeature.minVersion,
|
|
323
|
+
parameterHint: recycleBinFeature.param,
|
|
321
324
|
currentVersion: (await engine.capabilityProbe.getKernelInfo(ctx))
|
|
322
325
|
.kernelVersion,
|
|
323
326
|
});
|
|
@@ -331,13 +334,13 @@ export async function restoreRecycleBinTable(engine, input, ctx, opts) {
|
|
|
331
334
|
throw new UnsupportedFeatureError("recycle_bin", recycleBinFeature.reason ??
|
|
332
335
|
`Recycle bin requires kernel version >= ${recycleBinFeature.minVersion ?? "unknown"}.`, {
|
|
333
336
|
requiredVersion: recycleBinFeature.minVersion,
|
|
337
|
+
parameterHint: recycleBinFeature.param,
|
|
334
338
|
currentVersion: (await engine.capabilityProbe.getKernelInfo(ctx))
|
|
335
339
|
.kernelVersion,
|
|
336
340
|
});
|
|
337
341
|
}
|
|
338
342
|
return engine.executor.executeMutation(buildRestoreRecycleBinTableSql(input), ctx, {
|
|
339
343
|
...recycleBinMutationOptions(opts),
|
|
340
|
-
allowWithoutGlobalMutations: true,
|
|
341
344
|
allowReadonlyFallbackForMutations: true,
|
|
342
345
|
});
|
|
343
346
|
}
|
package/dist/engine/types.d.ts
CHANGED
package/dist/engine.js
CHANGED
|
@@ -22,7 +22,6 @@ function toDataSourceInfo(profile, defaultDatasource) {
|
|
|
22
22
|
host: profile.host,
|
|
23
23
|
port: profile.port,
|
|
24
24
|
database: profile.database,
|
|
25
|
-
hasMutationUser: profile.mutationUser !== undefined,
|
|
26
25
|
poolSize: profile.poolSize,
|
|
27
26
|
isDefault: profile.name === defaultDatasource,
|
|
28
27
|
};
|
|
@@ -35,7 +35,6 @@ export interface PoolHealth {
|
|
|
35
35
|
}
|
|
36
36
|
export interface ConnectionPool {
|
|
37
37
|
acquire(datasource: string, mode: PoolMode, opts?: {
|
|
38
|
-
allowWithoutGlobalMutations?: boolean;
|
|
39
38
|
allowReadonlyFallbackForMutations?: boolean;
|
|
40
39
|
}): Promise<Session>;
|
|
41
40
|
release(session: Session): Promise<void>;
|
|
@@ -92,7 +91,6 @@ export declare class ConnectionPoolManager implements ConnectionPool {
|
|
|
92
91
|
private readonly activeSessions;
|
|
93
92
|
constructor(options: ConnectionPoolManagerOptions);
|
|
94
93
|
acquire(datasource: string, mode: PoolMode, opts?: {
|
|
95
|
-
allowWithoutGlobalMutations?: boolean;
|
|
96
94
|
allowReadonlyFallbackForMutations?: boolean;
|
|
97
95
|
}): Promise<Session>;
|
|
98
96
|
release(session: Session): Promise<void>;
|
|
@@ -32,22 +32,12 @@ async function resolveTls(tls, secretResolver) {
|
|
|
32
32
|
}
|
|
33
33
|
return resolved;
|
|
34
34
|
}
|
|
35
|
-
function
|
|
36
|
-
|
|
37
|
-
|
|
35
|
+
function selectCredential(profile, mode, _opts = {}) {
|
|
36
|
+
void mode;
|
|
37
|
+
if (!profile.user) {
|
|
38
|
+
throw new ConnectionPoolError(`Datasource "${profile.name}" does not define SQL credentials. Call begin_sql_login and complete the secure local login form before executing SQL.`);
|
|
38
39
|
}
|
|
39
|
-
|
|
40
|
-
function selectCredential(profile, mode, opts = {}) {
|
|
41
|
-
if (mode === "ro") {
|
|
42
|
-
return profile.readonlyUser;
|
|
43
|
-
}
|
|
44
|
-
if (!profile.mutationUser) {
|
|
45
|
-
if (opts.allowReadonlyFallbackForMutations) {
|
|
46
|
-
return profile.readonlyUser;
|
|
47
|
-
}
|
|
48
|
-
throw new ConnectionPoolError(`Mutation user is not configured for datasource "${profile.name}".`);
|
|
49
|
-
}
|
|
50
|
-
return profile.mutationUser;
|
|
40
|
+
return profile.user;
|
|
51
41
|
}
|
|
52
42
|
async function resolveCredentialValue(ref, secretResolver, context) {
|
|
53
43
|
try {
|
|
@@ -75,9 +65,6 @@ export class ConnectionPoolManager {
|
|
|
75
65
|
if (!profile) {
|
|
76
66
|
throw new ConnectionPoolError(`Datasource profile not found: "${datasource}".`);
|
|
77
67
|
}
|
|
78
|
-
if (mode === "rw" && !opts.allowWithoutGlobalMutations) {
|
|
79
|
-
ensureMutationAllowed(this.config);
|
|
80
|
-
}
|
|
81
68
|
const entry = await this.getOrCreatePool(profile, mode, opts);
|
|
82
69
|
let driverSession;
|
|
83
70
|
try {
|
|
@@ -122,16 +109,7 @@ export class ConnectionPoolManager {
|
|
|
122
109
|
async healthCheck(datasource) {
|
|
123
110
|
const modes = [];
|
|
124
111
|
modes.push(await this.healthCheckMode(datasource, "ro"));
|
|
125
|
-
|
|
126
|
-
modes.push({
|
|
127
|
-
mode: "rw",
|
|
128
|
-
status: "skipped",
|
|
129
|
-
message: "Mutation mode disabled by config.",
|
|
130
|
-
});
|
|
131
|
-
}
|
|
132
|
-
else {
|
|
133
|
-
modes.push(await this.healthCheckMode(datasource, "rw"));
|
|
134
|
-
}
|
|
112
|
+
modes.push(await this.healthCheckMode(datasource, "rw"));
|
|
135
113
|
return {
|
|
136
114
|
datasource,
|
|
137
115
|
checkedAt: new Date().toISOString(),
|
|
@@ -166,9 +144,6 @@ export class ConnectionPoolManager {
|
|
|
166
144
|
return { mode, status: "ok" };
|
|
167
145
|
}
|
|
168
146
|
catch (error) {
|
|
169
|
-
if (mode === "rw" && error instanceof ConnectionPoolError && /not configured/.test(error.message)) {
|
|
170
|
-
return { mode, status: "skipped", message: error.message };
|
|
171
|
-
}
|
|
172
147
|
return {
|
|
173
148
|
mode,
|
|
174
149
|
status: "error",
|
|
@@ -114,7 +114,6 @@ export class SqlExecutorImpl {
|
|
|
114
114
|
const startedAt = this.now();
|
|
115
115
|
const timeoutMs = opts.timeoutMs ?? ctx.limits.timeoutMs;
|
|
116
116
|
const session = await this.connectionPool.acquire(ctx.datasource, "rw", {
|
|
117
|
-
allowWithoutGlobalMutations: opts.allowWithoutGlobalMutations,
|
|
118
117
|
allowReadonlyFallbackForMutations: opts.allowReadonlyFallbackForMutations,
|
|
119
118
|
});
|
|
120
119
|
const active = this.beginQuery(queryId, session, ctx, "rw", startedAt);
|
package/dist/executor/types.d.ts
CHANGED
package/dist/taurus/flashback.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { createSqlParser } from "../safety/parser/index.js";
|
|
2
|
+
import { normalizeSql } from "../utils/hash.js";
|
|
1
3
|
const IDENTIFIER_PATTERN = /^[A-Za-z_][A-Za-z0-9_$]*$/;
|
|
2
4
|
const SQL_TIMESTAMP_PATTERN = /^(\d{4}-\d{2}-\d{2})[ T](\d{2}:\d{2}:\d{2})(?:\.\d{1,6})?$/;
|
|
3
5
|
const RELATIVE_DURATION_PATTERN = /(\d+)\s*(ms|milliseconds?|s|sec|secs|seconds?|m|min|mins|minutes?|h|hr|hrs|hours?|d|days?)/gi;
|
|
@@ -30,6 +32,15 @@ function quoteIdentifier(identifier, fieldName) {
|
|
|
30
32
|
}
|
|
31
33
|
return `\`${identifier}\``;
|
|
32
34
|
}
|
|
35
|
+
export function normalizeFlashbackWhereClause(where) {
|
|
36
|
+
const parser = createSqlParser("mysql");
|
|
37
|
+
const candidate = parser.normalize(`SELECT 1 FROM placeholder WHERE (${where})`);
|
|
38
|
+
const parsed = parser.parse(candidate.normalizedSql);
|
|
39
|
+
if (!parsed.ok || parsed.isMultiStatement || parsed.ast.kind !== "select") {
|
|
40
|
+
throw new Error("Invalid flashback where clause. Provide a single SQL expression.");
|
|
41
|
+
}
|
|
42
|
+
return normalizeSql(where);
|
|
43
|
+
}
|
|
33
44
|
export class FlashbackNoViewError extends Error {
|
|
34
45
|
details;
|
|
35
46
|
constructor(message, details) {
|
|
@@ -129,7 +140,7 @@ export function buildFlashbackSql(input, defaultDatabase, now = Date.now) {
|
|
|
129
140
|
];
|
|
130
141
|
const whereClause = input.where?.trim();
|
|
131
142
|
if (whereClause) {
|
|
132
|
-
clauses.push(`WHERE (${whereClause})`);
|
|
143
|
+
clauses.push(`WHERE (${normalizeFlashbackWhereClause(whereClause)})`);
|
|
133
144
|
}
|
|
134
145
|
if (input.limit !== undefined) {
|
|
135
146
|
if (!Number.isInteger(input.limit) || input.limit <= 0) {
|
|
@@ -137,7 +148,7 @@ export function buildFlashbackSql(input, defaultDatabase, now = Date.now) {
|
|
|
137
148
|
}
|
|
138
149
|
clauses.push(`LIMIT ${input.limit}`);
|
|
139
150
|
}
|
|
140
|
-
return clauses.join(" ");
|
|
151
|
+
return normalizeSql(clauses.join(" "));
|
|
141
152
|
}
|
|
142
153
|
export function flashbackReadonlyOptions(limit) {
|
|
143
154
|
if (limit === undefined) {
|
package/dist/utils/logger.js
CHANGED
|
@@ -8,6 +8,14 @@ const REDACT_PATHS = [
|
|
|
8
8
|
"secret",
|
|
9
9
|
"token",
|
|
10
10
|
"*.token",
|
|
11
|
+
"accessKeyId",
|
|
12
|
+
"*.accessKeyId",
|
|
13
|
+
"secretAccessKey",
|
|
14
|
+
"*.secretAccessKey",
|
|
15
|
+
"securityToken",
|
|
16
|
+
"*.securityToken",
|
|
17
|
+
"authToken",
|
|
18
|
+
"*.authToken",
|
|
11
19
|
];
|
|
12
20
|
function createDefaultDestination() {
|
|
13
21
|
return pino.destination({ fd: 2, sync: false });
|