nestjs-doctor 0.4.17 → 0.4.19

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 CHANGED
@@ -19,7 +19,7 @@
19
19
  </p>
20
20
 
21
21
  <p align="center">
22
- 40 built-in rules across <b>security</b>, <b>performance</b>, <b>correctness</b>, and <b>architecture</b>. Outputs a <b>0-100 score</b> with actionable diagnostics. Zero config. Monorepo support. Catches the anti-patterns that AI-generated code introduce (slop code).
22
+ 43 built-in rules across <b>security</b>, <b>performance</b>, <b>correctness</b>, <b>architecture</b>, and <b>schema</b>. Outputs a <b>0-100 score</b> with actionable diagnostics. Zero config. Monorepo support. Catches the anti-patterns that AI-generated code introduce (slop code).
23
23
  </p>
24
24
 
25
25
  ---
@@ -46,7 +46,7 @@ npx nestjs-doctor@latest . --verbose
46
46
  npx nestjs-doctor@latest . --report
47
47
  ```
48
48
 
49
- Self-contained HTML file with four sections: score summary, source-level diagnostics with code viewer, interactive module graph, and a custom rule playground. Opens in your browser.
49
+ Self-contained HTML file with five sections: score summary, source-level diagnostics with code viewer, interactive module graph, schema ER diagram, and a custom rule playground. Opens in your browser.
50
50
 
51
51
  ![Module Graph](https://nestjs.doctor/module-graph.png)
52
52
 
@@ -60,7 +60,7 @@ Install [NestJS Doctor](https://marketplace.visualstudio.com/items?itemName=rolo
60
60
  npm install -D nestjs-doctor
61
61
  ```
62
62
 
63
- Same 40 rules as the CLI, surfaced as inline diagnostics in the editor and in the Problems panel. Files are scanned on open and on save with a configurable debounce.
63
+ Same 43 rules as the CLI, surfaced as inline diagnostics in the editor and in the Problems panel. Files are scanned on open and on save with a configurable debounce.
64
64
 
65
65
  Use `NestJS Doctor: Scan Project` from the command palette to trigger a full scan manually.
66
66
 
@@ -169,10 +169,22 @@ Also works as a `"nestjs-doctor"` key in `package.json`.
169
169
  | `minScore` | `number` | Minimum passing score (0-100). Exits with code 1 if below threshold |
170
170
  | `ignore.rules` | `string[]` | Rule IDs to suppress |
171
171
  | `ignore.files` | `string[]` | Glob patterns for files whose diagnostics are hidden |
172
- | `rules` | `Record<string, boolean>` | Enable/disable individual rules |
172
+ | `rules` | `Record<string, RuleOverride \| boolean>` | Enable/disable individual rules, override severity, and pass rule-specific options |
173
173
  | `categories` | `Record<string, boolean>` | Enable/disable entire categories |
174
174
  | `customRulesDir` | `string` | Path to a directory containing custom rule files |
175
175
 
176
+ Example rule-specific override:
177
+
178
+ ```json
179
+ {
180
+ "rules": {
181
+ "architecture/no-manual-instantiation": {
182
+ "excludeClasses": ["Logger", "PinoLogger"]
183
+ }
184
+ }
185
+ }
186
+ ```
187
+
176
188
  ---
177
189
 
178
190
  ## Custom Rules
