dsh-plugin-dbgraph-service 0.0.0-stage → 0.1.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/lib/assert.d.ts +18 -0
- package/lib/assert.js +21 -0
- package/lib/brand.d.ts +36 -0
- package/lib/brand.js +35 -0
- package/lib/index.d.ts +256 -0
- package/lib/index.js +530 -0
- package/lib/invariant.d.ts +16 -0
- package/lib/invariant.js +22 -0
- package/lib/schema.d.ts +150 -0
- package/lib/schema.js +152 -0
- package/lib/tokenize.d.ts +45 -0
- package/lib/tokenize.js +205 -0
- package/lib/types.d.ts +598 -0
- package/lib/types.js +30 -0
- package/lib/zh-aliases.d.ts +72 -0
- package/lib/zh-aliases.js +246 -0
- package/package.json +62 -3
- package/README.md +0 -3
package/lib/index.js
ADDED
|
@@ -0,0 +1,530 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Service Definition for the database-graph capability seam (`ctx.dbgraph`).
|
|
3
|
+
*
|
|
4
|
+
* Three registries, each with a different arity rule, because they answer different questions:
|
|
5
|
+
*
|
|
6
|
+
* - **dialects** — one per {@link Engine}. A second dialect for the same engine is a configuration
|
|
7
|
+
* error, not a preference, so registration rejects it (`DBGRAPH_CONFLICT`).
|
|
8
|
+
* - **the store** — exactly one. Connections are data (rows in `connections.json`), not plugins, so
|
|
9
|
+
* nothing needs per-connection routing; a second store would only be ambiguous.
|
|
10
|
+
* - **connections** — zero or more, read from configuration on demand. Several connections being
|
|
11
|
+
* live at once is the normal case, which is the one place this seam deliberately differs from
|
|
12
|
+
* `ctx.codegraph`'s "exactly one claimant per root".
|
|
13
|
+
*
|
|
14
|
+
* The seam itself does no network I/O and no SQLite: dialects reach servers, the store reads the
|
|
15
|
+
* index, and {@link Dbgraph.find} is the only method that composes both — resolve offline, then
|
|
16
|
+
* execute once.
|
|
17
|
+
* @module dsh-plugin-dbgraph-service
|
|
18
|
+
*/
|
|
19
|
+
import { Service } from '@deepseek-ai/cordis';
|
|
20
|
+
import { HarnessError } from '@deepseek-ai/dsh-llm';
|
|
21
|
+
import { ConnectionId, credentialRefName, isValidConnectionId } from "./brand.js";
|
|
22
|
+
import { DEFAULT_ENUM_SAMPLE_LIMIT, DEFAULT_INDEX_STALE_HOURS } from "./schema.js";
|
|
23
|
+
import { ZH_ALIASES } from "./zh-aliases.js";
|
|
24
|
+
import { ENGINES } from "./types.js";
|
|
25
|
+
export { assertNever } from "./assert.js";
|
|
26
|
+
export { ConnectionId, credentialRefName, isValidConnectionId } from "./brand.js";
|
|
27
|
+
export { CONNECTIONS_FILE_NAME, DEFAULT_ENUM_SAMPLE_LIMIT, DEFAULT_INDEX_STALE_HOURS, DEFAULT_STATUS_DIGEST_OBJECTS, INDEX_FILE_NAME, INDEX_ROOT_DIRNAME, INDEX_META_KEYS, INDEX_SCHEMA_DDL, INDEX_SCHEMA_VERSION, STATUS_FILE_NAME, } from "./schema.js";
|
|
28
|
+
export { SENSITIVE_CJK_WORDS, SENSITIVE_COLUMN_PATTERNS, SENSITIVE_COLUMN_TOKENS, ZH_ALIASES, isSensitiveColumn, } from "./zh-aliases.js";
|
|
29
|
+
export { searchDocument, searchMatchQuery, tokenizeForSearch } from "./tokenize.js";
|
|
30
|
+
export { ENGINES, OBJECT_KINDS, EDGE_KINDS } from "./types.js";
|
|
31
|
+
/**
|
|
32
|
+
* Structured database-graph failure. Extends {@link HarnessError} with a stable `code` that callers
|
|
33
|
+
* route on instead of parsing `message`:
|
|
34
|
+
*
|
|
35
|
+
* `DBGRAPH_INVALID_PROVIDER`, `DBGRAPH_INVALID_REQUEST`, `DBGRAPH_CONFLICT`, `DBGRAPH_NO_STORE`,
|
|
36
|
+
* `DBGRAPH_NO_DIALECT`, `DBGRAPH_DRIVER_MISSING`, `DBGRAPH_UNKNOWN_CONNECTION`, `DBGRAPH_UNKNOWN_OBJECT`,
|
|
37
|
+
* `DBGRAPH_UNAVAILABLE`, `DBGRAPH_MALFORMED_INDEX`, `DBGRAPH_QUERY_FAILED`, `DBGRAPH_WRITE_FAILED`,
|
|
38
|
+
* `DBGRAPH_READ_ONLY`, `DBGRAPH_ABORTED`.
|
|
39
|
+
*
|
|
40
|
+
* Three of those are worth telling apart at a call site: `UNAVAILABLE` means the thing is not there
|
|
41
|
+
* (no index file, no server), `MALFORMED_INDEX` means it is there and this build cannot read it, and
|
|
42
|
+
* `READ_ONLY` means the request was refused rather than failing.
|
|
43
|
+
*/
|
|
44
|
+
export class DbgraphError extends HarnessError {
|
|
45
|
+
}
|
|
46
|
+
/** Default rows returned by a `find` or `sql` call when the caller names no limit. */
|
|
47
|
+
export const DEFAULT_MAX_ROWS = 50;
|
|
48
|
+
/** Hard ceiling on rows any one call returns, whatever the caller asks for. */
|
|
49
|
+
export const HARD_MAX_ROWS = 500;
|
|
50
|
+
/** Default per-statement timeout, in milliseconds. */
|
|
51
|
+
export const DEFAULT_QUERY_TIMEOUT_MS = 10_000;
|
|
52
|
+
/**
|
|
53
|
+
* `ctx.dbgraph`. Holds the dialect registry and the single store reservation; every read goes
|
|
54
|
+
* through whatever is registered at call time, so a store that unloads mid-session stops serving
|
|
55
|
+
* without leaving a stale route behind.
|
|
56
|
+
*/
|
|
57
|
+
export class Dbgraph extends Service {
|
|
58
|
+
dialects = new Map();
|
|
59
|
+
store;
|
|
60
|
+
config;
|
|
61
|
+
/**
|
|
62
|
+
* @param ctx - Cordis context.
|
|
63
|
+
* @param config - deployment bounds.
|
|
64
|
+
*/
|
|
65
|
+
constructor(ctx, config = {}) {
|
|
66
|
+
super(ctx, 'dbgraph');
|
|
67
|
+
this.config = config;
|
|
68
|
+
}
|
|
69
|
+
/** Hours after which an L2 snapshot is reported stale. */
|
|
70
|
+
get indexStaleHours() {
|
|
71
|
+
return this.config.indexStaleHours ?? DEFAULT_INDEX_STALE_HOURS;
|
|
72
|
+
}
|
|
73
|
+
/** Rows a call returns when the caller names no limit. */
|
|
74
|
+
get defaultMaxRows() {
|
|
75
|
+
return this.config.defaultMaxRows ?? DEFAULT_MAX_ROWS;
|
|
76
|
+
}
|
|
77
|
+
/** Statement timeout in milliseconds. */
|
|
78
|
+
get queryTimeoutMs() {
|
|
79
|
+
return this.config.queryTimeoutMs ?? DEFAULT_QUERY_TIMEOUT_MS;
|
|
80
|
+
}
|
|
81
|
+
/** Distinct-value sample size requested from a dialect. */
|
|
82
|
+
get enumSampleLimit() {
|
|
83
|
+
return this.config.enumSampleLimit ?? DEFAULT_ENUM_SAMPLE_LIMIT;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Whether `sql` may run a non-read statement. Off unless the deployment says otherwise.
|
|
87
|
+
* @returns the configured value; `false` when unset.
|
|
88
|
+
*/
|
|
89
|
+
get allowWrites() {
|
|
90
|
+
return this.config.allowWrites === true;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* The index root the registered store reads and writes, when one is mounted.
|
|
94
|
+
*
|
|
95
|
+
* The prompt digest and the `backgroundIndex` decision read the sidecar synchronously from this
|
|
96
|
+
* path. Deriving it here from the store — rather than each caller deriving its own from the
|
|
97
|
+
* environment — is what keeps the digest and the index describing the same directory.
|
|
98
|
+
* @returns the absolute root, or `undefined` when no store is registered.
|
|
99
|
+
*/
|
|
100
|
+
get storeRoot() {
|
|
101
|
+
return this.store?.root;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Register one engine's dialect.
|
|
105
|
+
* @param dialect - the dialect to serve.
|
|
106
|
+
* @returns the disposer that unregisters it.
|
|
107
|
+
*/
|
|
108
|
+
registerDialect(dialect) {
|
|
109
|
+
const engine = dialect.engine;
|
|
110
|
+
if (!ENGINES.includes(engine)) {
|
|
111
|
+
throw new DbgraphError(`"${String(engine)}" is not a known database engine`, 'DBGRAPH_INVALID_PROVIDER');
|
|
112
|
+
}
|
|
113
|
+
if (this.dialects.has(engine)) {
|
|
114
|
+
throw new DbgraphError(`a dialect for "${engine}" is already registered`, 'DBGRAPH_CONFLICT');
|
|
115
|
+
}
|
|
116
|
+
const dispose = this.ctx.effect(function* () {
|
|
117
|
+
this.dialects.set(engine, dialect);
|
|
118
|
+
yield () => {
|
|
119
|
+
this.dialects.delete(engine);
|
|
120
|
+
};
|
|
121
|
+
}.bind(this), 'dbgraph.registerDialect()');
|
|
122
|
+
return () => void dispose();
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Register the offline index store.
|
|
126
|
+
* @param store - the store to serve every read and write.
|
|
127
|
+
* @returns the disposer that unregisters it.
|
|
128
|
+
*/
|
|
129
|
+
registerStore(store) {
|
|
130
|
+
if (this.store !== undefined && this.store !== store) {
|
|
131
|
+
throw new DbgraphError(`a database-graph store ("${this.store.id}") is already registered`, 'DBGRAPH_CONFLICT');
|
|
132
|
+
}
|
|
133
|
+
const dispose = this.ctx.effect(function* () {
|
|
134
|
+
this.store = store;
|
|
135
|
+
yield () => {
|
|
136
|
+
if (this.store === store)
|
|
137
|
+
this.store = undefined;
|
|
138
|
+
};
|
|
139
|
+
}.bind(this), 'dbgraph.registerStore()');
|
|
140
|
+
return () => void dispose();
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Every configured connection with its index state.
|
|
144
|
+
* @returns one entry per connection, secrets omitted.
|
|
145
|
+
*/
|
|
146
|
+
async listConnections() {
|
|
147
|
+
const stored = await this.readConnections();
|
|
148
|
+
const credentials = this.credentialSeam();
|
|
149
|
+
const summaries = [];
|
|
150
|
+
for (const entry of stored) {
|
|
151
|
+
summaries.push({
|
|
152
|
+
id: entry.id,
|
|
153
|
+
engine: entry.engine,
|
|
154
|
+
...(entry.label === undefined ? {} : { label: entry.label }),
|
|
155
|
+
...(entry.host === undefined ? {} : { host: entry.host }),
|
|
156
|
+
...(entry.database === undefined ? {} : { database: entry.database }),
|
|
157
|
+
credentialConfigured: await this.describeCredential(credentials, entry),
|
|
158
|
+
index: await this.indexStateOf(entry.id),
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
return summaries;
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Structural overview of one connection, from the index.
|
|
165
|
+
* @param request - the connection to describe.
|
|
166
|
+
* @param signal - aborts the read.
|
|
167
|
+
* @returns the overview.
|
|
168
|
+
*/
|
|
169
|
+
async schema(request, signal) {
|
|
170
|
+
throwIfAborted(signal);
|
|
171
|
+
return (await this.requireStore()).schema(request);
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Ranked object listing for one connection, from the index.
|
|
175
|
+
* @param request - the listing request.
|
|
176
|
+
* @param signal - aborts the read.
|
|
177
|
+
* @returns the listing.
|
|
178
|
+
*/
|
|
179
|
+
async objects(request, signal) {
|
|
180
|
+
throwIfAborted(signal);
|
|
181
|
+
return (await this.requireStore()).objects(request);
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* One object's columns, from the index.
|
|
185
|
+
* @param request - the columns request.
|
|
186
|
+
* @param signal - aborts the read.
|
|
187
|
+
* @returns the column list.
|
|
188
|
+
*/
|
|
189
|
+
async columns(request, signal) {
|
|
190
|
+
throwIfAborted(signal);
|
|
191
|
+
return (await this.requireStore()).columns(request);
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Resolve words to an object and columns offline, then execute one parameterized read.
|
|
195
|
+
*
|
|
196
|
+
* This is the method the plugin exists for: zero round-trips learning the schema, exactly one
|
|
197
|
+
* round-trip fetching the answer.
|
|
198
|
+
* @param request - what the user asked about.
|
|
199
|
+
* @param signal - aborts both the resolution and the execution.
|
|
200
|
+
* @returns the rows, or why there are none.
|
|
201
|
+
*/
|
|
202
|
+
async find(request, signal) {
|
|
203
|
+
const started = Date.now();
|
|
204
|
+
throwIfAborted(signal);
|
|
205
|
+
const store = await this.requireStore();
|
|
206
|
+
const resolution = await store.resolve(request);
|
|
207
|
+
const failed = {
|
|
208
|
+
status: resolution.status,
|
|
209
|
+
reason: resolution.reason,
|
|
210
|
+
confidence: resolution.confidence,
|
|
211
|
+
...(resolution.candidates === undefined ? {} : { candidates: resolution.candidates }),
|
|
212
|
+
...(resolution.resolved === undefined ? {} : { resolved: resolution.resolved }),
|
|
213
|
+
elapsedMs: Date.now() - started,
|
|
214
|
+
};
|
|
215
|
+
if (resolution.status !== 'resolved' || resolution.resolved === undefined)
|
|
216
|
+
return failed;
|
|
217
|
+
const stored = await this.requireConnection(request.connection);
|
|
218
|
+
const engine = stored.engine;
|
|
219
|
+
const dialect = await this.requireDialect(engine);
|
|
220
|
+
const limit = clampLimit(request.limit, this.defaultMaxRows);
|
|
221
|
+
const columns = resolution.resolved.columns.map(column => quoteIdentifier(engine, column.column));
|
|
222
|
+
const clause = resolution.predicate === undefined || resolution.predicate.clause === ''
|
|
223
|
+
? ''
|
|
224
|
+
: ` ${resolution.predicate.clause}`;
|
|
225
|
+
const sql = `SELECT ${columns.length === 0 ? '*' : columns.join(', ')}`
|
|
226
|
+
+ ` FROM ${qualifiedName(engine, resolution.resolved.object)}${clause} LIMIT ${limit}`;
|
|
227
|
+
const executed = await dialect.execute(this.dialectInput(stored, await this.passwordOf(stored), signal), sql, { maxRows: limit, timeoutMs: this.queryTimeoutMs, readOnly: true }, resolution.predicate?.values ?? []);
|
|
228
|
+
return {
|
|
229
|
+
status: 'resolved',
|
|
230
|
+
reason: resolution.reason,
|
|
231
|
+
resolved: resolution.resolved,
|
|
232
|
+
confidence: resolution.confidence,
|
|
233
|
+
sql: renderStatement(sql, resolution.predicate?.values ?? []),
|
|
234
|
+
rows: executed.rows,
|
|
235
|
+
rowCount: executed.rowCount,
|
|
236
|
+
truncated: executed.truncated,
|
|
237
|
+
redacted: executed.redacted,
|
|
238
|
+
elapsedMs: Date.now() - started,
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Execute one statement against the server, with no semantic resolution.
|
|
243
|
+
* @param request - the statement and its limits.
|
|
244
|
+
* @param signal - aborts the execution.
|
|
245
|
+
* @returns the result set.
|
|
246
|
+
*/
|
|
247
|
+
async sql(request, signal) {
|
|
248
|
+
throwIfAborted(signal);
|
|
249
|
+
const stored = await this.requireConnection(request.connection);
|
|
250
|
+
const engine = stored.engine;
|
|
251
|
+
const dialect = await this.requireDialect(engine);
|
|
252
|
+
return dialect.execute(this.dialectInput(stored, await this.passwordOf(stored), signal), request.query, {
|
|
253
|
+
maxRows: clampLimit(request.limit, this.defaultMaxRows),
|
|
254
|
+
timeoutMs: request.timeoutMs ?? this.queryTimeoutMs,
|
|
255
|
+
// Never derived from the connection's masking mode: masking decides what a result shows,
|
|
256
|
+
// not whether a statement may write. `allowWrites` is the one switch that opens the gate.
|
|
257
|
+
readOnly: !this.allowWrites,
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Objects a change to one object reaches, from the index.
|
|
262
|
+
* @param request - the object to trace.
|
|
263
|
+
* @param signal - aborts the read.
|
|
264
|
+
* @returns the affected objects.
|
|
265
|
+
*/
|
|
266
|
+
async impact(request, signal) {
|
|
267
|
+
throwIfAborted(signal);
|
|
268
|
+
return (await this.requireStore()).impact(request);
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Objects referencing one object, from the index.
|
|
272
|
+
* @param request - the object to trace.
|
|
273
|
+
* @param signal - aborts the read.
|
|
274
|
+
* @returns the referencing objects.
|
|
275
|
+
*/
|
|
276
|
+
async refs(request, signal) {
|
|
277
|
+
throwIfAborted(signal);
|
|
278
|
+
return (await this.requireStore()).refs(request);
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* Build or refresh an index. With no `connection`, every configured connection is indexed and the
|
|
282
|
+
* first report is returned; use {@link indexMany} when the caller needs all of them.
|
|
283
|
+
* @param request - which connection and how deep.
|
|
284
|
+
* @param signal - aborts the run.
|
|
285
|
+
* @returns the first connection's report.
|
|
286
|
+
*/
|
|
287
|
+
async index(request, signal) {
|
|
288
|
+
const reports = await this.indexMany(request, signal);
|
|
289
|
+
const first = reports[0];
|
|
290
|
+
if (first === undefined) {
|
|
291
|
+
throw new DbgraphError('no connection was configured to index', 'DBGRAPH_UNKNOWN_CONNECTION');
|
|
292
|
+
}
|
|
293
|
+
return first;
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* Index every target connection in one call.
|
|
297
|
+
* @param request - which connections and how deep.
|
|
298
|
+
* @param signal - aborts the run.
|
|
299
|
+
* @returns one report per connection, in configuration order.
|
|
300
|
+
*/
|
|
301
|
+
async indexMany(request, signal) {
|
|
302
|
+
const store = await this.requireStore();
|
|
303
|
+
const targets = request.connection === undefined
|
|
304
|
+
? await this.readConnections()
|
|
305
|
+
: [await this.requireConnection(request.connection)];
|
|
306
|
+
const reports = [];
|
|
307
|
+
for (const target of targets) {
|
|
308
|
+
throwIfAborted(signal);
|
|
309
|
+
reports.push(await this.indexOne(store, target, request, signal));
|
|
310
|
+
}
|
|
311
|
+
return reports;
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* Resolve one connection's password through the host credential seam.
|
|
315
|
+
* @param connection - the connection to resolve for.
|
|
316
|
+
* @returns the plaintext password, or undefined when the seam is absent or the ref is unconfigured.
|
|
317
|
+
*/
|
|
318
|
+
async passwordOf(connection) {
|
|
319
|
+
const credentials = this.credentialSeam();
|
|
320
|
+
if (credentials === undefined)
|
|
321
|
+
return undefined;
|
|
322
|
+
const ref = connection.credentialRef ?? credentialRefName(ConnectionId(connection.id));
|
|
323
|
+
return (await credentials.resolve(ref))?.value;
|
|
324
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* The configured connections, as the store's configuration file lists them.
|
|
327
|
+
* @returns the connection list in configuration order.
|
|
328
|
+
*/
|
|
329
|
+
async readConnections() {
|
|
330
|
+
return (await this.requireStore()).connections();
|
|
331
|
+
}
|
|
332
|
+
async indexOne(store, connection, request, signal) {
|
|
333
|
+
const dialect = await this.requireDialect(connection.engine);
|
|
334
|
+
const input = await this.dialectInputFor(connection, signal);
|
|
335
|
+
const started = Date.now();
|
|
336
|
+
const warnings = [];
|
|
337
|
+
const l1 = await dialect.fetchL1(input);
|
|
338
|
+
let catalog = l1;
|
|
339
|
+
if (request.mode !== 'l1') {
|
|
340
|
+
try {
|
|
341
|
+
catalog = await dialect.fetchL2(input, l1);
|
|
342
|
+
}
|
|
343
|
+
catch (error) {
|
|
344
|
+
// An enrichment failure must not throw away the structural snapshot already in hand: an L1
|
|
345
|
+
// index answers "which columns exist", which is most of the value. Record why and continue.
|
|
346
|
+
warnings.push(`enrichment failed, index stays at L1: ${messageOf(error)}`);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
const report = await store.write(ConnectionId(connection.id), catalog, {
|
|
350
|
+
writer: request.writer ?? 'auto',
|
|
351
|
+
aliases: ZH_ALIASES,
|
|
352
|
+
});
|
|
353
|
+
return { ...report, elapsedMs: Date.now() - started, warnings: [...report.warnings, ...warnings] };
|
|
354
|
+
}
|
|
355
|
+
async dialectInputFor(connection, signal) {
|
|
356
|
+
const config = {
|
|
357
|
+
id: connection.id,
|
|
358
|
+
engine: connection.engine,
|
|
359
|
+
...(connection.label === undefined ? {} : { label: connection.label }),
|
|
360
|
+
...(connection.host === undefined ? {} : { host: connection.host }),
|
|
361
|
+
...(connection.port === undefined ? {} : { port: connection.port }),
|
|
362
|
+
...(connection.database === undefined ? {} : { database: connection.database }),
|
|
363
|
+
...(connection.user === undefined ? {} : { user: connection.user }),
|
|
364
|
+
...(connection.credentialRef === undefined ? {} : { credentialRef: connection.credentialRef }),
|
|
365
|
+
...(connection.sensitiveColumns === 'show' ? { sensitiveColumns: 'show' } : {}),
|
|
366
|
+
...(connection.sensitivePatterns === undefined ? {} : { sensitivePatterns: connection.sensitivePatterns }),
|
|
367
|
+
};
|
|
368
|
+
const password = await this.passwordOf(connection);
|
|
369
|
+
return {
|
|
370
|
+
connection: config,
|
|
371
|
+
...(password === undefined ? {} : { password }),
|
|
372
|
+
enumSampleLimit: this.enumSampleLimit,
|
|
373
|
+
queryTimeoutMs: this.queryTimeoutMs,
|
|
374
|
+
...(signal === undefined ? {} : { signal }),
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
dialectInput(connection, password, signal) {
|
|
378
|
+
const config = {
|
|
379
|
+
id: connection.id,
|
|
380
|
+
engine: connection.engine,
|
|
381
|
+
...(connection.label === undefined ? {} : { label: connection.label }),
|
|
382
|
+
...(connection.host === undefined ? {} : { host: connection.host }),
|
|
383
|
+
...(connection.port === undefined ? {} : { port: connection.port }),
|
|
384
|
+
...(connection.database === undefined ? {} : { database: connection.database }),
|
|
385
|
+
...(connection.user === undefined ? {} : { user: connection.user }),
|
|
386
|
+
...(connection.credentialRef === undefined ? {} : { credentialRef: connection.credentialRef }),
|
|
387
|
+
...(connection.sensitiveColumns === 'show' ? { sensitiveColumns: 'show' } : {}),
|
|
388
|
+
...(connection.sensitivePatterns === undefined ? {} : { sensitivePatterns: connection.sensitivePatterns }),
|
|
389
|
+
};
|
|
390
|
+
return {
|
|
391
|
+
connection: config,
|
|
392
|
+
...(password === undefined ? {} : { password }),
|
|
393
|
+
enumSampleLimit: this.enumSampleLimit,
|
|
394
|
+
queryTimeoutMs: this.queryTimeoutMs,
|
|
395
|
+
...(signal === undefined ? {} : { signal }),
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
async indexStateOf(id) {
|
|
399
|
+
const store = this.store;
|
|
400
|
+
if (store === undefined)
|
|
401
|
+
return { objects: 0, columns: 0, stale: true };
|
|
402
|
+
return store.state(id);
|
|
403
|
+
}
|
|
404
|
+
async describeCredential(credentials, connection) {
|
|
405
|
+
if (credentials === undefined)
|
|
406
|
+
return false;
|
|
407
|
+
const ref = connection.credentialRef ?? credentialRefName(ConnectionId(connection.id));
|
|
408
|
+
try {
|
|
409
|
+
return (await credentials.describe(ref)).configured;
|
|
410
|
+
}
|
|
411
|
+
catch {
|
|
412
|
+
// A provider that throws is "not configured" for a listing: a status line must never fail.
|
|
413
|
+
return false;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
/**
|
|
417
|
+
* The host's credential seam, when this composition has one. Looked up per call rather than
|
|
418
|
+
* captured, so a profile that mounts it later starts working without a restart.
|
|
419
|
+
* @returns the seam, or undefined.
|
|
420
|
+
*/
|
|
421
|
+
credentialSeam() {
|
|
422
|
+
const lookup = this.ctx.get;
|
|
423
|
+
if (typeof lookup !== 'function')
|
|
424
|
+
return undefined;
|
|
425
|
+
const seam = lookup.call(this.ctx, 'credentials');
|
|
426
|
+
return seam === undefined ? undefined : seam;
|
|
427
|
+
}
|
|
428
|
+
async requireStore() {
|
|
429
|
+
if (this.store === undefined) {
|
|
430
|
+
throw new DbgraphError('no database-graph store is registered; mount dsh-plugin-dbgraph-store', 'DBGRAPH_NO_STORE');
|
|
431
|
+
}
|
|
432
|
+
return this.store;
|
|
433
|
+
}
|
|
434
|
+
async requireDialect(engine) {
|
|
435
|
+
const dialect = this.dialects.get(engine);
|
|
436
|
+
if (dialect === undefined) {
|
|
437
|
+
throw new DbgraphError(`no dialect for "${engine}" is registered; mount a catalog package that provides it`, 'DBGRAPH_NO_DIALECT');
|
|
438
|
+
}
|
|
439
|
+
return dialect;
|
|
440
|
+
}
|
|
441
|
+
async requireConnection(id) {
|
|
442
|
+
if (!isValidConnectionId(id)) {
|
|
443
|
+
throw new DbgraphError(`"${id}" is not a usable connection id`, 'DBGRAPH_UNKNOWN_CONNECTION');
|
|
444
|
+
}
|
|
445
|
+
const all = await this.readConnections();
|
|
446
|
+
const found = all.find(entry => entry.id === id);
|
|
447
|
+
if (found === undefined) {
|
|
448
|
+
throw new DbgraphError(`no connection "${id}" is configured (known: ${all.map(entry => entry.id).join(', ') || 'none'})`, 'DBGRAPH_UNKNOWN_CONNECTION');
|
|
449
|
+
}
|
|
450
|
+
return found;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
/**
|
|
454
|
+
* Quote an identifier the way an engine expects.
|
|
455
|
+
* @param engine - which dialect's quoting rules to apply.
|
|
456
|
+
* @param name - the identifier.
|
|
457
|
+
* @returns the quoted identifier.
|
|
458
|
+
*/
|
|
459
|
+
export function quoteIdentifier(engine, name) {
|
|
460
|
+
if (engine === 'mysql')
|
|
461
|
+
return `\`${name.replace(/`/g, '``')}\``;
|
|
462
|
+
if (engine === 'mongodb' || engine === 'redis')
|
|
463
|
+
return name;
|
|
464
|
+
return `"${name.replace(/"/g, '""')}"`;
|
|
465
|
+
}
|
|
466
|
+
/**
|
|
467
|
+
* Render a fully qualified object name.
|
|
468
|
+
* @param engine - which dialect's quoting rules to apply.
|
|
469
|
+
* @param object - name, possibly schema-qualified.
|
|
470
|
+
* @returns the quoted, dotted name.
|
|
471
|
+
*/
|
|
472
|
+
export function qualifiedName(engine, object) {
|
|
473
|
+
return object.split('.').map(part => quoteIdentifier(engine, part)).join('.');
|
|
474
|
+
}
|
|
475
|
+
/**
|
|
476
|
+
* Render a statement with its bound values substituted, for display only.
|
|
477
|
+
*
|
|
478
|
+
* The result is never executed: it exists so a model reading the answer can see what condition was
|
|
479
|
+
* applied without guessing. Every value is rendered as a quoted literal.
|
|
480
|
+
* @param sql - the statement with `?` placeholders.
|
|
481
|
+
* @param values - the bound values, in placeholder order.
|
|
482
|
+
* @returns the statement with literals substituted.
|
|
483
|
+
*/
|
|
484
|
+
export function renderStatement(sql, values) {
|
|
485
|
+
let index = 0;
|
|
486
|
+
return sql.replace(/\?/g, () => {
|
|
487
|
+
const value = values[index];
|
|
488
|
+
index += 1;
|
|
489
|
+
if (value === null || value === undefined)
|
|
490
|
+
return 'NULL';
|
|
491
|
+
if (typeof value === 'number')
|
|
492
|
+
return String(value);
|
|
493
|
+
return `'${String(value).replace(/'/g, "''")}'`;
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
/**
|
|
497
|
+
* Clamp a caller's row limit into the configured ceiling.
|
|
498
|
+
* @param requested - the caller's value, when one was named.
|
|
499
|
+
* @param fallback - the configured default.
|
|
500
|
+
* @returns a limit between 1 and {@link HARD_MAX_ROWS}.
|
|
501
|
+
*/
|
|
502
|
+
export function clampLimit(requested, fallback) {
|
|
503
|
+
const value = requested ?? fallback;
|
|
504
|
+
if (!Number.isFinite(value) || value < 1)
|
|
505
|
+
return Math.min(fallback, HARD_MAX_ROWS);
|
|
506
|
+
return Math.min(Math.floor(value), HARD_MAX_ROWS);
|
|
507
|
+
}
|
|
508
|
+
/**
|
|
509
|
+
* Throw when a call was aborted before it started.
|
|
510
|
+
* @param signal - the caller's abort signal, when one was supplied.
|
|
511
|
+
*/
|
|
512
|
+
function throwIfAborted(signal) {
|
|
513
|
+
if (signal?.aborted === true) {
|
|
514
|
+
throw new DbgraphError('the call was aborted', 'DBGRAPH_ABORTED');
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
/**
|
|
518
|
+
* One error's message, for appending to a warning list.
|
|
519
|
+
* @param error - the thrown value.
|
|
520
|
+
* @returns a non-empty message.
|
|
521
|
+
*/
|
|
522
|
+
function messageOf(error) {
|
|
523
|
+
if (error instanceof Error)
|
|
524
|
+
return error.message;
|
|
525
|
+
if (typeof error === 'string')
|
|
526
|
+
return error;
|
|
527
|
+
return JSON.stringify(error) ?? 'unknown error';
|
|
528
|
+
}
|
|
529
|
+
export default Dbgraph;
|
|
530
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Package-owned invariant companion for `dsh-plugin-dbgraph-service`.
|
|
3
|
+
* @module dsh-plugin-dbgraph-service/invariant
|
|
4
|
+
*/
|
|
5
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
6
|
+
/** Cordis companion plugin name. */
|
|
7
|
+
export declare const name = "dbgraph-invariant";
|
|
8
|
+
/** Service required before the companion can reserve package ownership. */
|
|
9
|
+
export declare const inject: string[];
|
|
10
|
+
/**
|
|
11
|
+
* Register this package's invariant companion.
|
|
12
|
+
* @param ctx - Cordis context carrying the invariant service.
|
|
13
|
+
* @returns the installed registration's disposer after setup succeeds.
|
|
14
|
+
*/
|
|
15
|
+
export declare const apply: (ctx: Context) => Promise<() => void>;
|
|
16
|
+
//# sourceMappingURL=invariant.d.ts.map
|
package/lib/invariant.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Package-owned invariant companion for `dsh-plugin-dbgraph-service`.
|
|
3
|
+
* @module dsh-plugin-dbgraph-service/invariant
|
|
4
|
+
*/
|
|
5
|
+
const PACKAGE_NAME = 'dsh-plugin-dbgraph-service';
|
|
6
|
+
/** Cordis companion plugin name. */
|
|
7
|
+
export const name = 'dbgraph-invariant';
|
|
8
|
+
/** Service required before the companion can reserve package ownership. */
|
|
9
|
+
export const inject = ['invariants'];
|
|
10
|
+
/**
|
|
11
|
+
* No runtime invariant: the dialect registry and the single store slot are private state resolved
|
|
12
|
+
* per call, and the seam emits no lifecycle event an owned relation could be compared against.
|
|
13
|
+
*/
|
|
14
|
+
const install = () => { };
|
|
15
|
+
/**
|
|
16
|
+
* Register this package's invariant companion.
|
|
17
|
+
* @param ctx - Cordis context carrying the invariant service.
|
|
18
|
+
* @returns the installed registration's disposer after setup succeeds.
|
|
19
|
+
*/
|
|
20
|
+
export const apply = (ctx) => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install));
|
|
21
|
+
/* jscpd:ignore-end */
|
|
22
|
+
//# sourceMappingURL=invariant.js.map
|