autodocsync 1.0.0 → 1.0.1
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 +275 -0
- package/package.json +2 -2
- package/src/check-ollama.js +3 -2
- package/src/openapi-gen.js +11 -4
- package/src/scanners/expressScanner.js +24 -1
- package/src/scanners/modelScanner.js +125 -13
package/README.md
ADDED
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
# AutoDocSync
|
|
2
|
+
|
|
3
|
+
   
|
|
4
|
+
|
|
5
|
+
**Automated OpenAPI Documentation with AST & Local LLMs**
|
|
6
|
+
|
|
7
|
+
AutoDocSync is a high-performance CLI tool for Express.js developers. It bridges the gap between your source code and documentation by using **TypeScript AST (Abstract Syntax Tree)** to statically analyze your project and **Local AI (Ollama)** to generate human-readable endpoint descriptions — entirely on your machine.
|
|
8
|
+
|
|
9
|
+
> [!NOTE]
|
|
10
|
+
> AutoDocSync was built to solve the "Stale Docs" problem. By making documentation generation part of your workflow, your OpenAPI spec stays accurate as your code evolves.
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## How It Works
|
|
15
|
+
|
|
16
|
+
AutoDocSync does not guess your routes or rely on manual annotations. It performs a deep static analysis of your Express project to understand exactly how your API behaves.
|
|
17
|
+
|
|
18
|
+
```mermaid
|
|
19
|
+
graph TD
|
|
20
|
+
A[Source Code] --> B{AST Scanner}
|
|
21
|
+
B -- "Express Routes" --> C[Logic Extraction]
|
|
22
|
+
B -- "Mongoose Models" --> D[Schema Parsing]
|
|
23
|
+
C --> E[Context Builder]
|
|
24
|
+
D --> E
|
|
25
|
+
E --> F((Local Ollama))
|
|
26
|
+
F -- "Mistral" --> G[AI Enhancement]
|
|
27
|
+
G --> H[Final OpenAPI 3.0 Spec]
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
**Deep Integration**
|
|
31
|
+
|
|
32
|
+
- **AST Parsing** — Powered by `ts-morph`, the scanner traces `.use()` calls to resolve full URL prefixes and identifies imported handlers and middlewares.
|
|
33
|
+
- **Payload Extraction** — Automatically detects request bodies, query parameters, and URL params directly from your handler logic.
|
|
34
|
+
- **Smart Schemas** — Parses Mongoose models to construct accurate OpenAPI components, including required fields, enums, and contextual examples (e.g., `email` → `user@example.com`).
|
|
35
|
+
- **Privacy-First AI** — Uses a local Ollama instance. No API keys, no data sent to the cloud, and zero inference cost.
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## Quick Start
|
|
40
|
+
|
|
41
|
+
Follow these steps in order to get AutoDocSync running in your Express project.
|
|
42
|
+
|
|
43
|
+
### Step 1 — Install AutoDocSync
|
|
44
|
+
|
|
45
|
+
Inside your Express project root, install AutoDocSync as a development dependency:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
npm install --save-dev autodocsync
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### Step 2 — Install Ollama
|
|
52
|
+
|
|
53
|
+
AutoDocSync uses a locally running Ollama instance to generate AI-powered descriptions.
|
|
54
|
+
|
|
55
|
+
Download and install Ollama from the official site: [https://ollama.com](https://ollama.com)
|
|
56
|
+
|
|
57
|
+
Once installed, verify it is running:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
ollama --version
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Step 3 — Pull the Mistral Model
|
|
64
|
+
|
|
65
|
+
AutoDocSync uses the Mistral model for description generation. Pull it with:
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
ollama pull mistral
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
> [!TIP]
|
|
72
|
+
> This is a one-time download. Once pulled, the model is cached locally and reused across all future runs.
|
|
73
|
+
|
|
74
|
+
### Step 4 — Generate Your Documentation
|
|
75
|
+
|
|
76
|
+
With everything set up, run the following command from your Express project root:
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
npx autodocsync generate . -o ./docs
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
This scans your project and writes a production-ready `openapi.json` file to the `./docs` directory.
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
## Example Output
|
|
87
|
+
|
|
88
|
+
Running `npx autodocsync generate . -o ./docs` on a standard Express + Mongoose project produces a file like `docs/openapi.json`. Below is a realistic excerpt:
|
|
89
|
+
|
|
90
|
+
```json
|
|
91
|
+
{
|
|
92
|
+
"openapi": "3.0.3",
|
|
93
|
+
"info": {
|
|
94
|
+
"title": "My Backend Service",
|
|
95
|
+
"description": "Industry standard API documentation for professional backend integration.",
|
|
96
|
+
"version": "1.0.0",
|
|
97
|
+
"contact": {
|
|
98
|
+
"name": "Engineering Team Support"
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
"servers": [
|
|
102
|
+
{
|
|
103
|
+
"url": "/api/v1",
|
|
104
|
+
"description": "Primary Production/Active version API server"
|
|
105
|
+
}
|
|
106
|
+
],
|
|
107
|
+
"tags": [
|
|
108
|
+
{ "name": "Auth", "description": "Authentication algorithms, secure login, and session management" },
|
|
109
|
+
{ "name": "User", "description": "User profile management, account settings, and event registrations" }
|
|
110
|
+
],
|
|
111
|
+
"paths": {
|
|
112
|
+
"/auth/login": {
|
|
113
|
+
"post": {
|
|
114
|
+
"tags": ["Auth"],
|
|
115
|
+
"summary": "Login user",
|
|
116
|
+
"description": "Authenticates a registered user using their email and password. Returns a signed JWT token and sets an httpOnly access token cookie on success.",
|
|
117
|
+
"security": [],
|
|
118
|
+
"requestBody": {
|
|
119
|
+
"required": true,
|
|
120
|
+
"content": {
|
|
121
|
+
"application/json": {
|
|
122
|
+
"schema": {
|
|
123
|
+
"type": "object",
|
|
124
|
+
"properties": {
|
|
125
|
+
"email": { "type": "string", "example": "user@example.com" },
|
|
126
|
+
"password": { "type": "string", "example": "********" }
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
},
|
|
132
|
+
"responses": {
|
|
133
|
+
"200": {
|
|
134
|
+
"description": "Successful operation",
|
|
135
|
+
"content": {
|
|
136
|
+
"application/json": {
|
|
137
|
+
"schema": { "$ref": "#/components/schemas/SuccessResponse" }
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
"401": {
|
|
142
|
+
"description": "Action failed due to client or server error",
|
|
143
|
+
"content": {
|
|
144
|
+
"application/json": {
|
|
145
|
+
"schema": { "$ref": "#/components/schemas/ErrorResponse" }
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
},
|
|
152
|
+
"/users/{id}": {
|
|
153
|
+
"get": {
|
|
154
|
+
"tags": ["User"],
|
|
155
|
+
"summary": "Get user by id",
|
|
156
|
+
"description": "Retrieves the full profile of a user by their MongoDB ObjectId. Requires a valid bearer token or session cookie.",
|
|
157
|
+
"parameters": [
|
|
158
|
+
{
|
|
159
|
+
"name": "id",
|
|
160
|
+
"in": "path",
|
|
161
|
+
"required": true,
|
|
162
|
+
"schema": { "type": "string", "example": "60d0fe4f5311236168a109ca" }
|
|
163
|
+
}
|
|
164
|
+
],
|
|
165
|
+
"responses": {
|
|
166
|
+
"200": {
|
|
167
|
+
"description": "Successful operation",
|
|
168
|
+
"content": {
|
|
169
|
+
"application/json": {
|
|
170
|
+
"schema": { "$ref": "#/components/schemas/SuccessResponse" }
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
},
|
|
174
|
+
"404": {
|
|
175
|
+
"description": "Action failed due to client or server error",
|
|
176
|
+
"content": {
|
|
177
|
+
"application/json": {
|
|
178
|
+
"schema": { "$ref": "#/components/schemas/ErrorResponse" }
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
},
|
|
186
|
+
"components": {
|
|
187
|
+
"securitySchemes": {
|
|
188
|
+
"cookieAuth": { "type": "apiKey", "in": "cookie", "name": "accessToken" },
|
|
189
|
+
"bearerAuth": { "type": "http", "scheme": "bearer", "bearerFormat": "JWT" }
|
|
190
|
+
},
|
|
191
|
+
"schemas": {
|
|
192
|
+
"SuccessResponse": {
|
|
193
|
+
"type": "object",
|
|
194
|
+
"properties": {
|
|
195
|
+
"success": { "type": "boolean", "example": true },
|
|
196
|
+
"message": { "type": "string", "example": "Operation completed successfully." },
|
|
197
|
+
"data": { "type": "object", "nullable": true }
|
|
198
|
+
}
|
|
199
|
+
},
|
|
200
|
+
"ErrorResponse": {
|
|
201
|
+
"type": "object",
|
|
202
|
+
"properties": {
|
|
203
|
+
"success": { "type": "boolean", "example": false },
|
|
204
|
+
"message": { "type": "string", "example": "Detailed error message explaining the failure." },
|
|
205
|
+
"error": { "type": "object", "nullable": true }
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
},
|
|
210
|
+
"security": [
|
|
211
|
+
{ "cookieAuth": [] },
|
|
212
|
+
{ "bearerAuth": [] }
|
|
213
|
+
]
|
|
214
|
+
}
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
> [!TIP]
|
|
218
|
+
> Drop the generated `openapi.json` into [Swagger UI](https://swagger.io/tools/swagger-ui/) or [Redoc](https://redocly.com/redoc/) for instant, interactive API documentation.
|
|
219
|
+
|
|
220
|
+
---
|
|
221
|
+
|
|
222
|
+
## CLI Reference
|
|
223
|
+
|
|
224
|
+
| Command | Argument | Option | Description |
|
|
225
|
+
| :--- | :--- | :--- | :--- |
|
|
226
|
+
| `generate` | `[projectDir]` | `-o, --output <dir>` | Scans the project and writes `openapi.json` to the specified output directory. |
|
|
227
|
+
|
|
228
|
+
---
|
|
229
|
+
|
|
230
|
+
## Why AutoDocSync?
|
|
231
|
+
|
|
232
|
+
| Feature | Benefit |
|
|
233
|
+
| :--- | :--- |
|
|
234
|
+
| Real-time accuracy | Docs are always in sync with your actual code — no drift. |
|
|
235
|
+
| $0 inference cost | Runs on your own hardware. No API subscriptions required. |
|
|
236
|
+
| OpenAPI 3.0 compliant | Output is compatible with Swagger UI, Redoc, and any standard tooling. |
|
|
237
|
+
| Zero manual effort | No `@swagger` JSDoc comments or route decorators needed. |
|
|
238
|
+
|
|
239
|
+
---
|
|
240
|
+
|
|
241
|
+
## Project Structure
|
|
242
|
+
|
|
243
|
+
| Path | Purpose |
|
|
244
|
+
| :--- | :--- |
|
|
245
|
+
| `bin/index.js` | CLI entry point and argument parsing. |
|
|
246
|
+
| `src/scanners/` | AST logic for Express route and Mongoose model parsing. |
|
|
247
|
+
| `src/llm.js` | Integration with the local Ollama API. |
|
|
248
|
+
| `src/openapi-gen.js` | Core logic for compiling and writing the OpenAPI spec. |
|
|
249
|
+
|
|
250
|
+
---
|
|
251
|
+
|
|
252
|
+
## Troubleshooting
|
|
253
|
+
|
|
254
|
+
| Issue | Resolution |
|
|
255
|
+
| :--- | :--- |
|
|
256
|
+
| Ollama is not running | Open the Ollama desktop app or run `ollama serve` in a separate terminal. |
|
|
257
|
+
| Model not found | Run `ollama pull mistral` to download the required model. |
|
|
258
|
+
| Slow generation or timeout | Large codebases may take a moment. Ensure at least **8 GB of RAM** is available and Ollama is idle before running. |
|
|
259
|
+
| Empty or partial output | Confirm your project uses standard Express route patterns (`.get`, `.post`, `.use`, etc.). |
|
|
260
|
+
|
|
261
|
+
---
|
|
262
|
+
|
|
263
|
+
## Contributing
|
|
264
|
+
|
|
265
|
+
Contributions are welcome. Whether it's adding support for additional frameworks (Fastify, NestJS, Hono) or improving the LLM prompts for better descriptions, feel free to open an issue or submit a pull request.
|
|
266
|
+
|
|
267
|
+
---
|
|
268
|
+
|
|
269
|
+
## License
|
|
270
|
+
|
|
271
|
+
Distributed under the **MIT License**.
|
|
272
|
+
|
|
273
|
+
---
|
|
274
|
+
|
|
275
|
+
*Built for developers who want documentation that actually keeps up with their code.*
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "autodocsync",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "Combines TS code parsing and local LLMs to generate OpenAPI specs automatically",
|
|
5
5
|
"main": "src/openapi-gen.js",
|
|
6
6
|
"type": "module",
|
|
@@ -27,4 +27,4 @@
|
|
|
27
27
|
"glob": "^10.3.10",
|
|
28
28
|
"ts-morph": "^21.0.1"
|
|
29
29
|
}
|
|
30
|
-
}
|
|
30
|
+
}
|
package/src/check-ollama.js
CHANGED
|
@@ -25,6 +25,7 @@ export function printOllamaGuide() {
|
|
|
25
25
|
║ autodocs-sync — setup required ║
|
|
26
26
|
╠══════════════════════════════════════════════════╣
|
|
27
27
|
║ Ollama is not running on this machine. ║
|
|
28
|
+
║ ❌ Run: ollama serve ║
|
|
28
29
|
║ ║
|
|
29
30
|
║ To enable AI-powered docs: ║
|
|
30
31
|
║ ║
|
|
@@ -35,7 +36,7 @@ export function printOllamaGuide() {
|
|
|
35
36
|
║ $ ollama pull mistral ║
|
|
36
37
|
║ ║
|
|
37
38
|
║ 3. Regenerate docs manually after setup ║
|
|
38
|
-
║ $ npx
|
|
39
|
+
║ $ npx autodocsync generate . -o ./docs ║
|
|
39
40
|
╚══════════════════════════════════════════════════╝
|
|
40
41
|
`);
|
|
41
42
|
}
|
|
@@ -44,7 +45,7 @@ export function printModelGuide() {
|
|
|
44
45
|
console.log(`
|
|
45
46
|
[autodocs] Ollama is running but mistral is not pulled yet.
|
|
46
47
|
Run: ollama pull mistral
|
|
47
|
-
Then run: npx
|
|
48
|
+
Then run: npx autodocsync generate . -o ./docs
|
|
48
49
|
`);
|
|
49
50
|
}
|
|
50
51
|
|
package/src/openapi-gen.js
CHANGED
|
@@ -127,7 +127,14 @@ export function generateOpenAPI(scan, enhancedDescriptions = {}) {
|
|
|
127
127
|
let description =
|
|
128
128
|
enhancedDescriptions[route.path]?.[route.method] ||
|
|
129
129
|
`Performs a ${route.method} operation on the ${relativePath} resource.`;
|
|
130
|
-
|
|
130
|
+
|
|
131
|
+
const isCleanHandler =
|
|
132
|
+
route.handlerName &&
|
|
133
|
+
route.handlerName !== "[Anonymous]" &&
|
|
134
|
+
!route.handlerName.includes("{") &&
|
|
135
|
+
!route.handlerName.includes("=>");
|
|
136
|
+
|
|
137
|
+
if (!enhancedDescriptions[route.path]?.[route.method] && isCleanHandler) {
|
|
131
138
|
description += `\n\n**Implementation details:** This endpoint is handled by \`${route.handlerName}\`.`;
|
|
132
139
|
}
|
|
133
140
|
|
|
@@ -139,9 +146,9 @@ export function generateOpenAPI(scan, enhancedDescriptions = {}) {
|
|
|
139
146
|
|
|
140
147
|
const operation = {
|
|
141
148
|
tags: [tagName],
|
|
142
|
-
summary:
|
|
143
|
-
route.handlerName
|
|
144
|
-
|
|
149
|
+
summary: isCleanHandler
|
|
150
|
+
? camelToTitle(route.handlerName)
|
|
151
|
+
: `${route.method} ${relativePath}`,
|
|
145
152
|
description: description,
|
|
146
153
|
responses: {},
|
|
147
154
|
};
|
|
@@ -41,11 +41,16 @@ export function scanExpressProject(project, root) {
|
|
|
41
41
|
for (const sourceFile of project.getSourceFiles()) {
|
|
42
42
|
const routePrefix = filePrefixMap.get(sourceFile.getFilePath()) || "";
|
|
43
43
|
const callExpressions = sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression);
|
|
44
|
+
const excludeCallers = ["axios", "fetch", "superagent", "request"];
|
|
44
45
|
|
|
45
46
|
for (const callExpr of callExpressions) {
|
|
46
47
|
const expression = callExpr.getExpression();
|
|
47
48
|
if (expression.getKind() !== SyntaxKind.PropertyAccessExpression) continue;
|
|
48
49
|
|
|
50
|
+
// Skip common HTTP clients to avoid false positives (e.g., axios.post)
|
|
51
|
+
const callerName = expression.getExpression().getText();
|
|
52
|
+
if (excludeCallers.includes(callerName.toLowerCase())) continue;
|
|
53
|
+
|
|
49
54
|
const methodName = expression.getName();
|
|
50
55
|
if (!methods.includes(methodName)) continue;
|
|
51
56
|
|
|
@@ -57,6 +62,10 @@ export function scanExpressProject(project, root) {
|
|
|
57
62
|
// Pattern 1: router.get('/path', handler)
|
|
58
63
|
if (args.length >= 2 && args[0].getKind() === SyntaxKind.StringLiteral) {
|
|
59
64
|
pathStr = args[0].getLiteralText();
|
|
65
|
+
|
|
66
|
+
// Skip external URLs (http:// or https://)
|
|
67
|
+
if (pathStr.startsWith("http")) continue;
|
|
68
|
+
|
|
60
69
|
for (let i = 1; i < args.length - 1; i++) {
|
|
61
70
|
middlewares.push(args[i].getText());
|
|
62
71
|
}
|
|
@@ -70,6 +79,10 @@ export function scanExpressProject(project, root) {
|
|
|
70
79
|
const routeArgs = routeCall.getArguments();
|
|
71
80
|
if (routeArgs.length > 0 && routeArgs[0].getKind() === SyntaxKind.StringLiteral) {
|
|
72
81
|
pathStr = routeArgs[0].getLiteralText();
|
|
82
|
+
|
|
83
|
+
// Skip external URLs (http:// or https://)
|
|
84
|
+
if (pathStr.startsWith("http")) continue;
|
|
85
|
+
|
|
73
86
|
for (let i = 0; i < args.length - 1; i++) {
|
|
74
87
|
middlewares.push(args[i].getText());
|
|
75
88
|
}
|
|
@@ -80,16 +93,26 @@ export function scanExpressProject(project, root) {
|
|
|
80
93
|
|
|
81
94
|
if (!pathStr || !handlerNode) continue;
|
|
82
95
|
|
|
96
|
+
// Ensure the handler is actually a function or a reference (exclude object literals)
|
|
97
|
+
const isFunctionHandler = [SyntaxKind.Identifier, SyntaxKind.FunctionExpression, SyntaxKind.ArrowFunction, SyntaxKind.CallExpression].includes(handlerNode.getKind());
|
|
98
|
+
if (!isFunctionHandler) continue;
|
|
99
|
+
|
|
83
100
|
const fullPath = (routePrefix + pathStr).replace(/\/\//g, "/").replace(/\/$/, "") || "/";
|
|
84
101
|
const resolvedHandler = resolveHandler(handlerNode);
|
|
85
102
|
const analysis = analyzeHandler(resolvedHandler);
|
|
86
103
|
|
|
104
|
+
// Clean handler name: if it's an inline function, mark it for the generator to handle
|
|
105
|
+
let handlerName = handlerNode.getText();
|
|
106
|
+
if (handlerName.length > 50 || handlerName.includes("{") || handlerName.includes("=>")) {
|
|
107
|
+
handlerName = `[Anonymous]`;
|
|
108
|
+
}
|
|
109
|
+
|
|
87
110
|
routes.push({
|
|
88
111
|
method: methodName.toUpperCase(),
|
|
89
112
|
path: fullPath,
|
|
90
113
|
file: path.relative(root, sourceFile.getFilePath()),
|
|
91
114
|
middlewares,
|
|
92
|
-
handlerName:
|
|
115
|
+
handlerName: handlerName,
|
|
93
116
|
...analysis,
|
|
94
117
|
handlerCode: resolvedHandler ? resolvedHandler.getText().slice(0, 500) : ""
|
|
95
118
|
});
|
|
@@ -7,12 +7,19 @@ export function scanModels(project) {
|
|
|
7
7
|
const models = {};
|
|
8
8
|
|
|
9
9
|
for (const sourceFile of project.getSourceFiles()) {
|
|
10
|
-
|
|
10
|
+
const schemaAliases = getMongooseSchemaAliases(sourceFile);
|
|
11
11
|
const newExpressions = sourceFile.getDescendantsOfKind(SyntaxKind.NewExpression);
|
|
12
12
|
|
|
13
13
|
for (const newExpr of newExpressions) {
|
|
14
14
|
const typeText = newExpr.getExpression().getText();
|
|
15
|
-
|
|
15
|
+
|
|
16
|
+
// Detect schema instantiations based on dynamic imports or standard signatures
|
|
17
|
+
let isSchema = false;
|
|
18
|
+
if (schemaAliases.has(typeText) || typeText.endsWith("Schema")) {
|
|
19
|
+
isSchema = true;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (isSchema) {
|
|
16
23
|
const args = newExpr.getArguments();
|
|
17
24
|
if (args.length > 0 && args[0].getKind() === SyntaxKind.ObjectLiteralExpression) {
|
|
18
25
|
const schemaObj = args[0];
|
|
@@ -29,26 +36,122 @@ export function scanModels(project) {
|
|
|
29
36
|
return models;
|
|
30
37
|
}
|
|
31
38
|
|
|
39
|
+
function getMongooseSchemaAliases(sourceFile) {
|
|
40
|
+
const aliases = new Set(["Schema", "mongoose.Schema"]);
|
|
41
|
+
|
|
42
|
+
// Check ES6 imports
|
|
43
|
+
for (const imp of sourceFile.getImportDeclarations()) {
|
|
44
|
+
if (imp.getModuleSpecifierValue() === 'mongoose') {
|
|
45
|
+
const defaultImport = imp.getDefaultImport();
|
|
46
|
+
if (defaultImport) {
|
|
47
|
+
aliases.add(`${defaultImport.getText()}.Schema`);
|
|
48
|
+
}
|
|
49
|
+
for (const namedImport of imp.getNamedImports()) {
|
|
50
|
+
if (namedImport.getName() === "Schema") {
|
|
51
|
+
aliases.add(namedImport.getAliasNode() ? namedImport.getAliasNode().getText() : "Schema");
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Check CommonJS requires
|
|
58
|
+
for (const stmt of sourceFile.getVariableStatements()) {
|
|
59
|
+
for (const dec of stmt.getDeclarations()) {
|
|
60
|
+
const init = dec.getInitializer();
|
|
61
|
+
if (init && init.getKind() === SyntaxKind.CallExpression && init.getExpression().getText() === "require") {
|
|
62
|
+
const args = init.getArguments();
|
|
63
|
+
if (args.length > 0 && (args[0].getText() === "'mongoose'" || args[0].getText() === '"mongoose"')) {
|
|
64
|
+
const nameNode = dec.getNameNode();
|
|
65
|
+
if (nameNode.getKind() === SyntaxKind.Identifier) {
|
|
66
|
+
aliases.add(`${nameNode.getText()}.Schema`);
|
|
67
|
+
} else if (nameNode.getKind() === SyntaxKind.ObjectBindingPattern) {
|
|
68
|
+
for (const el of nameNode.getElements()) {
|
|
69
|
+
if (el.getPropertyNameNode() && el.getPropertyNameNode().getText() === "Schema") {
|
|
70
|
+
aliases.add(el.getNameNode().getText());
|
|
71
|
+
} else if (!el.getPropertyNameNode() && el.getNameNode().getText() === "Schema") {
|
|
72
|
+
aliases.add("Schema");
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return aliases;
|
|
82
|
+
}
|
|
83
|
+
|
|
32
84
|
function inferModelName(sourceFile, node) {
|
|
33
|
-
// Try to find
|
|
85
|
+
// 1. Try to find if it's passed directly to mongoose.model('Name', new Schema(...)) or similar
|
|
86
|
+
const parentCall = node.getFirstAncestorByKind(SyntaxKind.CallExpression);
|
|
87
|
+
if (parentCall) {
|
|
88
|
+
const exprText = parentCall.getExpression().getText();
|
|
89
|
+
if (exprText.includes("mongoose.model") || exprText.includes(".model")) {
|
|
90
|
+
const args = parentCall.getArguments();
|
|
91
|
+
if (args.length > 0) {
|
|
92
|
+
const name = resolveStringValue(args[0]);
|
|
93
|
+
if (name) return name;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// 2. Try to find the variable name it's assigned to
|
|
34
99
|
const varDec = node.getFirstAncestorByKind(SyntaxKind.VariableDeclaration);
|
|
35
100
|
if (varDec) {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
101
|
+
const varName = varDec.getName();
|
|
102
|
+
|
|
103
|
+
// Look for model registered somewhere else in the same file: mongoose.model('ModelName', varName)
|
|
104
|
+
const calls = sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression);
|
|
105
|
+
for (const call of calls) {
|
|
106
|
+
const exprText = call.getExpression().getText();
|
|
107
|
+
if (exprText.includes("model")) {
|
|
108
|
+
const args = call.getArguments();
|
|
109
|
+
if (args.length >= 2 && args[1].getText() === varName) {
|
|
110
|
+
const name = resolveStringValue(args[0]);
|
|
111
|
+
if (name) return name;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Remove 'Schema' suffix if present as a fallback
|
|
117
|
+
return varName.replace(/Schema$/i, "");
|
|
39
118
|
}
|
|
40
119
|
|
|
41
|
-
// Check if it's exported as a model: mongoose.model('User', userSchema)
|
|
42
|
-
const sourceFileText = sourceFile.getText();
|
|
43
|
-
const modelMatch = sourceFileText.match(/mongoose\.model\(['"]([^'"]+)['"]/);
|
|
44
|
-
if (modelMatch) {
|
|
45
|
-
return modelMatch[1];
|
|
46
|
-
}
|
|
47
|
-
|
|
48
120
|
// Fallback to filename
|
|
49
121
|
return sourceFile.getBaseNameWithoutExtension();
|
|
50
122
|
}
|
|
51
123
|
|
|
124
|
+
function resolveStringValue(node) {
|
|
125
|
+
if (node.getKind() === SyntaxKind.StringLiteral || node.getKind() === SyntaxKind.NoSubstitutionTemplateLiteral) {
|
|
126
|
+
return node.getLiteralText ? node.getLiteralText() : node.getText().replace(/['"]/g, "");
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Advanced: Try standard TS resolution for constants (e.g., MODEL_NAMES.USER)
|
|
130
|
+
try {
|
|
131
|
+
const symbol = node.getSymbol();
|
|
132
|
+
if (symbol) {
|
|
133
|
+
const declarations = symbol.getDeclarations();
|
|
134
|
+
if (declarations && declarations.length > 0) {
|
|
135
|
+
const decl = declarations[0];
|
|
136
|
+
if (decl.getKind() === SyntaxKind.PropertyAssignment || decl.getKind() === SyntaxKind.VariableDeclaration) {
|
|
137
|
+
const init = decl.getInitializer();
|
|
138
|
+
if (init && (init.getKind() === SyntaxKind.StringLiteral || init.getKind() === SyntaxKind.NoSubstitutionTemplateLiteral)) {
|
|
139
|
+
return init.getLiteralText ? init.getLiteralText() : init.getText().replace(/['"]/g, "");
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
if (decl.getKind() === SyntaxKind.EnumMember) {
|
|
143
|
+
const val = decl.getValue();
|
|
144
|
+
if (typeof val === 'string') return val;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
} catch(e) {
|
|
149
|
+
// Ignore resolution errors if ts-morph typechecker is unavailable
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
|
|
52
155
|
function parseSchemaObject(obj) {
|
|
53
156
|
const properties = {
|
|
54
157
|
_id: { type: "string", example: "60d0fe4f5311236168a109ca" }
|
|
@@ -86,6 +189,15 @@ function parseFieldInfo(node, fieldName = "") {
|
|
|
86
189
|
type = mapMongooseType(text);
|
|
87
190
|
} else if (node.getKind() === SyntaxKind.ObjectLiteralExpression) {
|
|
88
191
|
const typeProp = node.getProperty("type");
|
|
192
|
+
|
|
193
|
+
// Recursive feature: If there is no 'type' property, treat as a nested subdocument schema
|
|
194
|
+
if (!typeProp) {
|
|
195
|
+
return {
|
|
196
|
+
schema: parseSchemaObject(node),
|
|
197
|
+
required: false
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
89
201
|
if (typeProp && typeProp.getKind() === SyntaxKind.PropertyAssignment) {
|
|
90
202
|
const typeInit = typeProp.getInitializer();
|
|
91
203
|
if (typeInit) type = mapMongooseType(typeInit.getText());
|