334 lines
12 KiB
JavaScript
334 lines
12 KiB
JavaScript
#!/usr/bin/env node
|
|
"use strict";
|
|
/**
|
|
* Post-Process Specs
|
|
*
|
|
* Applies content overlays and tag configuration to bundled OpenAPI specs.
|
|
* Runs after `getswagger.sh` bundles specs and before
|
|
* `generate-openapi-articles.ts` generates Hugo pages.
|
|
*
|
|
* Replaces Redocly decorators for:
|
|
* - info.yml overlays (title, description, version, license, contact, x-* fields)
|
|
* - servers.yml overlays (replaces spec.servers array)
|
|
* - tags.yml config (rename, describe, add x-related links to tags)
|
|
*
|
|
* Usage:
|
|
* node api-docs/scripts/dist/post-process-specs.js # All products
|
|
* node api-docs/scripts/dist/post-process-specs.js influxdb3/core # One product
|
|
*
|
|
* @module post-process-specs
|
|
*/
|
|
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 });
|
|
const fs = __importStar(require("fs"));
|
|
const path = __importStar(require("path"));
|
|
const yaml = __importStar(require("js-yaml"));
|
|
// ---------------------------------------------------------------------------
|
|
// Constants
|
|
// ---------------------------------------------------------------------------
|
|
const LOG_PREFIX = '[post-process]';
|
|
/** Build output directory for resolved specs. Source specs are never mutated. */
|
|
const BUILD_DIR = '_build';
|
|
/** Product directories that contain a .config.yml with `apis:` entries. */
|
|
const PRODUCT_DIRS = [
|
|
'influxdb3/core',
|
|
'influxdb3/enterprise',
|
|
'influxdb3/cloud-dedicated',
|
|
'influxdb3/cloud-serverless',
|
|
'influxdb3/clustered',
|
|
'influxdb/cloud',
|
|
'influxdb/v2',
|
|
'influxdb/v1',
|
|
'enterprise_influxdb/v1',
|
|
];
|
|
// ---------------------------------------------------------------------------
|
|
// Helpers
|
|
// ---------------------------------------------------------------------------
|
|
/**
|
|
* Parse a YAML file and return the parsed object, or null if the file does
|
|
* not exist.
|
|
*/
|
|
function loadYaml(filePath) {
|
|
if (!fs.existsSync(filePath))
|
|
return null;
|
|
const raw = fs.readFileSync(filePath, 'utf8');
|
|
return yaml.load(raw);
|
|
}
|
|
/**
|
|
* Write an object to a YAML file.
|
|
*/
|
|
function writeYaml(filePath, data) {
|
|
fs.writeFileSync(filePath, yaml.dump(data, { lineWidth: -1 }), 'utf8');
|
|
}
|
|
function log(msg) {
|
|
process.stderr.write(`${LOG_PREFIX} ${msg}\n`);
|
|
}
|
|
// ---------------------------------------------------------------------------
|
|
// Content overlays
|
|
// ---------------------------------------------------------------------------
|
|
/**
|
|
* Resolve a content file path using the same convention as the Redocly
|
|
* docs-content.cjs helper: try API-specific directory first, fall back to
|
|
* product-level directory.
|
|
*
|
|
* @param filename - e.g. 'info.yml' or 'servers.yml'
|
|
* @param specDir - Absolute path to the directory containing the spec file.
|
|
* @param productAbsDir - Absolute path to the product directory.
|
|
* @returns Absolute path to the content file, or null if not found.
|
|
*/
|
|
function resolveContentFile(filename, specDir, productAbsDir) {
|
|
// API-specific: {specDir}/content/{filename}
|
|
const apiSpecific = path.join(specDir, 'content', filename);
|
|
if (fs.existsSync(apiSpecific))
|
|
return apiSpecific;
|
|
// Product-level fallback: {productAbsDir}/content/{filename}
|
|
const productLevel = path.join(productAbsDir, 'content', filename);
|
|
if (fs.existsSync(productLevel))
|
|
return productLevel;
|
|
return null;
|
|
}
|
|
/**
|
|
* Apply info.yml overlay to the spec. Merges each field present in the
|
|
* overlay into spec.info, preserving fields not mentioned in the overlay.
|
|
*
|
|
* @returns true if any fields were applied.
|
|
*/
|
|
function applyInfoOverlay(spec, specDir, productAbsDir, label) {
|
|
const infoPath = resolveContentFile('info.yml', specDir, productAbsDir);
|
|
if (!infoPath)
|
|
return false;
|
|
const overlay = loadYaml(infoPath);
|
|
if (!overlay)
|
|
return false;
|
|
if (!spec.info)
|
|
spec.info = {};
|
|
let applied = 0;
|
|
for (const [key, value] of Object.entries(overlay)) {
|
|
spec.info[key] = value;
|
|
applied++;
|
|
}
|
|
if (applied > 0) {
|
|
log(`${label}: applied ${applied} info field(s) from ${path.relative(productAbsDir, infoPath)}`);
|
|
}
|
|
return applied > 0;
|
|
}
|
|
/**
|
|
* Apply servers.yml overlay to the spec. Replaces spec.servers entirely.
|
|
*
|
|
* @returns true if servers were applied.
|
|
*/
|
|
function applyServersOverlay(spec, specDir, productAbsDir, label) {
|
|
const serversPath = resolveContentFile('servers.yml', specDir, productAbsDir);
|
|
if (!serversPath)
|
|
return false;
|
|
const servers = loadYaml(serversPath);
|
|
if (!servers || !Array.isArray(servers))
|
|
return false;
|
|
spec.servers = servers;
|
|
log(`${label}: applied ${servers.length} server(s) from ${path.relative(productAbsDir, serversPath)}`);
|
|
return true;
|
|
}
|
|
// ---------------------------------------------------------------------------
|
|
// Tag config
|
|
// ---------------------------------------------------------------------------
|
|
/**
|
|
* Collect every tag name referenced across all operations in the spec.
|
|
*/
|
|
function collectOperationTags(spec) {
|
|
const found = new Set();
|
|
for (const pathItem of Object.values(spec.paths ?? {})) {
|
|
for (const operation of Object.values(pathItem)) {
|
|
if (operation &&
|
|
typeof operation === 'object' &&
|
|
Array.isArray(operation.tags)) {
|
|
for (const t of operation.tags)
|
|
found.add(t);
|
|
}
|
|
}
|
|
}
|
|
return found;
|
|
}
|
|
/**
|
|
* Rename a tag throughout the spec: in `tags[]` and in every operation.
|
|
*/
|
|
function renameTag(spec, oldName, newName) {
|
|
for (const tag of spec.tags ?? []) {
|
|
if (tag.name === oldName)
|
|
tag.name = newName;
|
|
}
|
|
for (const pathItem of Object.values(spec.paths ?? {})) {
|
|
for (const operation of Object.values(pathItem)) {
|
|
if (operation &&
|
|
typeof operation === 'object' &&
|
|
Array.isArray(operation.tags)) {
|
|
operation.tags = operation.tags.map((t) => t === oldName ? newName : t);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
/**
|
|
* Apply tag config from a `tags.yml` file to the spec.
|
|
*
|
|
* @returns true if any tags were patched.
|
|
*/
|
|
function applyTagConfig(spec, tagConfigPath, label) {
|
|
const tagsCfg = loadYaml(tagConfigPath);
|
|
if (!tagsCfg || !tagsCfg.tags) {
|
|
log(`${label}: tags.yml has no 'tags' key — skipping`);
|
|
return false;
|
|
}
|
|
if (!Array.isArray(spec.tags))
|
|
spec.tags = [];
|
|
const operationTags = collectOperationTags(spec);
|
|
const configKeys = Object.keys(tagsCfg.tags);
|
|
// Warn: config references a tag not in the spec
|
|
for (const cfgKey of configKeys) {
|
|
const effectiveName = tagsCfg.tags[cfgKey]?.rename ?? cfgKey;
|
|
if (!operationTags.has(cfgKey) && !operationTags.has(effectiveName)) {
|
|
log(`WARN ${label}: config tag '${cfgKey}' not found in spec operations`);
|
|
}
|
|
}
|
|
// Warn: spec has operation tags with no config entry
|
|
Array.from(operationTags).forEach((opTag) => {
|
|
const hasEntry = configKeys.some((k) => k === opTag || tagsCfg.tags[k]?.rename === opTag);
|
|
if (!hasEntry) {
|
|
log(`WARN ${label}: spec tag '${opTag}' has no config entry in tags.yml`);
|
|
}
|
|
});
|
|
// Apply transformations
|
|
for (const [tagKey, cfg] of Object.entries(tagsCfg.tags)) {
|
|
if (cfg.rename) {
|
|
log(`${label}: renaming tag '${tagKey}' → '${cfg.rename}'`);
|
|
renameTag(spec, tagKey, cfg.rename);
|
|
}
|
|
const resolvedName = cfg.rename ?? tagKey;
|
|
let tagObj = spec.tags.find((t) => t.name === resolvedName);
|
|
if (!tagObj) {
|
|
tagObj = { name: resolvedName };
|
|
spec.tags.push(tagObj);
|
|
}
|
|
if (cfg.description !== undefined)
|
|
tagObj.description = cfg.description.trim();
|
|
if (cfg['x-traitTag'] !== undefined)
|
|
tagObj['x-traitTag'] = cfg['x-traitTag'];
|
|
if (cfg['x-related'] !== undefined)
|
|
tagObj['x-related'] = cfg['x-related'];
|
|
}
|
|
log(`${label}: patched ${configKeys.length} tag(s)`);
|
|
return true;
|
|
}
|
|
// ---------------------------------------------------------------------------
|
|
// Core logic
|
|
// ---------------------------------------------------------------------------
|
|
/**
|
|
* Process a single product directory: read `.config.yml`, find spec files,
|
|
* apply content overlays and tag configs, write resolved specs to _build/.
|
|
*
|
|
* Source specs in api-docs/ are never mutated. Resolved output goes to
|
|
* api-docs/_build/{productDir}/{specFile} for downstream consumers
|
|
* (Redoc HTML, generate-openapi-articles.ts).
|
|
*/
|
|
function processProduct(apiDocsRoot, productDir) {
|
|
const productAbsDir = path.join(apiDocsRoot, productDir);
|
|
const configPath = path.join(productAbsDir, '.config.yml');
|
|
const config = loadYaml(configPath);
|
|
if (!config || !config.apis) {
|
|
log(`${productDir}: no .config.yml or no 'apis' key — skipping`);
|
|
return;
|
|
}
|
|
for (const [apiKey, apiEntry] of Object.entries(config.apis)) {
|
|
const specRelPath = apiEntry.root;
|
|
const specAbsPath = path.join(productAbsDir, specRelPath);
|
|
const specDir = path.join(productAbsDir, path.dirname(specRelPath));
|
|
const label = path.join(productDir, specRelPath);
|
|
if (!fs.existsSync(specAbsPath)) {
|
|
log(`${label}: spec not found at ${specAbsPath} — skipping`);
|
|
continue;
|
|
}
|
|
// Load spec once
|
|
const spec = loadYaml(specAbsPath);
|
|
if (!spec) {
|
|
log(`${label}: failed to parse spec — skipping`);
|
|
continue;
|
|
}
|
|
// Apply all transforms
|
|
applyInfoOverlay(spec, specDir, productAbsDir, label);
|
|
applyServersOverlay(spec, specDir, productAbsDir, label);
|
|
const tagConfigPath = path.join(specDir, 'tags.yml');
|
|
if (fs.existsSync(tagConfigPath)) {
|
|
applyTagConfig(spec, tagConfigPath, label);
|
|
}
|
|
// Write resolved spec to _build/, mirroring the source path structure
|
|
const outPath = path.join(apiDocsRoot, BUILD_DIR, productDir, specRelPath);
|
|
const outDir = path.dirname(outPath);
|
|
if (!fs.existsSync(outDir)) {
|
|
fs.mkdirSync(outDir, { recursive: true });
|
|
}
|
|
writeYaml(outPath, spec);
|
|
log(`${label}: wrote ${path.relative(apiDocsRoot, outPath)}`);
|
|
}
|
|
}
|
|
// ---------------------------------------------------------------------------
|
|
// Entry point
|
|
// ---------------------------------------------------------------------------
|
|
function main() {
|
|
const args = process.argv.slice(2);
|
|
// Optional --root <path> flag for testing — overrides the default resolution.
|
|
let apiDocsRoot = path.resolve(__dirname, '../..'); // api-docs/scripts/dist -> api-docs/
|
|
let targetProduct;
|
|
for (let i = 0; i < args.length; i++) {
|
|
if (args[i] === '--root' && args[i + 1]) {
|
|
apiDocsRoot = path.resolve(args[i + 1]);
|
|
i++;
|
|
}
|
|
else {
|
|
targetProduct = args[i];
|
|
}
|
|
}
|
|
const products = targetProduct ? [targetProduct] : PRODUCT_DIRS;
|
|
let hasError = false;
|
|
for (const productDir of products) {
|
|
try {
|
|
processProduct(apiDocsRoot, productDir);
|
|
}
|
|
catch (err) {
|
|
log(`ERROR ${productDir}: ${err.message}`);
|
|
hasError = true;
|
|
}
|
|
}
|
|
process.exit(hasError ? 1 : 0);
|
|
}
|
|
main();
|
|
//# sourceMappingURL=post-process-specs.js.map
|