agentlangcli
Version:
A command-line interface tool for AgentLang
296 lines • 13.3 kB
JavaScript
import { z } from 'zod';
import yaml from 'yaml';
import { OpenApiGeneratorV3, OpenAPIRegistry, extendZodWithOpenApi } from '@asteasolutions/zod-to-openapi';
import { getUserModuleNames, fetchModule } from 'agentlang/out/runtime/module.js';
import * as fs from 'node:fs/promises';
import * as path from 'node:path';
import Converter from 'openapi-to-postmanv2';
import { execSync } from 'child_process';
extendZodWithOpenApi(z);
const registry = new OpenAPIRegistry();
const bearerAuth = registry.registerComponent('securitySchemes', 'bearerAuth', {
type: 'http',
scheme: 'bearer',
bearerFormat: 'JWT',
});
function getOpenApiDocumentation(registry, name, version) {
const generator = new OpenApiGeneratorV3(registry.definitions);
return generator.generateDocument({
openapi: '3.0.0',
info: {
version,
title: name,
description: 'This is the API',
},
servers: [{ url: 'v1' }],
});
}
function writeDocumentation(registry, docDir, name, version) {
const docs = getOpenApiDocumentation(registry, name, version);
const fileContent = yaml.stringify(docs);
void fs.mkdir(path.join(docDir, 'docs'), { recursive: true });
void fs.writeFile(`${docDir}/docs/openapi.yml`, fileContent, {
encoding: 'utf-8',
});
}
function findRelationshipPaths(moduleName, entityName) {
const module = fetchModule(moduleName);
const relationships = module.getContainsRelationshipEntries();
function findPathsRecursive(currentEntity, visited, currentPath) {
const matchingRels = relationships.filter(rel => rel.node2.origName === currentEntity);
if (matchingRels.length === 0) {
return currentPath.length > 0 ? [currentPath] : [];
}
let allPaths = [];
for (const rel of matchingRels) {
const node1Entity = rel.node1.origName;
if (visited.has(node1Entity)) {
continue;
}
const newVisited = new Set(visited);
newVisited.add(node1Entity);
const newPath = [...currentPath, rel.name, `{${node1Entity.toLowerCase()}}`, node1Entity];
const paths = findPathsRecursive(node1Entity, newVisited, newPath);
allPaths = allPaths.concat(paths);
}
return allPaths;
}
const visited = new Set([entityName]);
return findPathsRecursive(entityName, visited, [entityName]);
}
function getOneOfValues(properties) {
const oneOf = properties.get('one-of') || new Set();
return Array.from(oneOf);
}
function createZodSchemaFromEntitySchema(schema) {
return z.object(Object.fromEntries(Array.from(schema.entries()).map(([key, value]) => {
const k = key;
let v = value.properties && value.properties.get('one-of')
? z.enum(getOneOfValues(value.properties))
: value.type === 'String'
? z.string()
: value.type === 'Int'
? z.number()
: value.type === 'Number'
? z.number()
: value.type === 'Email'
? z.string()
: value.type === 'Date'
? z.string()
: value.type === 'Time'
? z.string()
: value.type === 'DateTime'
? z.string()
: value.type === 'Boolean'
? z.boolean()
: value.type === 'UUID'
? z.string().uuid()
: value.type === 'URL'
? z.string()
: value.type === 'Path'
? z.string()
: value.type === 'Map'
? z.object({})
: value.type === 'Any'
? z.any()
: z.any();
if (value.properties && value.properties.get('optional')) {
v = v.optional();
}
if (value.properties && value.properties.get('default')) {
const deflt = value.properties.get('default');
switch (deflt) {
case 'uuid()':
v = z.string().uuid().optional();
break;
case 'now()':
v = z.date().optional();
break;
default:
v = v.default(deflt);
}
}
return [k, v];
})));
}
function registerEntityEndpoint(method, path, tags, filterPaths, entitySchema) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const endpointConfig = {
method,
path,
security: [{ [bearerAuth.name]: [] }],
tags,
responses: {
200: {
description: 'Success',
content: {
'application/json': {
schema: method === 'get' ? z.array(entitySchema) : entitySchema,
},
},
},
404: {
description: 'Not Found',
},
500: {
description: 'Internal Server Error',
},
},
};
if (method === 'post' || method === 'put') {
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
endpointConfig.request = {
body: {
content: {
'application/json': {
schema: entitySchema,
},
},
},
};
}
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
endpointConfig.parameters = filterPaths.map(path => {
return {
name: path,
in: 'path',
required: true,
schema: { type: 'string' },
};
});
if (method === 'post') {
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
delete endpointConfig.responses['404'];
}
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
registry.registerPath(endpointConfig);
}
function generateEntitiesEntries() {
const modules = getUserModuleNames();
return modules.map((moduleName) => {
const module = fetchModule(moduleName);
const entities = module.getEntityEntries();
return entities.map((entity) => {
const entityPath = `${moduleName}/${entity.name}`;
const relatinotionshipPaths = findRelationshipPaths(moduleName, entity.name);
const entitySchema = createZodSchemaFromEntitySchema(entity.schema).openapi(`${entity.name}Schema`);
registerEntityEndpoint('post', `/${entityPath}`, [entity.name], [], entitySchema);
registerEntityEndpoint('get', `/${entityPath}`, [entity.name], [], entitySchema);
registerEntityEndpoint('put', `/${entityPath}/{id}`, [entity.name], ['id'], entitySchema);
registerEntityEndpoint('delete', `/${entityPath}/{id}`, [entity.name], ['id'], entitySchema);
if (relatinotionshipPaths.length > 1) {
relatinotionshipPaths.forEach(path => {
const relationshipPath = path.reverse().join('/');
const filterPaths = path
.filter(segment => segment.startsWith('{') && segment.endsWith('}'))
.map(segment => segment.slice(1, -1));
registerEntityEndpoint('post', `/${relationshipPath}`, [`${entity.name} (${path[path.length - 2]})`], filterPaths, entitySchema);
registerEntityEndpoint('get', `/${relationshipPath}`, [`${entity.name} (${path[path.length - 2]})`], filterPaths, entitySchema);
registerEntityEndpoint('put', `/${relationshipPath}/{id}`, [`${entity.name} (${path[path.length - 2]})`], filterPaths.concat(['id']), entitySchema);
registerEntityEndpoint('delete', `/${relationshipPath}/{id}`, [`${entity.name} (${path[path.length - 2]})`], filterPaths.concat(['id']), entitySchema);
});
}
});
});
}
function generateEventsEntries() {
const modules = getUserModuleNames();
return modules.map((moduleName) => {
const module = fetchModule(moduleName);
const events = module.getEventEntries();
return events.map((event) => {
const eventPath = `${moduleName}/${event.name}`;
const eventSchema = createZodSchemaFromEntitySchema(event.schema).openapi(`${event.name}Schema`);
const sc = z.object({
[eventPath]: eventSchema,
});
registry.registerPath({
method: 'post',
path: `/${eventPath}`,
security: [{ [bearerAuth.name]: [] }],
tags: ['Events'],
request: {
body: {
content: {
'application/json': {
schema: sc,
},
},
},
},
responses: {
200: {
description: 'Success',
},
404: {
description: 'Not Found',
},
500: {
description: 'Internal Server Error',
},
},
});
return {
path: eventPath,
name: event.name,
schema: event.schema,
};
});
});
}
export const generateSwaggerDoc = async (fileName, options) => {
// eslint-disable-next-line no-console
console.log('Generating documentation...');
const docDir = path.dirname(fileName) === '.' ? process.cwd() : path.resolve(process.cwd(), fileName);
const packagePath = path.join(docDir, 'package.json');
const packageContent = await fs.readFile(packagePath, 'utf-8');
const pkg = JSON.parse(packageContent);
const name = pkg.name || 'app';
const version = pkg.version || '0.0.1';
generateEntitiesEntries();
generateEventsEntries();
writeDocumentation(registry, docDir, name, version);
if (options === null || options === void 0 ? void 0 : options.outputHtml) {
await generateHtmlDocumentation(registry, docDir, name, version);
}
if (options === null || options === void 0 ? void 0 : options.outputPostman) {
await generatePostmanCollection(registry, docDir, name, version);
}
};
async function generateHtmlDocumentation(registry, docDir, _name, _version) {
const outputPath = `${docDir}/docs/index.html`;
const yamlContent = await fs.readFile(`${docDir}/docs/openapi.yml`, 'utf8');
const jsonContent = JSON.stringify(yaml.parse(yamlContent), null, 2);
await fs.writeFile(`${docDir}/docs/openapi.json`, jsonContent, { encoding: 'utf-8' });
// eslint-disable-next-line no-console
console.log('OpenAPI JSON generated: docs/openapi.json');
try {
execSync(`redocly build-docs ${docDir}/docs/openapi.json -o ${outputPath}`);
// eslint-disable-next-line no-console
console.log('HTML documentation generated: docs/index.html');
}
catch (error) {
// eslint-disable-next-line no-console
console.error('Failed to generate HTML documentation:', error);
}
}
async function generatePostmanCollection(registry, docDir, _name, _version) {
const openapiData = await fs.readFile(`${docDir}/docs/openapi.yml`, 'utf8');
// eslint-disable-next-line @typescript-eslint/no-explicit-any
Converter.convert({ type: 'string', data: openapiData }, {}, (err, conversionResult) => {
var _a, _b;
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
if (!conversionResult.result) {
// eslint-disable-next-line no-console, @typescript-eslint/no-unsafe-member-access
console.log('Could not convert', conversionResult.reason);
}
else {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
const collection = (_b = (_a = conversionResult.output) === null || _a === void 0 ? void 0 : _a[0]) === null || _b === void 0 ? void 0 : _b.data;
void fs.writeFile(`${docDir}/docs/postman.json`, JSON.stringify(collection, null, 2), { encoding: 'utf-8' });
// eslint-disable-next-line no-console
console.log('Postman collection generated: docs/postman.json');
}
});
}
//# sourceMappingURL=docs.js.map