@memberjunction/db-auto-doc 6.2.0-edge.3 → 6.2.0-edge.5
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 +200 -3
- package/dist/commands/analyze.d.ts +1 -0
- package/dist/commands/analyze.d.ts.map +1 -1
- package/dist/commands/analyze.js +6 -0
- package/dist/commands/analyze.js.map +1 -1
- package/dist/commands/export.d.ts +32 -15
- package/dist/commands/export.d.ts.map +1 -1
- package/dist/commands/export.js +144 -14
- package/dist/commands/export.js.map +1 -1
- package/dist/commands/init.js +1 -1
- package/dist/commands/init.js.map +1 -1
- package/dist/core/AnalysisEngine.d.ts +15 -6
- package/dist/core/AnalysisEngine.d.ts.map +1 -1
- package/dist/core/AnalysisEngine.js +41 -40
- package/dist/core/AnalysisEngine.js.map +1 -1
- package/dist/core/AnalysisOrchestrator.d.ts +25 -1
- package/dist/core/AnalysisOrchestrator.d.ts.map +1 -1
- package/dist/core/AnalysisOrchestrator.js +163 -27
- package/dist/core/AnalysisOrchestrator.js.map +1 -1
- package/dist/core/PKProposalGate.d.ts +22 -0
- package/dist/core/PKProposalGate.d.ts.map +1 -0
- package/dist/core/PKProposalGate.js +52 -0
- package/dist/core/PKProposalGate.js.map +1 -0
- package/dist/database/Database.d.ts +8 -0
- package/dist/database/Database.d.ts.map +1 -1
- package/dist/database/Database.js +23 -1
- package/dist/database/Database.js.map +1 -1
- package/dist/discovery/EnumCandidateGate.d.ts +19 -0
- package/dist/discovery/EnumCandidateGate.d.ts.map +1 -1
- package/dist/discovery/EnumCandidateGate.js +47 -5
- package/dist/discovery/EnumCandidateGate.js.map +1 -1
- package/dist/discovery/FKGraphWalker.d.ts +35 -0
- package/dist/discovery/FKGraphWalker.d.ts.map +1 -1
- package/dist/discovery/FKGraphWalker.js +113 -12
- package/dist/discovery/FKGraphWalker.js.map +1 -1
- package/dist/discovery/LLMDiscoveryValidator.d.ts +2 -0
- package/dist/discovery/LLMDiscoveryValidator.d.ts.map +1 -1
- package/dist/discovery/LLMDiscoveryValidator.js +20 -9
- package/dist/discovery/LLMDiscoveryValidator.js.map +1 -1
- package/dist/discovery/OrganicKeyDetector.d.ts.map +1 -1
- package/dist/discovery/OrganicKeyDetector.js +8 -2
- package/dist/discovery/OrganicKeyDetector.js.map +1 -1
- package/dist/discovery/StructuralPhase.d.ts +27 -5
- package/dist/discovery/StructuralPhase.d.ts.map +1 -1
- package/dist/discovery/StructuralPhase.js +42 -9
- package/dist/discovery/StructuralPhase.js.map +1 -1
- package/dist/discovery/TransitiveBridgeDetector.d.ts +5 -1
- package/dist/discovery/TransitiveBridgeDetector.d.ts.map +1 -1
- package/dist/discovery/TransitiveBridgeDetector.js +25 -3
- package/dist/discovery/TransitiveBridgeDetector.js.map +1 -1
- package/dist/drivers/BaseAutoDocDriver.d.ts +6 -1
- package/dist/drivers/BaseAutoDocDriver.d.ts.map +1 -1
- package/dist/drivers/BaseAutoDocDriver.js +3 -5
- package/dist/drivers/BaseAutoDocDriver.js.map +1 -1
- package/dist/drivers/IndexCatalog.d.ts +54 -0
- package/dist/drivers/IndexCatalog.d.ts.map +1 -0
- package/dist/drivers/IndexCatalog.js +145 -0
- package/dist/drivers/IndexCatalog.js.map +1 -0
- package/dist/drivers/MySQLDriver.d.ts +5 -1
- package/dist/drivers/MySQLDriver.d.ts.map +1 -1
- package/dist/drivers/MySQLDriver.js +15 -6
- package/dist/drivers/MySQLDriver.js.map +1 -1
- package/dist/drivers/PostgreSQLDriver.d.ts +14 -2
- package/dist/drivers/PostgreSQLDriver.d.ts.map +1 -1
- package/dist/drivers/PostgreSQLDriver.js +52 -19
- package/dist/drivers/PostgreSQLDriver.js.map +1 -1
- package/dist/drivers/SQLServerDriver.d.ts +5 -1
- package/dist/drivers/SQLServerDriver.d.ts.map +1 -1
- package/dist/drivers/SQLServerDriver.js +13 -1
- package/dist/drivers/SQLServerDriver.js.map +1 -1
- package/dist/generators/AdditionalSchemaInfoGenerator.d.ts +34 -0
- package/dist/generators/AdditionalSchemaInfoGenerator.d.ts.map +1 -1
- package/dist/generators/AdditionalSchemaInfoGenerator.js +85 -4
- package/dist/generators/AdditionalSchemaInfoGenerator.js.map +1 -1
- package/dist/generators/ErdClusterGenerator.d.ts +44 -0
- package/dist/generators/ErdClusterGenerator.d.ts.map +1 -0
- package/dist/generators/ErdClusterGenerator.js +216 -0
- package/dist/generators/ErdClusterGenerator.js.map +1 -0
- package/dist/generators/ErdClusterer.d.ts +130 -0
- package/dist/generators/ErdClusterer.d.ts.map +1 -0
- package/dist/generators/ErdClusterer.js +308 -0
- package/dist/generators/ErdClusterer.js.map +1 -0
- package/dist/generators/HTMLGenerator.d.ts +6 -0
- package/dist/generators/HTMLGenerator.d.ts.map +1 -1
- package/dist/generators/HTMLGenerator.js +41 -6
- package/dist/generators/HTMLGenerator.js.map +1 -1
- package/dist/generators/MermaidGenerator.d.ts +3 -0
- package/dist/generators/MermaidGenerator.d.ts.map +1 -1
- package/dist/generators/MermaidGenerator.js +4 -0
- package/dist/generators/MermaidGenerator.js.map +1 -1
- package/dist/generators/index.d.ts +2 -0
- package/dist/generators/index.d.ts.map +1 -1
- package/dist/generators/index.js +2 -0
- package/dist/generators/index.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/plugins/BaseAutoDocPlugin.d.ts +33 -0
- package/dist/plugins/BaseAutoDocPlugin.d.ts.map +1 -0
- package/dist/plugins/BaseAutoDocPlugin.js +70 -0
- package/dist/plugins/BaseAutoDocPlugin.js.map +1 -0
- package/dist/plugins/PluginManager.d.ts +54 -0
- package/dist/plugins/PluginManager.d.ts.map +1 -0
- package/dist/plugins/PluginManager.js +115 -0
- package/dist/plugins/PluginManager.js.map +1 -0
- package/dist/plugins/builtins.d.ts +13 -0
- package/dist/plugins/builtins.d.ts.map +1 -0
- package/dist/plugins/builtins.js +12 -0
- package/dist/plugins/builtins.js.map +1 -0
- package/dist/plugins/index-advisor/IndexAdvisorPlugin.d.ts +25 -0
- package/dist/plugins/index-advisor/IndexAdvisorPlugin.d.ts.map +1 -0
- package/dist/plugins/index-advisor/IndexAdvisorPlugin.js +128 -0
- package/dist/plugins/index-advisor/IndexAdvisorPlugin.js.map +1 -0
- package/dist/plugins/index-advisor/IndexAdvisorTypes.d.ts +111 -0
- package/dist/plugins/index-advisor/IndexAdvisorTypes.d.ts.map +1 -0
- package/dist/plugins/index-advisor/IndexAdvisorTypes.js +5 -0
- package/dist/plugins/index-advisor/IndexAdvisorTypes.js.map +1 -0
- package/dist/plugins/index-advisor/IndexLLMReview.d.ts +48 -0
- package/dist/plugins/index-advisor/IndexLLMReview.d.ts.map +1 -0
- package/dist/plugins/index-advisor/IndexLLMReview.js +181 -0
- package/dist/plugins/index-advisor/IndexLLMReview.js.map +1 -0
- package/dist/plugins/index-advisor/IndexMigrationGenerator.d.ts +28 -0
- package/dist/plugins/index-advisor/IndexMigrationGenerator.d.ts.map +1 -0
- package/dist/plugins/index-advisor/IndexMigrationGenerator.js +98 -0
- package/dist/plugins/index-advisor/IndexMigrationGenerator.js.map +1 -0
- package/dist/plugins/index-advisor/IndexRules.d.ts +19 -0
- package/dist/plugins/index-advisor/IndexRules.d.ts.map +1 -0
- package/dist/plugins/index-advisor/IndexRules.js +154 -0
- package/dist/plugins/index-advisor/IndexRules.js.map +1 -0
- package/dist/plugins/index.d.ts +13 -0
- package/dist/plugins/index.d.ts.map +1 -0
- package/dist/plugins/index.js +13 -0
- package/dist/plugins/index.js.map +1 -0
- package/dist/plugins/types.d.ts +82 -0
- package/dist/plugins/types.d.ts.map +1 -0
- package/dist/plugins/types.js +9 -0
- package/dist/plugins/types.js.map +1 -0
- package/dist/types/analysis.d.ts +2 -0
- package/dist/types/analysis.d.ts.map +1 -1
- package/dist/types/config.d.ts +52 -0
- package/dist/types/config.d.ts.map +1 -1
- package/dist/types/config.js +7 -4
- package/dist/types/config.js.map +1 -1
- package/dist/types/discovery.d.ts +2 -0
- package/dist/types/discovery.d.ts.map +1 -1
- package/dist/types/driver.d.ts +30 -0
- package/dist/types/driver.d.ts.map +1 -1
- package/dist/types/driver.js +5 -1
- package/dist/types/driver.js.map +1 -1
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +1 -0
- package/dist/types/index.js.map +1 -1
- package/dist/types/state.d.ts +36 -0
- package/dist/types/state.d.ts.map +1 -1
- package/dist/utils/config-loader.d.ts +5 -0
- package/dist/utils/config-loader.d.ts.map +1 -1
- package/dist/utils/config-loader.js +38 -1
- package/dist/utils/config-loader.js.map +1 -1
- package/dist/utils/index-hints.d.ts +36 -0
- package/dist/utils/index-hints.d.ts.map +1 -0
- package/dist/utils/index-hints.js +65 -0
- package/dist/utils/index-hints.js.map +1 -0
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +1 -0
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/llm-factory.d.ts +1 -0
- package/dist/utils/llm-factory.d.ts.map +1 -1
- package/dist/utils/llm-factory.js +6 -1
- package/dist/utils/llm-factory.js.map +1 -1
- package/package.json +5 -5
- package/prompts/fk-pruning-table.md +13 -1
- package/prompts/index-review.md +60 -0
- package/prompts/pk-pruning-table.md +10 -0
- package/prompts/table-analysis.md +11 -0
package/README.md
CHANGED
|
@@ -63,8 +63,9 @@ graph TD
|
|
|
63
63
|
- **CodeGen Integration** -- Emit `additionalSchemaInfo.json` for MemberJunction CodeGen soft FK/PK support
|
|
64
64
|
- **Granular Guardrails** -- Multi-level resource controls (run, phase, iteration limits)
|
|
65
65
|
- **Resume Capability** -- Pause and resume analysis from checkpoint state files
|
|
66
|
+
- **Index Advisor** -- Built-in plugin that reads existing indexes, proposes missing ones (uncovered foreign keys, discovered primary keys) with an optional LLM review, flags redundant indexes, and exports the proposals as a SQL migration. See [Index Advisor](#index-advisor-built-in-plugin)
|
|
66
67
|
- **Programmatic API** -- Use as a library in your own applications
|
|
67
|
-
- **Extensible** -- Custom database drivers and
|
|
68
|
+
- **Extensible** -- Custom database drivers, plus a [plugin system](#plugins) for adding steps before, during and after a run and new export formats
|
|
68
69
|
|
|
69
70
|
### Output Formats
|
|
70
71
|
- **SQL Scripts** -- Database-specific metadata scripts (extended properties, comments)
|
|
@@ -74,6 +75,7 @@ graph TD
|
|
|
74
75
|
- **Mermaid Diagrams** -- Standalone ERD files (.mmd and .html)
|
|
75
76
|
- **Analysis Reports** -- Detailed metrics and quality assessments
|
|
76
77
|
- **Additional Schema Info** -- CodeGen-compatible JSON for soft FK/PK metadata
|
|
78
|
+
- **Index Migration** -- Flyway-style SQL migration creating the Index Advisor's proposed indexes (`export --format index-migration`)
|
|
77
79
|
|
|
78
80
|
## Installation
|
|
79
81
|
|
|
@@ -162,6 +164,38 @@ This will:
|
|
|
162
164
|
- Perform sanity checks
|
|
163
165
|
- Save state to `db-doc-state.json`
|
|
164
166
|
|
|
167
|
+
#### Large databases
|
|
168
|
+
|
|
169
|
+
```bash
|
|
170
|
+
# Skip the table data sampling pass (row counts, distinct counts, sample values)
|
|
171
|
+
db-auto-doc analyze --skip-sampling # or "analysis": { "skipSampling": true }
|
|
172
|
+
|
|
173
|
+
# An interrupted sampling pass resumes where it stopped: state is saved every 25 tables and a
|
|
174
|
+
# resume samples only the tables that have no statistics yet
|
|
175
|
+
db-auto-doc analyze --resume ./output/run-3/state.json
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
Sampling is what the enum/value-list verdicts and the organic-key cardinality gate are based on, so with
|
|
179
|
+
`--skip-sampling` those verdicts fall back to the LLM's judgement. Organic-key detection is skipped with
|
|
180
|
+
a warning when no table has column statistics; on a `--resume --skip-sampling` of a run that already
|
|
181
|
+
sampled, the earlier statistics are kept and detection still runs.
|
|
182
|
+
|
|
183
|
+
#### Seed context from files
|
|
184
|
+
|
|
185
|
+
Long-form context (a data dictionary, an engineering assessment) can live in files instead of inline
|
|
186
|
+
`customInstructions`; their contents are appended to it:
|
|
187
|
+
|
|
188
|
+
```json
|
|
189
|
+
"seedContext": {
|
|
190
|
+
"overallPurpose": "Association management system",
|
|
191
|
+
"file": "./context/data-dictionary.md",
|
|
192
|
+
"files": ["./context/assessment.md", "./context/glossary.md"]
|
|
193
|
+
}
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
Paths are relative to `config.json`. The text is sent with every table prompt, so keep it to what the
|
|
197
|
+
model needs for descriptions rather than a full specification.
|
|
198
|
+
|
|
165
199
|
### 3. Generate Sample Queries (Optional)
|
|
166
200
|
|
|
167
201
|
Generate reference SQL queries for AI agent training:
|
|
@@ -319,6 +353,68 @@ Export only AI-discovered relationships (exclude hard DB constraints):
|
|
|
319
353
|
db-auto-doc export --schema-info --schema-info-discovered-only --confidence-threshold 70
|
|
320
354
|
```
|
|
321
355
|
|
|
356
|
+
Plugins can contribute their own export formats. List them, then generate one or more with `--format` (repeatable):
|
|
357
|
+
|
|
358
|
+
```bash
|
|
359
|
+
db-auto-doc export --state-file ./output/run-1/state.json --list-formats
|
|
360
|
+
db-auto-doc export --state-file ./output/run-1/state.json --format index-migration
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
Dialect-specific formats take the database platform from the state file; override it with `--provider sqlserver|postgresql|mysql`. `--file-prefix` replaces a format's default file-name prefix (for example a migration version such as `V202610081500__`).
|
|
364
|
+
|
|
365
|
+
#### Subject-area diagrams for large schemas
|
|
366
|
+
|
|
367
|
+
Mermaid refuses a diagram whose source exceeds ~50,000 characters, and a schema of a few hundred tables
|
|
368
|
+
passes that long before it becomes readable. `--erd-mode clusters` splits the foreign-key graph into
|
|
369
|
+
hub-centred subject areas and writes one diagram per area to `erd/`, with an index and a "Subject Areas"
|
|
370
|
+
section in `documentation.html`:
|
|
371
|
+
|
|
372
|
+
```bash
|
|
373
|
+
db-auto-doc export --html --mermaid --erd-mode clusters
|
|
374
|
+
db-auto-doc export --mermaid --erd-mode both --erd-columns keys # keep the full diagram too
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
How the areas are chosen:
|
|
378
|
+
|
|
379
|
+
1. **Reference tables** — tables that reference nothing and hold fewer than `lookupMaxRows` rows
|
|
380
|
+
(status codes, types, flags) — are listed beside each diagram rather than drawn. Left in, a flag table
|
|
381
|
+
referenced by sixty others links every area to every other.
|
|
382
|
+
2. **Hubs** are tables referenced by at least `hubMinInbound` other tables.
|
|
383
|
+
3. Every other connected table joins its nearest hub; an area smaller than `minClusterSize` folds into
|
|
384
|
+
the neighbour it links to most.
|
|
385
|
+
4. An area larger than `pageSize` is paginated, with the hub on every page.
|
|
386
|
+
|
|
387
|
+
Defaults live in `config.json` and any of them can be overridden per run:
|
|
388
|
+
|
|
389
|
+
```json
|
|
390
|
+
"output": {
|
|
391
|
+
"erd": {
|
|
392
|
+
"mode": "clusters",
|
|
393
|
+
"columns": "keys",
|
|
394
|
+
"hubMinInbound": 8,
|
|
395
|
+
"lookupMaxRows": 5000,
|
|
396
|
+
"pageSize": 50,
|
|
397
|
+
"minClusterSize": 4,
|
|
398
|
+
"hubs": ["Invoice"],
|
|
399
|
+
"lookupTables": ["AuditFlag"],
|
|
400
|
+
"excludeTables": ["Customer.bak"]
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
| Flag | Overrides |
|
|
406
|
+
|------|-----------|
|
|
407
|
+
| `--erd-mode full\|clusters\|both` | `erd.mode` |
|
|
408
|
+
| `--erd-columns all\|keys\|none` | `erd.columns` (clusters default to `keys`) |
|
|
409
|
+
| `--erd-hub-min-inbound N` | `erd.hubMinInbound` |
|
|
410
|
+
| `--erd-lookup-max-rows N` | `erd.lookupMaxRows` |
|
|
411
|
+
| `--erd-page-size N` | `erd.pageSize` |
|
|
412
|
+
| `--erd-min-cluster-size N` | `erd.minClusterSize` |
|
|
413
|
+
|
|
414
|
+
`erd/index.md` names every area's members, the reference tables, the tables with no foreign keys and any
|
|
415
|
+
connected tables that reached no hub, so a wrong-looking split can be traced to a threshold or pinned with
|
|
416
|
+
`hubs` / `lookupTables` / `excludeTables`.
|
|
417
|
+
|
|
322
418
|
### 5. Export Sample Queries to Metadata (Optional)
|
|
323
419
|
|
|
324
420
|
Transform generated sample queries into MemberJunction metadata format for syncing to the database:
|
|
@@ -417,6 +513,16 @@ Triggered automatically when:
|
|
|
417
513
|
- Tables lack primary key constraints
|
|
418
514
|
- Insufficient foreign key relationships detected (below threshold)
|
|
419
515
|
|
|
516
|
+
### Existing Indexes as Evidence
|
|
517
|
+
|
|
518
|
+
Databases often have indexes where no key is declared: developers index the columns they join and filter on. DBAutoDoc reads every table's indexes right after introspection and stores them in the state file (`schemas[].tables[].indexes`). The LLM sees them in the discovery validation pass, table analysis, and PK/FK pruning:
|
|
519
|
+
|
|
520
|
+
- A `UNIQUE` index or primary key on non-null columns is strong evidence for the primary key (or an alternate key).
|
|
521
|
+
- A non-unique index led by an ID- or code-like column often marks a foreign key.
|
|
522
|
+
- In FK pruning, each candidate says whether its target column has a unique index.
|
|
523
|
+
|
|
524
|
+
They are hints, not proof, and are weighed with names, statistics and value overlap. Turn this off with `"analysis": { "useExistingIndexes": false }`. A resumed run reads indexes for any table whose state has none yet. If a schema's catalog can't be read, the run continues without its indexes.
|
|
525
|
+
|
|
420
526
|
### Organic Key Detection (Optional)
|
|
421
527
|
|
|
422
528
|
Foreign keys require **shared exact values**. Organic keys (MemberJunction [PR #2193](https://github.com/MemberJunction/MJ/pull/2193)) capture the weaker-but-broader condition of **shared identity**: two columns refer to the same real-world thing once each is canonicalized through its own transformation. That joins far more cross-system data than exact-match FKs -- e.g. the same phone number stored as `+1 (555) 123-4567`, `5551234567`, and `555.123.4567` across HubSpot, Zendesk, and QuickBooks.
|
|
@@ -434,6 +540,97 @@ Results are written into `additionalSchemaInfo.json` (see [CodeGen Integration](
|
|
|
434
540
|
|
|
435
541
|
> **Note:** Embeddings route through MemberJunction's `BaseEmbeddings` drivers (OpenAI, Mistral, Azure, Bedrock, Ollama, local), defaulting to `OpenAIEmbedding`. The detection pass therefore requires an embedding provider with a valid key configured.
|
|
436
542
|
|
|
543
|
+
### Plugins
|
|
544
|
+
|
|
545
|
+
A plugin adds work to an analysis run and, optionally, new export formats. Plugins are classes registered with MemberJunction's class factory (`@RegisterClass` from `@memberjunction/global`, which needs no running MemberJunction system) and enabled in the config file.
|
|
546
|
+
|
|
547
|
+
**Hooks.** A plugin overrides any of these, in run order. Each receives the state, the config, the plugin's options, the run folder, a logger, and (from `AfterIntrospection` on) the open database connection:
|
|
548
|
+
|
|
549
|
+
| Hook | Runs |
|
|
550
|
+
|---|---|
|
|
551
|
+
| `OnPreRun` | after the state loads, before connecting |
|
|
552
|
+
| `OnAfterIntrospection` | after schema introspection and data sampling |
|
|
553
|
+
| `OnAfterDiscovery` | after primary/foreign key discovery |
|
|
554
|
+
| `OnAfterDescriptions` | after the LLM description loop |
|
|
555
|
+
| `OnAfterPruning` | after key pruning (also when pruning is not configured) |
|
|
556
|
+
| `OnPostRun` | last, while the database connection is still open |
|
|
557
|
+
|
|
558
|
+
Plugins run in `Priority` order (lower first, default 100). A hook that throws is logged and recorded in the state file, and the run continues. Each plugin keeps its hook history and any data it saves in its own section, `state.plugins.<Name>`. Hooks also run on `--resume`, so they should be safe to run more than once.
|
|
559
|
+
|
|
560
|
+
**Configuration.** Built-in plugins run by default. External plugins are listed with a `Module`: an npm package name or a file path (relative paths resolve from the current directory), loaded at startup.
|
|
561
|
+
|
|
562
|
+
```json
|
|
563
|
+
{
|
|
564
|
+
"plugins": [
|
|
565
|
+
{ "Name": "IndexAdvisor", "Options": { "MinRowCount": 5000 } },
|
|
566
|
+
{ "Name": "MyCustomCheck", "Module": "./plugins/my-custom-check.js", "Priority": 50, "Options": { "Strict": true } },
|
|
567
|
+
{ "Name": "SomeBuiltIn", "Enabled": false }
|
|
568
|
+
]
|
|
569
|
+
}
|
|
570
|
+
```
|
|
571
|
+
|
|
572
|
+
An enabled plugin whose name is not registered fails the run at startup, naming the plugins that are registered.
|
|
573
|
+
|
|
574
|
+
**Writing a plugin:**
|
|
575
|
+
|
|
576
|
+
```typescript
|
|
577
|
+
import { RegisterClass } from '@memberjunction/global';
|
|
578
|
+
import { BaseAutoDocPlugin, AutoDocPluginContext, AutoDocExporter } from '@memberjunction/db-auto-doc';
|
|
579
|
+
|
|
580
|
+
@RegisterClass(BaseAutoDocPlugin, 'MyCustomCheck')
|
|
581
|
+
export class MyCustomCheck extends BaseAutoDocPlugin {
|
|
582
|
+
public override async OnPostRun(context: AutoDocPluginContext): Promise<void> {
|
|
583
|
+
const tables = context.State.schemas.flatMap(s => s.tables);
|
|
584
|
+
this.SetData(context, 'MyCustomCheck', { TablesSeen: tables.length });
|
|
585
|
+
context.Log(`MyCustomCheck saw ${tables.length} tables`);
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
public override GetExporters(): AutoDocExporter[] {
|
|
589
|
+
return [{
|
|
590
|
+
Format: 'my-report',
|
|
591
|
+
Description: 'One-line summary from MyCustomCheck',
|
|
592
|
+
Generate: (state) => [{ FileName: 'my-report.txt', Content: JSON.stringify(state.plugins?.['MyCustomCheck']?.Data) }]
|
|
593
|
+
}];
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
```
|
|
597
|
+
|
|
598
|
+
### Index Advisor (Built-in Plugin)
|
|
599
|
+
|
|
600
|
+
The Index Advisor runs at the end of every analysis (`OnPostRun`) unless turned off. It never changes the database: it records proposals in the state file, and `export --format index-migration` writes them as a migration for someone to review and apply.
|
|
601
|
+
|
|
602
|
+
1. **Reads existing indexes** on every analyzed schema: SQL Server `sys.indexes`, PostgreSQL `pg_index`, MySQL `information_schema.STATISTICS`. Primary-key, unique, partial (filtered), expression and INCLUDE columns are all captured; an expression key part keeps its position (as `(expression)`), so an index *led* by an expression never counts as covering the column after it. A schema whose indexes cannot be read is recorded in `SkippedSchemas`, and no proposals are made for it.
|
|
603
|
+
2. **Proposes indexes with deterministic rules:**
|
|
604
|
+
- a foreign key (declared, or discovered and `confirmed` at `KeyMinConfidence` or higher) whose column has no index starting with it. A partial index does not count as covering;
|
|
605
|
+
- a confirmed discovered primary key on a table with no index on it.
|
|
606
|
+
|
|
607
|
+
Tables below `MinRowCount` are skipped, and so are leading columns that make poor index keys (large text/JSON/binary types, or two or fewer distinct values). Proposals are ranked by table size.
|
|
608
|
+
3. **LLM review** (on by default). For up to `MaxLLMTables` tables, largest first, that carry a proposal or appear in a [sample query](#sample-query-generation), the model sees the column statistics, descriptions, existing indexes, rule-based proposals and the queries that use the table. It may **keep, drop or modify** each proposal (for example make it composite, or add INCLUDE columns) and **add** new ones, each with a reason. The model returns structured decisions, never SQL. Every change is validated: the columns must exist, the index must not duplicate an existing or planned one, and the leading column must be a usable key. Rejected suggestions are recorded with the reason. If a table's review call fails, that table keeps its rule-based proposals. Use the `indexAdvisor` key in `ai.modelOverrides` to review with a different model.
|
|
609
|
+
4. **Flags redundant indexes** whose key columns are a leading prefix of (or identical to) another index of the same kind on the same table. They are reported only, never dropped. Unique, primary-key, partial and expression indexes are never flagged.
|
|
610
|
+
|
|
611
|
+
**Options** (`plugins[].Options` for `IndexAdvisor`):
|
|
612
|
+
|
|
613
|
+
| Option | Default | Meaning |
|
|
614
|
+
|---|---|---|
|
|
615
|
+
| `MinRowCount` | `1000` | Tables with fewer rows get no proposals |
|
|
616
|
+
| `IncludeDiscoveredFKs` | `true` | Also index discovered (not only declared) foreign keys |
|
|
617
|
+
| `KeyMinConfidence` | `90` | Minimum confidence for a discovered key to be used |
|
|
618
|
+
| `LLMReview` | `true` | Run the LLM review layer |
|
|
619
|
+
| `MaxLLMTables` | `40` | Most tables the review looks at |
|
|
620
|
+
| `Concurrently` | `true` | PostgreSQL: `CREATE INDEX CONCURRENTLY` (see below) |
|
|
621
|
+
|
|
622
|
+
Turn the plugin off with `{ "Name": "IndexAdvisor", "Enabled": false }`.
|
|
623
|
+
|
|
624
|
+
**The migration** (`export --format index-migration`) is named `V<yyyyMMddHHmm>__dbautodoc_indexes.sql` from the analysis time (override the prefix with `--file-prefix`), highest-priority index first, each with a comment giving its source and reason. Index names are `IX_<table>_<columns>`, shortened with a hash suffix to the platform's identifier limit. On PostgreSQL, where index names must be unique across a schema, every name carries a hash of its exact table and columns so two similar names can't collide. Reasons are written on a single comment line, so multi-line text from the LLM can't spill into the SQL.
|
|
625
|
+
|
|
626
|
+
| Platform | Statement |
|
|
627
|
+
|---|---|
|
|
628
|
+
| SQL Server | `CREATE NONCLUSTERED INDEX … INCLUDE (…)` guarded by `IF NOT EXISTS` on `sys.indexes`, so the script can be re-run |
|
|
629
|
+
| PostgreSQL | `CREATE INDEX CONCURRENTLY IF NOT EXISTS … INCLUDE (…)` |
|
|
630
|
+
| MySQL | `CREATE INDEX …` (MySQL has no `IF NOT EXISTS` for indexes, so the script cannot be re-run; INCLUDE columns are omitted) |
|
|
631
|
+
|
|
632
|
+
> **PostgreSQL and `CONCURRENTLY`:** building concurrently does not lock writes on a live database, but it cannot run inside a transaction. The exporter therefore also writes `<migration>.sql.conf` containing `executeInTransaction=false`, which Flyway honors. **Skyway 0.6.x always runs each migration inside a transaction**, so a `CONCURRENTLY` migration fails there ("cannot run inside a transaction block"). Until Skyway supports a per-migration opt-out, apply the file with `psql` or Flyway, or set `"Concurrently": false` (which locks writes on each table while its index builds).
|
|
633
|
+
|
|
437
634
|
### Sample Query Generation
|
|
438
635
|
|
|
439
636
|
DBAutoDoc can generate reference SQL queries for AI agents, solving the **query alignment problem** where multi-query patterns (summary + detail) have inconsistent filtering logic:
|
|
@@ -978,7 +1175,7 @@ Use different models for different pipeline phases. A cheaper/faster model for b
|
|
|
978
1175
|
{
|
|
979
1176
|
"ai": {
|
|
980
1177
|
"provider": "gemini",
|
|
981
|
-
"model": "gemini-3-flash
|
|
1178
|
+
"model": "gemini-3.8-flash",
|
|
982
1179
|
"modelOverrides": {
|
|
983
1180
|
"fkPruning": {
|
|
984
1181
|
"model": "gemini-3.1-pro-preview",
|
|
@@ -1017,7 +1214,7 @@ DBAutoDoc integrates with [MemberJunction's AI provider system](../../packages/A
|
|
|
1017
1214
|
```json
|
|
1018
1215
|
{
|
|
1019
1216
|
"provider": "gemini",
|
|
1020
|
-
"model": "gemini-3-flash
|
|
1217
|
+
"model": "gemini-3.8-flash",
|
|
1021
1218
|
"apiKey": "..."
|
|
1022
1219
|
}
|
|
1023
1220
|
```
|
|
@@ -11,6 +11,7 @@ export default class Analyze extends Command {
|
|
|
11
11
|
'max-iterations': import("@oclif/core/lib/interfaces/parser.js").OptionFlag<number, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
|
|
12
12
|
'reanalyze-below': import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
|
|
13
13
|
'pruning-only': import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
|
|
14
|
+
'skip-sampling': import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
|
|
14
15
|
};
|
|
15
16
|
static args: {};
|
|
16
17
|
run(): Promise<void>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"analyze.d.ts","sourceRoot":"","sources":["../../src/commands/analyze.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,OAAO,EAAS,MAAM,aAAa,CAAC;AAM7C,MAAM,CAAC,OAAO,OAAO,OAAQ,SAAQ,OAAO;IAC1C,MAAM,CAAC,WAAW,SAAiD;IAEnE,MAAM,CAAC,QAAQ,WAOb;IAEF,MAAM,CAAC,KAAK
|
|
1
|
+
{"version":3,"file":"analyze.d.ts","sourceRoot":"","sources":["../../src/commands/analyze.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,OAAO,EAAS,MAAM,aAAa,CAAC;AAM7C,MAAM,CAAC,OAAO,OAAO,OAAQ,SAAQ,OAAO;IAC1C,MAAM,CAAC,WAAW,SAAiD;IAEnE,MAAM,CAAC,QAAQ,WAOb;IAEF,MAAM,CAAC,KAAK;;;;;;;MA4BV;IAEF,MAAM,CAAC,IAAI,KAAM;IAEX,GAAG,IAAI,OAAO,CAAC,IAAI,CAAC;CAuE3B"}
|
package/dist/commands/analyze.js
CHANGED
|
@@ -39,6 +39,10 @@ export default class Analyze extends Command {
|
|
|
39
39
|
'pruning-only': Flags.boolean({
|
|
40
40
|
description: 'Skip discovery and analysis iterations. Only run the FK pruning pass on an existing state file. Requires --resume.',
|
|
41
41
|
default: false
|
|
42
|
+
}),
|
|
43
|
+
'skip-sampling': Flags.boolean({
|
|
44
|
+
description: 'Skip table data sampling pass (useful for large databases or when schema/dictionary is already provided)',
|
|
45
|
+
default: false
|
|
42
46
|
})
|
|
43
47
|
}; }
|
|
44
48
|
static { this.args = {}; }
|
|
@@ -68,6 +72,8 @@ export default class Analyze extends Command {
|
|
|
68
72
|
resumeFromState: flags.resume,
|
|
69
73
|
reanalyzeBelowConfidence,
|
|
70
74
|
maxIterations,
|
|
75
|
+
// --pruning-only runs the FK pruning pass alone; sampling would be new work it never asked for
|
|
76
|
+
skipSampling: flags['skip-sampling'] || flags['pruning-only'],
|
|
71
77
|
onProgress: (message, data) => {
|
|
72
78
|
if (data) {
|
|
73
79
|
spinner.succeed(`${message}: ${JSON.stringify(data)}`);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"analyze.js","sourceRoot":"","sources":["../../src/commands/analyze.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"analyze.js","sourceRoot":"","sources":["../../src/commands/analyze.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,26 +1,43 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Export command - Generate SQL and Markdown documentation
|
|
3
3
|
*/
|
|
4
|
-
import { Command } from '@oclif/core';
|
|
4
|
+
import { Command, Interfaces } from '@oclif/core';
|
|
5
|
+
import { ErdColumnMode, ErdMode } from '../types/config.js';
|
|
5
6
|
export default class Export extends Command {
|
|
6
7
|
static description: string;
|
|
7
8
|
static examples: string[];
|
|
8
9
|
static flags: {
|
|
9
|
-
'state-file':
|
|
10
|
-
'output-dir':
|
|
11
|
-
sql:
|
|
12
|
-
markdown:
|
|
13
|
-
html:
|
|
14
|
-
csv:
|
|
15
|
-
mermaid:
|
|
16
|
-
report:
|
|
17
|
-
'schema-info':
|
|
18
|
-
'schema-info-discovered-only':
|
|
19
|
-
'schema-info-confirmed-only':
|
|
20
|
-
apply:
|
|
21
|
-
'approved-only':
|
|
22
|
-
'confidence-threshold':
|
|
10
|
+
'state-file': Interfaces.OptionFlag<string, Interfaces.CustomOptions>;
|
|
11
|
+
'output-dir': Interfaces.OptionFlag<string, Interfaces.CustomOptions>;
|
|
12
|
+
sql: Interfaces.BooleanFlag<boolean>;
|
|
13
|
+
markdown: Interfaces.BooleanFlag<boolean>;
|
|
14
|
+
html: Interfaces.BooleanFlag<boolean>;
|
|
15
|
+
csv: Interfaces.BooleanFlag<boolean>;
|
|
16
|
+
mermaid: Interfaces.BooleanFlag<boolean>;
|
|
17
|
+
report: Interfaces.BooleanFlag<boolean>;
|
|
18
|
+
'schema-info': Interfaces.BooleanFlag<boolean>;
|
|
19
|
+
'schema-info-discovered-only': Interfaces.BooleanFlag<boolean>;
|
|
20
|
+
'schema-info-confirmed-only': Interfaces.BooleanFlag<boolean>;
|
|
21
|
+
apply: Interfaces.BooleanFlag<boolean>;
|
|
22
|
+
'approved-only': Interfaces.BooleanFlag<boolean>;
|
|
23
|
+
'confidence-threshold': Interfaces.OptionFlag<string, Interfaces.CustomOptions>;
|
|
24
|
+
'erd-mode': Interfaces.OptionFlag<ErdMode, Interfaces.CustomOptions>;
|
|
25
|
+
'erd-columns': Interfaces.OptionFlag<ErdColumnMode, Interfaces.CustomOptions>;
|
|
26
|
+
'erd-hub-min-inbound': Interfaces.OptionFlag<number, Interfaces.CustomOptions>;
|
|
27
|
+
'erd-lookup-max-rows': Interfaces.OptionFlag<number, Interfaces.CustomOptions>;
|
|
28
|
+
'erd-page-size': Interfaces.OptionFlag<number, Interfaces.CustomOptions>;
|
|
29
|
+
'erd-min-cluster-size': Interfaces.OptionFlag<number, Interfaces.CustomOptions>;
|
|
30
|
+
format: Interfaces.OptionFlag<string[], Interfaces.CustomOptions>;
|
|
31
|
+
'list-formats': Interfaces.BooleanFlag<boolean>;
|
|
32
|
+
provider: Interfaces.OptionFlag<string, Interfaces.CustomOptions>;
|
|
33
|
+
'file-prefix': Interfaces.OptionFlag<string, Interfaces.CustomOptions>;
|
|
23
34
|
};
|
|
24
35
|
run(): Promise<void>;
|
|
36
|
+
/** Merge the ERD settings: defaults, then config.output.erd, then the export flags. */
|
|
37
|
+
private resolveErdConfig;
|
|
38
|
+
/** Loads built-in plugins plus any external plugin modules named in the config. */
|
|
39
|
+
private loadPluginExporters;
|
|
40
|
+
private listFormats;
|
|
41
|
+
private runPluginExports;
|
|
25
42
|
}
|
|
26
43
|
//# sourceMappingURL=export.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"export.d.ts","sourceRoot":"","sources":["../../src/commands/export.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,OAAO,EAAS,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"export.d.ts","sourceRoot":"","sources":["../../src/commands/export.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,OAAO,EAAS,UAAU,EAAE,MAAM,aAAa,CAAC;AAezD,OAAO,EAAgB,aAAa,EAAa,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAerF,MAAM,CAAC,OAAO,OAAO,MAAO,SAAQ,OAAO;IACzC,MAAM,CAAC,WAAW,SAAkF;IAEpG,MAAM,CAAC,QAAQ,WAUb;IAEF,MAAM,CAAC,KAAK;;;;;;;;;;;;;;;;;;;;;;;;;MA+CV;IAEI,GAAG,IAAI,OAAO,CAAC,IAAI,CAAC;IA0O1B,uFAAuF;IACvF,OAAO,CAAC,gBAAgB;IAYxB,mFAAmF;YACrE,mBAAmB;IAMjC,OAAO,CAAC,WAAW;YAUL,gBAAgB;CAsB/B"}
|
package/dist/commands/export.js
CHANGED
|
@@ -14,8 +14,16 @@ import { ReportGenerator } from '../generators/ReportGenerator.js';
|
|
|
14
14
|
import { HTMLGenerator } from '../generators/HTMLGenerator.js';
|
|
15
15
|
import { CSVGenerator } from '../generators/CSVGenerator.js';
|
|
16
16
|
import { MermaidGenerator } from '../generators/MermaidGenerator.js';
|
|
17
|
+
import { ErdClusterer } from '../generators/ErdClusterer.js';
|
|
18
|
+
import { ErdClusterGenerator } from '../generators/ErdClusterGenerator.js';
|
|
19
|
+
import { ERD_DEFAULTS } from '../types/config.js';
|
|
17
20
|
import { AdditionalSchemaInfoGenerator } from '../generators/AdditionalSchemaInfoGenerator.js';
|
|
21
|
+
import { PluginManager } from '../plugins/PluginManager.js';
|
|
18
22
|
// DatabaseConnection is lazy-imported only when --apply is used (requires DB credentials)
|
|
23
|
+
/** Sub-directory of the output directory that holds the subject-area diagrams. */
|
|
24
|
+
const ERD_CLUSTER_DIR = 'erd';
|
|
25
|
+
const ERD_MODES = ['full', 'clusters', 'both'];
|
|
26
|
+
const ERD_COLUMN_MODES = ['all', 'keys', 'none'];
|
|
19
27
|
export default class Export extends Command {
|
|
20
28
|
static { this.description = 'Export documentation in multiple formats (SQL, Markdown, HTML, CSV, Mermaid)'; }
|
|
21
29
|
static { this.examples = [
|
|
@@ -25,7 +33,9 @@ export default class Export extends Command {
|
|
|
25
33
|
'$ db-auto-doc export --html',
|
|
26
34
|
'$ db-auto-doc export --csv',
|
|
27
35
|
'$ db-auto-doc export --mermaid',
|
|
28
|
-
'$ db-auto-doc export --sql --markdown --html --csv --mermaid --apply'
|
|
36
|
+
'$ db-auto-doc export --sql --markdown --html --csv --mermaid --apply',
|
|
37
|
+
'$ db-auto-doc export --mermaid --erd-mode clusters',
|
|
38
|
+
'$ db-auto-doc export --html --mermaid --erd-mode clusters --erd-hub-min-inbound 12 --erd-page-size 40'
|
|
29
39
|
]; }
|
|
30
40
|
static { this.flags = {
|
|
31
41
|
'state-file': Flags.string({ description: 'Path to state JSON file', char: 's' }),
|
|
@@ -41,7 +51,39 @@ export default class Export extends Command {
|
|
|
41
51
|
'schema-info-confirmed-only': Flags.boolean({ description: 'Only include confirmed candidates in schema info', default: false }),
|
|
42
52
|
apply: Flags.boolean({ description: 'Apply SQL to database', default: false }),
|
|
43
53
|
'approved-only': Flags.boolean({ description: 'Only export approved items', default: false }),
|
|
44
|
-
'confidence-threshold': Flags.string({ description: 'Minimum confidence threshold', default: '0' })
|
|
54
|
+
'confidence-threshold': Flags.string({ description: 'Minimum confidence threshold', default: '0' }),
|
|
55
|
+
'erd-mode': Flags.custom({ options: ERD_MODES })({
|
|
56
|
+
description: 'ERD layout: full (one diagram), clusters (one per subject area), both. Overrides output.erd.mode'
|
|
57
|
+
}),
|
|
58
|
+
'erd-columns': Flags.custom({ options: ERD_COLUMN_MODES })({
|
|
59
|
+
description: 'Columns drawn per entity: all, keys (PK/FK only), none. Overrides output.erd.columns'
|
|
60
|
+
}),
|
|
61
|
+
'erd-hub-min-inbound': Flags.integer({
|
|
62
|
+
min: 1,
|
|
63
|
+
description: `A table referenced by at least this many tables is a subject-area hub (default ${ERD_DEFAULTS.hubMinInbound})`
|
|
64
|
+
}),
|
|
65
|
+
'erd-lookup-max-rows': Flags.integer({
|
|
66
|
+
min: 1,
|
|
67
|
+
description: `A table that references nothing and has fewer rows than this is a reference table, listed not drawn (default ${ERD_DEFAULTS.lookupMaxRows})`
|
|
68
|
+
}),
|
|
69
|
+
'erd-page-size': Flags.integer({
|
|
70
|
+
min: 1,
|
|
71
|
+
description: `Tables per diagram page beyond the hub (default ${ERD_DEFAULTS.pageSize})`
|
|
72
|
+
}),
|
|
73
|
+
'erd-min-cluster-size': Flags.integer({
|
|
74
|
+
min: 1,
|
|
75
|
+
description: `Subject areas smaller than this fold into a neighbour (default ${ERD_DEFAULTS.minClusterSize})`
|
|
76
|
+
}),
|
|
77
|
+
format: Flags.string({
|
|
78
|
+
description: 'Plugin export format to generate (repeatable). See --list-formats.',
|
|
79
|
+
multiple: true
|
|
80
|
+
}),
|
|
81
|
+
'list-formats': Flags.boolean({ description: 'List the export formats contributed by plugins and exit', default: false }),
|
|
82
|
+
provider: Flags.string({
|
|
83
|
+
description: 'Database platform for dialect-specific output (defaults to the state file, then config, then sqlserver)',
|
|
84
|
+
options: ['sqlserver', 'mysql', 'postgresql']
|
|
85
|
+
}),
|
|
86
|
+
'file-prefix': Flags.string({ description: 'File name prefix override for plugin exports (e.g. a migration version prefix)' })
|
|
45
87
|
}; }
|
|
46
88
|
async run() {
|
|
47
89
|
const { flags } = await this.parse(Export);
|
|
@@ -76,8 +118,18 @@ export default class Export extends Command {
|
|
|
76
118
|
spinner.succeed('State loaded');
|
|
77
119
|
// Ensure output directory exists
|
|
78
120
|
await fs.mkdir(outputDir, { recursive: true });
|
|
121
|
+
// Plugin-contributed formats (--format / --list-formats)
|
|
122
|
+
const exporters = await this.loadPluginExporters(config?.plugins);
|
|
123
|
+
if (flags['list-formats']) {
|
|
124
|
+
this.listFormats(exporters);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
if (flags.format?.length) {
|
|
128
|
+
const provider = (flags.provider ?? state.database.provider ?? config?.database?.provider ?? 'sqlserver');
|
|
129
|
+
await this.runPluginExports(flags.format, exporters, state, outputDir, { Provider: provider, FileNamePrefix: flags['file-prefix'] }, spinner);
|
|
130
|
+
}
|
|
79
131
|
// Default to SQL + Markdown if no specific format flags provided
|
|
80
|
-
const anyFormatSpecified = flags.sql || flags.markdown || flags.html || flags.csv || flags.mermaid || flags.report || flags['schema-info'];
|
|
132
|
+
const anyFormatSpecified = flags.sql || flags.markdown || flags.html || flags.csv || flags.mermaid || flags.report || flags['schema-info'] || !!flags.format?.length;
|
|
81
133
|
const generateSQL = flags.sql || !anyFormatSpecified;
|
|
82
134
|
const generateMarkdown = flags.markdown || !anyFormatSpecified;
|
|
83
135
|
const generateHTML = flags.html;
|
|
@@ -139,11 +191,29 @@ export default class Export extends Command {
|
|
|
139
191
|
await fs.writeFile(mdPath, markdown, 'utf-8');
|
|
140
192
|
spinner.succeed(`Markdown documentation saved to ${mdPath}`);
|
|
141
193
|
}
|
|
194
|
+
// ERD options: export flags override config.output.erd, which overrides the defaults.
|
|
195
|
+
const erd = this.resolveErdConfig(config?.output?.erd, flags);
|
|
196
|
+
// The plan is built whenever an output needs it: the HTML's Subject Areas section links to the
|
|
197
|
+
// cluster pages, so requesting --html in clusters mode also writes them.
|
|
198
|
+
let clusterPlan = (generateMermaid || generateHTML) && erd.mode !== 'full'
|
|
199
|
+
? new ErdClusterer().Plan(state, erd)
|
|
200
|
+
: undefined;
|
|
201
|
+
for (const warning of clusterPlan?.Warnings ?? []) {
|
|
202
|
+
this.warn(`ERD ${warning}`);
|
|
203
|
+
}
|
|
204
|
+
if (clusterPlan && clusterPlan.Clusters.length === 0) {
|
|
205
|
+
// Nothing is referenced often enough to anchor an area: fall back to the single diagram
|
|
206
|
+
// rather than leaving the documentation with no ERD at all.
|
|
207
|
+
this.warn(`ERD: no table is referenced by at least ${clusterPlan.Options.hubMinInbound} others, so there are no subject areas; `
|
|
208
|
+
+ 'drawing the full diagram instead (lower --erd-hub-min-inbound to cluster)');
|
|
209
|
+
clusterPlan = undefined;
|
|
210
|
+
erd.mode = 'full';
|
|
211
|
+
}
|
|
142
212
|
// Generate HTML
|
|
143
213
|
if (generateHTML) {
|
|
144
214
|
spinner.start('Generating HTML documentation');
|
|
145
215
|
const htmlGen = new HTMLGenerator();
|
|
146
|
-
const html = htmlGen.generate(state, generatorOptions);
|
|
216
|
+
const html = htmlGen.generate(state, { ...generatorOptions, clusterPlan, clusterDir: ERD_CLUSTER_DIR });
|
|
147
217
|
const htmlPath = path.join(outputDir, 'documentation.html');
|
|
148
218
|
await fs.writeFile(htmlPath, html, 'utf-8');
|
|
149
219
|
spinner.succeed(`HTML documentation saved to ${htmlPath}`);
|
|
@@ -159,17 +229,35 @@ export default class Export extends Command {
|
|
|
159
229
|
await fs.writeFile(columnsPath, csvExport.columns, 'utf-8');
|
|
160
230
|
spinner.succeed(`CSV exports saved to ${tablesPath} and ${columnsPath}`);
|
|
161
231
|
}
|
|
162
|
-
// Generate Mermaid
|
|
232
|
+
// Generate Mermaid: the schema-wide diagram, the clustered subject areas, or both
|
|
163
233
|
if (generateMermaid) {
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
234
|
+
if (erd.mode === 'full' || erd.mode === 'both') {
|
|
235
|
+
spinner.start('Generating Mermaid diagram');
|
|
236
|
+
const mermaidGen = new MermaidGenerator();
|
|
237
|
+
const fullOptions = { ...generatorOptions, columns: erd.columns === 'keys' || erd.columns === 'none' ? erd.columns : 'all' };
|
|
238
|
+
const mermaidDiagram = mermaidGen.generate(state, fullOptions);
|
|
239
|
+
const mermaidHtml = mermaidGen.generateHtml(state, fullOptions);
|
|
240
|
+
const mermaidPath = path.join(outputDir, 'erd.mmd');
|
|
241
|
+
const mermaidHtmlPath = path.join(outputDir, 'erd.html');
|
|
242
|
+
await fs.writeFile(mermaidPath, mermaidDiagram, 'utf-8');
|
|
243
|
+
await fs.writeFile(mermaidHtmlPath, mermaidHtml, 'utf-8');
|
|
244
|
+
spinner.succeed(`Mermaid diagram saved to ${mermaidPath} and ${mermaidHtmlPath}`);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
if (clusterPlan) {
|
|
248
|
+
spinner.start('Generating subject-area diagrams');
|
|
249
|
+
const clusterGen = new ErdClusterGenerator();
|
|
250
|
+
const output = clusterGen.Generate(state, clusterPlan, { columns: erd.columns ?? 'keys' });
|
|
251
|
+
const clusterDir = path.join(outputDir, ERD_CLUSTER_DIR);
|
|
252
|
+
await fs.mkdir(clusterDir, { recursive: true });
|
|
253
|
+
for (const file of output.Files) {
|
|
254
|
+
await fs.writeFile(path.join(clusterDir, `${file.BaseName}.mmd`), file.Mermaid, 'utf-8');
|
|
255
|
+
await fs.writeFile(path.join(clusterDir, `${file.BaseName}.html`), file.Html, 'utf-8');
|
|
256
|
+
}
|
|
257
|
+
await fs.writeFile(path.join(clusterDir, 'index.html'), output.IndexHtml, 'utf-8');
|
|
258
|
+
await fs.writeFile(path.join(clusterDir, 'index.md'), output.IndexMarkdown, 'utf-8');
|
|
259
|
+
const largest = output.Files.reduce((n, f) => Math.max(n, f.Mermaid.length), 0);
|
|
260
|
+
spinner.succeed(`${clusterPlan.Clusters.length} subject areas (${output.Files.length} diagram pages, largest ${largest.toLocaleString()} chars) saved to ${clusterDir}`);
|
|
173
261
|
}
|
|
174
262
|
// Generate Report
|
|
175
263
|
if (flags.report) {
|
|
@@ -201,5 +289,47 @@ export default class Export extends Command {
|
|
|
201
289
|
this.error(error.message);
|
|
202
290
|
}
|
|
203
291
|
}
|
|
292
|
+
/** Merge the ERD settings: defaults, then config.output.erd, then the export flags. */
|
|
293
|
+
resolveErdConfig(configured, flags) {
|
|
294
|
+
return {
|
|
295
|
+
...configured,
|
|
296
|
+
mode: flags['erd-mode'] ?? configured?.mode ?? 'full',
|
|
297
|
+
columns: flags['erd-columns'] ?? configured?.columns,
|
|
298
|
+
hubMinInbound: flags['erd-hub-min-inbound'] ?? configured?.hubMinInbound,
|
|
299
|
+
lookupMaxRows: flags['erd-lookup-max-rows'] ?? configured?.lookupMaxRows,
|
|
300
|
+
pageSize: flags['erd-page-size'] ?? configured?.pageSize,
|
|
301
|
+
minClusterSize: flags['erd-min-cluster-size'] ?? configured?.minClusterSize,
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
/** Loads built-in plugins plus any external plugin modules named in the config. */
|
|
305
|
+
async loadPluginExporters(entries) {
|
|
306
|
+
const plugins = new PluginManager();
|
|
307
|
+
await plugins.Load(entries);
|
|
308
|
+
return plugins.GetExporters();
|
|
309
|
+
}
|
|
310
|
+
listFormats(exporters) {
|
|
311
|
+
if (exporters.length === 0) {
|
|
312
|
+
this.log('No plugin export formats are available.');
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
for (const e of exporters) {
|
|
316
|
+
this.log(`${chalk.cyan(e.Format)} ${e.Description}`);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
async runPluginExports(formats, exporters, state, outputDir, options, spinner) {
|
|
320
|
+
for (const format of formats) {
|
|
321
|
+
const exporter = exporters.find((e) => e.Format.toLowerCase() === format.toLowerCase());
|
|
322
|
+
if (!exporter) {
|
|
323
|
+
const known = exporters.map((e) => e.Format).join(', ') || '(none)';
|
|
324
|
+
throw new Error(`Unknown export format '${format}'. Available: ${known}`);
|
|
325
|
+
}
|
|
326
|
+
spinner.start(`Generating ${exporter.Format}`);
|
|
327
|
+
const files = exporter.Generate(state, options);
|
|
328
|
+
for (const file of files) {
|
|
329
|
+
await fs.writeFile(path.join(outputDir, file.FileName), file.Content, 'utf-8');
|
|
330
|
+
}
|
|
331
|
+
spinner.succeed(`${exporter.Format}: ${files.length ? files.map((f) => f.FileName).join(', ') : 'nothing to export'}`);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
204
334
|
}
|
|
205
335
|
//# sourceMappingURL=export.js.map
|