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
JavaScript
#!/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<$