next-openapi-gen 0.10.4 → 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/dist/cli.d.ts +4 -0
- package/dist/cli.js +8599 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +8645 -26
- package/dist/next/index.d.ts +1 -0
- package/dist/next/index.js +7965 -0
- package/dist/react-router/index.d.ts +1 -0
- package/dist/react-router/index.js +7134 -0
- package/dist/vite/index.d.ts +1 -0
- package/dist/vite/index.js +7134 -0
- package/package.json +102 -79
- package/{dist/components/rapidoc.js → templates/init/ui/nextjs/rapidoc.tsx} +16 -20
- package/templates/init/ui/nextjs/redoc.tsx +11 -0
- package/{dist/components/scalar.js → templates/init/ui/nextjs/scalar.tsx} +15 -21
- package/{dist/components/stoplight.js → templates/init/ui/nextjs/stoplight.tsx} +11 -17
- package/templates/init/ui/nextjs/swagger.tsx +17 -0
- package/templates/init/ui/reactrouter/rapidoc.tsx +15 -0
- package/templates/init/ui/reactrouter/redoc.tsx +9 -0
- package/templates/init/ui/reactrouter/scalar.tsx +14 -0
- package/templates/init/ui/reactrouter/stoplight.tsx +10 -0
- package/templates/init/ui/reactrouter/swagger.tsx +11 -0
- package/templates/init/ui/tanstack/rapidoc.tsx +21 -0
- package/templates/init/ui/tanstack/redoc.tsx +14 -0
- package/templates/init/ui/tanstack/scalar.tsx +19 -0
- package/templates/init/ui/tanstack/stoplight.tsx +15 -0
- package/templates/init/ui/tanstack/swagger.tsx +16 -0
- package/templates/init/ui/template-types.d.ts +9 -0
- package/README.md +0 -1047
- package/dist/commands/generate.js +0 -24
- package/dist/commands/init.js +0 -194
- package/dist/components/redoc.js +0 -17
- package/dist/components/swagger.js +0 -21
- package/dist/lib/app-router-strategy.js +0 -66
- package/dist/lib/drizzle-zod-processor.js +0 -329
- package/dist/lib/logger.js +0 -39
- package/dist/lib/openapi-generator.js +0 -171
- package/dist/lib/pages-router-strategy.js +0 -198
- package/dist/lib/route-processor.js +0 -347
- package/dist/lib/router-strategy.js +0 -1
- package/dist/lib/schema-processor.js +0 -1612
- package/dist/lib/utils.js +0 -284
- package/dist/lib/zod-converter.js +0 -2133
- package/dist/openapi-template.js +0 -99
- package/dist/types.js +0 -1
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
import fs from "fs";
|
|
2
|
-
import fse from "fs-extra";
|
|
3
|
-
import path from "path";
|
|
4
|
-
import ora from "ora";
|
|
5
|
-
import { OpenApiGenerator } from "../lib/openapi-generator.js";
|
|
6
|
-
export async function generate(options) {
|
|
7
|
-
const { template } = options;
|
|
8
|
-
const spinner = ora("Generating OpenAPI specification...\n").start();
|
|
9
|
-
const generator = new OpenApiGenerator({
|
|
10
|
-
templatePath: template,
|
|
11
|
-
});
|
|
12
|
-
const config = generator.getConfig();
|
|
13
|
-
// Create api dir if not exists
|
|
14
|
-
const apiDir = path.resolve(config.apiDir);
|
|
15
|
-
await fse.ensureDir(apiDir);
|
|
16
|
-
// Use user-defined output directory
|
|
17
|
-
const outputDir = path.resolve(config.outputDir);
|
|
18
|
-
await fse.ensureDir(outputDir);
|
|
19
|
-
const apiDocs = generator.generate();
|
|
20
|
-
// Write api docs
|
|
21
|
-
const outputFile = path.join(outputDir, config.outputFile);
|
|
22
|
-
fs.writeFileSync(outputFile, JSON.stringify(apiDocs, null, 2));
|
|
23
|
-
spinner.succeed(`OpenAPI specification generated at ${outputFile}`);
|
|
24
|
-
}
|
package/dist/commands/init.js
DELETED
|
@@ -1,194 +0,0 @@
|
|
|
1
|
-
import path from "path";
|
|
2
|
-
import fse from "fs-extra";
|
|
3
|
-
import fs from "fs";
|
|
4
|
-
import ora from "ora";
|
|
5
|
-
import { exec } from "child_process";
|
|
6
|
-
import util from "util";
|
|
7
|
-
import openapiTemplate from "../openapi-template.js";
|
|
8
|
-
import { scalarDeps, scalarDevDeps, ScalarUI } from "../components/scalar.js";
|
|
9
|
-
import { swaggerDeps, swaggerDevDeps, SwaggerUI } from "../components/swagger.js";
|
|
10
|
-
import { redocDeps, redocDevDeps, RedocUI } from "../components/redoc.js";
|
|
11
|
-
import { stoplightDeps, stoplightDevDeps, StoplightUI } from "../components/stoplight.js";
|
|
12
|
-
import { rapidocDeps, rapidocDevDeps, RapidocUI } from "../components/rapidoc.js";
|
|
13
|
-
const execPromise = util.promisify(exec);
|
|
14
|
-
const spinner = ora("Initializing project with OpenAPI template...\n");
|
|
15
|
-
async function hasDependency(packageName) {
|
|
16
|
-
try {
|
|
17
|
-
const packageJsonPath = path.join(process.cwd(), "package.json");
|
|
18
|
-
const packageJson = await fse.readJson(packageJsonPath);
|
|
19
|
-
return !!(packageJson.dependencies?.[packageName] || packageJson.devDependencies?.[packageName]);
|
|
20
|
-
}
|
|
21
|
-
catch {
|
|
22
|
-
return false;
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
const getPackageManager = async () => {
|
|
26
|
-
let currentDir = process.cwd();
|
|
27
|
-
while (true) {
|
|
28
|
-
// Check for Yarn lock file
|
|
29
|
-
if (fs.existsSync(path.join(currentDir, "yarn.lock"))) {
|
|
30
|
-
return "yarn";
|
|
31
|
-
}
|
|
32
|
-
// Check for PNPM lock file
|
|
33
|
-
if (fs.existsSync(path.join(currentDir, "pnpm-lock.yaml"))) {
|
|
34
|
-
return "pnpm";
|
|
35
|
-
}
|
|
36
|
-
// If we're at the root directory, break the loop
|
|
37
|
-
const parentDir = path.dirname(currentDir);
|
|
38
|
-
if (parentDir === currentDir) {
|
|
39
|
-
break; // We've reached the root
|
|
40
|
-
}
|
|
41
|
-
currentDir = parentDir; // Move up one directory
|
|
42
|
-
}
|
|
43
|
-
// Default to npm if no lock files are found
|
|
44
|
-
return "npm";
|
|
45
|
-
};
|
|
46
|
-
function getDocsPage(ui, outputFile) {
|
|
47
|
-
let DocsComponent = ScalarUI;
|
|
48
|
-
if (ui === "swagger") {
|
|
49
|
-
DocsComponent = SwaggerUI;
|
|
50
|
-
}
|
|
51
|
-
else if (ui === "redoc") {
|
|
52
|
-
DocsComponent = RedocUI;
|
|
53
|
-
}
|
|
54
|
-
else if (ui === "stoplight") {
|
|
55
|
-
DocsComponent = StoplightUI;
|
|
56
|
-
}
|
|
57
|
-
else if (ui === "rapidoc") {
|
|
58
|
-
DocsComponent = RapidocUI;
|
|
59
|
-
}
|
|
60
|
-
return DocsComponent(outputFile);
|
|
61
|
-
}
|
|
62
|
-
function getDocsPageInstallFlags(ui, packageManager) {
|
|
63
|
-
let installFlags = "";
|
|
64
|
-
if (ui === "swagger") {
|
|
65
|
-
// @temp: swagger-ui-react does not support React 19 now.
|
|
66
|
-
if (packageManager === "pnpm") {
|
|
67
|
-
installFlags = "--no-strict-peer-dependencies";
|
|
68
|
-
}
|
|
69
|
-
else if (packageManager === "yarn") {
|
|
70
|
-
installFlags = ""; // flag for legacy peer deps is not needed for yarn
|
|
71
|
-
}
|
|
72
|
-
else {
|
|
73
|
-
installFlags = "--legacy-peer-deps";
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
return installFlags;
|
|
77
|
-
}
|
|
78
|
-
function getDocsPageDependencies(ui) {
|
|
79
|
-
let deps = [];
|
|
80
|
-
if (ui === "scalar") {
|
|
81
|
-
deps = scalarDeps;
|
|
82
|
-
}
|
|
83
|
-
else if (ui === "swagger") {
|
|
84
|
-
deps = swaggerDeps;
|
|
85
|
-
}
|
|
86
|
-
else if (ui === "redoc") {
|
|
87
|
-
deps = redocDeps;
|
|
88
|
-
}
|
|
89
|
-
else if (ui === "stoplight") {
|
|
90
|
-
deps = stoplightDeps;
|
|
91
|
-
}
|
|
92
|
-
else if (ui === "rapidoc") {
|
|
93
|
-
deps = rapidocDeps;
|
|
94
|
-
}
|
|
95
|
-
return deps.join(" ");
|
|
96
|
-
}
|
|
97
|
-
function getDocsPageDevDependencies(ui) {
|
|
98
|
-
let devDeps = [];
|
|
99
|
-
if (ui === "scalar") {
|
|
100
|
-
devDeps = scalarDevDeps;
|
|
101
|
-
}
|
|
102
|
-
else if (ui === "swagger") {
|
|
103
|
-
devDeps = swaggerDevDeps;
|
|
104
|
-
}
|
|
105
|
-
else if (ui === "redoc") {
|
|
106
|
-
devDeps = redocDevDeps;
|
|
107
|
-
}
|
|
108
|
-
else if (ui === "stoplight") {
|
|
109
|
-
devDeps = stoplightDevDeps;
|
|
110
|
-
}
|
|
111
|
-
else if (ui === "rapidoc") {
|
|
112
|
-
devDeps = rapidocDevDeps;
|
|
113
|
-
}
|
|
114
|
-
return devDeps.join(" ");
|
|
115
|
-
}
|
|
116
|
-
async function createDocsPage(ui, outputFile) {
|
|
117
|
-
if (ui === "none") {
|
|
118
|
-
return;
|
|
119
|
-
}
|
|
120
|
-
const paths = ["app", "api-docs"];
|
|
121
|
-
const srcPath = path.join(process.cwd(), "src");
|
|
122
|
-
if (fs.existsSync(srcPath)) {
|
|
123
|
-
paths.unshift("src");
|
|
124
|
-
}
|
|
125
|
-
const docsDir = path.join(process.cwd(), ...paths);
|
|
126
|
-
await fs.promises.mkdir(docsDir, { recursive: true });
|
|
127
|
-
const docsPage = getDocsPage(ui, outputFile);
|
|
128
|
-
const componentPath = path.join(docsDir, "page.tsx");
|
|
129
|
-
await fs.promises.writeFile(componentPath, docsPage.trim());
|
|
130
|
-
spinner.succeed(`Created ${paths.join("/")}/page.tsx for ${ui}.`);
|
|
131
|
-
}
|
|
132
|
-
async function installDependencies(ui, schema) {
|
|
133
|
-
const packageManager = await getPackageManager();
|
|
134
|
-
const installCmd = `${packageManager} ${packageManager === "npm" ? "install" : "add"}`;
|
|
135
|
-
// Install UI dependencies
|
|
136
|
-
if (ui !== "none") {
|
|
137
|
-
const deps = getDocsPageDependencies(ui);
|
|
138
|
-
const devDeps = getDocsPageDevDependencies(ui);
|
|
139
|
-
const flags = getDocsPageInstallFlags(ui, packageManager);
|
|
140
|
-
if (deps) {
|
|
141
|
-
spinner.succeed(`Installing ${deps} dependencies...`);
|
|
142
|
-
await execPromise(`${installCmd} ${deps} ${flags}`);
|
|
143
|
-
spinner.succeed(`Successfully installed ${deps}.`);
|
|
144
|
-
}
|
|
145
|
-
if (devDeps) {
|
|
146
|
-
const devFlag = packageManager === "npm" ? "--save-dev" : "-D";
|
|
147
|
-
spinner.succeed(`Installing ${devDeps} dev dependencies...`);
|
|
148
|
-
await execPromise(`${installCmd} ${devFlag} ${devDeps} ${flags}`);
|
|
149
|
-
spinner.succeed(`Successfully installed ${devDeps}.`);
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
// Install schema dependencies
|
|
153
|
-
const schemaTypes = Array.isArray(schema) ? schema : [schema];
|
|
154
|
-
for (const schemaType of schemaTypes) {
|
|
155
|
-
if (schemaType === "zod" && !(await hasDependency("zod"))) {
|
|
156
|
-
spinner.succeed(`Installing zod...`);
|
|
157
|
-
await execPromise(`${installCmd} zod`);
|
|
158
|
-
spinner.succeed(`Successfully installed zod.`);
|
|
159
|
-
}
|
|
160
|
-
else if (schemaType === "typescript" && !(await hasDependency("typescript"))) {
|
|
161
|
-
const devFlag = packageManager === "npm" ? "--save-dev" : "-D";
|
|
162
|
-
spinner.succeed(`Installing typescript...`);
|
|
163
|
-
await execPromise(`${installCmd} ${devFlag} typescript`);
|
|
164
|
-
spinner.succeed(`Successfully installed typescript.`);
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
function extendOpenApiTemplate(spec, options) {
|
|
169
|
-
spec.ui = options.ui ?? spec.ui;
|
|
170
|
-
spec.docsUrl = options.docsUrl ?? spec.docsUrl;
|
|
171
|
-
spec.schemaType = options.schema ?? spec.schemaType;
|
|
172
|
-
}
|
|
173
|
-
function getOutputPath(output) {
|
|
174
|
-
if (output) {
|
|
175
|
-
return path.isAbsolute(output) ? output : path.join(process.cwd(), output);
|
|
176
|
-
}
|
|
177
|
-
return path.join(process.cwd(), "next.openapi.json");
|
|
178
|
-
}
|
|
179
|
-
export async function init(options) {
|
|
180
|
-
const { ui, output, schema } = options;
|
|
181
|
-
spinner.start();
|
|
182
|
-
try {
|
|
183
|
-
const outputPath = getOutputPath(output);
|
|
184
|
-
const template = { ...openapiTemplate };
|
|
185
|
-
extendOpenApiTemplate(template, options);
|
|
186
|
-
await fse.writeJson(outputPath, template, { spaces: 2 });
|
|
187
|
-
spinner.succeed(`Created OpenAPI template in ${outputPath}`);
|
|
188
|
-
createDocsPage(ui, template.outputFile);
|
|
189
|
-
installDependencies(ui, schema);
|
|
190
|
-
}
|
|
191
|
-
catch (error) {
|
|
192
|
-
spinner.fail(`Failed to initialize project: ${error.message}`);
|
|
193
|
-
}
|
|
194
|
-
}
|
package/dist/components/redoc.js
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
export const redocDeps = ["redoc"];
|
|
2
|
-
export const redocDevDeps = [];
|
|
3
|
-
export function RedocUI(outputFile) {
|
|
4
|
-
return `
|
|
5
|
-
"use client";
|
|
6
|
-
|
|
7
|
-
import { RedocStandalone } from "redoc";
|
|
8
|
-
|
|
9
|
-
export default async function ApiDocsPage() {
|
|
10
|
-
return (
|
|
11
|
-
<section>
|
|
12
|
-
<RedocStandalone specUrl="/${outputFile}" />
|
|
13
|
-
</section>
|
|
14
|
-
);
|
|
15
|
-
}
|
|
16
|
-
`;
|
|
17
|
-
}
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
export const swaggerDeps = ["swagger-ui", "swagger-ui-react"];
|
|
2
|
-
export const swaggerDevDeps = ["@types/swagger-ui-react"];
|
|
3
|
-
export function SwaggerUI(outputFile) {
|
|
4
|
-
return `
|
|
5
|
-
import "swagger-ui-react/swagger-ui.css";
|
|
6
|
-
|
|
7
|
-
import dynamic from "next/dynamic";
|
|
8
|
-
|
|
9
|
-
const SwaggerUI = dynamic(() => import("swagger-ui-react"), {
|
|
10
|
-
loading: () => <p>Loading Component...</p>,
|
|
11
|
-
});
|
|
12
|
-
|
|
13
|
-
export default async function ApiDocsPage() {
|
|
14
|
-
return (
|
|
15
|
-
<section>
|
|
16
|
-
<SwaggerUI url="/${outputFile}" />
|
|
17
|
-
</section>
|
|
18
|
-
);
|
|
19
|
-
}
|
|
20
|
-
`;
|
|
21
|
-
}
|
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
import * as t from "@babel/types";
|
|
2
|
-
import fs from "fs";
|
|
3
|
-
import traverseModule from "@babel/traverse";
|
|
4
|
-
const traverse = traverseModule.default || traverseModule;
|
|
5
|
-
import { HTTP_METHODS } from "./router-strategy.js";
|
|
6
|
-
import { extractJSDocComments, parseTypeScriptFile } from "./utils.js";
|
|
7
|
-
export class AppRouterStrategy {
|
|
8
|
-
config;
|
|
9
|
-
constructor(config) {
|
|
10
|
-
this.config = config;
|
|
11
|
-
}
|
|
12
|
-
shouldProcessFile(fileName) {
|
|
13
|
-
return fileName === "route.ts" || fileName === "route.tsx";
|
|
14
|
-
}
|
|
15
|
-
processFile(filePath, addRoute) {
|
|
16
|
-
const content = fs.readFileSync(filePath, "utf-8");
|
|
17
|
-
const ast = parseTypeScriptFile(content);
|
|
18
|
-
traverse(ast, {
|
|
19
|
-
ExportNamedDeclaration: (path) => {
|
|
20
|
-
const declaration = path.node.declaration;
|
|
21
|
-
if (t.isFunctionDeclaration(declaration) &&
|
|
22
|
-
t.isIdentifier(declaration.id)) {
|
|
23
|
-
if (HTTP_METHODS.includes(declaration.id.name)) {
|
|
24
|
-
const dataTypes = extractJSDocComments(path);
|
|
25
|
-
addRoute(declaration.id.name, filePath, dataTypes);
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
if (t.isVariableDeclaration(declaration)) {
|
|
29
|
-
declaration.declarations.forEach((decl) => {
|
|
30
|
-
if (t.isVariableDeclarator(decl) && t.isIdentifier(decl.id)) {
|
|
31
|
-
if (HTTP_METHODS.includes(decl.id.name)) {
|
|
32
|
-
const dataTypes = extractJSDocComments(path);
|
|
33
|
-
addRoute(decl.id.name, filePath, dataTypes);
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
});
|
|
37
|
-
}
|
|
38
|
-
},
|
|
39
|
-
});
|
|
40
|
-
}
|
|
41
|
-
getRoutePath(filePath) {
|
|
42
|
-
const normalizedPath = filePath.replaceAll("\\", "/");
|
|
43
|
-
const normalizedApiDir = this.config.apiDir
|
|
44
|
-
.replaceAll("\\", "/")
|
|
45
|
-
.replace(/^\.\//, "")
|
|
46
|
-
.replace(/\/$/, "");
|
|
47
|
-
const apiDirIndex = normalizedPath.indexOf(normalizedApiDir);
|
|
48
|
-
if (apiDirIndex === -1) {
|
|
49
|
-
throw new Error(`Could not find apiDir "${this.config.apiDir}" in file path "${filePath}"`);
|
|
50
|
-
}
|
|
51
|
-
let relativePath = normalizedPath.substring(apiDirIndex + normalizedApiDir.length);
|
|
52
|
-
// Remove the /route.ts or /route.tsx suffix
|
|
53
|
-
relativePath = relativePath.replace(/\/route\.tsx?$/, "");
|
|
54
|
-
if (!relativePath.startsWith("/")) {
|
|
55
|
-
relativePath = "/" + relativePath;
|
|
56
|
-
}
|
|
57
|
-
relativePath = relativePath.replace(/\/$/, "");
|
|
58
|
-
// Remove Next.js route groups (folders in parentheses like (authenticated))
|
|
59
|
-
relativePath = relativePath.replace(/\/\([^)]+\)/g, "");
|
|
60
|
-
// Handle catch-all routes before dynamic routes
|
|
61
|
-
relativePath = relativePath.replace(/\/\[\.\.\.(.*?)\]/g, "/{$1}");
|
|
62
|
-
// Convert Next.js dynamic route syntax to OpenAPI parameter syntax
|
|
63
|
-
relativePath = relativePath.replace(/\/\[([^\]]+)\]/g, "/{$1}");
|
|
64
|
-
return relativePath || "/";
|
|
65
|
-
}
|
|
66
|
-
}
|
|
@@ -1,329 +0,0 @@
|
|
|
1
|
-
import * as t from "@babel/types";
|
|
2
|
-
import { logger } from "./logger.js";
|
|
3
|
-
/**
|
|
4
|
-
* Processor for drizzle-zod schemas
|
|
5
|
-
*
|
|
6
|
-
* Drizzle-zod is a library that generates Zod schemas from Drizzle ORM table definitions.
|
|
7
|
-
* It provides helper functions like:
|
|
8
|
-
* - createInsertSchema(tableDefinition, refinements)
|
|
9
|
-
* - createSelectSchema(tableDefinition, refinements)
|
|
10
|
-
*
|
|
11
|
-
* This processor extracts field definitions and refinements to generate OpenAPI schemas.
|
|
12
|
-
*/
|
|
13
|
-
export class DrizzleZodProcessor {
|
|
14
|
-
/**
|
|
15
|
-
* Known drizzle-zod helper function names
|
|
16
|
-
*/
|
|
17
|
-
static DRIZZLE_ZOD_HELPERS = [
|
|
18
|
-
"createInsertSchema",
|
|
19
|
-
"createSelectSchema",
|
|
20
|
-
"createUpdateSchema",
|
|
21
|
-
];
|
|
22
|
-
/**
|
|
23
|
-
* Process a drizzle-zod schema node
|
|
24
|
-
*
|
|
25
|
-
* @param node - The CallExpression node representing a drizzle-zod function call
|
|
26
|
-
* @returns OpenAPI schema object
|
|
27
|
-
*/
|
|
28
|
-
static processSchema(node) {
|
|
29
|
-
const functionName = t.isIdentifier(node.callee)
|
|
30
|
-
? node.callee.name
|
|
31
|
-
: "unknown";
|
|
32
|
-
logger.debug(`Processing drizzle-zod schema: ${functionName}`);
|
|
33
|
-
const schema = {
|
|
34
|
-
type: "object",
|
|
35
|
-
properties: {},
|
|
36
|
-
required: [],
|
|
37
|
-
};
|
|
38
|
-
// Check if there's a refinements object (second argument)
|
|
39
|
-
if (node.arguments.length > 1 && t.isObjectExpression(node.arguments[1])) {
|
|
40
|
-
const refinements = node.arguments[1];
|
|
41
|
-
// Process each property in the refinements object
|
|
42
|
-
refinements.properties.forEach((prop) => {
|
|
43
|
-
if (t.isObjectProperty(prop) || t.isObjectMethod(prop)) {
|
|
44
|
-
const key = this.extractPropertyKey(prop);
|
|
45
|
-
if (!key)
|
|
46
|
-
return;
|
|
47
|
-
// The value is typically an arrow function: (schema) => schema.field.method()
|
|
48
|
-
if (t.isObjectProperty(prop) &&
|
|
49
|
-
t.isArrowFunctionExpression(prop.value)) {
|
|
50
|
-
const arrowFunc = prop.value;
|
|
51
|
-
const fieldSchema = this.extractFieldSchema(arrowFunc.body);
|
|
52
|
-
if (fieldSchema) {
|
|
53
|
-
schema.properties[key] = fieldSchema;
|
|
54
|
-
// Determine if field is required based on schema modifiers
|
|
55
|
-
if (!this.isFieldOptional(arrowFunc.body)) {
|
|
56
|
-
schema.required.push(key);
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
});
|
|
62
|
-
}
|
|
63
|
-
// If no properties were extracted, return a generic object schema
|
|
64
|
-
if (Object.keys(schema.properties).length === 0) {
|
|
65
|
-
logger.debug("No properties extracted from drizzle-zod schema, returning generic object");
|
|
66
|
-
return { type: "object" };
|
|
67
|
-
}
|
|
68
|
-
return schema;
|
|
69
|
-
}
|
|
70
|
-
/**
|
|
71
|
-
* Extract property key from object property or method
|
|
72
|
-
*/
|
|
73
|
-
static extractPropertyKey(prop) {
|
|
74
|
-
if (t.isIdentifier(prop.key)) {
|
|
75
|
-
return prop.key.name;
|
|
76
|
-
}
|
|
77
|
-
if (t.isStringLiteral(prop.key)) {
|
|
78
|
-
return prop.key.value;
|
|
79
|
-
}
|
|
80
|
-
return null;
|
|
81
|
-
}
|
|
82
|
-
/**
|
|
83
|
-
* Extract OpenAPI schema from a drizzle-zod field refinement
|
|
84
|
-
*
|
|
85
|
-
* Handles patterns like:
|
|
86
|
-
* - schema.field
|
|
87
|
-
* - schema.field.min(1)
|
|
88
|
-
* - schema.field.min(1).max(100).email()
|
|
89
|
-
*/
|
|
90
|
-
static extractFieldSchema(node) {
|
|
91
|
-
// Handle member expressions like: schema.field
|
|
92
|
-
if (t.isMemberExpression(node)) {
|
|
93
|
-
if (t.isIdentifier(node.property)) {
|
|
94
|
-
const fieldType = node.property.name;
|
|
95
|
-
return this.mapFieldTypeToOpenApi(fieldType);
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
// Handle call expressions (chained methods like schema.field.min(1).max(100))
|
|
99
|
-
if (t.isCallExpression(node)) {
|
|
100
|
-
const baseSchema = this.extractFieldSchema(t.isMemberExpression(node.callee) ? node.callee.object : node);
|
|
101
|
-
if (baseSchema && t.isMemberExpression(node.callee)) {
|
|
102
|
-
const methodName = t.isIdentifier(node.callee.property)
|
|
103
|
-
? node.callee.property.name
|
|
104
|
-
: null;
|
|
105
|
-
if (methodName) {
|
|
106
|
-
return this.applyZodMethod(baseSchema, methodName, node.arguments);
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
return baseSchema;
|
|
110
|
-
}
|
|
111
|
-
return null;
|
|
112
|
-
}
|
|
113
|
-
/**
|
|
114
|
-
* Check if a drizzle-zod field is optional
|
|
115
|
-
*/
|
|
116
|
-
static isFieldOptional(node) {
|
|
117
|
-
if (t.isCallExpression(node) && t.isMemberExpression(node.callee)) {
|
|
118
|
-
const methodName = t.isIdentifier(node.callee.property)
|
|
119
|
-
? node.callee.property.name
|
|
120
|
-
: null;
|
|
121
|
-
if (methodName === "optional" ||
|
|
122
|
-
methodName === "nullish") {
|
|
123
|
-
return true;
|
|
124
|
-
}
|
|
125
|
-
// Check parent chain recursively
|
|
126
|
-
return this.isFieldOptional(node.callee.object);
|
|
127
|
-
}
|
|
128
|
-
return false;
|
|
129
|
-
}
|
|
130
|
-
/**
|
|
131
|
-
* Map Drizzle field types to OpenAPI types
|
|
132
|
-
*
|
|
133
|
-
* This provides intelligent mapping based on common field naming patterns.
|
|
134
|
-
* For more accurate type detection, the drizzle table schema would need to be analyzed.
|
|
135
|
-
*/
|
|
136
|
-
static mapFieldTypeToOpenApi(fieldType) {
|
|
137
|
-
// Common mappings based on field naming conventions
|
|
138
|
-
const lowercaseField = fieldType.toLowerCase();
|
|
139
|
-
// String types
|
|
140
|
-
if (lowercaseField.includes("title") ||
|
|
141
|
-
lowercaseField.includes("name") ||
|
|
142
|
-
lowercaseField.includes("description") ||
|
|
143
|
-
lowercaseField.includes("content") ||
|
|
144
|
-
lowercaseField.includes("text") ||
|
|
145
|
-
lowercaseField.includes("slug") ||
|
|
146
|
-
lowercaseField.includes("email") ||
|
|
147
|
-
lowercaseField.includes("url") ||
|
|
148
|
-
lowercaseField.includes("phone")) {
|
|
149
|
-
const schema = { type: "string" };
|
|
150
|
-
// Add format hints
|
|
151
|
-
if (lowercaseField.includes("email")) {
|
|
152
|
-
schema.format = "email";
|
|
153
|
-
}
|
|
154
|
-
else if (lowercaseField.includes("url") ||
|
|
155
|
-
lowercaseField.includes("uri")) {
|
|
156
|
-
schema.format = "uri";
|
|
157
|
-
}
|
|
158
|
-
else if (lowercaseField.includes("uuid")) {
|
|
159
|
-
schema.format = "uuid";
|
|
160
|
-
}
|
|
161
|
-
return schema;
|
|
162
|
-
}
|
|
163
|
-
// Integer types
|
|
164
|
-
if (lowercaseField.includes("id") ||
|
|
165
|
-
lowercaseField.includes("count") ||
|
|
166
|
-
lowercaseField.includes("stock") ||
|
|
167
|
-
lowercaseField.includes("quantity") ||
|
|
168
|
-
lowercaseField.includes("age") ||
|
|
169
|
-
lowercaseField.includes("year")) {
|
|
170
|
-
return { type: "integer" };
|
|
171
|
-
}
|
|
172
|
-
// Number types
|
|
173
|
-
if (lowercaseField.includes("price") ||
|
|
174
|
-
lowercaseField.includes("amount") ||
|
|
175
|
-
lowercaseField.includes("rate") ||
|
|
176
|
-
lowercaseField.includes("percent")) {
|
|
177
|
-
return { type: "number" };
|
|
178
|
-
}
|
|
179
|
-
// Boolean types
|
|
180
|
-
if (lowercaseField.startsWith("is") ||
|
|
181
|
-
lowercaseField.startsWith("has") ||
|
|
182
|
-
lowercaseField.includes("active") ||
|
|
183
|
-
lowercaseField.includes("enabled") ||
|
|
184
|
-
lowercaseField.includes("published")) {
|
|
185
|
-
return { type: "boolean" };
|
|
186
|
-
}
|
|
187
|
-
// Date/time types
|
|
188
|
-
if (lowercaseField.includes("date") ||
|
|
189
|
-
lowercaseField.includes("time") ||
|
|
190
|
-
lowercaseField.includes("createdat") ||
|
|
191
|
-
lowercaseField.includes("updatedat") ||
|
|
192
|
-
lowercaseField.includes("deletedat")) {
|
|
193
|
-
return { type: "string", format: "date-time" };
|
|
194
|
-
}
|
|
195
|
-
// Default to string for unknown types
|
|
196
|
-
return { type: "string" };
|
|
197
|
-
}
|
|
198
|
-
/**
|
|
199
|
-
* Apply a Zod validation method to a schema
|
|
200
|
-
*
|
|
201
|
-
* Translates Zod validation methods to OpenAPI constraints:
|
|
202
|
-
* - min/max for strings become minLength/maxLength
|
|
203
|
-
* - min/max for numbers become minimum/maximum
|
|
204
|
-
* - email/url/uuid become format constraints
|
|
205
|
-
*/
|
|
206
|
-
static applyZodMethod(schema, methodName, args) {
|
|
207
|
-
const result = { ...schema };
|
|
208
|
-
switch (methodName) {
|
|
209
|
-
case "min":
|
|
210
|
-
if (args.length > 0 && t.isNumericLiteral(args[0])) {
|
|
211
|
-
if (schema.type === "string") {
|
|
212
|
-
result.minLength = args[0].value;
|
|
213
|
-
}
|
|
214
|
-
else if (schema.type === "number" || schema.type === "integer") {
|
|
215
|
-
result.minimum = args[0].value;
|
|
216
|
-
}
|
|
217
|
-
else if (schema.type === "array") {
|
|
218
|
-
result.minItems = args[0].value;
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
break;
|
|
222
|
-
case "max":
|
|
223
|
-
if (args.length > 0 && t.isNumericLiteral(args[0])) {
|
|
224
|
-
if (schema.type === "string") {
|
|
225
|
-
result.maxLength = args[0].value;
|
|
226
|
-
}
|
|
227
|
-
else if (schema.type === "number" || schema.type === "integer") {
|
|
228
|
-
result.maximum = args[0].value;
|
|
229
|
-
}
|
|
230
|
-
else if (schema.type === "array") {
|
|
231
|
-
result.maxItems = args[0].value;
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
break;
|
|
235
|
-
case "length":
|
|
236
|
-
if (args.length > 0 && t.isNumericLiteral(args[0])) {
|
|
237
|
-
if (schema.type === "string") {
|
|
238
|
-
result.minLength = args[0].value;
|
|
239
|
-
result.maxLength = args[0].value;
|
|
240
|
-
}
|
|
241
|
-
else if (schema.type === "array") {
|
|
242
|
-
result.minItems = args[0].value;
|
|
243
|
-
result.maxItems = args[0].value;
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
break;
|
|
247
|
-
case "email":
|
|
248
|
-
result.format = "email";
|
|
249
|
-
break;
|
|
250
|
-
case "url":
|
|
251
|
-
result.format = "uri";
|
|
252
|
-
break;
|
|
253
|
-
case "uuid":
|
|
254
|
-
result.format = "uuid";
|
|
255
|
-
break;
|
|
256
|
-
case "datetime":
|
|
257
|
-
result.format = "date-time";
|
|
258
|
-
break;
|
|
259
|
-
case "regex":
|
|
260
|
-
if (args.length > 0) {
|
|
261
|
-
// Try to extract pattern from regex literal
|
|
262
|
-
if (t.isRegExpLiteral(args[0])) {
|
|
263
|
-
result.pattern = args[0].pattern;
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
break;
|
|
267
|
-
case "positive":
|
|
268
|
-
if (schema.type === "number" || schema.type === "integer") {
|
|
269
|
-
result.minimum = 0;
|
|
270
|
-
result.exclusiveMinimum = true;
|
|
271
|
-
}
|
|
272
|
-
break;
|
|
273
|
-
case "nonnegative":
|
|
274
|
-
if (schema.type === "number" || schema.type === "integer") {
|
|
275
|
-
result.minimum = 0;
|
|
276
|
-
}
|
|
277
|
-
break;
|
|
278
|
-
case "negative":
|
|
279
|
-
if (schema.type === "number" || schema.type === "integer") {
|
|
280
|
-
result.maximum = 0;
|
|
281
|
-
result.exclusiveMaximum = true;
|
|
282
|
-
}
|
|
283
|
-
break;
|
|
284
|
-
case "nonpositive":
|
|
285
|
-
if (schema.type === "number" || schema.type === "integer") {
|
|
286
|
-
result.maximum = 0;
|
|
287
|
-
}
|
|
288
|
-
break;
|
|
289
|
-
case "int":
|
|
290
|
-
result.type = "integer";
|
|
291
|
-
break;
|
|
292
|
-
case "optional":
|
|
293
|
-
// Handled by isFieldOptional check, no schema modification needed
|
|
294
|
-
break;
|
|
295
|
-
case "nullable":
|
|
296
|
-
result.nullable = true;
|
|
297
|
-
break;
|
|
298
|
-
case "nullish":
|
|
299
|
-
result.nullable = true;
|
|
300
|
-
break;
|
|
301
|
-
case "describe":
|
|
302
|
-
if (args.length > 0 && t.isStringLiteral(args[0])) {
|
|
303
|
-
result.description = args[0].value;
|
|
304
|
-
}
|
|
305
|
-
break;
|
|
306
|
-
case "default":
|
|
307
|
-
if (args.length > 0) {
|
|
308
|
-
// Extract default value
|
|
309
|
-
if (t.isStringLiteral(args[0])) {
|
|
310
|
-
result.default = args[0].value;
|
|
311
|
-
}
|
|
312
|
-
else if (t.isNumericLiteral(args[0])) {
|
|
313
|
-
result.default = args[0].value;
|
|
314
|
-
}
|
|
315
|
-
else if (t.isBooleanLiteral(args[0])) {
|
|
316
|
-
result.default = args[0].value;
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
break;
|
|
320
|
-
}
|
|
321
|
-
return result;
|
|
322
|
-
}
|
|
323
|
-
/**
|
|
324
|
-
* Check if a function name is a drizzle-zod helper
|
|
325
|
-
*/
|
|
326
|
-
static isDrizzleZodHelper(name) {
|
|
327
|
-
return this.DRIZZLE_ZOD_HELPERS.includes(name);
|
|
328
|
-
}
|
|
329
|
-
}
|
package/dist/lib/logger.js
DELETED
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
class Logger {
|
|
2
|
-
config = null;
|
|
3
|
-
init(config) {
|
|
4
|
-
this.config = config;
|
|
5
|
-
}
|
|
6
|
-
getCallerInfo() {
|
|
7
|
-
const stack = new Error().stack;
|
|
8
|
-
if (!stack)
|
|
9
|
-
return 'Unknown';
|
|
10
|
-
const lines = stack.split('\n');
|
|
11
|
-
// Skip: Error, getCallerInfo, log/warn/error
|
|
12
|
-
const callerLine = lines[3] || lines[2];
|
|
13
|
-
// Extract class/function name
|
|
14
|
-
const match = callerLine.match(/at (\w+)\.(\w+)|at (\w+)/);
|
|
15
|
-
if (match) {
|
|
16
|
-
return match[1] || match[3] || 'Unknown';
|
|
17
|
-
}
|
|
18
|
-
return 'Unknown';
|
|
19
|
-
}
|
|
20
|
-
log(message, ...args) {
|
|
21
|
-
const source = this.getCallerInfo();
|
|
22
|
-
console.log(`[${source}] ${message}`, ...args);
|
|
23
|
-
}
|
|
24
|
-
warn(message, ...args) {
|
|
25
|
-
const source = this.getCallerInfo();
|
|
26
|
-
console.warn(`[${source}] ${message}`, ...args);
|
|
27
|
-
}
|
|
28
|
-
error(message, ...args) {
|
|
29
|
-
const source = this.getCallerInfo();
|
|
30
|
-
console.error(`[${source}] ${message}`, ...args);
|
|
31
|
-
}
|
|
32
|
-
debug(message, ...args) {
|
|
33
|
-
if (this.config?.debug) {
|
|
34
|
-
const source = this.getCallerInfo();
|
|
35
|
-
console.log(`[${source}] ${message}`, ...args);
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
export const logger = new Logger();
|