Documentation updates
This commit is contained in:
parent
ba6e2bd3ff
commit
f271813f14
4
packages/api-generator/.gitignore
vendored
Normal file
4
packages/api-generator/.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
node_modules/
|
||||||
|
dist/
|
||||||
|
*.log
|
||||||
|
.DS_Store
|
4
packages/api-generator/.npmignore
Normal file
4
packages/api-generator/.npmignore
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
src/
|
||||||
|
tsconfig.json
|
||||||
|
.gitignore
|
||||||
|
node_modules/
|
82
packages/api-generator/README.md
Normal file
82
packages/api-generator/README.md
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
# G1 API Generator
|
||||||
|
|
||||||
|
A command-line tool for generating TypeScript API clients from OpenAPI schemas.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Downloads OpenAPI schema from a specified URL
|
||||||
|
- Generates TypeScript interfaces and API client code
|
||||||
|
- Post-processes generated code to fix common issues
|
||||||
|
- Supports HTTPS with option to skip TLS verification
|
||||||
|
- Detects and warns about duplicate DTOs in the schema
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install from G1 package registry
|
||||||
|
npm install @g1/api-generator --save-dev
|
||||||
|
|
||||||
|
# Or with pnpm
|
||||||
|
pnpm add @g1/api-generator -D
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Command Line
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Basic usage
|
||||||
|
g1-api-generator https://your-api-server/openapi/schema.json ./src/lib/api
|
||||||
|
|
||||||
|
# Skip TLS verification (useful for local development with self-signed certificates)
|
||||||
|
g1-api-generator https://localhost:7205/openapi/schema.json ./src/lib/api --skip-tls-verify
|
||||||
|
```
|
||||||
|
|
||||||
|
### In package.json scripts
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"scripts": {
|
||||||
|
"api:generate": "g1-api-generator https://localhost:7205/openapi/schema.json src/lib/api"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Programmatic Usage
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { generateApiClient } from '@g1/api-generator';
|
||||||
|
|
||||||
|
async function generateApi() {
|
||||||
|
await generateApiClient({
|
||||||
|
schemaUrl: 'https://your-api-server/openapi/schema.json',
|
||||||
|
outputDir: './src/lib/api',
|
||||||
|
skipTlsVerify: true, // Optional, default: false
|
||||||
|
runPostProcessing: true // Optional, default: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
generateApi().catch(console.error);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Generated Code Structure
|
||||||
|
|
||||||
|
The generated code is organized as follows:
|
||||||
|
|
||||||
|
- `models/` - TypeScript interfaces for API models
|
||||||
|
- `services/` - API client services for making requests
|
||||||
|
- `core/` - Core functionality for the API client
|
||||||
|
- `open-api.json` - The downloaded and processed OpenAPI schema
|
||||||
|
|
||||||
|
## Post-Processing
|
||||||
|
|
||||||
|
The tool automatically applies the following post-processing to the generated code:
|
||||||
|
|
||||||
|
1. Removes import statements for `void` type
|
||||||
|
2. Replaces usages of `void` as a type with `any`
|
||||||
|
3. Detects and replaces self-referencing DTOs with `any`
|
||||||
|
4. Adds auto-generated comments to files
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
UNLICENSED - Private package for Generation One use only.
|
33
packages/api-generator/package.json
Normal file
33
packages/api-generator/package.json
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
"name": "@g1/api-generator",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"description": "API client generator for OpenAPI schemas",
|
||||||
|
"main": "dist/index.js",
|
||||||
|
"bin": {
|
||||||
|
"g1-api-generator": "./dist/cli.js"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsc",
|
||||||
|
"prepublishOnly": "npm run build"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"openapi",
|
||||||
|
"api",
|
||||||
|
"generator",
|
||||||
|
"typescript"
|
||||||
|
],
|
||||||
|
"author": "Generation One",
|
||||||
|
"license": "UNLICENSED",
|
||||||
|
"private": true,
|
||||||
|
"dependencies": {
|
||||||
|
"axios": "^1.8.4",
|
||||||
|
"openapi-typescript-codegen": "^0.29.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^20",
|
||||||
|
"typescript": "^5"
|
||||||
|
},
|
||||||
|
"publishConfig": {
|
||||||
|
"registry": "https://git.generation.one/api/packages/GenerationOne/npm/"
|
||||||
|
}
|
||||||
|
}
|
41
packages/api-generator/src/cli.ts
Normal file
41
packages/api-generator/src/cli.ts
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
import { generateApiClient } from './index';
|
||||||
|
|
||||||
|
// Parse command line arguments
|
||||||
|
function parseArgs(): { schemaUrl: string; outputDir: string; skipTlsVerify: boolean } {
|
||||||
|
const args = process.argv.slice(2);
|
||||||
|
|
||||||
|
if (args.length < 2) {
|
||||||
|
console.error('Usage: g1-api-generator <schemaUrl> <outputDir> [--skip-tls-verify]');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const schemaUrl = args[0];
|
||||||
|
const outputDir = args[1];
|
||||||
|
const skipTlsVerify = args.includes('--skip-tls-verify');
|
||||||
|
|
||||||
|
return { schemaUrl, outputDir, skipTlsVerify };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Main CLI function
|
||||||
|
async function main() {
|
||||||
|
try {
|
||||||
|
const { schemaUrl, outputDir, skipTlsVerify } = parseArgs();
|
||||||
|
|
||||||
|
await generateApiClient({
|
||||||
|
schemaUrl,
|
||||||
|
outputDir,
|
||||||
|
skipTlsVerify,
|
||||||
|
runPostProcessing: true
|
||||||
|
});
|
||||||
|
|
||||||
|
process.exit(0);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error:', error instanceof Error ? error.message : error);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run the CLI
|
||||||
|
main();
|
195
packages/api-generator/src/index.ts
Normal file
195
packages/api-generator/src/index.ts
Normal file
@ -0,0 +1,195 @@
|
|||||||
|
import axios from 'axios';
|
||||||
|
import fs from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
import { execSync } from 'child_process';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Options for the API generator
|
||||||
|
*/
|
||||||
|
export interface ApiGeneratorOptions {
|
||||||
|
/**
|
||||||
|
* URL to the OpenAPI schema
|
||||||
|
*/
|
||||||
|
schemaUrl: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Output directory for the generated API client
|
||||||
|
*/
|
||||||
|
outputDir: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether to skip TLS verification (useful for local development)
|
||||||
|
* @default false
|
||||||
|
*/
|
||||||
|
skipTlsVerify?: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether to run post-processing on the generated files
|
||||||
|
* @default true
|
||||||
|
*/
|
||||||
|
runPostProcessing?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Downloads the OpenAPI schema from the specified URL
|
||||||
|
*/
|
||||||
|
export async function downloadSchema(options: ApiGeneratorOptions): Promise<string> {
|
||||||
|
const { schemaUrl, outputDir, skipTlsVerify = false } = options;
|
||||||
|
const outPath = path.join(outputDir, 'open-api.json');
|
||||||
|
|
||||||
|
console.log(`Downloading schema from ${schemaUrl}...`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Create output directory if it doesn't exist
|
||||||
|
fs.mkdirSync(path.dirname(outPath), { recursive: true });
|
||||||
|
|
||||||
|
// Download schema
|
||||||
|
const axiosConfig = skipTlsVerify ? { httpsAgent: new (require('https').Agent)({ rejectUnauthorized: false }) } : {};
|
||||||
|
const response = await axios.get(schemaUrl, axiosConfig);
|
||||||
|
|
||||||
|
// Process schema
|
||||||
|
const schema = response.data;
|
||||||
|
|
||||||
|
// Remove servers array to avoid hardcoded URLs
|
||||||
|
if (schema.servers) {
|
||||||
|
delete schema.servers;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write schema to file
|
||||||
|
fs.writeFileSync(outPath, JSON.stringify(schema, null, 2));
|
||||||
|
|
||||||
|
// Check for duplicate DTOs
|
||||||
|
console.log('Checking for duplicate DTOs in schema...');
|
||||||
|
const schemaContent = fs.readFileSync(outPath, 'utf8');
|
||||||
|
const duplicateDtoRefRegex = /"\$ref":\s*"#\/components\/schemas\/\w+2"/;
|
||||||
|
|
||||||
|
if (duplicateDtoRefRegex.test(schemaContent)) {
|
||||||
|
throw new Error(
|
||||||
|
"The schema contains duplicate DTOs (pattern: $ref: '#/components/schemas/...Name2'). " +
|
||||||
|
"This is a transient error, usually due to a bug in OpenAPI generators. " +
|
||||||
|
"Try restarting the API server."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Schema downloaded and processed successfully.');
|
||||||
|
return outPath;
|
||||||
|
} catch (error) {
|
||||||
|
if (axios.isAxiosError(error)) {
|
||||||
|
throw new Error(`Failed to download schema: ${error.message}`);
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates the API client using openapi-typescript-codegen
|
||||||
|
*/
|
||||||
|
export function generateClient(schemaPath: string, outputDir: string): void {
|
||||||
|
console.log('Generating API client...');
|
||||||
|
|
||||||
|
const codegenCmd = [
|
||||||
|
'npx openapi-typescript-codegen',
|
||||||
|
'--input', schemaPath,
|
||||||
|
'--output', outputDir,
|
||||||
|
'--client', 'axios',
|
||||||
|
'--useOptions'
|
||||||
|
].join(' ');
|
||||||
|
|
||||||
|
execSync(codegenCmd, { stdio: 'inherit' });
|
||||||
|
|
||||||
|
// Add auto-generated comment to each file
|
||||||
|
const files = fs.readdirSync(outputDir);
|
||||||
|
for (const file of files) {
|
||||||
|
const filePath = path.join(outputDir, file);
|
||||||
|
if (file.startsWith('openapi') && file.endsWith('.ts')) {
|
||||||
|
const content = fs.readFileSync(filePath, 'utf8');
|
||||||
|
const modifiedContent = `/* AUTOGENERATED - DO NOT EDIT */\n\n${content}`;
|
||||||
|
fs.writeFileSync(filePath, modifiedContent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('API client generated successfully.');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Post-processes the generated API client files
|
||||||
|
*/
|
||||||
|
export function postProcessFiles(outputDir: string): void {
|
||||||
|
console.log('Post-processing generated files...');
|
||||||
|
|
||||||
|
const modelsDir = path.join(outputDir, 'models');
|
||||||
|
const servicesDir = path.join(outputDir, 'services');
|
||||||
|
|
||||||
|
const directoriesToProcess = [
|
||||||
|
modelsDir,
|
||||||
|
servicesDir
|
||||||
|
];
|
||||||
|
|
||||||
|
function processFile(filePath: string): void {
|
||||||
|
let fileContent = fs.readFileSync(filePath, 'utf8');
|
||||||
|
|
||||||
|
// Remove import statements for 'void'
|
||||||
|
fileContent = fileContent.replace(/import type \{ void \} from '.*';/g, '');
|
||||||
|
|
||||||
|
// Replace usages of 'void' as a type with 'any'
|
||||||
|
fileContent = fileContent.replace(/: void,/g, ': any,');
|
||||||
|
fileContent = fileContent.replace(/: void \{/g, ': any {');
|
||||||
|
fileContent = fileContent.replace(/: void;/g, ': any;');
|
||||||
|
|
||||||
|
// Detect and replace self-referencing DTOs with 'any'
|
||||||
|
const typeDefinitionRegex = /export type (\w+) = (.*?);/gs;
|
||||||
|
let match;
|
||||||
|
while ((match = typeDefinitionRegex.exec(fileContent)) !== null) {
|
||||||
|
const typeName = match[1];
|
||||||
|
const typeDefinition = match[2];
|
||||||
|
if (typeDefinition.includes(typeName)) {
|
||||||
|
const newTypeDefinition = typeDefinition.replace(new RegExp(`\\b${typeName}\\b`, 'g'), 'any');
|
||||||
|
fileContent = fileContent.replace(typeDefinition, newTypeDefinition);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.writeFileSync(filePath, fileContent, 'utf8');
|
||||||
|
}
|
||||||
|
|
||||||
|
function processDirectory(dirPath: string): void {
|
||||||
|
if (!fs.existsSync(dirPath)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const files = fs.readdirSync(dirPath);
|
||||||
|
files.forEach(file => {
|
||||||
|
const filePath = path.join(dirPath, file);
|
||||||
|
const stat = fs.statSync(filePath);
|
||||||
|
if (stat.isDirectory()) {
|
||||||
|
processDirectory(filePath);
|
||||||
|
} else if (path.extname(file) === '.ts') {
|
||||||
|
processFile(filePath);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
directoriesToProcess.forEach(dir => {
|
||||||
|
processDirectory(dir);
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('Post-processing complete.');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main function to generate the API client
|
||||||
|
*/
|
||||||
|
export async function generateApiClient(options: ApiGeneratorOptions): Promise<void> {
|
||||||
|
try {
|
||||||
|
const schemaPath = await downloadSchema(options);
|
||||||
|
generateClient(schemaPath, options.outputDir);
|
||||||
|
|
||||||
|
if (options.runPostProcessing !== false) {
|
||||||
|
postProcessFiles(options.outputDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`API client successfully generated in ${options.outputDir}`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error generating API client:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
15
packages/api-generator/tsconfig.json
Normal file
15
packages/api-generator/tsconfig.json
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2018",
|
||||||
|
"module": "CommonJS",
|
||||||
|
"declaration": true,
|
||||||
|
"outDir": "./dist",
|
||||||
|
"strict": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"resolveJsonModule": true
|
||||||
|
},
|
||||||
|
"include": ["src/**/*"],
|
||||||
|
"exclude": ["node_modules", "dist"]
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user