UNPKG

nextjs-django-client

Version:

A comprehensive, type-safe SDK for seamlessly integrating Next.js 15+ applications with Django REST Framework backends

1,427 lines (1,403 loc) 3.59 MB
#!/usr/bin/env node import { Command } from 'commander'; import { watch } from 'chokidar'; import fs4, { promises, realpathSync, statSync } from 'fs'; import * as path10 from 'path'; import path10__default, { dirname } from 'path'; import { createRequire, builtinModules } from 'module'; import * as url from 'url'; import url__default, { fileURLToPath, pathToFileURL, URL as URL$1 } from 'url'; import * as fs$4 from 'fs/promises'; import fs__default from 'fs/promises'; import process4 from 'process'; import assert2 from 'assert'; import v8 from 'v8'; import { format, inspect } from 'util'; /** * OpenAPI specification parser and validator */ class OpenAPIParser { spec; resolvedRefs = new Map(); constructor(spec) { this.spec = spec; } /** * Parse the OpenAPI specification */ async parse() { this.validateSpec(); await this.resolveReferences(); const operations = this.parseOperations(); const schemas = this.parseSchemas(); const components = this.parseComponents(); return { spec: this.spec, operations, schemas, components, }; } /** * Validate the OpenAPI specification */ validateSpec() { if (!this.spec.openapi) { throw new Error('Missing openapi version'); } if (!this.spec.openapi.startsWith('3.')) { throw new Error(`Unsupported OpenAPI version: ${this.spec.openapi}. Only 3.x is supported.`); } if (!this.spec.info) { throw new Error('Missing info section'); } if (!this.spec.info.title) { throw new Error('Missing info.title'); } if (!this.spec.info.version) { throw new Error('Missing info.version'); } if (!this.spec.paths) { throw new Error('Missing paths section'); } } /** * Resolve all $ref references in the specification */ async resolveReferences() { // This is a simplified implementation // In a full implementation, you would recursively resolve all $ref references // including external references to other files const resolveRef = (obj) => { if (obj && typeof obj === 'object') { if (obj.$ref) { const refPath = obj.$ref; if (refPath.startsWith('#/')) { // Internal reference const path = refPath.substring(2).split('/'); let resolved = this.spec; for (const segment of path) { resolved = resolved[segment]; if (!resolved) { throw new Error(`Invalid reference path: ${refPath}`); } } this.resolvedRefs.set(refPath, resolved); return resolved; } else { // External reference - would need to fetch and resolve throw new Error(`External references not yet supported: ${refPath}`); } } else { // Recursively resolve references in nested objects const resolved = Array.isArray(obj) ? [] : {}; for (const [key, value] of Object.entries(obj)) { resolved[key] = resolveRef(value); } return resolved; } } return obj; }; this.spec = resolveRef(this.spec); } /** * Parse all operations from paths */ parseOperations() { const operations = []; for (const [path, pathItem] of Object.entries(this.spec.paths)) { const methods = ['get', 'post', 'put', 'patch', 'delete', 'head', 'options', 'trace']; for (const method of methods) { const operation = pathItem[method]; if (operation) { operations.push(this.parseOperation(method, path, operation)); } } } return operations; } /** * Parse a single operation */ parseOperation(method, path, operation) { const id = operation.operationId || this.generateOperationId(method, path); const parsedOperation = { id, method: method.toUpperCase(), path, summary: operation.summary || '', description: operation.description || '', tags: operation.tags || [], parameters: this.parseParameters(operation.parameters || []), responses: this.parseResponses(operation.responses), security: this.parseSecurity(operation.security || []), deprecated: operation.deprecated || false, }; if (operation.requestBody) { parsedOperation.requestBody = this.parseRequestBody(operation.requestBody); } return parsedOperation; } /** * Generate operation ID from method and path */ generateOperationId(method, path) { // Convert path like /users/{id}/posts to getUsersPosts const pathParts = path .split('/') .filter(part => part && !part.startsWith('{')) .map(part => part.charAt(0).toUpperCase() + part.slice(1)); return method.toLowerCase() + pathParts.join(''); } /** * Parse operation parameters */ parseParameters(parameters) { return parameters.map(param => { const resolved = this.resolveReference(param); return { name: resolved.name, in: resolved.in, required: resolved.required || resolved.in === 'path', schema: this.parseSchema(resolved.schema || { type: 'string' }), ...(resolved.description && { description: resolved.description }), }; }); } /** * Parse request body */ parseRequestBody(requestBody) { const resolved = this.resolveReference(requestBody); const content = {}; for (const [mediaType, mediaTypeObj] of Object.entries(resolved.content || {})) { content[mediaType] = { schema: this.parseSchema(mediaTypeObj.schema || { type: 'object' }), examples: mediaTypeObj.examples, }; } return { required: resolved.required || false, content, description: resolved.description, }; } /** * Parse operation responses */ parseResponses(responses) { const parsedResponses = []; for (const [statusCode, response] of Object.entries(responses)) { const resolved = this.resolveReference(response); const content = {}; if (resolved.content) { for (const [mediaType, mediaTypeObj] of Object.entries(resolved.content)) { content[mediaType] = { schema: this.parseSchema(mediaTypeObj.schema || { type: 'object' }), examples: mediaTypeObj.examples, }; } } const responseObj = { statusCode, description: resolved.description || '', }; if (Object.keys(content).length > 0) { responseObj.content = content; } if (resolved.headers) { responseObj.headers = this.parseHeaders(resolved.headers); } parsedResponses.push(responseObj); } return parsedResponses; } /** * Parse headers */ parseHeaders(headers) { const parsedHeaders = {}; for (const [name, header] of Object.entries(headers)) { const resolved = this.resolveReference(header); parsedHeaders[name] = { required: resolved.required || false, schema: this.parseSchema(resolved.schema || { type: 'string' }), description: resolved.description, }; } return parsedHeaders; } /** * Parse security requirements */ parseSecurity(security) { const parsedSecurity = []; for (const securityReq of security) { for (const [name, scopes] of Object.entries(securityReq)) { const scheme = this.spec.components?.securitySchemes?.[name]; if (scheme) { const resolved = this.resolveReference(scheme); parsedSecurity.push({ type: resolved.type, name, in: resolved.in, scheme: resolved.scheme, scopes: scopes, }); } } } return parsedSecurity; } /** * Parse all schemas from components */ parseSchemas() { const schemas = []; if (this.spec.components?.schemas) { for (const [name, schema] of Object.entries(this.spec.components.schemas)) { const parsedSchema = this.parseSchema(schema, name); schemas.push(parsedSchema); } } return schemas; } /** * Parse a single schema */ parseSchema(schema, name) { const resolved = this.resolveReference(schema); const parsedSchema = { type: resolved.type || 'any', required: false, // Will be set by parent context nullable: resolved.nullable || false, originalSchema: schema, }; if (name) parsedSchema.name = name; if (resolved.description) parsedSchema.description = resolved.description; if (resolved.properties) parsedSchema.properties = this.parseProperties(resolved.properties); if (resolved.items) parsedSchema.items = this.parseSchema(resolved.items); if (resolved.enum) parsedSchema.enum = resolved.enum; if (resolved.format) parsedSchema.format = resolved.format; if (resolved.pattern) parsedSchema.pattern = resolved.pattern; if (resolved.minimum !== undefined) parsedSchema.minimum = resolved.minimum; if (resolved.maximum !== undefined) parsedSchema.maximum = resolved.maximum; if (resolved.minLength !== undefined) parsedSchema.minLength = resolved.minLength; if (resolved.maxLength !== undefined) parsedSchema.maxLength = resolved.maxLength; if (resolved.minItems !== undefined) parsedSchema.minItems = resolved.minItems; if (resolved.maxItems !== undefined) parsedSchema.maxItems = resolved.maxItems; if (resolved.default !== undefined) parsedSchema.default = resolved.default; if (resolved.example !== undefined) parsedSchema.example = resolved.example; if (this.isReference(schema)) parsedSchema.ref = schema.$ref; return parsedSchema; } /** * Parse object properties */ parseProperties(properties) { const parsedProperties = {}; for (const [name, property] of Object.entries(properties)) { parsedProperties[name] = this.parseSchema(property, name); } return parsedProperties; } /** * Parse components */ parseComponents() { const schemas = {}; const securitySchemes = {}; if (this.spec.components?.schemas) { for (const [name, schema] of Object.entries(this.spec.components.schemas)) { schemas[name] = this.parseSchema(schema, name); } } if (this.spec.components?.securitySchemes) { for (const [name, scheme] of Object.entries(this.spec.components.securitySchemes)) { const resolved = this.resolveReference(scheme); securitySchemes[name] = { type: resolved.type, name, in: resolved.in, scheme: resolved.scheme, scopes: [], }; } } return { schemas, securitySchemes, }; } /** * Check if object is a reference */ isReference(obj) { return obj && typeof obj === 'object' && '$ref' in obj; } /** * Resolve a reference or return the object as-is */ resolveReference(obj) { if (this.isReference(obj)) { const resolved = this.resolvedRefs.get(obj.$ref); if (resolved) { return resolved; } throw new Error(`Unresolved reference: ${obj.$ref}`); } return obj; } } /** * Parse OpenAPI specification from URL, file, or object (CLI version with fs support) * This function is only for CLI usage and includes Node.js fs operations */ async function parseOpenAPISpec(input) { let spec; if (typeof input === 'string') { if (input.startsWith('http')) { // Fetch from URL const response = await fetch(input); if (!response.ok) { throw new Error(`Failed to fetch OpenAPI spec: ${response.statusText}`); } spec = await response.json(); } else { // Load from file (Node.js environment) if (typeof window === 'undefined') { try { const fs = await import('fs'); const path = await import('path'); const fullPath = path.resolve(input); const content = fs.readFileSync(fullPath, 'utf-8'); if (input.endsWith('.yaml') || input.endsWith('.yml')) { // Would need YAML parser throw new Error('YAML parsing not yet implemented. Please provide a JSON file.'); } else { spec = JSON.parse(content); } } catch (error) { if (error instanceof Error && error.message.includes('YAML parsing')) { throw error; } throw new Error(`Failed to load OpenAPI spec from file: ${input}. Make sure the file exists and is valid JSON.`); } } else { throw new Error('File loading not supported in browser environment. Please provide a URL or use the spec object directly.'); } } } else { spec = input; } const parser = new OpenAPIParser(spec); return await parser.parse(); } /** * TypeScript type generator for OpenAPI specifications */ class TypeScriptTypeGenerator { config; generatedTypes = new Set(); constructor(config) { this.config = config; } /** * Generate TypeScript types from parsed OpenAPI spec */ generateTypes(spec) { const imports = this.generateImports(); const baseTypes = this.generateBaseTypes(); const schemaTypes = this.generateSchemaTypes(spec.schemas); const operationTypes = this.generateOperationTypes(spec.operations); const utilityTypes = this.generateUtilityTypes(); return [ this.generateHeader(spec), imports, baseTypes, schemaTypes, operationTypes, utilityTypes, ].filter(Boolean).join('\n\n'); } /** * Generate file header with metadata */ generateHeader(spec) { const { title, version, description } = spec.spec.info; return `/** * Generated TypeScript types for ${title} * Version: ${version} * ${description ? `Description: ${description}` : ''} * * Generated on: ${new Date().toISOString()} * Generator: NextJS Django Client SDK * * DO NOT EDIT - This file is auto-generated */`; } /** * Generate necessary imports */ generateImports() { const imports = [ "// Base types for API operations", "export interface ApiResponse<T = any> {", " data: T;", " status: number;", " statusText: string;", " headers: Record<string, string>;", "}", "", "export interface ApiError {", " message: string;", " status?: number;", " code?: string;", " details?: any;", "}", ]; if (this.config.additionalImports) { imports.unshift(...this.config.additionalImports); } return imports.join('\n'); } /** * Generate base utility types */ generateBaseTypes() { return `// Utility types export type Optional<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>; export type RequiredKeys<T> = { [K in keyof T]-?: {} extends Pick<T, K> ? never : K }[keyof T]; export type OptionalKeys<T> = { [K in keyof T]-?: {} extends Pick<T, K> ? K : never }[keyof T]; // Pagination types export interface PaginatedResponse<T> { count: number; next: string | null; previous: string | null; results: T[]; } // Query parameter types export interface QueryParams { [key: string]: string | number | boolean | string[] | number[] | boolean[] | undefined; }`; } /** * Generate types for all schemas */ generateSchemaTypes(schemas) { const types = []; // Generate component schemas first for (const schema of schemas) { if (schema.name) { types.push(this.generateSchemaType(schema)); } } return types.join('\n\n'); } /** * Generate a single schema type */ generateSchemaType(schema) { const typeName = this.getTypeName(schema.name); if (this.generatedTypes.has(typeName)) { return ''; } this.generatedTypes.add(typeName); const typeDefinition = this.generateTypeDefinition(schema); const description = schema.description ? `/** ${schema.description} */\n` : ''; // Use 'type' for union types (enums), 'interface' for objects const isUnionType = schema.type === 'string' && schema.enum; const keyword = isUnionType ? 'type' : 'interface'; const separator = isUnionType ? ' = ' : ' '; return `${description}export ${keyword} ${typeName}${separator}${typeDefinition}`; } /** * Generate type definition for a schema */ generateTypeDefinition(schema) { switch (schema.type) { case 'object': return this.generateObjectType(schema); case 'array': return this.generateArrayType(schema); case 'string': case 'number': case 'integer': case 'boolean': return this.generatePrimitiveType(schema); default: return '{}'; } } /** * Generate object type definition */ generateObjectType(schema) { if (!schema.properties) { return '{ [key: string]: any; }'; } const properties = []; for (const [propName, propSchema] of Object.entries(schema.properties)) { const isRequired = schema.originalSchema && 'required' in schema.originalSchema && Array.isArray(schema.originalSchema.required) && schema.originalSchema.required.includes(propName); const optional = isRequired ? '' : '?'; const nullable = propSchema.nullable ? ' | null' : ''; const propType = this.getPropertyType(propSchema); const description = propSchema.description ? ` /** ${propSchema.description} */\n` : ''; properties.push(`${description} ${propName}${optional}: ${propType}${nullable};`); } return `{\n${properties.join('\n')}\n}`; } /** * Generate array type definition */ generateArrayType(schema) { if (!schema.items) { return 'any[]'; } const itemType = this.getPropertyType(schema.items); return `${itemType}[]`; } /** * Generate primitive type definition */ generatePrimitiveType(schema) { if (schema.enum) { const enumValues = schema.enum.map(value => typeof value === 'string' ? `'${value}'` : String(value)).join(' | '); return enumValues; } switch (schema.type) { case 'string': return 'string'; case 'number': case 'integer': return 'number'; case 'boolean': return 'boolean'; default: return 'any'; } } /** * Get TypeScript type for a property */ getPropertyType(schema) { if (schema.ref) { // Reference to another schema const refName = schema.ref.split('/').pop(); return this.getTypeName(refName); } switch (schema.type) { case 'object': if (schema.properties) { return this.generateObjectType(schema); } return '{ [key: string]: any; }'; case 'array': return this.generateArrayType(schema); case 'string': case 'number': case 'integer': case 'boolean': return this.generatePrimitiveType(schema); default: return 'any'; } } /** * Generate types for all operations */ generateOperationTypes(operations) { const types = []; for (const operation of operations) { types.push(this.generateOperationType(operation)); } return types.join('\n\n'); } /** * Generate types for a single operation */ generateOperationType(operation) { const operationName = this.getOperationTypeName(operation.id); const requestType = this.generateRequestType(operation); const responseType = this.generateResponseType(operation); const parameterTypes = this.generateParameterTypes(operation); const description = operation.description || operation.summary; const comment = description ? `/** ${description} */\n` : ''; return [ comment + `export namespace ${operationName} {`, parameterTypes, requestType, responseType, '}', ].filter(Boolean).join('\n'); } /** * Generate parameter types for an operation */ generateParameterTypes(operation) { if (operation.parameters.length === 0) { return ''; } const pathParams = operation.parameters.filter(p => p.in === 'path'); const queryParams = operation.parameters.filter(p => p.in === 'query'); const headerParams = operation.parameters.filter(p => p.in === 'header'); const types = []; if (pathParams.length > 0) { types.push(this.generateParameterInterface('PathParams', pathParams)); } if (queryParams.length > 0) { types.push(this.generateParameterInterface('QueryParams', queryParams)); } if (headerParams.length > 0) { types.push(this.generateParameterInterface('HeaderParams', headerParams)); } return types.join('\n\n'); } /** * Generate parameter interface */ generateParameterInterface(name, parameters) { const properties = parameters.map(param => { const optional = param.required ? '' : '?'; const type = this.getPropertyType(param.schema); const description = param.description ? ` /** ${param.description} */\n` : ''; return `${description} ${param.name}${optional}: ${type};`; }); return ` export interface ${name} {\n${properties.join('\n')}\n }`; } /** * Generate request type for an operation */ generateRequestType(operation) { if (!operation.requestBody) { return ''; } const contentTypes = Object.keys(operation.requestBody.content); if (contentTypes.length === 0) { return ''; } // Use the first content type (usually application/json) const contentType = contentTypes[0]; const mediaType = operation.requestBody.content[contentType]; const bodyType = this.getPropertyType(mediaType.schema); return ` export type RequestBody = ${bodyType};`; } /** * Generate response type for an operation */ generateResponseType(operation) { const successResponses = operation.responses.filter(r => r.statusCode.startsWith('2') || r.statusCode === 'default'); if (successResponses.length === 0) { return ' export type Response = void;'; } const responseTypes = successResponses.map(response => { if (!response.content) { return 'void'; } const contentTypes = Object.keys(response.content); if (contentTypes.length === 0) { return 'void'; } const contentType = contentTypes[0]; const mediaType = response.content[contentType]; return this.getPropertyType(mediaType.schema); }); const uniqueTypes = [...new Set(responseTypes)]; const responseType = uniqueTypes.length === 1 ? uniqueTypes[0] : uniqueTypes.join(' | '); return ` export type Response = ${responseType};`; } /** * Generate utility types */ generateUtilityTypes() { return `// Operation utility types export type OperationMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'HEAD' | 'OPTIONS'; export interface OperationConfig { method: OperationMethod; path: string; pathParams?: Record<string, string | number>; queryParams?: QueryParams; headers?: Record<string, string>; body?: any; } // Hook state types export interface UseQueryState<T> { data: T | undefined; error: ApiError | null; isLoading: boolean; isError: boolean; isSuccess: boolean; refetch: () => Promise<void>; } export interface UseMutationState<T, V = any> { data: T | undefined; error: ApiError | null; isLoading: boolean; isError: boolean; isSuccess: boolean; mutate: (variables: V) => Promise<T>; reset: () => void; }`; } /** * Get formatted type name */ getTypeName(name) { const formatted = this.toPascalCase(name); const prefix = this.config.typePrefix || ''; const suffix = this.config.typeSuffix || ''; return `${prefix}${formatted}${suffix}`; } /** * Get formatted operation type name */ getOperationTypeName(operationId) { return this.toPascalCase(operationId); } /** * Convert string to PascalCase */ toPascalCase(str) { return str .replace(/[^a-zA-Z0-9]/g, ' ') .split(' ') .filter(Boolean) .map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()) .join(''); } } /** * API client generator for OpenAPI specifications */ class ApiClientGenerator { config; constructor(config) { this.config = config; } /** * Generate API client from parsed OpenAPI spec */ generateClient(spec) { const className = this.config.clientName || this.getClientName(spec.spec.info.title); const imports = this.generateImports(); const classDefinition = this.generateClassDefinition(className, spec); return [ this.generateHeader(spec), imports, classDefinition, ].join('\n\n'); } /** * Generate file header */ generateHeader(spec) { const { title, version } = spec.spec.info; return `/** * Generated API Client for ${title} * Version: ${version} * * Generated on: ${new Date().toISOString()} * Generator: NextJS Django Client SDK * * DO NOT EDIT - This file is auto-generated */`; } /** * Generate necessary imports */ generateImports() { const imports = [ "import { createDjangoApiClient } from 'nextjs-django-client';", "import type { ApiClient, ApiResponse, ApiError, RequestOptions } from 'nextjs-django-client';", ]; return imports.join('\n'); } /** * Generate main class definition */ generateClassDefinition(className, spec) { const constructor = this.generateConstructor(); const methods = this.generateMethods(spec.operations); const utilities = this.generateUtilities(); return `export class ${className} { private client: ApiClient; ${constructor} ${methods} ${utilities} }`; } /** * Generate constructor */ generateConstructor() { return ` constructor(baseURL: string, options?: { timeout?: number; headers?: Record<string, string> }) { this.client = createDjangoApiClient(baseURL, { timeout: options?.timeout || 30000, defaultHeaders: { 'Content-Type': 'application/json', 'Accept': 'application/json', ...options?.headers, }, }); }`; } /** * Generate all API methods */ generateMethods(operations) { const methods = operations.map(operation => this.generateMethod(operation)); return methods.join('\n\n'); } /** * Generate a single API method */ generateMethod(operation) { const methodName = this.getMethodName(operation.id); const parameters = this.generateMethodParameters(operation); const pathParams = operation.parameters.filter(p => p.in === 'path'); const pathConstruction = this.generatePathConstruction(operation.path, pathParams); const requestConfig = this.generateRequestConfig(operation); const returnType = this.generateReturnType(operation); const description = operation.description || operation.summary; const comment = description ? ` /**\n * ${description}\n */\n` : ''; const deprecated = operation.deprecated ? ' /** @deprecated */\n' : ''; const methodCall = this.generateMethodCall(operation); return `${comment}${deprecated} async ${methodName}(${parameters}): Promise<${returnType}> { const path = ${pathConstruction}; const config = ${requestConfig}; try { const data = await ${methodCall}; return { data, status: 200, statusText: 'OK', headers: {}, } as ${returnType}; } catch (error) { throw this.handleError(error); } }`; } /** * Generate method parameters */ generateMethodParameters(operation) { const params = []; // Path parameters const pathParams = operation.parameters.filter(p => p.in === 'path'); if (pathParams.length > 0) { const pathParamType = `{ ${pathParams.map(p => `${p.name}: ${this.getParameterType(p)}`).join('; ')} }`; params.push(`pathParams: ${pathParamType}`); } // Query parameters const queryParams = operation.parameters.filter(p => p.in === 'query'); if (queryParams.length > 0) { const requiredQueryParams = queryParams.filter(p => p.required); const optionalQueryParams = queryParams.filter(p => !p.required); if (requiredQueryParams.length > 0 && optionalQueryParams.length > 0) { const requiredType = `{ ${requiredQueryParams.map(p => `${p.name}: ${this.getParameterType(p)}`).join('; ')} }`; const optionalType = `{ ${optionalQueryParams.map(p => `${p.name}?: ${this.getParameterType(p)}`).join('; ')} }`; params.push(`queryParams: ${requiredType} & Partial<${optionalType}>`); } else if (requiredQueryParams.length > 0) { const queryParamType = `{ ${requiredQueryParams.map(p => `${p.name}: ${this.getParameterType(p)}`).join('; ')} }`; params.push(`queryParams: ${queryParamType}`); } else { const queryParamType = `{ ${optionalQueryParams.map(p => `${p.name}?: ${this.getParameterType(p)}`).join('; ')} }`; params.push(`queryParams?: ${queryParamType}`); } } // Request body if (operation.requestBody) { const bodyType = this.getRequestBodyType(operation); const required = operation.requestBody.required ? '' : '?'; params.push(`body${required}: ${bodyType}`); } // Header parameters const headerParams = operation.parameters.filter(p => p.in === 'header'); if (headerParams.length > 0) { const headerParamType = `{ ${headerParams.map(p => `${p.name}${p.required ? '' : '?'}: ${this.getParameterType(p)}`).join('; ')} }`; params.push(`headers?: ${headerParamType}`); } // Options parameter params.push('options?: RequestOptions'); return params.join(', '); } /** * Generate path construction code */ generatePathConstruction(path, pathParams) { if (pathParams.length === 0) { return `'${path}'`; } let pathConstruction = `'${path}'`; for (const param of pathParams) { pathConstruction = pathConstruction.replace(`{${param.name}}`, `\${pathParams.${param.name}}`); } return `\`${pathConstruction.slice(1, -1)}\``; } /** * Generate request configuration */ generateRequestConfig(operation) { const queryParams = operation.parameters.filter(p => p.in === 'query'); const headerParams = operation.parameters.filter(p => p.in === 'header'); const configParts = []; if (queryParams.length > 0) { configParts.push('params: queryParams'); } if (headerParams.length > 0) { configParts.push('headers: { ...headers, ...options?.headers }'); } else { configParts.push('headers: options?.headers'); } configParts.push('timeout: options?.timeout'); configParts.push('signal: options?.signal'); return `{ ${configParts.join(', ')} }`; } /** * Get parameter TypeScript type */ getParameterType(param) { switch (param.schema.type) { case 'string': return 'string'; case 'number': case 'integer': return 'number'; case 'boolean': return 'boolean'; case 'array': const itemType = param.schema.items ? this.getSchemaType(param.schema.items) : 'any'; return `${itemType}[]`; default: return 'any'; } } /** * Get schema TypeScript type */ getSchemaType(schema) { if (!schema) { return 'any'; } // Handle references if (schema.$ref) { const refName = schema.$ref.split('/').pop(); return refName || 'any'; } // Handle arrays if (schema.type === 'array') { const itemType = schema.items ? this.getSchemaType(schema.items) : 'any'; return `${itemType}[]`; } // Handle objects if (schema.type === 'object') { if (schema.properties) { const props = Object.entries(schema.properties).map(([key, prop]) => { const propType = this.getSchemaType(prop); const isRequired = Array.isArray(schema.required) && schema.required.includes(key); const optional = isRequired ? '' : '?'; return `${key}${optional}: ${propType}`; }); return `{ ${props.join('; ')} }`; } return 'Record<string, any>'; } // Handle primitive types switch (schema.type) { case 'string': return 'string'; case 'number': case 'integer': return 'number'; case 'boolean': return 'boolean'; default: return 'any'; } } /** * Generate return type for method */ generateReturnType(operation) { const responseType = this.getResponseDataType(operation); return `ApiResponse<${responseType}>`; } /** * Get response data type */ getResponseDataType(operation) { const successResponses = operation.responses.filter(r => r.statusCode.startsWith('2') || r.statusCode === 'default'); if (successResponses.length === 0) { return 'void'; } // Use the first success response const response = successResponses[0]; if (!response.content) { return 'void'; } const contentTypes = Object.keys(response.content); if (contentTypes.length === 0) { return 'void'; } // Use application/json if available, otherwise first content type const contentType = contentTypes.includes('application/json') ? 'application/json' : contentTypes[0]; const mediaType = response.content[contentType]; return this.getSchemaType(mediaType.schema) || 'any'; } /** * Get request body type */ getRequestBodyType(operation) { if (!operation.requestBody) { return 'any'; } const contentTypes = Object.keys(operation.requestBody.content); if (contentTypes.length === 0) { return 'any'; } const contentType = contentTypes.includes('application/json') ? 'application/json' : contentTypes[0]; const mediaType = operation.requestBody.content[contentType]; return this.getSchemaType(mediaType.schema) || 'any'; } /** * Generate the correct method call based on HTTP method */ generateMethodCall(operation) { const method = operation.method.toLowerCase(); const responseType = this.getResponseDataType(operation); switch (method) { case 'get': case 'delete': // GET and DELETE don't take a data parameter return `this.client.${method}<${responseType}>(path, config)`; case 'post': case 'put': case 'patch': // POST, PUT, PATCH take a data parameter const bodyParam = operation.requestBody ? 'body' : 'undefined'; return `this.client.${method}<${responseType}>(path, ${bodyParam}, config)`; default: return `this.client.${method}<${responseType}>(path, config)`; } } /** * Generate utility methods */ generateUtilities() { const errorHandler = this.generateErrorHandler(); return errorHandler; } /** * Generate error handler */ generateErrorHandler() { return ` private handleError(error: any): ApiError { if (error.response) { return { message: error.response.data?.message || error.response.statusText || 'Request failed', status: error.response.status, code: error.response.data?.code, details: error.response.data, }; } else if (error.request) { return { message: 'Network error - no response received', code: 'NETWORK_ERROR', }; } else { return { message: error.message || 'Unknown error occurred', code: 'UNKNOWN_ERROR', }; } }`; } /** * Get client class name from API title */ getClientName(title) { return this.toPascalCase(title) + 'Client'; } /** * Get method name from operation ID */ getMethodName(operationId) { return this.toCamelCase(operationId); } /** * Convert string to PascalCase */ toPascalCase(str) { return str .replace(/[^a-zA-Z0-9]/g, ' ') .split(' ') .filter(Boolean) .map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()) .join(''); } /** * Convert string to camelCase */ toCamelCase(str) { const pascalCase = this.toPascalCase(str); return pascalCase.charAt(0).toLowerCase() + pascalCase.slice(1); } } /** * React hooks generator for OpenAPI specifications */ class ReactHooksGenerator { config; constructor(config) { this.config = config; } /** * Generate React hooks from parsed OpenAPI spec */ generateHooks(spec) { const imports = this.generateImports(); const queryHooks = this.generateQueryHooks(spec.operations); const mutationHooks = this.generateMutationHooks(spec.operations); const utilityHooks = this.generateUtilityHooks(); return [ this.generateHeader(spec), imports, queryHooks, mutationHooks, utilityHooks, ].filter(Boolean).join('\n\n'); } /** * Generate file header */ generateHeader(spec) { const { title, version } = spec.spec.info; return `/** * Generated React Hooks for ${title} * Version: ${version} * * Generated on: ${new Date().toISOString()} * Generator: NextJS Django Client SDK * * DO NOT EDIT - This file is auto-generated */`; } /** * Generate necessary imports */ generateImports() { const imports = [ "import { useState, useEffect, useCallback, useMemo } from 'react';", "import { createDjangoApiClient } from 'nextjs-django-client';", "import type { ApiClient, ApiResponse, ApiError } from 'nextjs-django-client';", ]; if (this.config.stateManagement === 'react-query') { imports.push("import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';"); imports.push("import type { UseQueryOptions, UseMutationOptions } from '@tanstack/react-query';"); } else if (this.config.stateManagement === 'swr') { imports.push("import useSWR, { useSWRConfig } from 'swr';"); imports.push("import type { SWRConfiguration } from 'swr';"); } // Add types for custom hooks imports.push(` // Hook options types interface QueryOptions { enabled?: boolean; staleTime?: number; gcTime?: number; // Renamed from cacheTime in React Query v5 retry?: number; queryOptions?: UseQueryOptions; } interface MutationOptions { onSuccess?: (data: any) => void; onError?: (error: ApiError) => void; mutationOptions?: UseMutationOptions; } `); // Add import for the generated client const clientName = this.config.clientName || 'ApiClient'; imports.push(`import { ${clientName} } from './client';`); // Add client instance creation imports.push(` // Create a client instance const client = new ${clientName}(process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000'); `); return imports.join('\n'); } /** * Generate query hooks (GET operations) */ generateQueryHooks(operations) { const queryOperations = operations.filter(op => op.method === 'GET'); const hooks = queryOperations.map(operation => this.generateQueryHook(operation)); if (hooks.length === 0) { return ''; } return `// Query Hooks (GET operations)\n${hooks.join('\n\n')}`; } /** * Generate a single query hook */ generateQueryHook(operation) { const hookName = this.getQueryHookName(operation.id); const parameters = this.generateHookParameters(operation, true); const keyGeneration = this.generateQueryKey(operation); const fetcherFunction = this.generateFetcher(operation); if (this.config.stateManagement === 'react-query') { return this.generateReactQueryHook(hookName, operation, parameters, keyGeneration, fetcherFunction); } else if (this.config.stateManagement === 'swr') { return this.generateSWRHook(hookName, operation, parameters, keyGeneration, fetcherFunction); } else { return this.generateCustomQueryHook(hookName, operation, parameters, fetcherFunction); } } /** * Generate React Query hook */ generateReactQueryHook(hookName, operation, parameters, keyGeneration, fetcherFunction) { const description = operation.description || operation.summary; const comment = description ? `/**\n * ${description}\n */\n` : ''; return `${comment}export function ${hookName}(${parameters}) { const queryKey = ${keyGeneration}; return useQuery({ queryKey, queryFn: ${fetcherFunction}, enabled: ${this.generateEnabledCondition(operation)}, staleTime: options?.staleTime ?? 5 * 60 * 1000, // 5 minutes gcTime: options?.gcTime ?? 10 * 60 * 1000, // 10 minutes retry: options?.retry ?? 3, ...options?.queryOptions, }); }`; } /** * Generate SWR hook */ generateSWRHook(hookName, operation, parameters, keyGeneration, fetcherFunction) { const description = operation.description || operation.summary; const comment = description ? `/**\n * ${description}\n */\n` : ''; return `${comment}export function ${hookName}(${parameters}) { const key = ${this.generateEnabledCondition(operation)} ? ${keyGeneration} : null; return useSWR(key, ${fetcherFunction}, { revalidateOnFocus: options?.revalidateOnFocus ?? false, revalidateOnReconnect: options?.revalidateOnReconnect ?? true, dedupingInterval: options?.dedupingInterval ?? 2000, errorRetryCount: options?.errorRetryCount ?? 3, ...options?.swrOptions, }); }`; } /** * Generate custom query hook */ generateCustomQueryHook(hookName, operation, parameters, fetcherFunction) { const description = operation.description || operation.summary; const comment = description ? `/**\n * ${description}\n */\n` : ''; const returnType = this.getQueryReturnType(operation); return `${comment}export function ${hookName}(${parameters}): UseQueryState<${returnType}> { const [data, setData] = useState<${returnType} | undefined>(undefined); const [error, setError] = useState<ApiError | null>(null); const [isLoading, setIsLoading] = useState(false); const fetchData = useCallback(async () => { if (!${this.generateEnabledCondition(operation)}) return; setIsLoading(true); setError(null); try { const result = await ${fetcherFunction}(); setData(result.data); } catch (err) { setError(err as ApiError); } finally { setIsLoading(false); } }, [${this.generateDependencyArray(operation)}]); useEffect(() => { fetchData(); }, [fetchData]); const refetch = useCallback(() => { return fetchData(); }, [fetchData]); return { data, error, isLoading, isError: !!error, isSuccess: !!data && !error, refetch, }; }`; } /** * Generate mutation hooks (POST, PUT, PATCH, DELETE operations) */ generateMutationHooks(operations) { const mutationOperations = operations.filter(op => ['POST', 'PUT', 'PATCH', 'DELETE'].includes(op.method)); const hooks = mutationOperations.map(operation => this.generateMutationHook(operation)); if (hooks.length === 0) { return ''; } return `// Mutation Hooks (POST, PUT, PATCH, DELETE operations)\n${hooks.join('\n\n')}`; } /** * Generate a single mutation hook */ generateMutationHook(operation) { const hookName = this.getMutationHookName(operation.id); const parameters = this.generateMutationParameters(operation); if (this.config.stateManagement === 'react-query') { return this.generateReactQueryMutation(hookName, operation, parameters); } else { return this.generateCustomMutationHook(hookName, operation, parameters); } } /** * Generate React Query mutation hook */ generateReactQueryMutation(hookName, operation, parameters) { const description = operation.description || operation.summary; const comment = description ? `/**\n * ${description}\n */\n` : ''; const mutationFunction = this.generateMutationFunction(operation); const variablesType = this.generateMutationVariablesType(operation); const returnType = this.getQueryReturnType(operation); return `${comment}export function ${hookName}(${parameters}) { const queryClient = useQueryClient(); return useMutation<ApiResponse<$