autodocsync 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/index.js ADDED
@@ -0,0 +1,123 @@
1
+ #!/usr/bin/env node
2
+
3
+ import fs from "fs";
4
+ import path from "path";
5
+ import { Command } from "commander";
6
+ import { scanProject } from "../src/scanners/index.js";
7
+ import {
8
+ checkOllama,
9
+ printOllamaGuide,
10
+ printModelGuide,
11
+ } from "../src/check-ollama.js";
12
+ import {
13
+ describeEndpointWithMistral,
14
+ summarizeProjectWithMistral,
15
+ } from "../src/llm.js";
16
+ import { generateOpenAPI } from "../src/openapi-gen.js";
17
+
18
+ const program = new Command();
19
+
20
+ program
21
+ .name("docsync")
22
+ .description("Generate OpenAPI docs")
23
+ .version("1.0.0");
24
+
25
+ program
26
+ .command("init")
27
+ .description("Initialize project")
28
+ .action(() => {
29
+ console.log("Init command running...");
30
+ });
31
+
32
+ program
33
+ .command("generate")
34
+ .argument("[projectDir]", "Project root", ".")
35
+ .option("-o, --output <dir>", "Output folder", ".")
36
+ .action(async (projectDir, options) => {
37
+ const projectRoot = path.resolve(process.cwd(), projectDir);
38
+ console.log("[docSync] Scanning project...");
39
+
40
+ const ollama = await checkOllama();
41
+ let useLlm = false;
42
+
43
+ if (!ollama.running) {
44
+ printOllamaGuide();
45
+ console.warn(
46
+ "[docSync] Warning: Ollama is not running. AI features disabled.",
47
+ );
48
+ } else if (!ollama.hasModel) {
49
+ printModelGuide();
50
+ console.warn(
51
+ "[docSync] Warning: Mistral model is missing. AI features disabled.",
52
+ );
53
+ } else {
54
+ useLlm = true;
55
+ }
56
+
57
+ const scan = await scanProject(projectRoot);
58
+ console.log(
59
+ `[docSync] Found ${scan.routes.length} routes, ${scan.files.length} files, ${Object.keys(scan.models).length} models`,
60
+ );
61
+
62
+ if (useLlm) {
63
+ if (scan.hasReadme) {
64
+ console.log("[docSync] Summarizing project from README...");
65
+ const summary = await summarizeProjectWithMistral(scan.existingReadme);
66
+ if (summary) scan.projectDescription = summary;
67
+ }
68
+
69
+ console.log("[docSync] Enhancing endpoint descriptions with Mistral...");
70
+ const enhancedDescriptions = {};
71
+ for (const route of scan.routes) {
72
+ const endpoint = {
73
+ method: route.method,
74
+ url: route.path,
75
+ };
76
+ try {
77
+ const desc = await describeEndpointWithMistral(endpoint);
78
+ if (!enhancedDescriptions[route.path])
79
+ enhancedDescriptions[route.path] = {};
80
+ enhancedDescriptions[route.path][route.method] = desc;
81
+ } catch (err) {
82
+ console.error(
83
+ `[docSync] Failed for ${route.method} ${route.path}:`,
84
+ err.message,
85
+ );
86
+ }
87
+ }
88
+
89
+ console.log("[docSync] Generating openapi.json...");
90
+ const openApiSpec = generateOpenAPI(scan, enhancedDescriptions);
91
+
92
+ const outDir = path.resolve(process.cwd(), options.output);
93
+ fs.mkdirSync(outDir, { recursive: true });
94
+
95
+ fs.writeFileSync(
96
+ path.join(outDir, "openapi.json"),
97
+ JSON.stringify(openApiSpec, null, 2),
98
+ "utf-8",
99
+ );
100
+ console.log(
101
+ `[docSync] SUCCESS: openapi.json generated at ${path.join(outDir, "openapi.json")}`,
102
+ );
103
+ } else {
104
+ console.log(
105
+ "[docSync] Generating openapi.json (without AI enhancement)...",
106
+ );
107
+ const openApiSpec = generateOpenAPI(scan);
108
+
109
+ const outDir = path.resolve(process.cwd(), options.output);
110
+ fs.mkdirSync(outDir, { recursive: true });
111
+
112
+ fs.writeFileSync(
113
+ path.join(outDir, "openapi.json"),
114
+ JSON.stringify(openApiSpec, null, 2),
115
+ "utf-8",
116
+ );
117
+ console.log(
118
+ `[docSync] SUCCESS: openapi.json generated at ${path.join(outDir, "openapi.json")}`,
119
+ );
120
+ }
121
+ });
122
+
123
+ program.parse();
package/package.json ADDED
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "autodocsync",
3
+ "version": "1.0.0",
4
+ "description": "Combines TS code parsing and local LLMs to generate OpenAPI specs automatically",
5
+ "main": "src/openapi-gen.js",
6
+ "type": "module",
7
+ "bin": {
8
+ "autodocsync": "bin/index.js"
9
+ },
10
+ "keywords": [
11
+ "openapi",
12
+ "swagger",
13
+ "autodoc",
14
+ "api",
15
+ "express"
16
+ ],
17
+ "author": "Arshdeep Anand",
18
+ "license": "MIT",
19
+ "scripts": {
20
+ "test": "echo \"Error: no test specified\" && exit 1",
21
+ "start": "node bin/index.js"
22
+ },
23
+ "dependencies": {
24
+ "axios": "^1.14.0",
25
+ "commander": "^11.1.0",
26
+ "express": "^5.2.1",
27
+ "glob": "^10.3.10",
28
+ "ts-morph": "^21.0.1"
29
+ }
30
+ }
@@ -0,0 +1,52 @@
1
+ import axios from "axios";
2
+
3
+ const OLLAMA_URL = "http://localhost:11434";
4
+
5
+ export async function checkOllama() {
6
+ try {
7
+ const res = await axios.get(`${OLLAMA_URL}/api/tags`, {
8
+ timeout: 30000,
9
+ });
10
+ const data = res.data;
11
+
12
+ // Check if our model is already pulled
13
+ const models = data.models?.map((m) => m.name) ?? [];
14
+ const hasModel = models.some((m) => m.startsWith("mistral"));
15
+
16
+ return { running: true, hasModel };
17
+ } catch {
18
+ return { running: false, hasModel: false };
19
+ }
20
+ }
21
+
22
+ export function printOllamaGuide() {
23
+ console.log(`
24
+ ╔══════════════════════════════════════════════════╗
25
+ ║ autodocs-sync — setup required ║
26
+ ╠══════════════════════════════════════════════════╣
27
+ ║ Ollama is not running on this machine. ║
28
+ ║ ║
29
+ ║ To enable AI-powered docs: ║
30
+ ║ ║
31
+ ║ 1. Install Ollama ║
32
+ ║ → https://ollama.com/download ║
33
+ ║ ║
34
+ ║ 2. Pull the model (~4.4 GB, one time) ║
35
+ ║ $ ollama pull mistral ║
36
+ ║ ║
37
+ ║ 3. Regenerate docs manually after setup ║
38
+ ║ $ npx autodocs ║
39
+ ╚══════════════════════════════════════════════════╝
40
+ `);
41
+ }
42
+
43
+ export function printModelGuide() {
44
+ console.log(`
45
+ [autodocs] Ollama is running but mistral is not pulled yet.
46
+ Run: ollama pull mistral
47
+ Then run: npx autodocs
48
+ `);
49
+ }
50
+
51
+ export const OLLAMA_MODEL = "mistral";
52
+ export const OLLAMA_API = OLLAMA_URL;
package/src/llm.js ADDED
@@ -0,0 +1,56 @@
1
+ import axios from "axios";
2
+
3
+ export async function describeEndpointWithMistral(endpoint) {
4
+ const method = endpoint.method.toUpperCase();
5
+ const url = endpoint.url;
6
+
7
+ // Extract the resource from the URL (e.g., /users, /posts, /comments)
8
+ const resourceMatch = url.match(/\/([a-zA-Z]+)(\?|$|\/{1})/);
9
+ const resource = resourceMatch ? resourceMatch[1] : 'resource';
10
+
11
+ try {
12
+ // Try to use Ollama HTTP API
13
+ const prompt = `As a Senior API Architect, generate a highly professional, detailed description for this API endpoint. Explain its purpose and its role in the system. Use industry-standard terminology.\n\nMethod: ${method}\nURL: ${url}\n\nReturn only the description text.`;
14
+
15
+ const response = await axios.post('http://localhost:11434/api/generate', {
16
+ model: 'mistral',
17
+ prompt: prompt,
18
+ stream: false
19
+ });
20
+
21
+ const data = response.data;
22
+ const description = data.response?.trim();
23
+ return description || generateDefaultDescription(method, resource, url);
24
+ } catch (error) {
25
+ // Fallback: Generate smart description based on URL and method
26
+ return generateDefaultDescription(method, resource, url);
27
+ }
28
+ }
29
+
30
+ export async function summarizeProjectWithMistral(readme) {
31
+ try {
32
+ const prompt = `Based on this README, generate a brief (1-2 sentences), professional description of this project for API documentation:\n\n${readme.slice(0, 3000)}`;
33
+ const response = await axios.post('http://localhost:11434/api/generate', {
34
+ model: 'mistral',
35
+ prompt: prompt,
36
+ stream: false
37
+ });
38
+
39
+ const data = response.data;
40
+ return data.response?.trim() || "Industry standard API documentation.";
41
+ } catch (error) {
42
+ console.error("[llm] Failed to summarize project:", error.message);
43
+ return "Industry standard API documentation.";
44
+ }
45
+ }
46
+
47
+ function generateDefaultDescription(method, resource, url) {
48
+ const descriptions = {
49
+ 'GET': `Retrieves ${resource} data from the server.`,
50
+ 'POST': `Creates a new ${resource.slice(0, -1) || 'resource'} and returns the created object.`,
51
+ 'PUT': `Updates an existing ${resource.slice(0, -1) || 'resource'} with the provided data.`,
52
+ 'DELETE': `Permanently deletes the specified ${resource.slice(0, -1) || 'resource'} from the server.`,
53
+ 'PATCH': `Partially updates a ${resource.slice(0, -1) || 'resource'} with the provided changes.`
54
+ };
55
+ return descriptions[method] || `Performs a ${method} request to the ${resource} endpoint.`;
56
+ }
@@ -0,0 +1,345 @@
1
+ import path from "path";
2
+ const tagDescriptionMap = {
3
+ Auth: "Authentication algorithms, secure login, and session management",
4
+ User: "User profile management, account settings, and event registrations",
5
+ Admin:
6
+ "Administrative operations for system configuration and user management",
7
+ Manager:
8
+ "Management interface for high-level operations, certificates, and analytics",
9
+ OTP: "One-time password operations for verification and secure resets",
10
+ Event: "Event management, registration tracking, and results processing",
11
+ Certificate: "Certificate generation, previewing, and status tracking",
12
+ Analytics: "Data analytics, system reporting, and performance metrics",
13
+ General: "General system and resource operations",
14
+ };
15
+
16
+ /**
17
+ * Generates an OpenAPI 3.0.3 specification from scanned project data.
18
+ */
19
+ export function generateOpenAPI(scan, enhancedDescriptions = {}) {
20
+ // 1. Determine common prefix (e.g., /api/v1)
21
+ const allPaths = scan.routes.map((r) => r.path);
22
+ const commonPrefix = getCommonPrefix(allPaths);
23
+ const serverPath = commonPrefix || "/api/v1";
24
+
25
+ const spec = {
26
+ openapi: "3.0.3",
27
+ info: {
28
+ title: (scan.projectName || "Backend Service")
29
+ .replace(/-/g, " ")
30
+ .replace(
31
+ /\w\S*/g,
32
+ (txt) => txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase(),
33
+ ),
34
+ description:
35
+ scan.projectDescription ||
36
+ "Industry standard API documentation for professional backend integration. Features secure authentication, comprehensive resource management, and detailed error handling.",
37
+ version: scan.projectVersion || "1.0.0",
38
+ contact: {
39
+ name: (scan.projectAuthor || "Engineering Team") + " Support",
40
+ },
41
+ },
42
+ servers: [
43
+ {
44
+ url: serverPath,
45
+ description: "Primary Production/Active version API server",
46
+ },
47
+ ],
48
+ tags: [],
49
+ paths: {},
50
+ components: {
51
+ securitySchemes: {
52
+ cookieAuth: {
53
+ type: "apiKey",
54
+ in: "cookie",
55
+ name: "accessToken",
56
+ },
57
+ bearerAuth: {
58
+ type: "http",
59
+ scheme: "bearer",
60
+ bearerFormat: "JWT",
61
+ },
62
+ },
63
+ schemas: {
64
+ ErrorResponse: {
65
+ type: "object",
66
+ properties: {
67
+ success: { type: "boolean", example: false },
68
+ message: {
69
+ type: "string",
70
+ example: "Detailed error message explaining the failure.",
71
+ },
72
+ error: { type: "object", nullable: true },
73
+ },
74
+ required: ["success", "message"],
75
+ },
76
+ SuccessResponse: {
77
+ type: "object",
78
+ properties: {
79
+ success: { type: "boolean", example: true },
80
+ message: {
81
+ type: "string",
82
+ example: "Operation completed successfully.",
83
+ },
84
+ data: { type: "object", nullable: true },
85
+ },
86
+ required: ["success", "message"],
87
+ },
88
+ ...scan.models,
89
+ },
90
+ },
91
+ security: [{ cookieAuth: [] }, { bearerAuth: [] }],
92
+ };
93
+
94
+ // PASS 1: Generate Grouped Tags
95
+ const usedTags = new Set();
96
+ scan.routes.forEach((r) => {
97
+ const tagName = inferTag(r, tagDescriptionMap);
98
+ usedTags.add(tagName);
99
+ });
100
+
101
+ spec.tags = Array.from(usedTags)
102
+ .map((name) => ({
103
+ name: name,
104
+ description:
105
+ tagDescriptionMap[name] || `${name} resource management and operations`,
106
+ }))
107
+ .sort((a, b) => a.name.localeCompare(b.name));
108
+
109
+ // Sort routes by path to ensure consistent openapi.json
110
+ const sortedRoutes = [...scan.routes].sort((a, b) =>
111
+ a.path.localeCompare(b.path),
112
+ );
113
+
114
+ // PASS 2: Generate Paths
115
+ for (const route of sortedRoutes) {
116
+ const relativePath =
117
+ commonPrefix && route.path.startsWith(commonPrefix)
118
+ ? route.path.replace(commonPrefix, "") || "/"
119
+ : route.path;
120
+
121
+ if (!spec.paths[relativePath]) spec.paths[relativePath] = {};
122
+
123
+ const methodLower = route.method.toLowerCase();
124
+ const tagName = inferTag(route, tagDescriptionMap);
125
+
126
+ // LLM or AI-enhanced description
127
+ let description =
128
+ enhancedDescriptions[route.path]?.[route.method] ||
129
+ `Performs a ${route.method} operation on the ${relativePath} resource.`;
130
+ if (!enhancedDescriptions[route.path]?.[route.method]) {
131
+ description += `\n\n**Implementation details:** This endpoint is handled by \`${route.handlerName}\`.`;
132
+ }
133
+
134
+ // Security detection
135
+ const isProtected = route.middlewares.some(
136
+ (m) =>
137
+ /auth|verify|jwt|protect|login/i.test(m) && !/login/i.test(route.path),
138
+ );
139
+
140
+ const operation = {
141
+ tags: [tagName],
142
+ summary: camelToTitle(
143
+ route.handlerName || `${route.method} ${relativePath}`,
144
+ ),
145
+ description: description,
146
+ responses: {},
147
+ };
148
+
149
+ if (!isProtected) operation.security = [];
150
+
151
+ // Request Body
152
+ if (["POST", "PUT", "PATCH"].includes(route.method)) {
153
+ operation.requestBody = {
154
+ required: true,
155
+ content: {
156
+ "application/json": {
157
+ schema: inferSchemaFromInputs(
158
+ route.inputs.body,
159
+ scan.models,
160
+ tagName,
161
+ ),
162
+ },
163
+ },
164
+ };
165
+ }
166
+
167
+ // Parameters (Path & Query)
168
+ if (route.inputs.params.length > 0 || route.inputs.query.length > 0) {
169
+ operation.parameters = [];
170
+ route.inputs.params.forEach((p) => {
171
+ operation.parameters.push({
172
+ name: p,
173
+ in: "path",
174
+ required: true,
175
+ schema: {
176
+ type: "string",
177
+ example: p.includes("id") ? "60d0fe4f5311236168a109ca" : "string",
178
+ },
179
+ });
180
+ });
181
+ route.inputs.query.forEach((p) => {
182
+ operation.parameters.push({
183
+ name: p,
184
+ in: "query",
185
+ required: false,
186
+ schema: { type: "string" },
187
+ });
188
+ });
189
+ }
190
+
191
+ // Responses
192
+ const distinctCodes = [
193
+ ...new Set(route.outputs.statusCodes.map((c) => c.replace(/['"]/g, ""))),
194
+ ];
195
+ distinctCodes.forEach((code) => {
196
+ operation.responses[code] = {
197
+ description: code.startsWith("2")
198
+ ? "Successful operation"
199
+ : "Action failed due to client or server error",
200
+ content: {
201
+ "application/json": {
202
+ schema: {
203
+ $ref: code.startsWith("2")
204
+ ? "#/components/schemas/SuccessResponse"
205
+ : "#/components/schemas/ErrorResponse",
206
+ },
207
+ },
208
+ },
209
+ };
210
+ });
211
+
212
+ if (Object.keys(operation.responses).length === 0) {
213
+ operation.responses["200"] = {
214
+ description: "Operation successful",
215
+ content: {
216
+ "application/json": {
217
+ schema: { $ref: "#/components/schemas/SuccessResponse" },
218
+ },
219
+ },
220
+ };
221
+ }
222
+
223
+ spec.paths[relativePath][methodLower] = operation;
224
+ }
225
+
226
+ return spec;
227
+ }
228
+
229
+ function inferTag(route, tagDescriptionMap = {}) {
230
+ const p = route.path.toLowerCase();
231
+ const h = (route.handlerName || "").toLowerCase();
232
+
233
+ // 1. Core Category Inferrer (Industry Standards)
234
+ if (p.includes("/admin") || h.includes("admin")) return "Admin";
235
+ if (p.includes("/manager") || h.includes("manager")) return "Manager";
236
+ if (
237
+ p.includes("/auth") ||
238
+ h.includes("login") ||
239
+ h.includes("logout") ||
240
+ h.includes("register") ||
241
+ h.includes("auth")
242
+ )
243
+ return "Auth";
244
+ if (p.includes("/user") || p.includes("/profile") || h.includes("user"))
245
+ return "User";
246
+ if (p.includes("/otp") || h.includes("otp") || p.includes("/email"))
247
+ return "OTP";
248
+ if (p.includes("/analytics") || h.includes("analytics")) return "Analytics";
249
+
250
+ // 2. Resource Inferrer (File-driven)
251
+ // Extracts 'Product' from 'product.routes.js'
252
+ if (route.file) {
253
+ const baseName = path.basename(route.file).split(".")[0].toLowerCase();
254
+ const cleanName = baseName.replace(/routes|router|controller/g, "").trim();
255
+ if (cleanName && !["app", "index", "server"].includes(cleanName)) {
256
+ return cleanName.charAt(0).toUpperCase() + cleanName.slice(1);
257
+ }
258
+ }
259
+
260
+ // 3. Structural Inferrer (Path-driven fallback)
261
+ const parts = p
262
+ .split("/")
263
+ .filter(Boolean)
264
+ .filter((s) => !["api", "v1", "v2", "v3", "public"].includes(s));
265
+ if (parts.length > 0) {
266
+ return parts[0].charAt(0).toUpperCase() + parts[0].slice(1);
267
+ }
268
+
269
+ return "General";
270
+ }
271
+
272
+ function getCommonPrefix(paths) {
273
+ if (paths.length < 2) return "";
274
+ const splitPaths = paths.map((p) => p.split("/").filter(Boolean));
275
+ const first = splitPaths[0];
276
+ const common = [];
277
+ for (let i = 0; i < first.length; i++) {
278
+ const segment = first[i];
279
+ // Majority matching (>= 80% to be safe)
280
+ const count = splitPaths.filter((p) => p[i] === segment).length;
281
+ if (count >= splitPaths.length * 0.8) {
282
+ common.push(segment);
283
+ } else {
284
+ break;
285
+ }
286
+ }
287
+ return common.length > 0 ? "/" + common.join("/") : "";
288
+ }
289
+
290
+ function camelToTitle(text) {
291
+ if (!text) return "";
292
+ const result = text
293
+ .replace(/Router|Controller/g, "")
294
+ .replace(/([A-Z])/g, " $1");
295
+ const titlized =
296
+ result.charAt(0).toUpperCase() + result.slice(1).toLowerCase().trim();
297
+ return titlized;
298
+ }
299
+
300
+ /**
301
+ * Heuristic to link field inputs to existing models or create an inline schema.
302
+ */
303
+ function inferSchemaFromInputs(fields, models, tagName) {
304
+ const potentialModel = tagName.replace(/s$/, "");
305
+ const modelName = Object.keys(models).find(
306
+ (m) => m.toLowerCase() === potentialModel.toLowerCase(),
307
+ );
308
+
309
+ // If many fields, and it's a resource tag, likely it's a model reference
310
+ if (modelName && fields.length > 4) {
311
+ return { $ref: `#/components/schemas/${modelName}` };
312
+ }
313
+
314
+ if (fields.length === 0) return { type: "object", example: {} };
315
+
316
+ const properties = {};
317
+ fields.forEach((f) => {
318
+ const description =
319
+ f.toLowerCase().includes("identifier") ||
320
+ f.toLowerCase().includes("email")
321
+ ? "Email or Username of the user"
322
+ : undefined;
323
+ properties[f] = {
324
+ type: "string",
325
+ ...(description ? { description } : {}),
326
+ example: generateExampleForField(f),
327
+ };
328
+ });
329
+
330
+ return {
331
+ type: "object",
332
+ properties,
333
+ };
334
+ }
335
+
336
+ function generateExampleForField(f) {
337
+ const n = f.toLowerCase();
338
+ if (n.includes("email")) return "user@example.com";
339
+ if (n.includes("username")) return "john_doe";
340
+ if (n.includes("identifier")) return "user@example.com";
341
+ if (n.includes("password")) return "********";
342
+ if (n.includes("identifier")) return "user@example.com";
343
+ if (n.includes("id")) return "60d0fe4f5311236168a109ca";
344
+ return "string";
345
+ }
@@ -0,0 +1,332 @@
1
+ import path from "path";
2
+ import { SyntaxKind } from "ts-morph";
3
+
4
+ /**
5
+ * Scans an Express project to extract routes, nested routers, middlewares, and handler logic.
6
+ */
7
+ export function scanExpressProject(project, root) {
8
+ const routes = [];
9
+ const filePrefixMap = new Map();
10
+
11
+ // PASS 1: Build a map of file paths to their URL prefixes by tracing .use() calls
12
+ for (let pass = 0; pass < 3; pass++) {
13
+ for (const sourceFile of project.getSourceFiles()) {
14
+ for (const call of sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression)) {
15
+ const exp = call.getExpression();
16
+ if (exp.getKind() !== SyntaxKind.PropertyAccessExpression || exp.getName() !== "use") continue;
17
+
18
+ const args = call.getArguments();
19
+ if (args.length < 1) continue;
20
+
21
+ let prefix = "";
22
+ const startIdx = args[0].getKind() === SyntaxKind.StringLiteral ? 1 : 0;
23
+ if (startIdx === 1) prefix = args[0].getLiteralText();
24
+
25
+ for (let i = startIdx; i < args.length; i++) {
26
+ const routerFile = resolveToSourceFile(args[i]);
27
+ if (routerFile && routerFile.getFilePath() !== sourceFile.getFilePath()) {
28
+ const basePrefix = filePrefixMap.get(sourceFile.getFilePath()) || "";
29
+ const fullPrefix = (basePrefix + prefix).replace(/\/+/g, "/").replace(/\/$/, "") || "/";
30
+ filePrefixMap.set(routerFile.getFilePath(), fullPrefix);
31
+ // DO NOT BREAK: We must map the prefix to all potential routers/middlewares in the chain
32
+ }
33
+ }
34
+ }
35
+ }
36
+ }
37
+
38
+ const methods = ["get", "post", "put", "patch", "delete", "options", "all"];
39
+
40
+ // PASS 2: Extract actual route definitions
41
+ for (const sourceFile of project.getSourceFiles()) {
42
+ const routePrefix = filePrefixMap.get(sourceFile.getFilePath()) || "";
43
+ const callExpressions = sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression);
44
+
45
+ for (const callExpr of callExpressions) {
46
+ const expression = callExpr.getExpression();
47
+ if (expression.getKind() !== SyntaxKind.PropertyAccessExpression) continue;
48
+
49
+ const methodName = expression.getName();
50
+ if (!methods.includes(methodName)) continue;
51
+
52
+ const args = callExpr.getArguments();
53
+ let pathStr = "";
54
+ let handlerNode = null;
55
+ let middlewares = [];
56
+
57
+ // Pattern 1: router.get('/path', handler)
58
+ if (args.length >= 2 && args[0].getKind() === SyntaxKind.StringLiteral) {
59
+ pathStr = args[0].getLiteralText();
60
+ for (let i = 1; i < args.length - 1; i++) {
61
+ middlewares.push(args[i].getText());
62
+ }
63
+ handlerNode = args[args.length - 1];
64
+ }
65
+ // Pattern 2: router.route('/path').get(handler)
66
+ else if (expression.getExpression().getKind() === SyntaxKind.CallExpression) {
67
+ const routeCall = expression.getExpression();
68
+ const routeExp = routeCall.getExpression();
69
+ if (routeExp.getKind() === SyntaxKind.PropertyAccessExpression && routeExp.getName() === "route") {
70
+ const routeArgs = routeCall.getArguments();
71
+ if (routeArgs.length > 0 && routeArgs[0].getKind() === SyntaxKind.StringLiteral) {
72
+ pathStr = routeArgs[0].getLiteralText();
73
+ for (let i = 0; i < args.length - 1; i++) {
74
+ middlewares.push(args[i].getText());
75
+ }
76
+ handlerNode = args[args.length - 1];
77
+ }
78
+ }
79
+ }
80
+
81
+ if (!pathStr || !handlerNode) continue;
82
+
83
+ const fullPath = (routePrefix + pathStr).replace(/\/\//g, "/").replace(/\/$/, "") || "/";
84
+ const resolvedHandler = resolveHandler(handlerNode);
85
+ const analysis = analyzeHandler(resolvedHandler);
86
+
87
+ routes.push({
88
+ method: methodName.toUpperCase(),
89
+ path: fullPath,
90
+ file: path.relative(root, sourceFile.getFilePath()),
91
+ middlewares,
92
+ handlerName: handlerNode.getText(),
93
+ ...analysis,
94
+ handlerCode: resolvedHandler ? resolvedHandler.getText().slice(0, 500) : ""
95
+ });
96
+ }
97
+ }
98
+
99
+ return routes;
100
+ }
101
+
102
+ function resolveToSourceFile(node) {
103
+ if (!node) return null;
104
+
105
+ const sourceFile = node.getSourceFile();
106
+
107
+ // 1. Try ESM Import resolution
108
+ if (node.getKind() === SyntaxKind.Identifier) {
109
+ const importDecls = sourceFile.getImportDeclarations();
110
+ for (const imp of importDecls) {
111
+ const namedImports = imp.getNamedImports();
112
+ const defaultImport = imp.getDefaultImport();
113
+ const namespaceImport = imp.getNamespaceImport();
114
+
115
+ if (defaultImport?.getText() === node.getText() ||
116
+ namedImports.some(ni => ni.getName() === node.getText()) ||
117
+ namespaceImport?.getText() === node.getText()) {
118
+ return manualResolve(sourceFile, imp.getModuleSpecifierValue());
119
+ }
120
+ }
121
+ }
122
+
123
+ // 2. Try TS-based resolution
124
+ const symbol = node.getSymbol();
125
+ if (symbol) {
126
+ const decls = symbol.getDeclarations();
127
+ for (const d of decls) {
128
+ const sf = d.getSourceFile();
129
+ if (sf && sf.getFilePath() !== sourceFile.getFilePath()) return sf;
130
+ }
131
+ }
132
+
133
+ // 3. Fallback for CommonJS: check for require()
134
+ if (node.getKind() === SyntaxKind.Identifier) {
135
+ const decls = node.getSymbol()?.getDeclarations() || [];
136
+ for (const d of decls) {
137
+ if (d.getKind() === SyntaxKind.VariableDeclaration) {
138
+ const init = d.getInitializer();
139
+ if (init && init.getKind() === SyntaxKind.CallExpression && init.getExpression().getText() === "require") {
140
+ const args = init.getArguments();
141
+ if (args.length > 0 && args[0].getKind() === SyntaxKind.StringLiteral) {
142
+ return manualResolve(sourceFile, args[0].getLiteralText());
143
+ }
144
+ }
145
+ }
146
+ }
147
+ }
148
+ return null;
149
+ }
150
+
151
+ /**
152
+ * Manually resolves a require or import path to its SourceFile.
153
+ */
154
+ function manualResolve(sourceFile, reqPath) {
155
+ if (!reqPath || typeof reqPath !== "string") return null;
156
+ const directory = path.dirname(sourceFile.getFilePath());
157
+ let normalizedPath = reqPath;
158
+
159
+ // Remove .js extension for resolution if present (typical in ESM)
160
+ if (normalizedPath.endsWith(".js")) {
161
+ normalizedPath = normalizedPath.slice(0, -3);
162
+ }
163
+
164
+ const absPath = path.resolve(directory, normalizedPath);
165
+ const project = sourceFile.getProject();
166
+
167
+ // Standard attempts with common extensions
168
+ const variants = [
169
+ absPath + ".js",
170
+ absPath + ".ts",
171
+ absPath,
172
+ path.join(absPath, "index.js"),
173
+ path.join(absPath, "index.ts")
174
+ ];
175
+
176
+ for (const v of variants) {
177
+ const sf = project.getSourceFile(v);
178
+ if (sf) return sf;
179
+ }
180
+
181
+ // Fallback: Case-insensitive match or contains match for robust resolution
182
+ const vLower = absPath.toLowerCase().replace(/\\/g, "/");
183
+ const sfFallback = project.getSourceFiles().find(f => {
184
+ const fp = f.getFilePath().toLowerCase().replace(/\\/g, "/");
185
+ return fp === vLower || fp === vLower + ".js" || fp === vLower + ".ts" || fp.endsWith(vLower + ".js");
186
+ });
187
+
188
+ return sfFallback;
189
+ }
190
+
191
+ function resolveHandler(node) {
192
+ if (!node) return null;
193
+
194
+ // Unwrap asyncHandler(func)
195
+ if (node.getKind() === SyntaxKind.CallExpression) {
196
+ const exp = node.getExpression();
197
+ if (exp.getText().includes("asyncHandler")) {
198
+ const args = node.getArguments();
199
+ if (args.length > 0) return resolveHandler(args[0]);
200
+ }
201
+ }
202
+
203
+ if (node.getKind() === SyntaxKind.PropertyAccessExpression) {
204
+ const obj = node.getExpression();
205
+ const prop = node.getName();
206
+ const objFile = resolveToSourceFile(obj);
207
+ if (objFile) {
208
+ // Look for 'exports.name = ...' or 'module.exports.name = ...'
209
+ const assignments = objFile.getDescendantsOfKind(SyntaxKind.BinaryExpression);
210
+ for (const ass of assignments) {
211
+ const left = ass.getLeft().getText();
212
+ if (left === `exports.${prop}` || left === `module.exports.${prop}`) return resolveHandler(ass.getRight());
213
+ }
214
+ // Or variable declaration
215
+ const varDec = objFile.getVariableDeclaration(prop);
216
+ if (varDec) return resolveHandler(varDec.getInitializer() || varDec);
217
+ }
218
+ }
219
+
220
+ if (node.getKind() === SyntaxKind.Identifier) {
221
+ const symbol = node.getSymbol();
222
+ if (symbol) {
223
+ const decls = symbol.getDeclarations();
224
+ for (const d of decls) {
225
+ if (d.getKind() === SyntaxKind.VariableDeclaration || d.getKind() === SyntaxKind.PropertyAssignment) {
226
+ return resolveHandler(d.getInitializer() || d);
227
+ }
228
+ if (d.getKind() === SyntaxKind.FunctionDeclaration) return d;
229
+ }
230
+ }
231
+
232
+ // Check Imports specifically if symbol failed
233
+ const sourceFile = node.getSourceFile();
234
+ const imports = sourceFile.getImportDeclarations();
235
+ for (const imp of imports) {
236
+ const named = imp.getNamedImports();
237
+ for (const n of named) {
238
+ if (n.getName() === node.getText()) {
239
+ const sf = manualResolve(sourceFile, imp.getModuleSpecifierValue());
240
+ if (sf) {
241
+ const vd = sf.getVariableDeclaration(node.getText()) || sf.getFunction(node.getText());
242
+ if (vd) return resolveHandler(vd.getInitializer?.() || vd);
243
+ }
244
+ }
245
+ }
246
+ }
247
+ }
248
+ return node;
249
+ }
250
+
251
+ function analyzeHandler(node) {
252
+ const inputs = { body: [], query: [], params: [] };
253
+ const outputs = { statusCodes: [], responseTypes: [] };
254
+
255
+ let reqName = "req";
256
+ let resName = "res";
257
+
258
+ if (typeof node.getParameters === "function") {
259
+ const params = node.getParameters();
260
+ if (params.length > 0) reqName = params[0].getName();
261
+ if (params.length > 1) resName = params[1].getName();
262
+ }
263
+
264
+ if (typeof node.getDescendants === "function") {
265
+ node.getDescendants().forEach(desc => {
266
+ const text = desc.getText();
267
+ if (text.startsWith(`${reqName}.body.`)) inputs.body.push(text.split(`${reqName}.body.`)[1].split(/[ .([]/)[0]);
268
+ if (text.startsWith(`${reqName}.body['`) || text.startsWith(`${reqName}.body["`)) {
269
+ const parts = text.split(/['"]/);
270
+ if (parts.length > 1) inputs.body.push(parts[1]);
271
+ }
272
+ if (text.startsWith(`${reqName}.query.`)) inputs.query.push(text.split(`${reqName}.query.`)[1].split(/[ .([]/)[0]);
273
+ if (text.startsWith(`${reqName}.query['`) || text.startsWith(`${reqName}.query["`)) {
274
+ const parts = text.split(/['"]/);
275
+ if (parts.length > 1) inputs.query.push(parts[1]);
276
+ }
277
+ if (text.startsWith(`${reqName}.params.`)) inputs.params.push(text.split(`${reqName}.params.`)[1].split(/[ .([]/)[0]);
278
+ if (text.startsWith(`${reqName}.params['`) || text.startsWith(`${reqName}.params["`)) {
279
+ const parts = text.split(/['"]/);
280
+ if (parts.length > 1) inputs.params.push(parts[1]);
281
+ }
282
+
283
+ if (desc.getKind() === SyntaxKind.VariableDeclaration) {
284
+ const nameNode = desc.getNameNode();
285
+ const init = desc.getInitializer();
286
+ if (nameNode.getKind() === SyntaxKind.ObjectBindingPattern && init) {
287
+ const initText = init.getText();
288
+ if (initText.includes(`${reqName}.body`)) nameNode.getElements().forEach(el => inputs.body.push(el.getName()));
289
+ if (initText.includes(`${reqName}.query`)) nameNode.getElements().forEach(el => inputs.query.push(el.getName()));
290
+ if (initText.includes(`${reqName}.params`)) nameNode.getElements().forEach(el => inputs.params.push(el.getName()));
291
+ }
292
+ }
293
+
294
+ if (desc.getKind() === SyntaxKind.CallExpression) {
295
+ const callExp = desc.getExpression();
296
+ const callText = callExp.getText();
297
+
298
+ // res.status(200)
299
+ if (callExp.getKind() === SyntaxKind.PropertyAccessExpression && callExp.getName() === "status") {
300
+ const obj = callExp.getExpression().getText();
301
+ if (obj === resName || obj.endsWith(`.${resName}`)) {
302
+ const args = desc.getArguments();
303
+ if (args.length > 0) outputs.statusCodes.push(args[0].getText());
304
+ }
305
+ }
306
+
307
+ // ApiError or new ApiError
308
+ if (callText.includes("ApiError")) {
309
+ const possibleCall = desc;
310
+ const args = possibleCall.getArguments();
311
+ if (args.length > 0) outputs.statusCodes.push(args[0].getText());
312
+ }
313
+
314
+ if (callText.includes(`.json`)) outputs.responseTypes.push("application/json");
315
+ if (callText.includes(`.send`)) outputs.responseTypes.push("text/plain");
316
+ if (callText.includes(`.download`)) outputs.responseTypes.push("application/octet-stream");
317
+ }
318
+ });
319
+ }
320
+
321
+ return {
322
+ inputs: {
323
+ body: [...new Set(inputs.body)],
324
+ query: [...new Set(inputs.query)],
325
+ params: [...new Set(inputs.params)]
326
+ },
327
+ outputs: {
328
+ statusCodes: [...new Set(outputs.statusCodes)].length > 0 ? [...new Set(outputs.statusCodes)] : ["200"],
329
+ responseTypes: [...new Set(outputs.responseTypes)].length > 0 ? [...new Set(outputs.responseTypes)] : ["application/json"]
330
+ }
331
+ };
332
+ }
@@ -0,0 +1,101 @@
1
+ import { readFile, access } from "fs/promises";
2
+ import path from "path";
3
+ import { globSync } from "glob";
4
+ import { Project } from "ts-morph";
5
+ import { scanExpressProject } from "./expressScanner.js";
6
+ import { scanModels } from "./modelScanner.js";
7
+
8
+ export async function scanProject(root) {
9
+ const result = {
10
+ projectName: "my-project",
11
+ projectDescription: "",
12
+ projectVersion: "1.0.0",
13
+ projectAuthor: "",
14
+ packages: { dependencies: {}, devDependencies: {} },
15
+ routes: [],
16
+ models: {},
17
+ files: [],
18
+ hasReadme: false,
19
+ existingReadme: "",
20
+ };
21
+
22
+ // =========================
23
+ // package.json
24
+ // =========================
25
+ const pkgPath = path.join(root, "package.json");
26
+
27
+ try {
28
+ await access(pkgPath);
29
+ const pkgData = await readFile(pkgPath, "utf-8");
30
+ const pkg = JSON.parse(pkgData);
31
+
32
+ result.projectName = pkg.name ?? "my-project";
33
+ result.projectDescription = pkg.description ?? "";
34
+ result.projectVersion = pkg.version ?? "1.0.0";
35
+ result.projectAuthor = pkg.author ?? "";
36
+ result.packages.dependencies = pkg.dependencies ?? {};
37
+ result.packages.devDependencies = pkg.devDependencies ?? {};
38
+ } catch {
39
+ // ignore if not found
40
+ }
41
+
42
+ // =========================
43
+ // README
44
+ // =========================
45
+ const readmePath = path.join(root, "README.md");
46
+
47
+ try {
48
+ await access(readmePath);
49
+ result.hasReadme = true;
50
+ result.existingReadme = await readFile(readmePath, "utf-8");
51
+ } catch {
52
+ // ignore if not found
53
+ }
54
+
55
+ // =========================
56
+ // Find Files
57
+ // =========================
58
+ const files = globSync("**/*.{js,ts,jsx,tsx}", {
59
+ cwd: root,
60
+ ignore: ["node_modules/**", "dist/**", ".git/**", "*.config.*", "tests/**"],
61
+ absolute: true,
62
+ });
63
+
64
+ result.files = files.map((f) => path.relative(root, f));
65
+
66
+ // =========================
67
+ // ts-morph Project
68
+ // =========================
69
+ const project = new Project({
70
+ compilerOptions: {
71
+ allowJs: true,
72
+ },
73
+ });
74
+
75
+ files.forEach((file) => {
76
+ project.addSourceFileAtPath(file);
77
+ });
78
+
79
+ // console.log(
80
+ // "Source File =",
81
+ // project.getSourceFiles().map((file) => file.getFilePath()),
82
+ // );
83
+
84
+ // =========================
85
+ // Detect Framework
86
+ // =========================
87
+ const deps = {
88
+ ...result.packages.dependencies,
89
+ ...result.packages.devDependencies,
90
+ };
91
+
92
+ // Always scan for models to find Mongoose schemas
93
+ result.models = scanModels(project);
94
+
95
+ if (deps["express"] || project.getSourceFiles().some(f => f.getText().includes('express'))) {
96
+ const expressRoutes = scanExpressProject(project, root);
97
+ result.routes = expressRoutes;
98
+ }
99
+
100
+ return result;
101
+ }
@@ -0,0 +1,204 @@
1
+ import { SyntaxKind } from "ts-morph";
2
+
3
+ /**
4
+ * Scans for Mongoose schemas in the project and converts them to OpenAPI schemas.
5
+ */
6
+ export function scanModels(project) {
7
+ const models = {};
8
+
9
+ for (const sourceFile of project.getSourceFiles()) {
10
+ // Look for new Schema(...) or mongoose.Schema(...)
11
+ const newExpressions = sourceFile.getDescendantsOfKind(SyntaxKind.NewExpression);
12
+
13
+ for (const newExpr of newExpressions) {
14
+ const typeText = newExpr.getExpression().getText();
15
+ if (typeText === "Schema" || typeText === "mongoose.Schema" || typeText.endsWith(".Schema")) {
16
+ const args = newExpr.getArguments();
17
+ if (args.length > 0 && args[0].getKind() === SyntaxKind.ObjectLiteralExpression) {
18
+ const schemaObj = args[0];
19
+ const modelName = inferModelName(sourceFile, newExpr);
20
+
21
+ // Capitalize model name for consistency
22
+ const capitalized = modelName.charAt(0).toUpperCase() + modelName.slice(1);
23
+ models[capitalized] = parseSchemaObject(schemaObj);
24
+ }
25
+ }
26
+ }
27
+ }
28
+
29
+ return models;
30
+ }
31
+
32
+ function inferModelName(sourceFile, node) {
33
+ // Try to find the variable name it's assigned to (e.g., const userSchema = new Schema(...))
34
+ const varDec = node.getFirstAncestorByKind(SyntaxKind.VariableDeclaration);
35
+ if (varDec) {
36
+ let name = varDec.getName();
37
+ // Remove 'Schema' suffix if present
38
+ return name.replace(/Schema$/i, "");
39
+ }
40
+
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
+ // Fallback to filename
49
+ return sourceFile.getBaseNameWithoutExtension();
50
+ }
51
+
52
+ function parseSchemaObject(obj) {
53
+ const properties = {
54
+ _id: { type: "string", example: "60d0fe4f5311236168a109ca" }
55
+ };
56
+ const required = [];
57
+
58
+ for (const property of obj.getProperties()) {
59
+ if (property.getKind() === SyntaxKind.PropertyAssignment) {
60
+ const name = property.getName().replace(/['"]/g, "");
61
+ const initializer = property.getInitializer();
62
+
63
+ if (initializer) {
64
+ const info = parseFieldInfo(initializer, name);
65
+ properties[name] = info.schema;
66
+ if (info.required) required.push(name);
67
+ }
68
+ }
69
+ }
70
+
71
+ return {
72
+ type: "object",
73
+ properties,
74
+ ...(required.length > 0 ? { required } : {})
75
+ };
76
+ }
77
+
78
+ function parseFieldInfo(node, fieldName = "") {
79
+ let type = "string";
80
+ let isRequired = false;
81
+ let enumValues = null;
82
+ let defaultValue = undefined;
83
+
84
+ if (node.getKind() === SyntaxKind.Identifier) {
85
+ const text = node.getText();
86
+ type = mapMongooseType(text);
87
+ } else if (node.getKind() === SyntaxKind.ObjectLiteralExpression) {
88
+ const typeProp = node.getProperty("type");
89
+ if (typeProp && typeProp.getKind() === SyntaxKind.PropertyAssignment) {
90
+ const typeInit = typeProp.getInitializer();
91
+ if (typeInit) type = mapMongooseType(typeInit.getText());
92
+ }
93
+
94
+ const requiredProp = node.getProperty("required");
95
+ if (requiredProp && (requiredProp.getKind() === SyntaxKind.PropertyAssignment || requiredProp.getKind() === SyntaxKind.ShorthandPropertyAssignment)) {
96
+ let val = "";
97
+ if (requiredProp.getKind() === SyntaxKind.PropertyAssignment) {
98
+ val = requiredProp.getInitializer().getText();
99
+ } else {
100
+ val = requiredProp.getName();
101
+ }
102
+ isRequired = val === "true" || val.startsWith("[true");
103
+ }
104
+
105
+ const enumProp = node.getProperty("enum");
106
+ if (enumProp && enumProp.getKind() === SyntaxKind.PropertyAssignment) {
107
+ const enumInit = enumProp.getInitializer();
108
+ if (enumInit.getKind() === SyntaxKind.ArrayLiteralExpression) {
109
+ enumValues = enumInit.getElements().map(e => e.getText().replace(/['"]/g, ""));
110
+ }
111
+ }
112
+
113
+ const defaultProp = node.getProperty("default");
114
+ if (defaultProp && defaultProp.getKind() === SyntaxKind.PropertyAssignment) {
115
+ defaultValue = defaultProp.getInitializer().getText().replace(/['"]/g, "");
116
+ if (type === "number") defaultValue = Number(defaultValue);
117
+ if (type === "boolean") defaultValue = defaultValue === "true";
118
+ }
119
+ } else if (node.getKind() === SyntaxKind.ArrayLiteralExpression) {
120
+ const elements = node.getElements();
121
+ if (elements.length > 0) {
122
+ const subInfo = parseFieldInfo(elements[0], fieldName);
123
+ return {
124
+ schema: { type: "array", items: subInfo.schema },
125
+ required: false
126
+ };
127
+ } else {
128
+ return { schema: { type: "array", items: { type: "object" } }, required: false };
129
+ }
130
+ }
131
+
132
+ const schema = { type };
133
+ if (enumValues) schema.enum = enumValues;
134
+
135
+ // Format detection
136
+ const format = detectFormat(fieldName, node.getText());
137
+ if (format) schema.format = format;
138
+
139
+ // Example generation
140
+ const example = generateExample(fieldName, type, enumValues);
141
+ if (example !== undefined) schema.example = example;
142
+
143
+ // Default value
144
+ if (defaultValue !== undefined) schema.default = defaultValue;
145
+
146
+ return {
147
+ schema,
148
+ required: isRequired
149
+ };
150
+ }
151
+
152
+ function detectFormat(name, text) {
153
+ const n = name.toLowerCase();
154
+ if (n.includes("email")) return "email";
155
+ if (n.includes("password")) return "password";
156
+ if (n.includes("url") || n.includes("uri")) return "uri";
157
+ if (n.includes("uuid")) return "uuid";
158
+ // Check if the type text suggests a Date
159
+ if (n.includes("date") || n.includes("at") || text.includes("Date") || text.includes("at")) return "date-time";
160
+ return undefined;
161
+ }
162
+
163
+ function generateExample(name, type, enumValues) {
164
+ if (enumValues && enumValues.length > 0) return enumValues[0];
165
+ const n = name.toLowerCase();
166
+ if (n.includes("email")) return "user@example.com";
167
+ if (n.includes("username")) return "john_doe";
168
+ if (n.includes("fullname") || n.includes("name")) return "John Doe";
169
+ if (n.includes("phone") || n.includes("mobile")) return "+1234567890";
170
+ if (n.includes("password")) return "********";
171
+ if (n.includes("url")) return "https://example.com";
172
+ if (n.includes("id") && !n.includes("guid")) return "60d0fe4f5311236168a109ca";
173
+
174
+ if (type === "number") {
175
+ if (n.includes("age")) return 25;
176
+ if (n.includes("count")) return 10;
177
+ if (n.includes("price")) return 99.99;
178
+ return 123;
179
+ }
180
+ if (type === "boolean") return true;
181
+ if (type === "string") {
182
+ if (n.includes("date") || n.includes("at")) return "2024-01-01T00:00:00Z";
183
+ if (n.includes("gender")) return "Male";
184
+ if (n.includes("city")) return "New York";
185
+ return n.charAt(0).toUpperCase() + n.slice(1);
186
+ }
187
+ return undefined;
188
+ }
189
+
190
+ function mapMongooseType(text) {
191
+ const map = {
192
+ "String": "string",
193
+ "Number": "number",
194
+ "Boolean": "boolean",
195
+ "Date": "string",
196
+ "ObjectId": "string",
197
+ "Schema.Types.ObjectId": "string",
198
+ "Buffer": "string",
199
+ "Mixed": "object",
200
+ "Map": "object",
201
+ "Decimal128": "number"
202
+ };
203
+ return map[text] || "string";
204
+ }