@elkindev/dbgraph 1.0.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/LICENSE +21 -0
- package/README.md +317 -0
- package/dist/chunk-2ESYSVXG.js +48 -0
- package/dist/chunk-CK6XTW3E.js +31 -0
- package/dist/chunk-JN26WCE7.js +231 -0
- package/dist/chunk-PQBJ4PR5.js +7994 -0
- package/dist/chunk-PXNV4PMV.js +3790 -0
- package/dist/chunk-TH4OVS4W.js +119 -0
- package/dist/cli.js +1827 -0
- package/dist/connectivity-outcome-GLCSH4UL.js +7 -0
- package/dist/errors-IVGCGSZR.js +31 -0
- package/dist/esm-WVQHRQ3J.js +5195 -0
- package/dist/index.cjs +125968 -0
- package/dist/index.d.cts +2692 -0
- package/dist/index.d.ts +2692 -0
- package/dist/index.js +8230 -0
- package/dist/lib-HSEC5IHY.js +32422 -0
- package/dist/mcp.js +7886 -0
- package/dist/mysql2-JRR5EBJC.js +15624 -0
- package/dist/open-WLBPYZLW.js +602 -0
- package/dist/parse-config-5TLAZCR3.js +8 -0
- package/dist/src-5YCYYWGV.js +147 -0
- package/dist/tedious-3WZBQM6C.js +59836 -0
- package/package.json +72 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ElkinDev
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
# dbgraph
|
|
2
|
+
|
|
3
|
+
> A codegraph for your database. dbgraph indexes a database catalog into a local
|
|
4
|
+
> graph of tables, columns, constraints, views, procedures, functions, triggers and
|
|
5
|
+
> their relationships, and serves it to AI agents over MCP — so one tool call answers
|
|
6
|
+
> what would otherwise cost several exploratory queries.
|
|
7
|
+
|
|
8
|
+
dbgraph reads a database's catalog (never its data), normalizes it into an
|
|
9
|
+
engine-agnostic graph, and persists that graph in a local SQLite + FTS5 index. AI
|
|
10
|
+
agents then query the graph over the Model Context Protocol (MCP) instead of
|
|
11
|
+
re-discovering the schema by hand.
|
|
12
|
+
|
|
13
|
+
- **Read-only by construction.** No command issues any write, DDL or DML against the
|
|
14
|
+
target database — only catalog `SELECT`s through the adapter port
|
|
15
|
+
(`openspec/specs/cli-config/spec.md` — "Read-only-against-target is INVIOLABLE").
|
|
16
|
+
- **100% local.** The graph lives in a local SQLite + FTS5 index; nothing leaves the
|
|
17
|
+
machine (`openspec/specs/graph-storage/spec.md`).
|
|
18
|
+
- **Served over MCP.** The eight tools `dbgraph_explore`, `dbgraph_search`,
|
|
19
|
+
`dbgraph_object`, `dbgraph_related`, `dbgraph_impact`, `dbgraph_path`,
|
|
20
|
+
`dbgraph_precheck` and `dbgraph_status` expose the graph to agents over the official
|
|
21
|
+
`@modelcontextprotocol/sdk` stdio transport
|
|
22
|
+
(`openspec/specs/mcp-server/spec.md` — Purpose).
|
|
23
|
+
|
|
24
|
+
> **Status.** This repository runs **from source today**. There is no published npm
|
|
25
|
+
> package or binary yet — the release workflow exists but has never been fired (see
|
|
26
|
+
> [Limitations](#limitations)). Standalone win-x64 / linux-x64 binaries are planned for
|
|
27
|
+
> v1.0.
|
|
28
|
+
|
|
29
|
+
## What it models
|
|
30
|
+
|
|
31
|
+
Every catalog object becomes a node; every relationship becomes an edge.
|
|
32
|
+
|
|
33
|
+
- **Nodes** (`openspec/specs/graph-model/spec.md` — Node taxonomy): `database`,
|
|
34
|
+
`schema`, `table`, `column`, `view`, `trigger`, `procedure`, `function`, `index`,
|
|
35
|
+
and `field` (for document stores).
|
|
36
|
+
- **Edges** (`openspec/specs/graph-model/spec.md` — Edge taxonomy): `references`
|
|
37
|
+
(declared foreign keys, plus an aggregated table→table form), `depends_on` (view
|
|
38
|
+
dependencies), `reads_from` / `writes_to` (parsed from module bodies), `fires_on`
|
|
39
|
+
(triggers, carrying the DML event), `indexes`, and `inferred_reference` (opt-in,
|
|
40
|
+
carrying `confidence: inferred` and a numeric `score`).
|
|
41
|
+
|
|
42
|
+
Every edge is classified by `confidence` — `declared`, `parsed`, or `inferred` — so
|
|
43
|
+
the graph never hides how it knows something. A module whose body cannot be analyzed
|
|
44
|
+
is flagged `has_dynamic_sql: true` rather than guessing its edges
|
|
45
|
+
(`openspec/specs/graph-normalization/spec.md` — "Dynamic SQL declares blindness").
|
|
46
|
+
|
|
47
|
+
## Feature matrix
|
|
48
|
+
|
|
49
|
+
Five engines are supported. Each cell below is transcribed from the engine's canonical
|
|
50
|
+
extraction spec and its truthful `CapabilityMatrix` — not from memory.
|
|
51
|
+
|
|
52
|
+
| Engine | Tables & columns | Views | Indexes | Constraints / FKs | Procedures / functions | Triggers | Inferred relationships | Connectivity |
|
|
53
|
+
|--------|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---|
|
|
54
|
+
| **SQLite** | ✓ | ✓ | ✓ | FK¹ | — | ✓ | ○ | local file (`better-sqlite3` / `node:sqlite`) |
|
|
55
|
+
| **SQL Server** | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ○ | SQL auth · NTLM · integrated (via `sqlcmd`) |
|
|
56
|
+
| **PostgreSQL** | ✓ | ✓² | ✓ | ✓ | ✓ | ✓ | ○ | host/port/db/user/password · optional SSL |
|
|
57
|
+
| **MySQL / MariaDB** | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ○ | host/port/db/user/password · optional SSL |
|
|
58
|
+
| **MongoDB** | collections + sampled fields³ | — | ✓ | —⁴ | — | — | ✓⁵ | `${env:VAR}` URI · database · sampleSize · TLS |
|
|
59
|
+
|
|
60
|
+
**Legend:** ✓ extracted · ○ opt-in, off by default · — not supported by the engine
|
|
61
|
+
(explicitly declared unsupported in its `CapabilityMatrix`).
|
|
62
|
+
|
|
63
|
+
**Per-cell sources:**
|
|
64
|
+
|
|
65
|
+
- **SQLite** — tables/columns, foreign keys (incl. composite), indexes
|
|
66
|
+
(unique/partial/expression), views and triggers per
|
|
67
|
+
`openspec/specs/sqlite-extraction/spec.md`. Its `CapabilityMatrix` declares
|
|
68
|
+
procedures, functions, sequences and collections **unsupported** (same spec —
|
|
69
|
+
"Truthful SQLite CapabilityMatrix"; `openspec/specs/cli-config/spec.md` — "SQLite
|
|
70
|
+
offers no procedures").
|
|
71
|
+
¹ SQLite exposes foreign keys (including composite) and primary-key membership on
|
|
72
|
+
columns; it has no separate named CHECK/UNIQUE constraint objects.
|
|
73
|
+
- **SQL Server** — tables/columns (types, nullability, defaults, computed); PK, FK
|
|
74
|
+
(composite), unique and check constraints; indexes (clustered/nonclustered, filtered,
|
|
75
|
+
included); views, procedures, functions and triggers with bodies per level; trigger
|
|
76
|
+
`fires_on` plus its read/write effects — per
|
|
77
|
+
`openspec/specs/mssql-extraction/spec.md`. Connectivity via SQL auth and NTLM, with
|
|
78
|
+
integrated security handled through an external `sqlcmd` strategy (same spec; Kerberos
|
|
79
|
+
SSO is unsupported).
|
|
80
|
+
- **PostgreSQL** — tables/columns (types, nullability, defaults, identity, generated);
|
|
81
|
+
PK, FK (composite), unique and CHECK constraints; indexes (partial, expression,
|
|
82
|
+
included); functions and procedures; triggers (event + timing) — per
|
|
83
|
+
`openspec/specs/pg-extraction/spec.md`.
|
|
84
|
+
² Views include materialized views.
|
|
85
|
+
- **MySQL / MariaDB** — tables/columns (types, nullability, defaults, AUTO_INCREMENT);
|
|
86
|
+
PK, FK (composite), unique and CHECK constraints; indexes (composite, uniqueness);
|
|
87
|
+
views, functions, procedures and triggers — per
|
|
88
|
+
`openspec/specs/mysql-extraction/spec.md`.
|
|
89
|
+
- **MongoDB** — collections via `listCollections`, with field structure **inferred by
|
|
90
|
+
sampling** documents through `$sample` (dotted paths, union BSON types, presence
|
|
91
|
+
frequency); indexes via `listIndexes`; `$jsonSchema` validators carried in `extra` —
|
|
92
|
+
per `openspec/specs/mongodb-extraction/spec.md`. Its `CapabilityMatrix` declares
|
|
93
|
+
`table`, `column`, `constraint`, `view`, `procedure`, `function`, `trigger` and
|
|
94
|
+
`sequence` **unsupported** (`supportsBodies: false`; same spec — "Truthful MongoDB
|
|
95
|
+
CapabilityMatrix"; `openspec/specs/cli-config/spec.md` — "MongoDB offers no
|
|
96
|
+
triggers").
|
|
97
|
+
³ Field structure is a statistical inference over a finite sample — honest only about
|
|
98
|
+
what the sample observed. Sampled **values are never persisted** (only field names,
|
|
99
|
+
types and frequencies).
|
|
100
|
+
⁴ MongoDB has no foreign keys; top-level `$jsonSchema` validators are carried in the
|
|
101
|
+
collection's `extra`.
|
|
102
|
+
⁵ For MongoDB, `inferred_reference` edges are the **only** relationships produced (an
|
|
103
|
+
`<entity>_id` field infers a reference to the target collection's `_id`) and inference
|
|
104
|
+
is auto-enabled by the presence of collection/field nodes.
|
|
105
|
+
|
|
106
|
+
**Inferred relationships** (the `○` cells) come from an opt-in, pure-core inference
|
|
107
|
+
engine that is **off by default** for the SQL engines. When enabled it emits
|
|
108
|
+
`inferred_reference` edges from column names and declared types alone — `<entity>_id`,
|
|
109
|
+
`<entity>Id`, `id_<entity>` resolved against real target tables and gated by type
|
|
110
|
+
compatibility — never by reading data values
|
|
111
|
+
(`openspec/specs/graph-normalization/spec.md` — "Opt-in structural inference of
|
|
112
|
+
references", "Inference is opt-in and OFF by default").
|
|
113
|
+
|
|
114
|
+
## Quickstart (from source)
|
|
115
|
+
|
|
116
|
+
There is no published package yet, so run from source. Requires **Node.js >= 22**.
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
git clone <this-repo> dbgraph
|
|
120
|
+
cd dbgraph
|
|
121
|
+
npm ci
|
|
122
|
+
npm run build
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Then point dbgraph at a database and build its graph. Using SQLite as an example:
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
# 1. Initialize: validates the connection, writes dbgraph.config.json at the project
|
|
129
|
+
# root (with ${env:VAR} references only — never plaintext secrets), gitignores the
|
|
130
|
+
# local .dbgraph index, and runs the first sync.
|
|
131
|
+
dbgraph init --dialect sqlite --file ./app.db
|
|
132
|
+
|
|
133
|
+
# 2. Re-sync after schema changes (incremental — skips extraction when the catalog
|
|
134
|
+
# fingerprint is unchanged; --full forces a rebuild).
|
|
135
|
+
dbgraph sync
|
|
136
|
+
|
|
137
|
+
# 3. Query the graph.
|
|
138
|
+
dbgraph query orders # ranked hits, each with its type and qualified name
|
|
139
|
+
dbgraph query orders --json # deterministic, machine-parseable JSON
|
|
140
|
+
dbgraph explore main.orders # a node and its grouped neighbors (use the QUALIFIED name)
|
|
141
|
+
dbgraph status # counts, last snapshot, and live drift
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
`init` writes an env-only config: every connection-identity field (host, port, user,
|
|
145
|
+
password, URL) is stored as a `${env:VAR}` reference and resolved from `process.env` at
|
|
146
|
+
runtime — a plaintext credential is rejected
|
|
147
|
+
(`openspec/specs/cli-config/spec.md` — "Plaintext credentials are rejected").
|
|
148
|
+
|
|
149
|
+
## CLI reference
|
|
150
|
+
|
|
151
|
+
`dbgraph <command> [options]`. Run `dbgraph --help` for the full banner, or
|
|
152
|
+
`dbgraph <command> --help` for per-command options.
|
|
153
|
+
|
|
154
|
+
| Command | What it does |
|
|
155
|
+
|---------|--------------|
|
|
156
|
+
| `init` | Initialize the graph index for a database (writes config, gitignores `.dbgraph`, runs first sync). |
|
|
157
|
+
| `sync` | Synchronize the graph with the database — incremental by fingerprint; `--full` forces a rebuild. |
|
|
158
|
+
| `status` | Show current graph counts, the last snapshot, and whether the live schema has drifted. |
|
|
159
|
+
| `query <term>` | Full-text search the graph; prints type + qualified name per hit. Exits 1 on zero hits. |
|
|
160
|
+
| `explore <qname>` | Show a node and its grouped neighbors. `--detail brief\|normal\|full`. |
|
|
161
|
+
| `diff <a> <b>` | Compare two snapshots per object (added/removed/modified). `--last` compares the two most recent. Exits 1 when changes exist (CI-gate). |
|
|
162
|
+
| `affected <script.sql>` | Analyze a DDL script and report impacted objects. `--json` for machine output; exits 1 when anything is affected. |
|
|
163
|
+
| `doctor` | Run a content-free connectivity self-test (safe to share). |
|
|
164
|
+
| `install` | Wire `dbgraph-mcp` into supported MCP agents. `--project` for project scope, `--remove` to undo. |
|
|
165
|
+
|
|
166
|
+
Global flags: `--json` (machine-parseable output where supported), `--quiet` / `-q`
|
|
167
|
+
(suppress info/progress, keep warnings/errors), `--help` / `-h`, `--version` / `-v`.
|
|
168
|
+
`--json` payloads on stdout are deterministic and byte-identical for the same graph and
|
|
169
|
+
input (`openspec/specs/cli-config/spec.md`; `src/cli/cli.ts` `USAGE_TEXT`;
|
|
170
|
+
`src/cli/dispatch.ts` `COMMAND_TABLE`).
|
|
171
|
+
|
|
172
|
+
Exit codes map to a fixed contract (`openspec/specs/cli-config/spec.md`): `0` success ·
|
|
173
|
+
`1` a "negative" result (zero query hits, or diff/affected found changes) · `2`
|
|
174
|
+
connection failure · `3` permission failure · `4` unsupported dialect.
|
|
175
|
+
|
|
176
|
+
## MCP integration
|
|
177
|
+
|
|
178
|
+
`dbgraph install` wires an idempotent `dbgraph-mcp` server entry into every supported
|
|
179
|
+
agent's MCP config, driven by a single `AGENT_TABLE` source of truth
|
|
180
|
+
(`openspec/specs/mcp-server/spec.md` — "dbgraph install idempotently wires the agent MCP
|
|
181
|
+
config"; `src/cli/commands/install.ts`).
|
|
182
|
+
|
|
183
|
+
```bash
|
|
184
|
+
dbgraph install # wire user-level config for every detected agent
|
|
185
|
+
dbgraph install --project # write project-scoped config in the current directory
|
|
186
|
+
dbgraph install --remove # remove the dbgraph-mcp entry from every detected agent
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
The written entry is `command` + `args` only (`npx -y dbgraph-mcp`) — never a secret.
|
|
190
|
+
Global install configures an agent **only if** its config file already exists;
|
|
191
|
+
`--project` (US-038) re-roots resolution at the current directory and **creates** absent
|
|
192
|
+
project files for the supported agents.
|
|
193
|
+
|
|
194
|
+
### Supported agents
|
|
195
|
+
|
|
196
|
+
| Agent | Config key & shape | Project-scoped file (`--project`) |
|
|
197
|
+
|-------|--------------------|-----------------------------------|
|
|
198
|
+
| Claude Code | `mcpServers.dbgraph-mcp = { command, args }` | `.mcp.json` |
|
|
199
|
+
| Cursor | `mcpServers.dbgraph-mcp = { command, args }` | `.cursor/mcp.json` |
|
|
200
|
+
| Gemini CLI | `mcpServers.dbgraph-mcp = { command, args }` | `.gemini/settings.json` |
|
|
201
|
+
| VS Code | `servers.dbgraph-mcp = { type: 'stdio', command, args }` | `.vscode/mcp.json` |
|
|
202
|
+
| opencode | `mcp.dbgraph-mcp = { type: 'local', command: [...] }` | `opencode.json` |
|
|
203
|
+
| Codex CLI | TOML `[mcp_servers.dbgraph-mcp]` | `.codex/config.toml` |
|
|
204
|
+
|
|
205
|
+
**Codex is trust-gated.** Codex loads project-scoped MCP servers only for **trusted**
|
|
206
|
+
projects, so `--project` reports it with a caveat verbatim:
|
|
207
|
+
|
|
208
|
+
```text
|
|
209
|
+
codex → written (requires trusted project: set trust_level in ~/.codex/config.toml)
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
### Env-var interpolation per agent
|
|
213
|
+
|
|
214
|
+
The `dbgraph-mcp` entry itself uses no interpolation (only `command` + `args`). But if
|
|
215
|
+
you hand-edit a config to add another server that needs a secret, each agent has its own
|
|
216
|
+
native env syntax — reference variables, never inline a credential:
|
|
217
|
+
|
|
218
|
+
| Agent | Config file | Env-var interpolation syntax |
|
|
219
|
+
|-------|-------------|------------------------------|
|
|
220
|
+
| Claude Code | `.mcp.json` | `${VAR}` |
|
|
221
|
+
| Cursor | `.cursor/mcp.json` | `${env:VAR}` |
|
|
222
|
+
| VS Code | `.vscode/mcp.json` | `${env:VAR}` + `inputs` |
|
|
223
|
+
| Gemini CLI | `.gemini/settings.json` | `$VAR` / `${VAR}` |
|
|
224
|
+
| opencode | `opencode.json` | `{env:VAR}` |
|
|
225
|
+
| Codex CLI | `.codex/config.toml` | TOML `env` tables — no string interpolation |
|
|
226
|
+
|
|
227
|
+
If no agent config is detected, `dbgraph install` prints a manual snippet for all six
|
|
228
|
+
formats and exits successfully.
|
|
229
|
+
|
|
230
|
+
### Serve over HTTP
|
|
231
|
+
|
|
232
|
+
`dbgraph mcp` speaks **stdio** by default. Add `--http` to serve the same read-only
|
|
233
|
+
8-tool surface over **Streamable HTTP** from one host, for several remote agents:
|
|
234
|
+
|
|
235
|
+
```bash
|
|
236
|
+
dbgraph mcp --http # endpoint at http://127.0.0.1:7423/mcp (loopback default)
|
|
237
|
+
dbgraph mcp --http --host 0.0.0.0 # bind all interfaces — prints a no-auth warning
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
**No authentication ships in v1** and the default bind is loopback (`127.0.0.1`) — that
|
|
241
|
+
loopback default is the primary containment. For any non-loopback exposure, front the
|
|
242
|
+
endpoint with a reverse proxy (TLS + auth) or network controls. HTTP mode adds no write
|
|
243
|
+
path and diagnostics stay content-free.
|
|
244
|
+
|
|
245
|
+
HTTP client config is **not** auto-wired (`dbgraph install` wires the stdio entry only) —
|
|
246
|
+
add the server by hand. Two shapes bite if copied across agents: **Gemini CLI** needs
|
|
247
|
+
`httpUrl` (a plain `url` silently selects deprecated SSE), and **Cursor** takes **no**
|
|
248
|
+
`type` field (it is inferred from the `url`). The verified 6/6 per-agent matrix, the
|
|
249
|
+
reverse-proxy model, and the pinned `--host 0.0.0.0` warning are in
|
|
250
|
+
[`docs/mcp-http.md`](docs/mcp-http.md).
|
|
251
|
+
|
|
252
|
+
## Troubleshooting
|
|
253
|
+
|
|
254
|
+
Run `dbgraph doctor` first — it prints a **content-free** capability report (engine,
|
|
255
|
+
native-driver presence, detected CLI tools with versions, ODBC presence, resolved
|
|
256
|
+
environment profile, and the strategy it would choose). It contains no schema name,
|
|
257
|
+
identifier, query result, or secret, so it is safe to paste into a bug report
|
|
258
|
+
(`openspec/specs/connectivity-diagnostics/spec.md` — "dbgraph doctor reports diagnostics
|
|
259
|
+
content-free"; `src/core/present/doctor.ts` `DoctorView`).
|
|
260
|
+
|
|
261
|
+
The real-world SQL Server connectivity breaks below are documented in
|
|
262
|
+
`docs/findings/connectivity-environments.md` (F-1..F-7) and handled by the
|
|
263
|
+
connectivity-strategy system (`openspec/specs/connectivity/spec.md`,
|
|
264
|
+
`openspec/specs/connectivity-diagnostics/spec.md`):
|
|
265
|
+
|
|
266
|
+
| # | Symptom | `doctor` field | Fix |
|
|
267
|
+
|---|---------|----------------|-----|
|
|
268
|
+
| F-1 | `sync` fails on an integrated-security SQL Server (no SQL login; the pure-JS driver can't do SSPI) | `cliTools` (sqlcmd absent) / `chosenStrategy: unavailable` | Install `sqlcmd`; dbgraph shells out with `sqlcmd -E` for integrated auth. |
|
|
269
|
+
| F-2 | Wrong `sqlcmd` flags for the installed variant/version | `resolvedProfile: <variant>@<versionRange>` | dbgraph selects flags per detected variant; if it reports an unrecognized profile, share the `doctor` output. |
|
|
270
|
+
| F-3 | Truncated / failed `FOR JSON` on a legacy `sqlcmd` | `resolvedProfile` (legacy) | dbgraph adapts flags per profile (e.g. `-y 0` alone on legacy 15.x). |
|
|
271
|
+
| F-4 | JSON parse fails at a chunk boundary | `resolvedProfile` (output shape) | dbgraph reassembles the 2033-char `FOR JSON` chunks without trimming. |
|
|
272
|
+
| F-5 | Non-ASCII characters corrupt the JSON stream | `resolvedProfile` (encoding) | dbgraph forces the `sqlcmd` output to codepage 65001 (`-f o:65001`). |
|
|
273
|
+
| F-6 | Malformed / partial tool output | actionable error (what was received, first N chars) — never a raw stack trace | Run `dbgraph doctor` and paste the content-free report. |
|
|
274
|
+
| F-7 | These `sqlcmd` transport breaks aren't caught by CI | — | Covered by recorded fixtures and an opt-in `sqlcmd` CI lane — see [CONTRIBUTING](CONTRIBUTING.md) and [Limitations](#limitations). |
|
|
275
|
+
|
|
276
|
+
A missing database driver surfaces the exact install command, not a stack trace —
|
|
277
|
+
`Required driver '<name>' is not installed. Run: npm i <name>`
|
|
278
|
+
(`openspec/specs/binary-distribution/spec.md`). A connection failure yields a typed,
|
|
279
|
+
non-blocking outcome offering at least three actionable options (run the catalog
|
|
280
|
+
`SELECT`s yourself, consented install, or import a manual dump) — never an unhandled
|
|
281
|
+
exception (`openspec/specs/connectivity-diagnostics/spec.md`).
|
|
282
|
+
|
|
283
|
+
## Limitations
|
|
284
|
+
|
|
285
|
+
Stated plainly, and by design:
|
|
286
|
+
|
|
287
|
+
- **No published release yet.** The package version is `0.0.0` and the `release.yml`
|
|
288
|
+
workflow, though written and trigger-guarded, has never been fired — no tag has been
|
|
289
|
+
pushed (`openspec/specs/binary-distribution/spec.md` — "release.yml is trigger-guarded
|
|
290
|
+
... and never fired"). Run from source today; standalone win-x64 / linux-x64 binaries
|
|
291
|
+
are planned for v1.0.
|
|
292
|
+
- **MongoDB structure is inferred, not declared.** Field structure comes from `$sample`;
|
|
293
|
+
it is honest only about what the sample observed. Sampled document **values are never
|
|
294
|
+
persisted** — only field names, types and presence frequencies survive
|
|
295
|
+
(`openspec/specs/cli-config/spec.md` — sampling is "structural only ... values never
|
|
296
|
+
stored"; `openspec/specs/mongodb-extraction/spec.md`).
|
|
297
|
+
- **Inferred relationships are opt-in.** For the SQL engines, inference is off by
|
|
298
|
+
default and must be enabled explicitly; it never reads data values
|
|
299
|
+
(`openspec/specs/graph-normalization/spec.md`).
|
|
300
|
+
- **SQL Server integrated auth needs `sqlcmd`.** The pure-JS driver cannot do
|
|
301
|
+
SSPI/Kerberos; integrated security is handled by shelling out to `sqlcmd -E`
|
|
302
|
+
(`openspec/specs/mssql-extraction/spec.md`; `docs/findings/connectivity-environments.md`
|
|
303
|
+
F-1).
|
|
304
|
+
- **The `sqlcmd` transport has a CI coverage gap.** Flag combinations, output shape,
|
|
305
|
+
chunking and encoding only surface on a real `sqlcmd` run; they are covered by recorded
|
|
306
|
+
fixtures and an opt-in CI lane, not the default unit matrix
|
|
307
|
+
(`docs/findings/connectivity-environments.md` F-7).
|
|
308
|
+
|
|
309
|
+
## Development
|
|
310
|
+
|
|
311
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md) for setup, the per-batch quality gate, the strict
|
|
312
|
+
TDD and spec-driven workflow, and the test tiers. Security posture and disclosure are in
|
|
313
|
+
[SECURITY.md](SECURITY.md).
|
|
314
|
+
|
|
315
|
+
## License
|
|
316
|
+
|
|
317
|
+
MIT
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
var __create = Object.create;
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
6
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
8
|
+
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
9
|
+
}) : x)(function(x) {
|
|
10
|
+
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
11
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
12
|
+
});
|
|
13
|
+
var __esm = (fn, res) => function __init() {
|
|
14
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
15
|
+
};
|
|
16
|
+
var __commonJS = (cb, mod) => function __require2() {
|
|
17
|
+
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
|
|
18
|
+
};
|
|
19
|
+
var __export = (target, all) => {
|
|
20
|
+
for (var name in all)
|
|
21
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
22
|
+
};
|
|
23
|
+
var __copyProps = (to, from, except, desc) => {
|
|
24
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
25
|
+
for (let key of __getOwnPropNames(from))
|
|
26
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
27
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
28
|
+
}
|
|
29
|
+
return to;
|
|
30
|
+
};
|
|
31
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
32
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
33
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
34
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
35
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
36
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
37
|
+
mod
|
|
38
|
+
));
|
|
39
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
40
|
+
|
|
41
|
+
export {
|
|
42
|
+
__require,
|
|
43
|
+
__esm,
|
|
44
|
+
__commonJS,
|
|
45
|
+
__export,
|
|
46
|
+
__toESM,
|
|
47
|
+
__toCommonJS
|
|
48
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// src/adapters/engines/_shared/connectivity-outcome.ts
|
|
2
|
+
function buildConnectivityOutcome(args) {
|
|
3
|
+
const options = [
|
|
4
|
+
{
|
|
5
|
+
kind: "run-it-yourself",
|
|
6
|
+
description: "Run these read-only catalog SELECT statements in your own client to extract the schema.",
|
|
7
|
+
queries: args.runItYourselfQueries
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
kind: "consented-install",
|
|
11
|
+
description: `Install the '${args.installTool}' driver or tool from the official source. Nothing is installed automatically \u2014 explicit consent required.`,
|
|
12
|
+
tool: args.installTool,
|
|
13
|
+
docUrl: args.installDocUrl
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
kind: "manual-dump",
|
|
17
|
+
description: `Import a combined JSON dump you produced externally. Place the file at: ${args.dumpPath}`,
|
|
18
|
+
outputPath: args.dumpPath
|
|
19
|
+
}
|
|
20
|
+
];
|
|
21
|
+
return {
|
|
22
|
+
engine: args.engine,
|
|
23
|
+
summary: args.summary,
|
|
24
|
+
attempts: args.attempts,
|
|
25
|
+
options
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export {
|
|
30
|
+
buildConnectivityOutcome
|
|
31
|
+
};
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ConfigError,
|
|
3
|
+
UnsupportedDialectError
|
|
4
|
+
} from "./chunk-TH4OVS4W.js";
|
|
5
|
+
|
|
6
|
+
// src/infra/config/schema.ts
|
|
7
|
+
var VALID_LEVELS = ["off", "metadata", "full"];
|
|
8
|
+
var SUPPORTED_DIALECTS = ["sqlite", "mssql", "pg", "mysql", "mongodb"];
|
|
9
|
+
|
|
10
|
+
// src/infra/config/parse-config.ts
|
|
11
|
+
function isRecord(v) {
|
|
12
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
13
|
+
}
|
|
14
|
+
function requireString(obj, field, context) {
|
|
15
|
+
const v = obj[field];
|
|
16
|
+
if (typeof v !== "string" || v.length === 0) {
|
|
17
|
+
throw new ConfigError(
|
|
18
|
+
`${context}: field "${field}" is required and must be a non-empty string.`
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
return v;
|
|
22
|
+
}
|
|
23
|
+
function optionalString(obj, field, context) {
|
|
24
|
+
const v = obj[field];
|
|
25
|
+
if (v === void 0) return void 0;
|
|
26
|
+
if (typeof v !== "string") {
|
|
27
|
+
throw new ConfigError(`${context}: field "${field}" must be a string when provided.`);
|
|
28
|
+
}
|
|
29
|
+
return v;
|
|
30
|
+
}
|
|
31
|
+
function parseLevels(raw) {
|
|
32
|
+
if (raw === void 0) return void 0;
|
|
33
|
+
if (!isRecord(raw)) {
|
|
34
|
+
throw new ConfigError(
|
|
35
|
+
'Config: "levels" must be an object mapping object type names to level strings.'
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
const result = {};
|
|
39
|
+
for (const [key, val] of Object.entries(raw)) {
|
|
40
|
+
if (!VALID_LEVELS.includes(val)) {
|
|
41
|
+
throw new ConfigError(
|
|
42
|
+
`Config: levels."${key}" must be one of ${VALID_LEVELS.join(", ")} \u2014 got "${String(val)}".`
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
result[key] = val;
|
|
46
|
+
}
|
|
47
|
+
return result;
|
|
48
|
+
}
|
|
49
|
+
function parseSqliteSource(raw) {
|
|
50
|
+
if (!isRecord(raw)) {
|
|
51
|
+
throw new ConfigError('Config: "source" must be an object for dialect "sqlite".');
|
|
52
|
+
}
|
|
53
|
+
const file = requireString(raw, "file", "source (sqlite)");
|
|
54
|
+
return { file };
|
|
55
|
+
}
|
|
56
|
+
function parseMssqlSource(raw) {
|
|
57
|
+
if (!isRecord(raw)) {
|
|
58
|
+
throw new ConfigError('Config: "source" must be an object for dialect "mssql".');
|
|
59
|
+
}
|
|
60
|
+
const server = requireString(raw, "server", "source (mssql)");
|
|
61
|
+
const database = requireString(raw, "database", "source (mssql)");
|
|
62
|
+
const port = optionalString(raw, "port", "source (mssql)");
|
|
63
|
+
const authRaw = raw["auth"];
|
|
64
|
+
let auth;
|
|
65
|
+
if (authRaw !== void 0) {
|
|
66
|
+
if (authRaw !== "sql" && authRaw !== "ntlm" && authRaw !== "integrated") {
|
|
67
|
+
throw new ConfigError(
|
|
68
|
+
'source (mssql): field "auth" must be one of "sql", "ntlm", or "integrated".'
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
auth = authRaw;
|
|
72
|
+
}
|
|
73
|
+
if (auth === "integrated") {
|
|
74
|
+
const result2 = { server, database, auth };
|
|
75
|
+
if (port !== void 0) result2.port = port;
|
|
76
|
+
return result2;
|
|
77
|
+
}
|
|
78
|
+
const user = requireString(raw, "user", "source (mssql)");
|
|
79
|
+
const password = requireString(raw, "password", "source (mssql)");
|
|
80
|
+
const domain = optionalString(raw, "domain", "source (mssql)");
|
|
81
|
+
const resolvedAuth = auth ?? (domain !== void 0 ? "ntlm" : "sql");
|
|
82
|
+
const result = { server, database, user, password, auth: resolvedAuth };
|
|
83
|
+
if (port !== void 0) result.port = port;
|
|
84
|
+
if (domain !== void 0) result.domain = domain;
|
|
85
|
+
return result;
|
|
86
|
+
}
|
|
87
|
+
var ENV_REF_RE = /^\$\{env:[A-Z_][A-Z0-9_]*\}$/;
|
|
88
|
+
function isEnvRef(value) {
|
|
89
|
+
return ENV_REF_RE.test(value);
|
|
90
|
+
}
|
|
91
|
+
function parsePgSource(raw) {
|
|
92
|
+
if (!isRecord(raw)) {
|
|
93
|
+
throw new ConfigError('Config: "source" must be an object for dialect "pg".');
|
|
94
|
+
}
|
|
95
|
+
const host = requireString(raw, "host", "source (pg)");
|
|
96
|
+
const database = requireString(raw, "database", "source (pg)");
|
|
97
|
+
const user = requireString(raw, "user", "source (pg)");
|
|
98
|
+
const password = requireString(raw, "password", "source (pg)");
|
|
99
|
+
if (!isEnvRef(password)) {
|
|
100
|
+
throw new ConfigError(
|
|
101
|
+
'source (pg): field "password" must be a ${env:VAR} reference, not a literal value. Use an environment variable reference such as ${env:PG_PASSWORD}.'
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
const port = optionalString(raw, "port", "source (pg)");
|
|
105
|
+
const ssl = optionalString(raw, "ssl", "source (pg)");
|
|
106
|
+
const schema = optionalString(raw, "schema", "source (pg)");
|
|
107
|
+
const result = { host, database, user, password };
|
|
108
|
+
if (port !== void 0) result.port = port;
|
|
109
|
+
if (ssl !== void 0) result.ssl = ssl;
|
|
110
|
+
if (schema !== void 0) result.schema = schema;
|
|
111
|
+
return result;
|
|
112
|
+
}
|
|
113
|
+
function parseMysqlSource(raw) {
|
|
114
|
+
if (!isRecord(raw)) {
|
|
115
|
+
throw new ConfigError('Config: "source" must be an object for dialect "mysql".');
|
|
116
|
+
}
|
|
117
|
+
const host = requireString(raw, "host", "source (mysql)");
|
|
118
|
+
const database = requireString(raw, "database", "source (mysql)");
|
|
119
|
+
const user = requireString(raw, "user", "source (mysql)");
|
|
120
|
+
const password = requireString(raw, "password", "source (mysql)");
|
|
121
|
+
if (!isEnvRef(password)) {
|
|
122
|
+
throw new ConfigError(
|
|
123
|
+
'source (mysql): field "password" must be a ${env:VAR} reference, not a literal value. Use an environment variable reference such as ${env:MYSQL_PASSWORD}.'
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
const port = optionalString(raw, "port", "source (mysql)");
|
|
127
|
+
const ssl = optionalString(raw, "ssl", "source (mysql)");
|
|
128
|
+
const result = { host, database, user, password };
|
|
129
|
+
if (port !== void 0) result.port = port;
|
|
130
|
+
if (ssl !== void 0) result.ssl = ssl;
|
|
131
|
+
return result;
|
|
132
|
+
}
|
|
133
|
+
function parseMongodbSource(raw) {
|
|
134
|
+
if (!isRecord(raw)) {
|
|
135
|
+
throw new ConfigError('Config: "source" must be an object for dialect "mongodb".');
|
|
136
|
+
}
|
|
137
|
+
const uri = requireString(raw, "uri", "source (mongodb)");
|
|
138
|
+
const database = requireString(raw, "database", "source (mongodb)");
|
|
139
|
+
if (!isEnvRef(uri)) {
|
|
140
|
+
throw new ConfigError(
|
|
141
|
+
'source (mongodb): field "uri" must be a ${env:VAR} reference, not a literal value. Use an environment variable reference such as ${env:MONGODB_URI}.'
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
const sampleSizeRaw = raw["sampleSize"];
|
|
145
|
+
let sampleSize;
|
|
146
|
+
if (sampleSizeRaw !== void 0) {
|
|
147
|
+
if (typeof sampleSizeRaw !== "number" || !Number.isFinite(sampleSizeRaw) || sampleSizeRaw <= 0) {
|
|
148
|
+
throw new ConfigError(
|
|
149
|
+
'source (mongodb): field "sampleSize" must be a positive number when provided.'
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
sampleSize = sampleSizeRaw;
|
|
153
|
+
}
|
|
154
|
+
const tlsRaw = raw["tls"];
|
|
155
|
+
let tls;
|
|
156
|
+
if (tlsRaw !== void 0) {
|
|
157
|
+
if (typeof tlsRaw !== "boolean") {
|
|
158
|
+
throw new ConfigError(
|
|
159
|
+
'source (mongodb): field "tls" must be a boolean when provided.'
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
tls = tlsRaw;
|
|
163
|
+
}
|
|
164
|
+
const result = { uri, database };
|
|
165
|
+
if (sampleSize !== void 0) result.sampleSize = sampleSize;
|
|
166
|
+
if (tls !== void 0) result.tls = tls;
|
|
167
|
+
return result;
|
|
168
|
+
}
|
|
169
|
+
function parseConfig(raw) {
|
|
170
|
+
if (!isRecord(raw)) {
|
|
171
|
+
throw new ConfigError(
|
|
172
|
+
"Config: root must be a JSON object. Got " + (raw === null ? "null" : typeof raw) + "."
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
const dialectRaw = raw["dialect"];
|
|
176
|
+
if (typeof dialectRaw !== "string" || dialectRaw.length === 0) {
|
|
177
|
+
throw new ConfigError(
|
|
178
|
+
`Config: field "dialect" is required and must be a non-empty string (supported: ${SUPPORTED_DIALECTS.join(", ")}).`
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
if (!SUPPORTED_DIALECTS.includes(dialectRaw)) {
|
|
182
|
+
throw new UnsupportedDialectError(dialectRaw);
|
|
183
|
+
}
|
|
184
|
+
if (!("source" in raw)) {
|
|
185
|
+
throw new ConfigError('Config: field "source" is required.');
|
|
186
|
+
}
|
|
187
|
+
const levels = parseLevels(raw["levels"]);
|
|
188
|
+
switch (dialectRaw) {
|
|
189
|
+
case "sqlite": {
|
|
190
|
+
const source = parseSqliteSource(raw["source"]);
|
|
191
|
+
const driverRaw = raw["driver"];
|
|
192
|
+
let driver;
|
|
193
|
+
if (driverRaw !== void 0) {
|
|
194
|
+
if (driverRaw !== "better-sqlite3" && driverRaw !== "node:sqlite") {
|
|
195
|
+
throw new ConfigError(
|
|
196
|
+
'Config: "driver" must be one of "better-sqlite3" or "node:sqlite".'
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
driver = driverRaw;
|
|
200
|
+
}
|
|
201
|
+
const cfg = levels !== void 0 ? driver !== void 0 ? { dialect: "sqlite", source, levels, driver } : { dialect: "sqlite", source, levels } : driver !== void 0 ? { dialect: "sqlite", source, driver } : { dialect: "sqlite", source };
|
|
202
|
+
return cfg;
|
|
203
|
+
}
|
|
204
|
+
case "mssql": {
|
|
205
|
+
const source = parseMssqlSource(raw["source"]);
|
|
206
|
+
const cfg = levels !== void 0 ? { dialect: "mssql", source, levels } : { dialect: "mssql", source };
|
|
207
|
+
return cfg;
|
|
208
|
+
}
|
|
209
|
+
case "pg": {
|
|
210
|
+
const source = parsePgSource(raw["source"]);
|
|
211
|
+
const cfg = levels !== void 0 ? { dialect: "pg", source, levels } : { dialect: "pg", source };
|
|
212
|
+
return cfg;
|
|
213
|
+
}
|
|
214
|
+
case "mysql": {
|
|
215
|
+
const source = parseMysqlSource(raw["source"]);
|
|
216
|
+
const cfg = levels !== void 0 ? { dialect: "mysql", source, levels } : { dialect: "mysql", source };
|
|
217
|
+
return cfg;
|
|
218
|
+
}
|
|
219
|
+
case "mongodb": {
|
|
220
|
+
const source = parseMongodbSource(raw["source"]);
|
|
221
|
+
const cfg = levels !== void 0 ? { dialect: "mongodb", source, levels } : { dialect: "mongodb", source };
|
|
222
|
+
return cfg;
|
|
223
|
+
}
|
|
224
|
+
default:
|
|
225
|
+
throw new UnsupportedDialectError(dialectRaw);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export {
|
|
230
|
+
parseConfig
|
|
231
|
+
};
|