@docyrus/tanstack-db-generator
Version:
Code generator utilities for TanStack Query / Database integration with Docyrus API
299 lines (296 loc) • 11.5 kB
JavaScript
;
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();
}