docs-v2/api-build-scripts/create-pages.mjs

228 lines
7.7 KiB
JavaScript

/**
* This script generates markdown files for each endpoint in the
* configured OpenAPI specs.
*/
import { writeFileSync, rmSync, readFileSync, existsSync, mkdirSync } from 'fs';
import * as yaml from 'js-yaml';
import { execCommand, getSwagger, isPlaceholderFragment } from './helpers.mjs';
import path from 'path';
import { fileURLToPath } from 'url';
import { getPageTemplate } from './templates.mjs';
import winston from 'winston';
// Get the current file's directory
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const logger = winston.createLogger({
level: 'info',
format: winston.format.json(),
defaultMeta: { service: 'user-service' },
transports: [
//
// - Write all logs with importance level of `error` or higher to `error.log`
// (i.e., error, fatal, but not other levels)
//
new winston.transports.File({ filename: path.join(__dirname, 'error.log'), level: 'error' }),
//
// - Write all logs with importance level of `info` or higher to `combined.log`
// (i.e., fatal, error, warn, and info, but not trace)
//
new winston.transports.File({ filename: path.join(__dirname, 'combined.log') }),
],
});
function getPathGroups(openapi) {
const pathGroups = {};
Object.keys(openapi.paths).sort()
.forEach((p) => {
const delimiter = '/';
let key = p.split(delimiter);
let isItemPath = isPlaceholderFragment(key[key.length - 1]);
if(isItemPath) {
key = key.slice(0, -1);
}
key = (key.slice(0, 4))
isItemPath = isPlaceholderFragment(key[key.length - 1]);
if(isItemPath) {
key = key.slice(0, -1);
}
const groupKey = key.join('/');
pathGroups[groupKey] = pathGroups[groupKey] || {};
pathGroups[groupKey][p] = openapi.paths[p];
});
return pathGroups;
}
function createPageIdentifier(uniqueName) {
return (`api-reference-${uniqueName}`).replace(/-/g, '_');
}
function createIndexPage(spec, params) {
const page = getPageTemplate('index');
const menuKey = Object.keys(params.menu)[0];
const menu = {
[menuKey]: {...page.menu, ...params.menu[menuKey]}
};
page.menu = menu;
params.menu[menuKey].name = spec.info.title;
// Create a unique identifier for the menu item
params.menu[menuKey].identifier = createPageIdentifier(params.api_name);
params.title = spec.info.title;
params.description = spec.info.description;
// Return params as a YAML frontmatter string
return (`---\n${yaml.dump(params)}\n---\n\n`)
.concat(spec.info.description, '\\n', "{{< children >}}")
}
function getPathTags(pathSpec) {
// Collect tags for methods in the path and resolve them to the tag objects
// defined in the spec.
// We assume that the first tag describes the path (aka, path group).
let tags = [];
Object.keys(pathSpec.paths).forEach( path => {
Object.keys(pathSpec.paths[path]).flatMap( method => {
if(!pathSpec.paths[path][method]['tags']) {
return [];
}
pathSpec.paths[path][method]['tags'].forEach( tag => {
tags.push(pathSpec.tags.find( t => t.name === tag && !t.traitTag));
});
});
});
return tags;
}
function getTraitTags(pathSpec) {
// Temporarily using trait tags for now, but we should migrate them to native
// Hugo content and frontmatter.
// Collect trait tags defined in the spec.
return pathSpec['tags'].filter( k => k[`x-traitTag`]);
}
// Create a page for each group of operations within a path ("path group")
// In OpenAPI, tags are used to group endpoints. OpenAPI doesn't allow
// a description field for a path, so we use the name and description of
// the first tag in the path.
// Returns a string containing the page content
// The page frontmatter contains an api.spec property to be rendered as the API reference doc for the path group.
function createPathGroupPage(pathGroup, pathSpec, params) {
const page = getPageTemplate('path');
const menuKey = Object.keys(params.menu)[0];
const menu = {
[menuKey]: {...page.menu, ...params.menu[menuKey]}
};
page.menu = menu;
params.title = pathGroup;
params.menu[menuKey].parent = pathSpec.info.title;
params.menu[menuKey].weight = 1;
params.menu[menuKey].name = params.list_title;
const primaryTag = getPathTags(pathSpec).flat()[0];
if(primaryTag) {
params.list_title = `${primaryTag['name']} ${pathGroup}`;
params.description = (primaryTag && primaryTag['description']) || '';
} else {
logger.log('warn', `Name: ${pathSpec.info.title} - No tags found for path group: ${pathGroup}`);
}
// Create a unique identifier for the menu item
params.menu[menuKey].identifier = createPageIdentifier(`${params.api_name}_${pathGroup}`);
params.api = {
spec: JSON.stringify(pathSpec),
path_group: pathGroup,
};
params.related = [];
if(pathSpec['x-influxdata-related-endpoints']) {
params.related = [...pathSpec['x-influxdata-related-endpoints']];
}
if(pathSpec['x-influxdata-related-content']) {
params.related = [
...params.related, ...pathSpec['x-influxdata-related-content']
];
}
// Return params as a YAML frontmatter string
return `---\n${yaml.dump(params)}\n---\n`;
}
export function createOverviewPage(spec, params) {
const page = getPageTemplate('overview');
const menuKey = Object.keys(params.menu)[0];
const menu = {
[menuKey]: {...page.menu, ...params.menu[menuKey]}
};
page.menu = menu;
// Create a unique identifier for the menu item
params.menu[menuKey].identifier = createPageIdentifier(`${params.api_name}-overview`);
// const overviewSpec = JSON.parse(JSON.stringify(spec));
// overviewSpec.paths = null;
// params.api = {spec: JSON.stringify(overviewSpec)};
let body = '';
getTraitTags(spec).forEach( traitTag => {
// toc = toc.concat(`- [${traitTag['name']}](#${(traitTag['name']).toLowerCase().replace(/ /g, '-')})`, "\n");
body = body.concat(traitTag['description'], "\n");
});
// Return params as a YAML frontmatter string
return (`---\n${yaml.dump(params)}\n---\n\n`)
.concat(spec.info.description, '\\n', body)
}
export function createAPIPages(params, specPath, docPath) {
try {
// Execute the script to fetch and bundle the configured spec.
execCommand(`${getSwagger} ${params.api_name} -B`);
logger.log('info', `Target: ${docPath} - Creating pages for: ${params.api_name} ${specPath}`);
const spec = yaml.load(readFileSync(specPath, 'utf8'));
if (!existsSync(docPath)) {
mkdirSync(docPath, { recursive: true });
};
// Deep copy the params object to avoid modifying the original
const paramsClone = JSON.stringify(params);
// Create the index page
writeFileSync(path.join(docPath, '_index.md'), createIndexPage(spec, JSON.parse(paramsClone)));
// // Create the overview page
writeFileSync(
path.join(docPath, 'overview.md'),
createOverviewPage(spec, JSON.parse(paramsClone)));
// Create a page for each group of operations within a path ("path group")
const pathGroups = getPathGroups(spec);
Object.keys(pathGroups).forEach( pathGroup => {
// Deep copy the spec object
const specClone = JSON.parse(JSON.stringify(spec));
specClone.paths = pathGroups[pathGroup];
const page = createPathGroupPage(pathGroup, specClone, JSON.parse(paramsClone));
// For readability, we'll write the page as a YAML
writeFileSync(path.join(docPath,
`${pathGroup.replaceAll('/', '-').replace(/^-/, '')}.md`),
page);
});
} catch (error) {
console.error(`Error creating API pages: ${docPath}`);
logger.log('error', error);
throw error;
}
}
export function deleteAPIPages(docPath) {
try {
rmSync(docPath, {recursive: true, force: true});
} catch (error) {
console.error(`Error deleting API pages: ${docPath}`);
}
}