From f271813f14c97b85e311d1d073d2db2a18e56551 Mon Sep 17 00:00:00 2001 From: hitchhiker Date: Thu, 17 Apr 2025 15:45:43 +0200 Subject: [PATCH] Documentation updates --- packages/api-generator/.gitignore | 4 + packages/api-generator/.npmignore | 4 + packages/api-generator/README.md | 82 +++++++++++ packages/api-generator/package.json | 33 +++++ packages/api-generator/src/cli.ts | 41 ++++++ packages/api-generator/src/index.ts | 195 +++++++++++++++++++++++++++ packages/api-generator/tsconfig.json | 15 +++ 7 files changed, 374 insertions(+) create mode 100644 packages/api-generator/.gitignore create mode 100644 packages/api-generator/.npmignore create mode 100644 packages/api-generator/README.md create mode 100644 packages/api-generator/package.json create mode 100644 packages/api-generator/src/cli.ts create mode 100644 packages/api-generator/src/index.ts create mode 100644 packages/api-generator/tsconfig.json diff --git a/packages/api-generator/.gitignore b/packages/api-generator/.gitignore new file mode 100644 index 0000000..dd6e803 --- /dev/null +++ b/packages/api-generator/.gitignore @@ -0,0 +1,4 @@ +node_modules/ +dist/ +*.log +.DS_Store diff --git a/packages/api-generator/.npmignore b/packages/api-generator/.npmignore new file mode 100644 index 0000000..08b03c8 --- /dev/null +++ b/packages/api-generator/.npmignore @@ -0,0 +1,4 @@ +src/ +tsconfig.json +.gitignore +node_modules/ diff --git a/packages/api-generator/README.md b/packages/api-generator/README.md new file mode 100644 index 0000000..ad2e843 --- /dev/null +++ b/packages/api-generator/README.md @@ -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. diff --git a/packages/api-generator/package.json b/packages/api-generator/package.json new file mode 100644 index 0000000..61ba3ba --- /dev/null +++ b/packages/api-generator/package.json @@ -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/" + } +} diff --git a/packages/api-generator/src/cli.ts b/packages/api-generator/src/cli.ts new file mode 100644 index 0000000..f1e80e4 --- /dev/null +++ b/packages/api-generator/src/cli.ts @@ -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 [--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(); diff --git a/packages/api-generator/src/index.ts b/packages/api-generator/src/index.ts new file mode 100644 index 0000000..a221217 --- /dev/null +++ b/packages/api-generator/src/index.ts @@ -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 { + 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 { + 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; + } +} diff --git a/packages/api-generator/tsconfig.json b/packages/api-generator/tsconfig.json new file mode 100644 index 0000000..8c8522f --- /dev/null +++ b/packages/api-generator/tsconfig.json @@ -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"] +}