@@ -189,7 +201,7 @@ import type { RuleContext } from "nestjs-doctor";
189
201
  export const noTodoComments = {
190
202
  meta: {
191
203
  id: "no-todo-comments",
192
- category: "correctness", // "security" | "performance" | "correctness" | "architecture"
204
+ category: "correctness", // "security" | "performance" | "correctness" | "architecture" | "schema"
193
205
  severity: "warning", // "error" | "warning" | "info"
194
206
  description: "TODO comments should be resolved before merging",
195
207
  help: "Replace the TODO with an implementation or open an issue.",
@@ -203,7 +215,7 @@ export const noTodoComments = {
203
215
  const pos = context.sourceFile.getLineAndColumnAtPos(match.index);
204
216
  context.report({
205
217
  message: "Unresolved TODO comment",
206
- file: context.filePath,
218
+ filePath: context.filePath,
207
219
  line: pos.line,
208
220
  });
209
221
  }
@@ -248,6 +260,19 @@ Output includes a combined score and a per-project breakdown.
248
260
 
249
261
  ---
250
262
 
263
+ ## Schema Analysis
264
+
265
+ Auto-detected from Prisma schema files (`schema.prisma`) or TypeORM entity decorators (`@Entity()`). When a schema is found, nestjs-doctor extracts entity-relationship data and:
266
+
267
+ - Renders an **interactive ER diagram** in the Schema tab of the HTML report (sidebar entity tree + canvas diagram + problems panel)
268
+ - Runs **3 schema-specific rules** covering primary keys, timestamps, and cascade configuration
269
+
270
+ Supported ORMs: **Prisma**, **TypeORM**.
271
+
272
+ See the [schema rules documentation](https://nestjs.doctor/docs/rules/schema) for the full rule list.
273
+
274
+ ---
275
+
251
276
  ## Scoring
252
277
 
253
278
  Weighted by severity and category, normalized by file count:
@@ -256,7 +281,8 @@ Weighted by severity and category, normalized by file count:
256
281
  |----------|--------|-|----------|------------|
257
282
  | error | 3.0 | | security | 1.5x |
258
283
  | warning | 1.5 | | correctness | 1.3x |
259
- | info | 0.5 | | architecture | 1.0x |
284
+ | info | 0.5 | | schema | 1.1x |
285
+ | | | | architecture | 1.0x |
260
286
  | | | | performance | 0.8x |
261
287
 
262
288
  | Score | Label |
@@ -287,7 +313,7 @@ mono.combined; // Merged DiagnoseResult
287
313
 
288
314
  ---
289
315
 
290
- ## Rules (40)
316
+ ## Rules (43)
291
317
 
292
318
  ### Security (9)
293
319
 
@@ -307,7 +333,7 @@ mono.combined; // Merged DiagnoseResult
307
333
 
308
334
  | Rule | Severity | What it catches |
309
335
  |------|----------|-----------------|
310
- | `no-missing-injectable` | error | Provider in module missing `@Injectable()` |
336
+ | `no-missing-injectable` | error | Provider with constructor deps missing `@Injectable()` |
311
337
  | `no-duplicate-routes` | error | Same method + path + version twice in a controller |
312
338
  | `no-missing-guard-method` | error | Guard class missing `canActivate()` |
313
339
  | `no-missing-pipe-method` | error | Pipe class missing `transform()` |
@@ -349,4 +375,10 @@ mono.combined; // Merged DiagnoseResult
349
375
  | `no-unused-module-exports` | info | Module exports unused by importers |
350
376
  | `no-orphan-modules` | info | Module never imported by any other module |
351
377
 
378
+ ### Schema (3)
352
379
 
380
+ | Rule | Severity | What it catches |
381
+ |------|----------|-----------------|
382
+ | `schema/require-primary-key` | error | Entity without a primary key column |
383
+ | `schema/require-timestamps` | warning | Entity missing createdAt/updatedAt columns |
384
+ | `schema/require-cascade-rule` | info | Relation missing explicit onDelete behavior |
@@ -1,6 +1,87 @@
1
1
  import { ClassDeclaration, Project, SourceFile } from "ts-morph";
2
2
 
3
- //#region src/engine/type-resolver.d.ts
3
+ //#region src/common/config.d.ts
4
+ interface RuleOverride {
5
+ enabled?: boolean;
6
+ excludeClasses?: string[];
7
+ options?: Record<string, unknown>;
8
+ severity?: Severity;
9
+ }
10
+ interface NestjsDoctorIgnoreConfig {
11
+ files?: string[];
12
+ rules?: string[];
13
+ }
14
+ interface NestjsDoctorConfig {
15
+ categories?: Partial<Record<Category, boolean>>;
16
+ customRulesDir?: string;
17
+ exclude?: string[];
18
+ ignore?: NestjsDoctorIgnoreConfig;
19
+ include?: string[];
20
+ minScore?: number;
21
+ rules?: Record<string, RuleOverride | boolean>;
22
+ }
23
+ //#endregion
24
+ //#region src/common/schema.d.ts
25
+ interface SchemaColumn {
26
+ defaultValue?: string;
27
+ hasIndex?: boolean;
28
+ isGenerated: boolean;
29
+ isNullable: boolean;
30
+ isPrimary: boolean;
31
+ isUnique: boolean;
32
+ name: string;
33
+ type: string;
34
+ }
35
+ interface SchemaRelation {
36
+ fromEntity: string;
37
+ isNullable: boolean;
38
+ onDelete?: string;
39
+ propertyName: string;
40
+ toEntity: string;
41
+ type: "one-to-one" | "one-to-many" | "many-to-one" | "many-to-many";
42
+ }
43
+ interface SchemaEntity {
44
+ columns: SchemaColumn[];
45
+ filePath: string;
46
+ indexes?: {
47
+ columns: string[];
48
+ isUnique: boolean;
49
+ }[];
50
+ name: string;
51
+ relations: SchemaRelation[];
52
+ tableName: string;
53
+ }
54
+ /**
55
+ * In-memory schema graph used during analysis.
56
+ * Entities are stored in a Map for O(1) lookup by name.
57
+ */
58
+ interface SchemaGraph {
59
+ entities: Map<string, SchemaEntity>;
60
+ orm: string;
61
+ relations: SchemaRelation[];
62
+ }
63
+ /**
64
+ * JSON-safe version of SchemaGraph for HTML reports and API responses.
65
+ * Entities are flattened to an array since Maps are not serializable.
66
+ */
67
+ interface SerializedSchemaGraph {
68
+ entities: SerializedSchemaEntity[];
69
+ orm: string;
70
+ relations: SchemaRelation[];
71
+ }
72
+ /**
73
+ * JSON-safe version of SchemaEntity (omits indexes since they are
74
+ * only needed during rule analysis, not in serialized output).
75
+ */
76
+ interface SerializedSchemaEntity {
77
+ columns: SchemaColumn[];
78
+ filePath: string;
79
+ name: string;
80
+ relations: SchemaRelation[];
81
+ tableName: string;
82
+ }
83
+ //#endregion
84
+ //#region src/engine/graph/type-resolver.d.ts
4
85
  interface ProviderInfo {
5
86
  classDeclaration: ClassDeclaration;
6
87
  dependencies: string[];
@@ -10,7 +91,7 @@ interface ProviderInfo {
10
91
  }
11
92
  declare function updateProvidersForFile(providers: Map<string, ProviderInfo>, project: Project, filePath: string): void;
12
93
  //#endregion
13
- //#region src/engine/module-graph.d.ts
94
+ //#region src/engine/graph/module-graph.d.ts
14
95
  interface ModuleNode {
15
96
  classDeclaration: ClassDeclaration;
16
97
  controllers: string[];
@@ -27,47 +108,8 @@ interface ModuleGraph {
27
108
  }
28
109
  declare function updateModuleGraphForFile(graph: ModuleGraph, project: Project, filePath: string): void;
29
110
  //#endregion
30
- //#region src/types/diagnostic.d.ts
31
- type Severity = "error" | "warning" | "info";
32
- type Category = "security" | "performance" | "correctness" | "architecture";
33
- interface SourceLine {
34
- line: number;
35
- text: string;
36
- }
37
- interface Diagnostic {
38
- category: Category;
39
- column: number;
40
- filePath: string;
41
- help: string;
42
- line: number;
43
- message: string;
44
- rule: string;
45
- scope?: RuleScope;
46
- severity: Severity;
47
- sourceLines?: SourceLine[];
48
- }
49
- //#endregion
50
- //#region src/types/config.d.ts
51
- interface RuleOverride {
52
- enabled?: boolean;
53
- severity?: Severity;
54
- }
55
- interface NestjsDoctorIgnoreConfig {
56
- files?: string[];
57
- rules?: string[];
58
- }
59
- interface NestjsDoctorConfig {
60
- categories?: Partial<Record<Category, boolean>>;
61
- customRulesDir?: string;
62
- exclude?: string[];
63
- ignore?: NestjsDoctorIgnoreConfig;
64
- include?: string[];
65
- minScore?: number;
66
- rules?: Record<string, RuleOverride | boolean>;
67
- }
68
- //#endregion
69
- //#region src/rules/types.d.ts
70
- type RuleScope = "file" | "project";
111
+ //#region src/engine/rules/types.d.ts
112
+ type RuleScope = "file" | "project" | "schema";
71
113
  interface RuleMeta {
72
114
  category: Category;
73
115
  description: string;
@@ -76,9 +118,10 @@ interface RuleMeta {
76
118
  scope?: RuleScope;
77
119
  severity: Severity;
78
120
  }
79
- interface RuleContext {
121
+ interface CodeRuleContext {
122
+ config?: NestjsDoctorConfig;
80
123
  filePath: string;
81
- report(diagnostic: Omit<Diagnostic, "rule" | "category" | "severity">): void;
124
+ report(diagnostic: Omit<CodeDiagnostic, "rule" | "category" | "severity" | "scope">): void;
82
125
  sourceFile: SourceFile;
83
126
  }
84
127
  interface ProjectRuleContext {
@@ -87,19 +130,57 @@ interface ProjectRuleContext {
87
130
  moduleGraph: ModuleGraph;
88
131
  project: Project;
89
132
  providers: Map<string, ProviderInfo>;
90
- report(diagnostic: Omit<Diagnostic, "rule" | "category" | "severity">): void;
133
+ report(diagnostic: Omit<CodeDiagnostic, "rule" | "category" | "severity" | "scope">): void;
134
+ }
135
+ interface SchemaRuleContext {
136
+ orm: string;
137
+ report(diagnostic: Omit<SchemaDiagnostic, "rule" | "category" | "severity" | "scope">): void;
138
+ schemaGraph: SchemaGraph;
91
139
  }
92
140
  interface Rule {
93
- check(context: RuleContext): void;
141
+ check(context: CodeRuleContext): void;
94
142
  meta: RuleMeta;
95
143
  }
96
144
  interface ProjectRule {
97
145
  check(context: ProjectRuleContext): void;
98
146
  meta: RuleMeta;
99
147
  }
100
- type AnyRule = Rule | ProjectRule;
148
+ interface SchemaRule {
149
+ check(context: SchemaRuleContext): void;
150
+ meta: RuleMeta;
151
+ }
152
+ type AnyRule = Rule | ProjectRule | SchemaRule;
101
153
  //#endregion
102
- //#region src/types/result.d.ts
154
+ //#region src/common/diagnostic.d.ts
155
+ type Severity = "error" | "warning" | "info";
156
+ type Category = "security" | "performance" | "correctness" | "architecture" | "schema";
157
+ interface SourceLine {
158
+ line: number;
159
+ text: string;
160
+ }
161
+ interface BaseDiagnostic {
162
+ category: Category;
163
+ filePath: string;
164
+ help: string;
165
+ message: string;
166
+ rule: string;
167
+ scope?: RuleScope;
168
+ severity: Severity;
169
+ }
170
+ interface CodeDiagnostic extends BaseDiagnostic {
171
+ column: number;
172
+ line: number;
173
+ sourceLines?: SourceLine[];
174
+ }
175
+ interface SchemaDiagnostic extends BaseDiagnostic {
176
+ entity: string;
177
+ schemaColumn?: string;
178
+ }
179
+ type Diagnostic = CodeDiagnostic | SchemaDiagnostic;
180
+ declare function isCodeDiagnostic(d: Diagnostic): d is CodeDiagnostic;
181
+ declare function isSchemaDiagnostic(d: Diagnostic): d is SchemaDiagnostic;
182
+ //#endregion
183
+ //#region src/common/result.d.ts
103
184
  interface Score {
104
185
  label: string;
105
186
  value: number;
@@ -128,6 +209,7 @@ interface DiagnoseResult {
128
209
  elapsedMs: number;
129
210
  project: ProjectInfo;
130
211
  ruleErrors: RuleErrorInfo[];
212
+ schema?: SerializedSchemaGraph;
131
213
  score: Score;
132
214
  summary: DiagnoseSummary;
133
215
  }
@@ -142,54 +224,116 @@ interface MonorepoResult {
142
224
  subProjects: SubProjectResult[];
143
225
  }
144
226
  //#endregion
145
- //#region src/core/scanner.d.ts
146
- interface ScanOptions {
147
- config?: string;
227
+ //#region src/common/errors.d.ts
228
+ declare class NestjsDoctorError extends Error {
229
+ constructor(message: string);
230
+ }
231
+ declare class ConfigurationError extends NestjsDoctorError {
232
+ constructor(message: string);
233
+ }
234
+ declare class ScanError extends NestjsDoctorError {
235
+ constructor(message: string);
236
+ }
237
+ declare class ValidationError extends NestjsDoctorError {
238
+ constructor(message: string);
239
+ }
240
+ //#endregion
241
+ //#region src/engine/rules/index.d.ts
242
+ declare function getRules(): AnyRule[];
243
+ //#endregion
244
+ //#region src/engine/config/scan-config.d.ts
245
+ interface ScanConfig {
246
+ combinedRules: AnyRule[];
247
+ config: NestjsDoctorConfig;
248
+ customRuleWarnings: string[];
249
+ fileRules: Rule[];
250
+ projectRules: ProjectRule[];
251
+ schemaRules: SchemaRule[];
148
252
  }
149
- interface ScanContext {
253
+ declare function resolveScanConfig(targetPath: string, configPath?: string): Promise<ScanConfig>;
254
+ //#endregion
255
+ //#region src/engine/project-detector.d.ts
256
+ interface MonorepoInfo {
257
+ projects: Map<string, string>;
258
+ }
259
+ //#endregion
260
+ //#region src/engine/analysis-context.d.ts
261
+ interface AnalysisContext {
150
262
  astProject: Project;
151
263
  config: NestjsDoctorConfig;
152
264
  fileRules: Rule[];
153
265
  files: string[];
154
266
  moduleGraph: ModuleGraph;
267
+ project: ProjectInfo;
155
268
  projectRules: ProjectRule[];
156
269
  providers: Map<string, ProviderInfo>;
270
+ schemaGraph?: SchemaGraph;
271
+ schemaRules: SchemaRule[];
157
272
  targetPath: string;
158
273
  }
159
- declare function prepareScan(targetPath: string, options?: ScanOptions): Promise<{
160
- context: ScanContext;
274
+ declare function buildAnalysisContext(targetPath: string, scanConfig: ScanConfig): Promise<AnalysisContext>;
275
+ declare function prepareAnalysis(targetPath: string, options?: {
276
+ config?: string;
277
+ }): Promise<{
278
+ context: AnalysisContext;
161
279
  customRuleWarnings: string[];
162
280
  }>;
163
- declare function updateFile(context: ScanContext, filePath: string): void;
164
- declare function scanFile(context: ScanContext, filePath: string): {
281
+ declare function updateFile(context: AnalysisContext, filePath: string): void;
282
+ //#endregion
283
+ //#region src/engine/diagnostician.d.ts
284
+ interface RawDiagnosticOutput {
285
+ diagnostics: Diagnostic[];
286
+ elapsedMs: number;
287
+ ruleErrors: RuleErrorInfo[];
288
+ }
289
+ declare function checkFile(context: AnalysisContext, filePath: string): {
165
290
  diagnostics: Diagnostic[];
166
291
  errors: RuleErrorInfo[];
167
292
  };
168
- declare function scanAllFiles(context: ScanContext): {
293
+ declare function checkAllFiles(context: AnalysisContext): {
169
294
  diagnostics: Diagnostic[];
170
295
  errors: RuleErrorInfo[];
171
296
  };
172
- declare function scanProject(context: ScanContext): {
297
+ declare function checkProject(context: AnalysisContext): {
298
+ diagnostics: Diagnostic[];
299
+ errors: RuleErrorInfo[];
300
+ };
301
+ declare function checkSchema(context: AnalysisContext): {
173
302
  diagnostics: Diagnostic[];
174
303
  errors: RuleErrorInfo[];
175
304
  };
176
305
  //#endregion
177
- //#region src/rules/index.d.ts
178
- declare function getRules(): AnyRule[];
179
- //#endregion
180
- //#region src/types/errors.d.ts
181
- declare class NestjsDoctorError extends Error {
182
- constructor(message: string);
183
- }
184
- declare class ConfigurationError extends NestjsDoctorError {
185
- constructor(message: string);
186
- }
187
- declare class ScanError extends NestjsDoctorError {
188
- constructor(message: string);
306
+ //#region src/engine/result-builder.d.ts
307
+ interface EngineResult {
308
+ customRuleWarnings: string[];
309
+ files: string[];
310
+ moduleGraph: ModuleGraph;
311
+ providers: Map<string, ProviderInfo>;
312
+ result: DiagnoseResult;
313
+ schemaGraph: SchemaGraph;
189
314
  }
190
- declare class ValidationError extends NestjsDoctorError {
191
- constructor(message: string);
315
+ interface MonorepoEngineResult {
316
+ customRuleWarnings: string[];
317
+ moduleGraphs: Map<string, ModuleGraph>;
318
+ result: MonorepoResult;
192
319
  }
320
+ declare function buildResult(context: AnalysisContext, rawOutput: RawDiagnosticOutput, customRuleWarnings?: string[]): EngineResult;
321
+ //#endregion
322
+ //#region src/engine/scanner.d.ts
323
+ type AutoScanResult = {
324
+ isMonorepo: true;
325
+ monorepo: MonorepoEngineResult;
326
+ } | {
327
+ isMonorepo: false;
328
+ single: EngineResult;
329
+ };
330
+ declare function autoScan(targetPath: string, options?: {
331
+ config?: string;
332
+ monorepo?: MonorepoInfo;
333
+ }): Promise<AutoScanResult>;
334
+ //#endregion
335
+ //#region src/engine/schema/extract.d.ts
336
+ declare function extractSchema(project: Project, files: string[], orm: string | null, targetPath: string): SchemaGraph;
193
337
  //#endregion
194
338
  //#region src/api/index.d.ts
195
339
  /**
@@ -218,5 +362,4 @@ declare function diagnoseMonorepo(path: string, options?: {
218
362
  config?: string;
219
363
  }): Promise<MonorepoResult>;
220
364
  //#endregion
221
- export { type AnyRule, type Category, ConfigurationError, type DiagnoseResult, type DiagnoseSummary, type Diagnostic, type MonorepoResult, type NestjsDoctorConfig, NestjsDoctorError, type ProjectInfo, type ProjectRule, type ProjectRuleContext, type Rule, type RuleContext, type RuleErrorInfo, type RuleMeta, type ScanContext, ScanError, type Score, type Severity, type SubProjectResult, ValidationError, diagnose, diagnoseMonorepo, getRules, prepareScan, scanAllFiles, scanFile, scanProject, updateFile, updateModuleGraphForFile, updateProvidersForFile };
222
- //# sourceMappingURL=index.d.mts.map
365
+ export { type AnalysisContext, type AnyRule, type AutoScanResult, type BaseDiagnostic, type Category, type CodeDiagnostic, type CodeRuleContext, type CodeRuleContext as RuleContext, ConfigurationError, type DiagnoseResult, type DiagnoseSummary, type Diagnostic, type MonorepoResult, type NestjsDoctorConfig, NestjsDoctorError, type ProjectInfo, type ProjectRule, type ProjectRuleContext, type RawDiagnosticOutput, type Rule, type RuleErrorInfo, type RuleMeta, type ScanConfig, ScanError, type SchemaColumn, type SchemaDiagnostic, type SchemaEntity, type SchemaGraph, type SchemaRelation, type SchemaRule, type SchemaRuleContext, type Score, type SerializedSchemaGraph, type Severity, type SubProjectResult, ValidationError, autoScan, buildAnalysisContext, buildResult, checkAllFiles, checkFile, checkProject, checkSchema, diagnose, diagnoseMonorepo, extractSchema, getRules, isCodeDiagnostic, isSchemaDiagnostic, prepareAnalysis, resolveScanConfig, updateFile, updateModuleGraphForFile, updateProvidersForFile };