typescript-scaffolder
Version:
 ### Unit Test Coverage: 97.12%
207 lines (206 loc) • 9.62 kB
JavaScript
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.generateWebhookRoute = generateWebhookRoute;
exports.generateWebhookRoutesFromFile = generateWebhookRoutesFromFile;
exports.generateWebhookRoutesFromPath = generateWebhookRoutesFromPath;
const path = __importStar(require("path"));
const ts_morph_1 = require("ts-morph");
const file_system_1 = require("../../utils/file-system");
const object_helpers_1 = require("../../utils/object-helpers");
const client_constructors_1 = require("../../utils/client-constructors");
const logger_1 = require("../../utils/logger");
const generate_webhook_fixture_1 = require("./generate-webhook-fixture");
/**
* Generates an Express router file for a single incoming webhook.
*
* @param webhook - The webhook definition to generate a route for.
* @param interfaceInputDir - Root path for interface .ts files.
* @param outputDir - Base output directory (e.g., src/codegen/routes/webhooks).
*/
async function generateWebhookRoute(webhook, interfaceInputDir, outputDir) {
const { handlerName, path: webhookPath, requestSchema } = webhook;
const funcName = `generateWebhookRoute`;
logger_1.Logger.debug(funcName, 'Starting webhook route generation...');
const pascalHandlerName = (0, object_helpers_1.toPascalCase)(handlerName);
const routeFile = path.join(outputDir, 'router.ts');
(0, file_system_1.ensureDir)(outputDir);
const interfaceImportPath = path.relative(outputDir, path.join(interfaceInputDir, requestSchema)).replace(/\\/g, '/');
// Handler files are now generated in the same routes/ directory, so use a relative local import.
const handlerImportPath = `./handle_${handlerName}`;
const serviceName = path.basename(interfaceInputDir);
const fixtureExportName = `mock${(0, object_helpers_1.toPascalCase)(requestSchema)}`;
const project = new ts_morph_1.Project();
const existing = project.addSourceFileAtPathIfExists(routeFile);
const sourceFile = existing ?? project.createSourceFile(routeFile, '', { overwrite: true });
// --- Helper functions for idempotency ---
const ensureDefaultImport = (moduleSpecifier, defaultName) => {
const imp = sourceFile.getImportDeclarations().find(d => d.getModuleSpecifierValue() === moduleSpecifier);
if (!imp) {
sourceFile.addImportDeclaration({ moduleSpecifier, defaultImport: defaultName });
return;
}
if (!imp.getDefaultImport()) {
imp.setDefaultImport(defaultName);
}
};
const ensureNamedImport = (moduleSpecifier, name, isTypeOnly = false) => {
const imp = sourceFile.getImportDeclarations().find(d => d.getModuleSpecifierValue() === moduleSpecifier);
if (!imp) {
sourceFile.addImportDeclaration({ moduleSpecifier, namedImports: [name], isTypeOnly });
return;
}
const has = imp.getNamedImports().some(n => n.getName() === name);
if (!has) {
imp.addNamedImport(name);
}
if (isTypeOnly && !imp.isTypeOnly())
imp.setIsTypeOnly(true);
};
const hasText = (snippet) => sourceFile.getFullText().includes(snippet);
const renderTestHeadersArg = (headers) => {
if (!headers || Object.keys(headers).length === 0)
return '';
const entries = Object.entries(headers).map(([k, v]) => {
const envMatch = /^\$\{ENV:([A-Z0-9_]+)\}$/.exec(v);
if (envMatch) {
const envName = envMatch[1];
return `'${k}': (process.env.${envName} ?? '')`;
}
return `'${k}': ${JSON.stringify(v)}`;
});
return `, { ${entries.join(', ')} }`;
};
// --- Idempotent imports ---
ensureDefaultImport('express', 'express');
ensureNamedImport(interfaceImportPath.startsWith('.') ? interfaceImportPath : `./${interfaceImportPath}`, requestSchema, true);
ensureNamedImport(handlerImportPath.startsWith('.') ? handlerImportPath : `./${handlerImportPath}`, `handle${pascalHandlerName}Webhook`);
ensureNamedImport(`./${requestSchema}.fixture`, fixtureExportName);
// --- Router bootstrap only once ---
if (!hasText('const router = express.Router()')) {
sourceFile.addStatements([
'const router = express.Router();',
'router.use(express.json());'
]);
}
// --- Main webhook route only once ---
if (!hasText(`router.post('${webhookPath}'`)) {
sourceFile.addStatements([
`router.post('${webhookPath}', async (req, res) => {
try {
const payload = req.body as ${requestSchema};
await handle${pascalHandlerName}Webhook(payload);
res.status(200).json({ ok: true });
} catch (error) {
console.error('Webhook error:', error);
res.status(500).json({ ok: false });
}
});`
]);
}
const testHeadersArg = renderTestHeadersArg(webhook.testHeaders);
// --- Test route only once ---
const testPath = `/test/${serviceName}-${handlerName}-webhook`;
if (!hasText(`router.post('${testPath}'`)) {
sourceFile.addStatements([
`router.post('${testPath}', async (_req, res) => {
try {
await handle${pascalHandlerName}Webhook(${fixtureExportName}${testHeadersArg});
res.status(200).json({ ok: true, message: 'Simulated webhook sent.' });
} catch (error) {
console.error('Webhook test error:', error);
res.status(500).json({ ok: false });
}
});`
]);
}
(0, generate_webhook_fixture_1.generateWebhookFixture)(requestSchema, interfaceImportPath, outputDir, project);
// --- Default export only once ---
if (!hasText('export default router')) {
sourceFile.addStatements(['export default router;']);
}
logger_1.Logger.debug(funcName, 'Webhook route generation complete');
await project.save();
}
/**
* Reads a WebhookConfigFile and generates Express routes for each incoming webhook.
*
* @param configPath - Path to the webhook config file.
* @param interfaceInputDir - Directory containing interface .ts files.
* @param outputDir - Output directory for generated routes.
*/
async function generateWebhookRoutesFromFile(configPath, interfaceInputDir, outputDir) {
const config = (0, file_system_1.readWebhookConfigFile)(configPath);
if (!config)
return;
for (const webhook of config.webhooks) {
if (webhook.direction !== 'incoming')
continue;
await generateWebhookRoute(webhook, interfaceInputDir, outputDir);
}
}
/**
* Generate Express webhook route files from all config files in a directory.
* @param configDir - Path to the webhook config file.
* @param interfacesRootDir - Directory containing interface .ts files.
* @param outputRootDir - Output directory for generated routes.
*/
async function generateWebhookRoutesFromPath(configDir, interfacesRootDir, outputRootDir) {
const funcName = 'generateWebhookRoutesFromPath';
logger_1.Logger.debug(funcName, 'Starting webhook route generation from config and interface directories...');
const { configFiles, interfaceNameToDirs } = (0, file_system_1.extractInterfaces)(configDir, interfacesRootDir);
for (const configPath of configFiles) {
const config = (0, file_system_1.readWebhookConfigFile)(configPath);
if (!config) {
continue;
}
const requiredSchemas = new Set();
for (const webhook of config.webhooks) {
if (webhook.direction === 'incoming') {
requiredSchemas.add(webhook.requestSchema);
}
}
const foundDir = (0, client_constructors_1.assertDirectoryContainingAllSchemas)(requiredSchemas, interfaceNameToDirs, configPath);
if (!foundDir) {
logger_1.Logger.warn(funcName, `Could not find a directory containing all schemas for config: ${configPath}`);
continue;
}
const relativeInterfaceDir = path.relative(interfacesRootDir, foundDir);
const outputDir = path.join(outputRootDir, relativeInterfaceDir);
(0, file_system_1.ensureDir)(outputDir);
await generateWebhookRoutesFromFile(configPath, foundDir, outputDir);
}
logger_1.Logger.info(funcName, 'Webhook route generation completed.');
}
;