czh-api 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.
@@ -0,0 +1,363 @@
1
+ import { OpenAPI, OpenAPIV3 } from 'openapi-types';
2
+
3
+ export interface Endpoint {
4
+ functionName: string;
5
+ description: string;
6
+ method: string;
7
+ path: string;
8
+ // For query and path parameters
9
+ requestParamsTypeName?: string;
10
+ // For request body
11
+ requestBodyTypeName?: string;
12
+ responseTypeName: string;
13
+ // List of types used to generate imports
14
+ referencedTypes: string[];
15
+ hasParams: boolean;
16
+ hasData: boolean;
17
+ contentType?: string;
18
+ jsdocParams: Array<{ name: string; type?: string; description?: string; required?: boolean }>;
19
+ }
20
+
21
+ export interface Module {
22
+ name: string;
23
+ endpoints: Endpoint[];
24
+ schemas: { [typeName: string]: OpenAPIV3.SchemaObject };
25
+ description?: string;
26
+ }
27
+
28
+ export type Modules = { [moduleName: string]: Module };
29
+
30
+ function isReferenceObject(obj: any): obj is OpenAPIV3.ReferenceObject {
31
+ return obj && typeof obj.$ref === 'string';
32
+ }
33
+
34
+ function getSchemaName(ref: string): string {
35
+ return ref.split('/').pop() || '';
36
+ }
37
+
38
+ function addSchemaWithDependencies(name: string, module: Module, allSchemas: { [key: string]: any }) {
39
+ if (module.schemas[name]) {
40
+ return; // Already added
41
+ }
42
+
43
+ const schema = allSchemas[name];
44
+ if (!schema) {
45
+ return; // Schema not found
46
+ }
47
+
48
+ module.schemas[name] = schema as OpenAPIV3.SchemaObject;
49
+
50
+ // Recursively add dependencies
51
+ JSON.stringify(schema, (key, value) => {
52
+ if (key === '$ref' && typeof value === 'string') {
53
+ const depName = getSchemaName(value);
54
+ addSchemaWithDependencies(depName, module, allSchemas);
55
+ }
56
+ return value;
57
+ });
58
+ }
59
+
60
+ function getModuleName(path: string): string {
61
+ const parts = path.split('/').filter(p => p && !p.startsWith('{'));
62
+ return parts[0] || 'default';
63
+ }
64
+
65
+ function extractJsdocParamsFromSchema(schema: OpenAPIV3.SchemaObject | OpenAPIV3.ReferenceObject, allSchemas: { [key: string]: any }): Required<Endpoint>['jsdocParams'] {
66
+ const params: Required<Endpoint>['jsdocParams'] = [];
67
+ if (!schema) return params;
68
+
69
+ let targetSchema: OpenAPIV3.SchemaObject;
70
+ if (isReferenceObject(schema)) {
71
+ const schemaName = getSchemaName(schema.$ref);
72
+ targetSchema = allSchemas[schemaName] as OpenAPIV3.SchemaObject;
73
+ } else {
74
+ targetSchema = schema;
75
+ }
76
+
77
+ if (targetSchema?.properties) {
78
+ for (const propName in targetSchema.properties) {
79
+ const prop = targetSchema.properties[propName] as OpenAPIV3.SchemaObject;
80
+
81
+ // 调试: 打印出当前处理的属性
82
+ // console.log(`正在处理属性: ${propName}`, '值为:', prop);
83
+
84
+ params.push({
85
+ name: propName,
86
+ type: prop?.type || 'any',
87
+ description: prop?.description || '',
88
+ required: targetSchema.required?.includes(propName)
89
+ });
90
+ }
91
+ }
92
+ return params;
93
+ }
94
+
95
+ function getModuleNameFromPackage(operation: OpenAPIV3.OperationObject): string | null {
96
+ const xPackage = (operation as any)['x-package'];
97
+ if (typeof xPackage === 'string' && xPackage.length > 0) {
98
+ const parts = xPackage.split('.');
99
+ const lastPart = parts[parts.length - 1];
100
+ // "Controller" is 10 characters
101
+ if (lastPart && lastPart.endsWith('Controller') && lastPart.length > 10) {
102
+ const rawName = lastPart.slice(0, -10);
103
+ return rawName.charAt(0).toLowerCase() + rawName.slice(1);
104
+ }
105
+ }
106
+ return null;
107
+ }
108
+
109
+ /**
110
+ * Processes the validated OpenAPI document into a structured format for code generation.
111
+ * @param api The bundled OpenAPI document.
112
+ * @returns A structured representation of modules and their endpoints.
113
+ */
114
+ export const processApi = (api: OpenAPI.Document, excludePaths: string[] = []): Modules => {
115
+ const modules: Modules = {};
116
+ const allSchemas = (api as OpenAPIV3.Document).components?.schemas || (api as any).definitions ||{};
117
+
118
+ const moduleFunctionNames: { [moduleName: string]: Set<string> } = {};
119
+
120
+ for (const path in api.paths) {
121
+ if (excludePaths.some(exclude => path.startsWith(exclude))) {
122
+ continue;
123
+ }
124
+
125
+ const pathItem = api.paths[path] as OpenAPIV3.PathItemObject;
126
+
127
+ for (const method in pathItem) {
128
+ // Filter for actual HTTP methods
129
+ if (!['get', 'post', 'put', 'delete', 'patch', 'options', 'head'].includes(method)) {
130
+ continue;
131
+ }
132
+
133
+ const operation = pathItem[method as keyof OpenAPIV3.PathItemObject] as OpenAPIV3.OperationObject;
134
+ if (!operation.tags) continue;
135
+
136
+ const moduleNameFromPackage = getModuleNameFromPackage(operation);
137
+ const moduleName = moduleNameFromPackage || getModuleName(path);
138
+
139
+ if (!modules[moduleName]) {
140
+ modules[moduleName] = {
141
+ name: moduleName,
142
+ endpoints: [],
143
+ schemas: {},
144
+ description: (operation.tags && operation.tags.length > 0) ? operation.tags[0] : '',
145
+ };
146
+ moduleFunctionNames[moduleName] = new Set();
147
+ }
148
+
149
+ // Generate function name from method and path
150
+ const pathParts = path.split('/').filter(p => p && !p.startsWith('{'));
151
+ const capitalizedPathParts = pathParts.map(part =>
152
+ part.split(/[._-]/)
153
+ .map(subPart => subPart.charAt(0).toUpperCase() + subPart.slice(1))
154
+ .join('')
155
+ );
156
+ const functionNameBase = `${method}${capitalizedPathParts.join('')}`;
157
+
158
+ let functionName = functionNameBase;
159
+ let counter = 1;
160
+ while (moduleFunctionNames[moduleName].has(functionName)) {
161
+ functionName = `${functionNameBase}_${counter}`;
162
+ counter++;
163
+ }
164
+ moduleFunctionNames[moduleName].add(functionName);
165
+
166
+ const referencedTypes: string[] = [];
167
+
168
+ let requestParamsTypeName: string | undefined = undefined;
169
+ let requestBodyTypeName: string | undefined = undefined;
170
+ let contentType: string | undefined = undefined;
171
+ const jsdocParams: Required<Endpoint>['jsdocParams'] = [];
172
+
173
+ // --- FormData Detection ---
174
+ let isFormData = false;
175
+ // Method 1: Check requestBody
176
+ if (operation.requestBody && !isReferenceObject(operation.requestBody)) {
177
+ if (operation.requestBody.content?.['multipart/form-data']) {
178
+ isFormData = true;
179
+ }
180
+ }
181
+ // Method 2: Check for binary formats in parameters (non-standard but common)
182
+ if (!isFormData && operation.parameters) {
183
+ if (operation.parameters.some(p => {
184
+ if (isReferenceObject(p)) return false;
185
+ // Treat as FormData if in: 'formData' is explicitly set
186
+ if (p.in === 'formData') return true;
187
+
188
+ const schema = p.schema as OpenAPIV3.SchemaObject;
189
+ // Treat as FormData if format is binary/byte (single file)
190
+ if (schema?.format === 'binary' || schema?.format === 'byte') return true;
191
+
192
+ // Treat as FormData if it's an array of binary/byte (multiple files)
193
+ if (schema?.type === 'array' && ((schema.items as OpenAPIV3.SchemaObject)?.format === 'binary' || (schema.items as OpenAPIV3.SchemaObject)?.format === 'byte')) {
194
+ return true;
195
+ }
196
+
197
+ return false;
198
+ })) {
199
+ isFormData = true;
200
+ }
201
+ }
202
+ if (isFormData) {
203
+ contentType = 'multipart/form-data';
204
+ }
205
+ // --- End FormData Detection ---
206
+
207
+ // Handle All Parameters
208
+ if (operation.parameters) {
209
+ const paramsSchema: OpenAPIV3.SchemaObject = {
210
+ type: 'object',
211
+ properties: {},
212
+ required: [],
213
+ };
214
+ // For FormData, we will build a schema for the body from query/formData parameters
215
+ const formDataSchema: OpenAPIV3.SchemaObject = {
216
+ type: 'object',
217
+ properties: {},
218
+ required: [],
219
+ };
220
+
221
+ for (const param of operation.parameters) {
222
+ if (!isReferenceObject(param)) {
223
+ const paramToAdd = {
224
+ name: param.name,
225
+ description: param.description || '',
226
+ required: param.required,
227
+ type: (param.schema as any)?.type || 'any'
228
+ };
229
+
230
+ // If the parameter's schema is a reference, expand its properties
231
+ if (param.in === 'query' && param.schema && isReferenceObject(param.schema)) {
232
+ const schemaName = getSchemaName(param.schema.$ref);
233
+ const refSchema = allSchemas[schemaName] as OpenAPIV3.SchemaObject;
234
+ if (refSchema && refSchema.properties) {
235
+ if(!paramsSchema.properties) paramsSchema.properties = {};
236
+ Object.assign(paramsSchema.properties, refSchema.properties);
237
+ if (refSchema.required) {
238
+ if(!paramsSchema.required) paramsSchema.required = [];
239
+ paramsSchema.required.push(...refSchema.required);
240
+ }
241
+ }
242
+ } else if (isFormData && (param.in === 'query' || param.in === 'formData')) {
243
+ // Collect params for the FormData body type
244
+ if(!formDataSchema.properties) formDataSchema.properties = {};
245
+ formDataSchema.properties[param.name] = param.schema as OpenAPIV3.SchemaObject;
246
+ if(param.required) {
247
+ if(!formDataSchema.required) formDataSchema.required = [];
248
+ formDataSchema.required.push(param.name);
249
+ }
250
+ } else if (param.in === 'path' || param.in === 'query') {
251
+ // Always collect path params
252
+ if(!paramsSchema.properties) paramsSchema.properties = {};
253
+ paramsSchema.properties[param.name] = param.schema as OpenAPIV3.SchemaObject;
254
+ if(param.required) {
255
+ if(!paramsSchema.required) paramsSchema.required = [];
256
+ paramsSchema.required.push(param.name);
257
+ }
258
+ }
259
+ }
260
+ }
261
+
262
+ if (isFormData && Object.keys(formDataSchema.properties || {}).length > 0) {
263
+ const typeName = `${functionName.charAt(0).toUpperCase() + functionName.slice(1)}Data`;
264
+ requestBodyTypeName = typeName;
265
+ modules[moduleName].schemas[typeName] = formDataSchema;
266
+ referencedTypes.push(typeName);
267
+ jsdocParams.push(...extractJsdocParamsFromSchema(formDataSchema, allSchemas));
268
+ }
269
+
270
+ if (Object.keys(paramsSchema.properties || {}).length > 0) {
271
+ requestParamsTypeName = `${functionName.charAt(0).toUpperCase() + functionName.slice(1)}Params`;
272
+ modules[moduleName].schemas[requestParamsTypeName] = paramsSchema;
273
+ referencedTypes.push(requestParamsTypeName);
274
+ jsdocParams.push(...extractJsdocParamsFromSchema(paramsSchema, allSchemas));
275
+ }
276
+ }
277
+
278
+ // Handle standard Request Body
279
+ if (operation.requestBody && !isReferenceObject(operation.requestBody) && !isFormData) {
280
+ const requestBody = operation.requestBody as OpenAPIV3.RequestBodyObject;
281
+ const jsonContent = requestBody.content?.['application/json'];
282
+
283
+ if (jsonContent?.schema) {
284
+ if (isReferenceObject(jsonContent.schema)) {
285
+ const name = getSchemaName(jsonContent.schema.$ref);
286
+ requestBodyTypeName = name;
287
+ addSchemaWithDependencies(name, modules[moduleName], allSchemas);
288
+ referencedTypes.push(name);
289
+ jsdocParams.push(...extractJsdocParamsFromSchema(jsonContent.schema, allSchemas));
290
+ } else {
291
+ // Handle inline schema (including arrays)
292
+ const schema = jsonContent.schema as OpenAPIV3.SchemaObject;
293
+ if (schema.type === 'array' || schema.type === 'object') {
294
+ const typeName = `${functionName.charAt(0).toUpperCase() + functionName.slice(1)}Data`;
295
+ requestBodyTypeName = typeName;
296
+ modules[moduleName].schemas[typeName] = schema;
297
+ referencedTypes.push(typeName);
298
+
299
+ // For arrays, extract jsdoc params from array items
300
+ if (schema.type === 'array' && schema.items) {
301
+ if (isReferenceObject(schema.items)) {
302
+ const itemSchemaName = getSchemaName(schema.items.$ref);
303
+ addSchemaWithDependencies(itemSchemaName, modules[moduleName], allSchemas);
304
+ } else {
305
+ jsdocParams.push(...extractJsdocParamsFromSchema(schema.items as OpenAPIV3.SchemaObject, allSchemas));
306
+ }
307
+ } else if (schema.type === 'object') {
308
+ jsdocParams.push(...extractJsdocParamsFromSchema(schema, allSchemas));
309
+ }
310
+ }
311
+ }
312
+ }
313
+ } else if (isFormData && operation.requestBody && !isReferenceObject(operation.requestBody) && !requestBodyTypeName) {
314
+ // Handle cases where FormData is defined in requestBody but we haven't processed it yet
315
+ const formDataContent = operation.requestBody.content?.['multipart/form-data'];
316
+ if (formDataContent?.schema) {
317
+ const typeName = `${functionName.charAt(0).toUpperCase() + functionName.slice(1)}Data`;
318
+ requestBodyTypeName = typeName;
319
+ modules[moduleName].schemas[typeName] = formDataContent.schema as OpenAPIV3.SchemaObject;
320
+ referencedTypes.push(typeName);
321
+ jsdocParams.push(...extractJsdocParamsFromSchema(formDataContent.schema, allSchemas));
322
+ }
323
+ }
324
+
325
+ // Handle Response Body
326
+ let responseTypeName = 'void';
327
+ const successResponse = operation.responses['200'] as OpenAPIV3.ResponseObject;
328
+ if(successResponse) {
329
+ const mediaType = successResponse.content?.['*/*'] || successResponse.content?.['application/json'];
330
+ const schema = mediaType?.schema;
331
+
332
+ if (schema && isReferenceObject(schema)) {
333
+ const name = getSchemaName(schema.$ref);
334
+ responseTypeName = name;
335
+ addSchemaWithDependencies(name, modules[moduleName], allSchemas);
336
+ referencedTypes.push(name);
337
+ }
338
+ }
339
+
340
+ // Convert path to template literal string for path parameters
341
+ const urlTemplate = path.replace(/\{(\w+)\}/g, '${params.$1}');
342
+
343
+ const endpoint: Endpoint = {
344
+ functionName,
345
+ description: operation.summary || operation.description || '',
346
+ method,
347
+ path: urlTemplate,
348
+ requestParamsTypeName,
349
+ requestBodyTypeName,
350
+ responseTypeName,
351
+ referencedTypes: [...new Set(referencedTypes)],
352
+ hasParams: !!requestParamsTypeName,
353
+ hasData: !!requestBodyTypeName,
354
+ contentType,
355
+ jsdocParams,
356
+ };
357
+
358
+ modules[moduleName].endpoints.push(endpoint);
359
+ }
360
+ }
361
+
362
+ return modules;
363
+ };
package/src/index.ts ADDED
@@ -0,0 +1,39 @@
1
+ #!/usr/bin/env node
2
+ /*
3
+ * @description:
4
+ * @Author: czh
5
+ * @Date: 2025-07-02 10:39:34
6
+ * @LastEditors: Czh
7
+ * @LastEditTime: 2025-07-02 10:39:34
8
+ */
9
+ import { Command } from 'commander';
10
+ import { handleInit } from './commands/init';
11
+ import { handleBuild } from './commands/build';
12
+ import path from 'path';
13
+ import fs from 'fs';
14
+
15
+ const program = new Command();
16
+
17
+ // Read package.json to get the version
18
+ const packageJson = JSON.parse(fs.readFileSync(path.join(__dirname, '../package.json'), 'utf-8'));
19
+
20
+ program
21
+ .name('czh-api')
22
+ .description('A CLI tool to generate API code from Swagger/OpenAPI documents.')
23
+ .version(packageJson.version, '-v, --version', 'output the current version');
24
+
25
+ program
26
+ .command('init')
27
+ .description('Initialize czh-api and create a config file and templates.')
28
+ .action(handleInit);
29
+
30
+ program
31
+ .command('build')
32
+ .description('Generate API code based on the configuration file.')
33
+ .action(handleBuild);
34
+
35
+ program.parse(process.argv);
36
+
37
+ if (!process.argv.slice(2).length) {
38
+ program.outputHelp();
39
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * @description {{description}}
3
+ {{#if jsdocParams}}
4
+ {{#each jsdocParams}}
5
+ * @param { {{this.type}} } {{#unless this.required}}[{{/unless}}{{this.name}}{{#unless this.required}}]{{/unless}} - {{this.description}}
6
+ {{/each}}
7
+ {{/if}}
8
+ */
9
+ export const {{functionName}} = ({{#if hasParams}}params: {{requestParamsTypeName}}{{/if}}{{#if hasData}}{{#if hasParams}}, {{/if}}data: {{requestBodyTypeName}}{{/if}}) => {
10
+ return http.request<{{responseTypeName}}>({
11
+ url: `{{path}}`,
12
+ method: '{{method}}',
13
+ {{#if hasParams}}
14
+ params,
15
+ {{/if}}
16
+ {{#if hasData}}
17
+ data,
18
+ {{/if}}
19
+ {{#if contentType}}
20
+ headers: { 'Content-Type': '{{contentType}}' },
21
+ {{/if}}
22
+ });
23
+ };
@@ -0,0 +1,3 @@
1
+ {{#each files}}
2
+ export * from './{{this}}';
3
+ {{/each}}
@@ -0,0 +1 @@
1
+ {{{content}}}
package/tsconfig.json ADDED
@@ -0,0 +1,16 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES6",
4
+ "module": "CommonJS",
5
+ "outDir": "./dist",
6
+ "rootDir": "./src",
7
+ "strict": true,
8
+ "esModuleInterop": true,
9
+ "skipLibCheck": true,
10
+ "forceConsistentCasingInFileNames": true,
11
+ "moduleResolution": "node",
12
+ "resolveJsonModule": true
13
+ },
14
+ "include": ["src/**/*.ts"],
15
+ "exclude": ["node_modules", "src/api", "czh-api.config.json", "czh-api-template"]
16
+ }