UNPKG

@docyrus/tanstack-db-generator

Version:

Code generator utilities for TanStack Query / Database integration with Docyrus API

299 lines (296 loc) 11.5 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.generateEndpointCollections = generateEndpointCollections; const typegen_1 = require("../utils/typegen"); const fs_1 = require("fs"); const path_1 = require("path"); async function generateEndpointCollections(spec, endpointGroups, outputDir) { // Generate individual collection files const exportLines = []; for (const group of endpointGroups) { const collectionContent = generateEndpointCollectionFile(group, spec); const fileName = `${group.name}.collection.ts`; (0, fs_1.writeFileSync)((0, path_1.join)(outputDir, fileName), collectionContent); const exportName = `${toPascalCase(group.name)}Collection`; exportLines.push(`export { ${exportName} } from './${group.name}.collection';`); } // Append exports to collections index const indexPath = (0, path_1.join)(outputDir, 'index.ts'); let indexContent = (0, fs_1.existsSync)(indexPath) ? (0, fs_1.readFileSync)(indexPath, 'utf-8') : `// Generated collections index\n`; let updated = false; for (const line of exportLines) { if (!indexContent.includes(line)) { indexContent += (indexContent.endsWith('\n') ? '' : '\n') + line + '\n'; updated = true; } } if (updated) { (0, fs_1.writeFileSync)(indexPath, indexContent); } } function generateEndpointCollectionFile(group, spec) { const collectionName = `${toPascalCase(group.name)}Collection`; const methods = group.endpoints.map(endpoint => { return generateEndpointMethod(endpoint, spec); }).join(',\n\n'); const typeNames = getImportTypes(group, spec); const inlineTypes = typeNames.length > 0 ? `${(0, typegen_1.generateInterfacesForNames)(spec, typeNames)}\n\n` : ''; return `// Generated collection for ${group.name} import { apiClient } from '../lib/api'; ${inlineTypes} export const ${collectionName} = { ${methods} }; `; } function generateEndpointMethod(endpoint, spec) { const methodName = endpoint.operationId; const hasPathParams = endpoint.path.includes('{'); const pathParams = extractPathParams(endpoint.path); const pathParamTypes = getPathParamTypes(endpoint); // Get parameter and return types const paramType = getRequestBodyType(endpoint.operation); const returnType = getResponseType(endpoint.operation); // Build function parameters const params = []; if (hasPathParams) { params.push(...pathParams.map(p => `${p}: ${pathParamTypes[p] || 'string'}`)); } if (endpoint.operation.requestBody) { params.push(`data: ${paramType || 'any'}`); } // Generate path with replacements let pathCode = `'${endpoint.path}'`; if (hasPathParams) { pathParams.forEach(param => { pathCode = `${pathCode}.replace('{${param}}', ${param}.toString())`; }); } const method = endpoint.method.toLowerCase(); const hasData = endpoint.operation.requestBody; const returnTypeAnnotation = returnType ? `: Promise<${returnType}>` : ''; const genericType = returnType ? `<${returnType}>` : ''; const jsDoc = buildEndpointJsDoc(endpoint, spec, { returnType, hasData, pathParams, pathParamTypes, }); return ` ${jsDoc}${methodName}: (${params.join(', ')})${returnTypeAnnotation} => apiClient.${method}${genericType}(${pathCode}${hasData ? ', data' : ''})`; } function extractPathParams(path) { const matches = path.match(/\{([^}]+)\}/g); if (!matches) return []; return matches.map(m => m.slice(1, -1)); } function getImportTypes(group, spec) { const types = new Set(); for (const endpoint of group.endpoints) { // Extract types from request/response schemas const refs = extractRefsFromOperation(endpoint.operation); refs.forEach(ref => { const typeName = ref.split('/').pop(); if (typeName) types.add(typeName); }); } return Array.from(types); } function extractRefsFromOperation(operation) { const refs = []; // From request body if (operation.requestBody?.content?.['application/json']?.schema?.$ref) { refs.push(operation.requestBody.content['application/json'].schema.$ref); } // From responses Object.values(operation.responses || {}).forEach((response) => { if (response.content?.['application/json']?.schema?.$ref) { refs.push(response.content['application/json'].schema.$ref); } // Check nested data property if (response.content?.['application/json']?.schema?.properties?.data?.$ref) { refs.push(response.content['application/json'].schema.properties.data.$ref); } // Check nested data array items if (response.content?.['application/json']?.schema?.properties?.data?.type === 'array' && response.content['application/json'].schema.properties.data.items?.$ref) { refs.push(response.content['application/json'].schema.properties.data.items.$ref); } // Check top-level array items if (response.content?.['application/json']?.schema?.type === 'array' && response.content['application/json'].schema.items?.$ref) { refs.push(response.content['application/json'].schema.items.$ref); } // Check deep patternProperties → array items $ref inside data const schema = response.content?.['application/json']?.schema; const dataSchema = schema?.properties?.data; if (dataSchema?.type === 'object' && dataSchema.patternProperties) { const found = findArrayItemRefInPatternProperties(dataSchema); if (found?.ref) refs.push(found.ref); } }); return refs; } function getRequestBodyType(operation) { const requestBody = operation.requestBody; if (!requestBody) return null; // Handle reference to requestBody if (typeof requestBody === 'string' || requestBody.$ref) { // This is a reference, we need to resolve it // For now, extract the type name from the operation ID or path return null; } if (!requestBody.content?.['application/json']?.schema) return null; const schema = requestBody.content['application/json'].schema; if (schema.$ref) { return schema.$ref.split('/').pop() || null; } return null; } function getResponseType(operation) { const successResponse = operation.responses?.['200'] || operation.responses?.['201']; if (!successResponse?.content?.['application/json']?.schema) return null; const schema = successResponse.content['application/json'].schema; // Check for direct $ref if (schema.$ref) { return schema.$ref.split('/').pop() || null; } // Check for data property with $ref (common pattern for wrapped responses) if (schema.properties?.data?.$ref) { return schema.properties.data.$ref.split('/').pop() || null; } // Check for array items if (schema.properties?.data?.type === 'array' && schema.properties.data.items?.$ref) { const typeName = schema.properties.data.items.$ref.split('/').pop(); return typeName ? `Array<${typeName}>` : null; } // Check for top-level array responses if (schema.type === 'array' && schema.items?.$ref) { const typeName = schema.items.$ref.split('/').pop(); return typeName ? `Array<${typeName}>` : null; } // Check for deep nested patternProperties producing arrays of some ref inside data if (schema.properties?.data?.type === 'object' && schema.properties.data.patternProperties) { const found = findArrayItemRefInPatternProperties(schema.properties.data); if (found && found.typeName && found.depth > 0) { // Build nested Record<string, ...> of the appropriate depth ending with typeName[] let t = `Array<${found.typeName}>`; for (let i = 0; i < found.depth; i++) { t = `Record<string, ${t}>`; } return t; } } return null; } function toPascalCase(str) { return str.split(/[_-]/) .map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()) .join(''); } function getPathParamTypes(endpoint) { const types = {}; const params = endpoint.operation?.parameters || []; for (const p of params) { if (p.in === 'path') { const t = getTsTypeFromSchema(p.schema); if (t) types[p.name] = t; } } return types; } function getTsTypeFromSchema(schema) { if (!schema) return null; if (schema.$ref) return schema.$ref.split('/').pop() || null; switch (schema.type) { case 'string': return 'string'; case 'number': case 'integer': return 'number'; case 'boolean': return 'boolean'; default: return null; } } // Recursively traverse patternProperties chain to find an array items $ref. function findArrayItemRefInPatternProperties(schema) { if (!schema || schema.type !== 'object' || !schema.patternProperties) return null; const patterns = schema.patternProperties; for (const key of Object.keys(patterns)) { const node = patterns[key]; if (!node) continue; if (node.type === 'array' && node.items?.$ref) { const ref = node.items.$ref; const typeName = ref.split('/').pop() || ''; return { ref, typeName, depth: 1 }; } if (node.type === 'object' && node.patternProperties) { const found = findArrayItemRefInPatternProperties(node); if (found) { return { ...found, depth: found.depth + 1 }; } } } return null; } function buildEndpointJsDoc(endpoint, spec, ctx) { const op = endpoint.operation || {}; const lines = []; const title = op.summary || ''; const desc = op.description || ''; if (title) lines.push(sanitizeJsDoc(title)); if (desc && desc !== title) lines.push(sanitizeJsDoc(desc)); // Params const parameters = Array.isArray(op.parameters) ? op.parameters : []; for (const p of parameters) { const param = resolveParameter(p, spec); if (!param) continue; if (param.in === 'path') { lines.push(`@param ${param.name} - ${sanitizeJsDoc(param.description || '')}`.trim()); } if (param.in === 'query') { // We don't expose individual query params for endpoints currently; skip } } if (ctx.hasData) { lines.push(`@param data - Request body`); } if (ctx.returnType) { lines.push(`@returns ${ctx.returnType}`); } if (lines.length === 0) return ''; return `/**\n * ${lines.join('\n * ')}\n */\n `; } function resolveParameter(p, spec) { if (p && p.$ref) { const ref = p.$ref; const path = ref.replace('#/', '').split('/'); let current = spec; for (const seg of path) current = current?.[seg]; return current || null; } return p; } function sanitizeJsDoc(text) { return String(text).replace(/\*\//g, '*\/').trim(); }