node-apis
Version:
🚀 Advanced TypeScript API generator with clean architecture, comprehensive testing, and automatic formatting. Generate production-ready Node.js APIs with complete integration test suites.
332 lines (308 loc) • 12.2 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.generateCrudHandlerContent = exports.getCrudHandlerFileNames = void 0;
const naming_utils_1 = require("../shared/utils/naming.utils");
/**
* Finds the ID field in the parsed type (either 'id' or '${moduleName}Id')
*/
const findIdField = (parsedType, moduleName) => {
const moduleIdField = `${moduleName}Id`;
// Prefer module-specific ID field if it exists
if (parsedType.fields.some(f => f.name === moduleIdField)) {
return moduleIdField;
}
// Fallback to generic 'id' field
if (parsedType.fields.some(f => f.name === 'id')) {
return 'id';
}
return null;
};
const getCrudHandlerFileNames = ({ moduleName }) => {
const naming = (0, naming_utils_1.getModuleNaming)(moduleName);
return [
`create.${naming.file}.ts`,
`get.${naming.file}.ts`,
`list.${naming.file}.ts`,
`delete.${naming.file}.ts`,
`update.${naming.file}.ts`,
];
};
exports.getCrudHandlerFileNames = getCrudHandlerFileNames;
const generateCrudHandlerContent = ({ operation, moduleName, parsedType, }) => {
const naming = (0, naming_utils_1.getModuleNaming)(moduleName);
const capitalizedOperation = operation.charAt(0).toUpperCase() + operation.slice(1);
// Generate operation-specific handler content
switch (operation) {
case 'create':
return generateTypedCreateHandlerContent(naming, capitalizedOperation, parsedType);
case 'get':
return generateTypedGetHandlerContent(naming, capitalizedOperation, parsedType);
case 'list':
return generateTypedListHandlerContent(naming, capitalizedOperation, parsedType);
case 'update':
return generateTypedUpdateHandlerContent(naming, capitalizedOperation, parsedType);
case 'delete':
return generateTypedDeleteHandlerContent(naming, capitalizedOperation, parsedType);
default:
return generateGenericHandlerContent(naming, capitalizedOperation, operation, parsedType);
}
};
exports.generateCrudHandlerContent = generateCrudHandlerContent;
/**
* Generates CREATE handler content with your preferred format
*/
const generateTypedCreateHandlerContent = (naming, _capitalizedOperation, parsedType) => {
const fieldDestructuring = parsedType.fields.length > 0
? parsedType.fields.map(field => field.name).join(',\n ')
: '// No fields defined in typePayload';
const fieldTypes = parsedType.fields.length > 0
? parsedType.fields.map(field => ` ${field.name}${field.optional ? '?' : ''}: ${field.type};`).join('\n')
: ' // No fields defined in typePayload';
const payloadObject = parsedType.fields.length > 0
? `{ ${parsedType.fields.map(field => field.optional
? `...(${field.name} !== undefined && { ${field.name} })`
: field.name).join(', ')} }`
: '{}';
return `import { TRPCError } from "@trpc/server";
import { TRPC_ERROR_CODES, ERROR_MESSAGES, ERROR_NAMES } from "~/server/constants/errors";
import { createScopedLogger } from "~/server/utils/logger";
import create from "../repository/create.${naming.file}";
import type { typeResult } from "../types/create.${naming.file}";
export default async function create${naming.class}Handler({
${fieldDestructuring}
requestId,
}: {
${fieldTypes}
requestId: string;
}): Promise<typeResult> {
const log = createScopedLogger({ requestId, feature: "${naming.constant}" });
try {
const startTime = Date.now();
log.info({ ${parsedType.fields.length > 0 ? parsedType.fields.map(f => f.name).join(', ') : ''} }, "CREATE handler started");
// Business logic here - direct repository call
const ${naming.variable} = await create(${payloadObject});
const duration = Date.now() - startTime;
log.info(
{
duration: \`\${duration}ms\`,
},
"CREATE handler completed successfully",
);
return ${naming.variable};
} catch (err) {
const error = err as Error;
log.error(
{
error: error.message,
stack: error.stack,
},
"CREATE handler error",
);
// Handle specific error types using ERROR_NAMES
if (error.name === ERROR_NAMES.VALIDATION) {
throw new TRPCError({
code: TRPC_ERROR_CODES.BAD_REQUEST,
message: error.message,
cause: error,
});
}
if (error.name === ERROR_NAMES.CONFLICT) {
throw new TRPCError({
code: TRPC_ERROR_CODES.CONFLICT,
message: ERROR_MESSAGES.ALREADY_EXISTS,
cause: error,
});
}
// Generic error fallback
throw new TRPCError({
code: TRPC_ERROR_CODES.INTERNAL_SERVER_ERROR,
message: ERROR_MESSAGES.INTERNAL_ERROR,
cause: error,
});
}
}
`;
};
/**
* Generates GET handler content with your preferred format
*/
const generateTypedGetHandlerContent = (naming, _capitalizedOperation, parsedType) => {
const fieldDestructuring = parsedType.fields.length > 0
? parsedType.fields.map(field => field.name).join(',\n ')
: '// No fields defined in typePayload';
const fieldTypes = parsedType.fields.length > 0
? parsedType.fields.map(field => ` ${field.name}${field.optional ? '?' : ''}: ${field.type};`).join('\n')
: ' // No fields defined in typePayload';
// GET typically uses the ID field from the payload
const idField = findIdField(parsedType, naming.variable);
const idAccess = idField || 'id';
return `import { TRPCError } from "@trpc/server";
import { TRPC_ERROR_CODES, ERROR_MESSAGES, ERROR_NAMES } from "~/server/constants/errors";
import { createScopedLogger } from "~/server/utils/logger";
import get from "../repository/get.${naming.file}";
import type { typeResult } from "../types/get.${naming.file}";
export default async function get${naming.class}Handler({
${fieldDestructuring}
requestId,
}: {
${fieldTypes}
requestId: string;
}): Promise<typeResult> {
const log = createScopedLogger({ requestId, feature: "${naming.constant}" });
try {
const startTime = Date.now();
log.info({ ${idAccess} }, "GET handler started");
// Business logic here - direct repository call
const ${naming.variable} = await get(${idAccess});
const duration = Date.now() - startTime;
log.info(
{
duration: \`\${duration}ms\`,
},
"GET handler completed successfully",
);
return ${naming.variable};
} catch (err) {
const error = err as Error;
log.error(
{
error: error.message,
stack: error.stack,
},
"GET handler error",
);
// Handle specific error types using ERROR_NAMES
if (error.name === ERROR_NAMES.NOT_FOUND) {
throw new TRPCError({
code: TRPC_ERROR_CODES.NOT_FOUND,
message: ERROR_MESSAGES.NOT_FOUND,
cause: error,
});
}
if (error.name === ERROR_NAMES.VALIDATION) {
throw new TRPCError({
code: TRPC_ERROR_CODES.BAD_REQUEST,
message: error.message,
cause: error,
});
}
// Generic error fallback
throw new TRPCError({
code: TRPC_ERROR_CODES.INTERNAL_SERVER_ERROR,
message: ERROR_MESSAGES.INTERNAL_ERROR,
cause: error,
});
}
}
`;
};
/**
* Generates generic handler content (fallback)
*/
const generateGenericHandlerContent = (naming, _capitalizedOperation, operation, parsedType) => {
const fieldDestructuring = parsedType.fields.length > 0
? parsedType.fields.map(field => field.name).join(',\n ')
: '// No fields defined in typePayload';
const fieldTypes = parsedType.fields.length > 0
? parsedType.fields.map(field => ` ${field.name}${field.optional ? '?' : ''}: ${field.type};`).join('\n')
: ' // No fields defined in typePayload';
// Generate the appropriate repository call based on operation
const getRepositoryCall = () => {
if (operation === 'delete') {
const idField = findIdField(parsedType, naming.variable);
const idAccess = idField || 'id';
const permanentAccess = parsedType.fields.find(f => f.name === 'permanent')
? 'permanent'
: 'false';
return `const result = await remove(${idAccess}, ${permanentAccess} || false);`;
}
else if (operation === 'list') {
const payloadObject = parsedType.fields.length > 0
? `{ ${parsedType.fields.map(field => field.optional
? `...(${field.name} !== undefined && { ${field.name} })`
: field.name).join(', ')} }`
: '{}';
return `const result = await list(${payloadObject});`;
}
else if (operation === 'update') {
const idField = findIdField(parsedType, naming.variable);
const idAccess = idField || 'id';
const nonIdFields = parsedType.fields.filter(f => f.name !== idField);
const updateObject = nonIdFields.length > 0
? `{ ${nonIdFields.map(field => field.optional
? `...(${field.name} !== undefined && { ${field.name} })`
: field.name).join(', ')} }`
: '{}';
return `const result = await update(${idAccess}, ${updateObject});`;
}
else {
return `// TODO: Implement ${operation} logic
throw new Error("Not implemented");`;
}
};
return `import { TRPCError } from "@trpc/server";
import { TRPC_ERROR_CODES, ERROR_MESSAGES, ERROR_NAMES } from "~/server/constants/errors";
import { createScopedLogger } from "~/server/utils/logger";
import ${operation === 'delete' ? 'remove' : operation === 'list' ? 'list' : operation} from "../repository/${operation}.${naming.file}";
import type { typeResult } from "../types/${operation}.${naming.file}";
export default async function ${operation}${naming.class}Handler({
${fieldDestructuring}
requestId,
}: {
${fieldTypes}
requestId: string;
}): Promise<typeResult> {
const log = createScopedLogger({ requestId, feature: "${naming.constant}" });
try {
const startTime = Date.now();
log.info({ ${parsedType.fields.length > 0 ? parsedType.fields.map(f => f.name).join(', ') : ''} }, "${operation.toUpperCase()} handler started");
// Business logic here - direct repository call
${getRepositoryCall()}
const duration = Date.now() - startTime;
log.info(
{
duration: \`\${duration}ms\`,
},
"${operation.toUpperCase()} handler completed successfully",
);
return result;
} catch (err) {
const error = err as Error;
log.error(
{
error: error.message,
stack: error.stack,
},
"${operation.toUpperCase()} handler error",
);
// Handle specific error types using ERROR_NAMES
${(operation === 'delete' || operation === 'update') &&
`if (error.name === ERROR_NAMES.NOT_FOUND) {
throw new TRPCError({
code: TRPC_ERROR_CODES.NOT_FOUND,
message: ERROR_MESSAGES.NOT_FOUND,
cause: error,
});
}
`}if (error.name === ERROR_NAMES.VALIDATION) {
throw new TRPCError({
code: TRPC_ERROR_CODES.BAD_REQUEST,
message: error.message,
cause: error,
});
}
// Generic error fallback
throw new TRPCError({
code: TRPC_ERROR_CODES.INTERNAL_SERVER_ERROR,
message: ERROR_MESSAGES.INTERNAL_ERROR,
cause: error,
});
}
}
`;
};
// Placeholder functions for other operations
const generateTypedListHandlerContent = (naming, capitalizedOperation, parsedType) => generateGenericHandlerContent(naming, capitalizedOperation, 'list', parsedType);
const generateTypedUpdateHandlerContent = (naming, capitalizedOperation, parsedType) => generateGenericHandlerContent(naming, capitalizedOperation, 'update', parsedType);
const generateTypedDeleteHandlerContent = (naming, capitalizedOperation, parsedType) => generateGenericHandlerContent(naming, capitalizedOperation, 'delete', parsedType);
//# sourceMappingURL=typed-crud.handlers.js.map