From 2fa00a36d2bf98d67227dfed7454782595c87cbc Mon Sep 17 00:00:00 2001 From: Jason Stirnaman Date: Fri, 12 Dec 2025 10:52:50 -0600 Subject: [PATCH] fix(api): Fix RapiDoc operation filtering and improve API reference quality - Restore original RapiDoc match-paths format (method /path) for proper filtering - Restrict operation tags to primary tag in tag-specific specs to prevent duplicates - Rename Token tag to Auth token for clarity in Core and Enterprise specs - Remove Table tag from cache operations (distinct_cache, last_cache) - Add build script combining API docs, Hugo, and Markdown generation - Skip summary rendering for conceptual pages - Add isConceptual check to hide operations in nav for conceptual pages --- api-docs/influxdb3/core/v3/ref.yml | 14 +- api-docs/influxdb3/enterprise/v3/ref.yml | 16 +- .../scripts/dist/generate-openapi-articles.js | 1039 +++++++-------- .../dist/openapi-paths-to-hugo-data/index.js | 1134 ++++++++--------- api-docs/scripts/generate-openapi-articles.ts | 1 + .../openapi-paths-to-hugo-data/index.ts | 6 +- assets/js/components/rapidoc-mini.ts | 1 + .../influxdb/influxdb3_core/articles.json | 188 +-- .../influxdb/influxdb3_core/articles.yml | 124 +- .../influxdb3_enterprise/articles.json | 212 ++- .../influxdb3_enterprise/articles.yml | 140 +- ...12-10-standalone-operation-pages-design.md | 171 +++ layouts/api/single.html | 12 +- layouts/partials/api/rapidoc-mini.html | 3 +- layouts/partials/sidebar/api-menu-items.html | 14 +- package.json | 1 + 16 files changed, 1470 insertions(+), 1606 deletions(-) create mode 100644 docs/plans/2024-12-10-standalone-operation-pages-design.md diff --git a/api-docs/influxdb3/core/v3/ref.yml b/api-docs/influxdb3/core/v3/ref.yml index a8d7c5469..e027f5af6 100644 --- a/api-docs/influxdb3/core/v3/ref.yml +++ b/api-docs/influxdb3/core/v3/ref.yml @@ -189,7 +189,7 @@ tags: description: Retrieve server metrics, status, and version information - name: Table description: Manage table schemas and data - - name: Token + - name: Auth token description: Manage tokens for authentication and authorization - name: Write data description: | @@ -1169,7 +1169,6 @@ paths: description: Creates a distinct cache for a table. tags: - Cache data - - Table requestBody: required: true content: @@ -1215,7 +1214,6 @@ paths: description: Cache not found. tags: - Cache data - - Table /api/v3/configure/last_cache: post: operationId: PostConfigureLastCache @@ -1240,7 +1238,6 @@ paths: description: Cache already exists. tags: - Cache data - - Table delete: operationId: DeleteConfigureLastCache summary: Delete last cache @@ -1270,7 +1267,6 @@ paths: description: Cache not found. tags: - Cache data - - Table /api/v3/configure/processing_engine_trigger: post: operationId: PostConfigureProcessingEngineTrigger @@ -1712,7 +1708,7 @@ paths: $ref: '#/components/responses/Unauthorized' tags: - Authentication - - Token + - Auth token /api/v3/configure/token/admin/regenerate: post: operationId: PostRegenerateAdminToken @@ -1731,7 +1727,7 @@ paths: $ref: '#/components/responses/Unauthorized' tags: - Authentication - - Token + - Auth token /api/v3/configure/token: delete: operationId: DeleteToken @@ -1755,7 +1751,7 @@ paths: description: Token not found. tags: - Authentication - - Token + - Auth token /api/v3/configure/token/named_admin: post: operationId: PostCreateNamedAdminToken @@ -1786,7 +1782,7 @@ paths: description: A token with this name already exists. tags: - Authentication - - Token + - Auth token /api/v3/plugins/files: put: operationId: PutPluginFile diff --git a/api-docs/influxdb3/enterprise/v3/ref.yml b/api-docs/influxdb3/enterprise/v3/ref.yml index b68a86a55..26236d7cd 100644 --- a/api-docs/influxdb3/enterprise/v3/ref.yml +++ b/api-docs/influxdb3/enterprise/v3/ref.yml @@ -158,7 +158,7 @@ tags: description: Retrieve server metrics, status, and version information - name: Table description: Manage table schemas and data - - name: Token + - name: Auth token description: Manage tokens for authentication and authorization - name: Write data description: | @@ -1191,7 +1191,6 @@ paths: description: Creates a distinct cache for a table. tags: - Cache data - - Table requestBody: required: true content: @@ -1236,7 +1235,6 @@ paths: description: Cache not found. tags: - Cache data - - Table /api/v3/configure/last_cache: post: operationId: PostConfigureLastCache @@ -1261,7 +1259,6 @@ paths: description: Cache already exists. tags: - Cache data - - Table delete: operationId: DeleteConfigureLastCache summary: Delete last cache @@ -1291,7 +1288,6 @@ paths: description: Cache not found. tags: - Cache data - - Table /api/v3/configure/processing_engine_trigger: post: operationId: PostConfigureProcessingEngineTrigger @@ -1732,7 +1728,7 @@ paths: $ref: '#/components/responses/Unauthorized' tags: - Authentication - - Token + - Auth token /api/v3/configure/token/admin: post: operationId: PostCreateAdminToken @@ -1753,7 +1749,7 @@ paths: $ref: '#/components/responses/Unauthorized' tags: - Authentication - - Token + - Auth token /api/v3/configure/token/admin/regenerate: post: operationId: PostRegenerateAdminToken @@ -1772,7 +1768,7 @@ paths: $ref: '#/components/responses/Unauthorized' tags: - Authentication - - Token + - Auth token /api/v3/configure/token: delete: operationId: DeleteToken @@ -1795,7 +1791,7 @@ paths: description: Token not found. tags: - Authentication - - Token + - Auth token /api/v3/configure/token/named_admin: post: operationId: PostCreateNamedAdminToken @@ -1825,7 +1821,7 @@ paths: description: A token with this name already exists. tags: - Authentication - - Token + - Auth token /api/v3/plugins/files: put: operationId: PutPluginFile diff --git a/api-docs/scripts/dist/generate-openapi-articles.js b/api-docs/scripts/dist/generate-openapi-articles.js index 828f8aacd..e6a185d30 100644 --- a/api-docs/scripts/dist/generate-openapi-articles.js +++ b/api-docs/scripts/dist/generate-openapi-articles.js @@ -1,5 +1,5 @@ #!/usr/bin/env node -'use strict'; +"use strict"; /** * Generate OpenAPI Articles Script * @@ -20,70 +20,47 @@ * * @module generate-openapi-articles */ -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; +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 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; + 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 }); +})(); +Object.defineProperty(exports, "__esModule", { value: true }); exports.productConfigs = void 0; exports.processProduct = processProduct; exports.generateDataFromOpenAPI = generateDataFromOpenAPI; exports.generatePagesFromArticleData = generatePagesFromArticleData; -const child_process_1 = require('child_process'); -const path = __importStar(require('path')); -const fs = __importStar(require('fs')); +const child_process_1 = require("child_process"); +const path = __importStar(require("path")); +const fs = __importStar(require("fs")); // Import the OpenAPI to Hugo converter const openapiPathsToHugo = require('./openapi-paths-to-hugo-data/index.js'); // Calculate the relative paths @@ -97,19 +74,20 @@ const API_DOCS_ROOT = 'api-docs'; * @throws Exits process with code 1 on error */ function execCommand(command, description) { - try { - if (description) { - console.log(`\n${description}...`); + try { + if (description) { + console.log(`\n${description}...`); + } + console.log(`Executing: ${command}\n`); + (0, child_process_1.execSync)(command, { stdio: 'inherit' }); } - console.log(`Executing: ${command}\n`); - (0, child_process_1.execSync)(command, { stdio: 'inherit' }); - } catch (error) { - console.error(`\n❌ Error executing command: ${command}`); - if (error instanceof Error) { - console.error(error.message); + catch (error) { + console.error(`\n❌ Error executing command: ${command}`); + if (error instanceof Error) { + console.error(error.message); + } + process.exit(1); } - process.exit(1); - } } /** * Generate a clean static directory name from a product key. @@ -119,12 +97,12 @@ function execCommand(command, description) { * @returns Clean directory name (e.g., 'influxdb-cloud-v2', 'influxdb3-core') */ function getStaticDirName(productKey) { - // For influxdb3_* products, convert underscore to hyphen and don't add prefix - if (productKey.startsWith('influxdb3_')) { - return productKey.replace('_', '-'); - } - // For other products, add 'influxdb-' prefix - return `influxdb-${productKey}`; + // For influxdb3_* products, convert underscore to hyphen and don't add prefix + if (productKey.startsWith('influxdb3_')) { + return productKey.replace('_', '-'); + } + // For other products, add 'influxdb-' prefix + return `influxdb-${productKey}`; } /** * Generate Hugo data files from OpenAPI specification @@ -134,14 +112,14 @@ function getStaticDirName(productKey) { * @param articleOutPath - Output path for article metadata */ function generateDataFromOpenAPI(specFile, dataOutPath, articleOutPath) { - if (!fs.existsSync(dataOutPath)) { - fs.mkdirSync(dataOutPath, { recursive: true }); - } - openapiPathsToHugo.generateHugoData({ - dataOutPath, - articleOutPath, - specFile, - }); + if (!fs.existsSync(dataOutPath)) { + fs.mkdirSync(dataOutPath, { recursive: true }); + } + openapiPathsToHugo.generateHugoData({ + dataOutPath, + articleOutPath, + specFile, + }); } /** * Generate Hugo content pages from article data @@ -152,122 +130,107 @@ function generateDataFromOpenAPI(specFile, dataOutPath, articleOutPath) { * @param options - Generation options */ function generatePagesFromArticleData(options) { - const { - articlesPath, - contentPath, - menuKey, - menuParent, - productDescription, - skipParentMenu, - } = options; - const yaml = require('js-yaml'); - const articlesFile = path.join(articlesPath, 'articles.yml'); - if (!fs.existsSync(articlesFile)) { - console.warn(`⚠️ Articles file not found: ${articlesFile}`); - return; - } - // Read articles data - const articlesContent = fs.readFileSync(articlesFile, 'utf8'); - const data = yaml.load(articlesContent); - if (!data.articles || !Array.isArray(data.articles)) { - console.warn(`⚠️ No articles found in ${articlesFile}`); - return; - } - // Ensure content directory exists - if (!fs.existsSync(contentPath)) { - fs.mkdirSync(contentPath, { recursive: true }); - } - // Determine the API parent directory from the first article's path - // e.g., if article path is "api/v1/health", the API root is "api" - const firstArticlePath = data.articles[0]?.path || ''; - const apiRootDir = firstArticlePath.split('/')[0]; - // Generate parent _index.md for the API section - if (apiRootDir) { - const apiParentDir = path.join(contentPath, apiRootDir); - const parentIndexFile = path.join(apiParentDir, '_index.md'); - if (!fs.existsSync(apiParentDir)) { - fs.mkdirSync(apiParentDir, { recursive: true }); + const { articlesPath, contentPath, menuKey, menuParent, productDescription, skipParentMenu, } = options; + const yaml = require('js-yaml'); + const articlesFile = path.join(articlesPath, 'articles.yml'); + if (!fs.existsSync(articlesFile)) { + console.warn(`⚠️ Articles file not found: ${articlesFile}`); + return; } - if (!fs.existsSync(parentIndexFile)) { - const parentFrontmatter = { - title: menuParent || 'HTTP API', - description: - productDescription || - 'API reference documentation for all available endpoints.', - weight: 104, - }; - // Add menu entry for parent page (unless skipParentMenu is true) - if (menuKey && !skipParentMenu) { - parentFrontmatter.menu = { - [menuKey]: { - name: menuParent || 'HTTP API', - }, - }; - } - const parentContent = `--- + // Read articles data + const articlesContent = fs.readFileSync(articlesFile, 'utf8'); + const data = yaml.load(articlesContent); + if (!data.articles || !Array.isArray(data.articles)) { + console.warn(`⚠️ No articles found in ${articlesFile}`); + return; + } + // Ensure content directory exists + if (!fs.existsSync(contentPath)) { + fs.mkdirSync(contentPath, { recursive: true }); + } + // Determine the API parent directory from the first article's path + // e.g., if article path is "api/v1/health", the API root is "api" + const firstArticlePath = data.articles[0]?.path || ''; + const apiRootDir = firstArticlePath.split('/')[0]; + // Generate parent _index.md for the API section + if (apiRootDir) { + const apiParentDir = path.join(contentPath, apiRootDir); + const parentIndexFile = path.join(apiParentDir, '_index.md'); + if (!fs.existsSync(apiParentDir)) { + fs.mkdirSync(apiParentDir, { recursive: true }); + } + if (!fs.existsSync(parentIndexFile)) { + const parentFrontmatter = { + title: menuParent || 'HTTP API', + description: productDescription || + 'API reference documentation for all available endpoints.', + weight: 104, + }; + // Add menu entry for parent page (unless skipParentMenu is true) + if (menuKey && !skipParentMenu) { + parentFrontmatter.menu = { + [menuKey]: { + name: menuParent || 'HTTP API', + }, + }; + } + const parentContent = `--- ${yaml.dump(parentFrontmatter)}--- `; - fs.writeFileSync(parentIndexFile, parentContent); - console.log(`✓ Generated parent index at ${parentIndexFile}`); + fs.writeFileSync(parentIndexFile, parentContent); + console.log(`✓ Generated parent index at ${parentIndexFile}`); + } } - } - // Generate a page for each article - for (const article of data.articles) { - const pagePath = path.join(contentPath, article.path); - const pageFile = path.join(pagePath, '_index.md'); - // Create directory if needed - if (!fs.existsSync(pagePath)) { - fs.mkdirSync(pagePath, { recursive: true }); - } - // Build frontmatter object - // Use menuName for display (actual endpoint path like /health) - // Fall back to name or path if menuName is not set - const displayName = - article.fields.menuName || article.fields.name || article.path; - const frontmatter = { - title: displayName, - description: `API reference for ${displayName}`, - type: 'api', - // Use explicit layout to override Hugo's default section template lookup - // (Hugo's section lookup ignores `type`, so we need `layout` for the 3-column API layout) - layout: 'list', - staticFilePath: article.fields.staticFilePath, - weight: 100, - }; - // Add menu entry if menuKey is provided - // Use menuName for menu display (shows actual endpoint path like /health) - if (menuKey) { - frontmatter.menu = { - [menuKey]: { - name: displayName, - ...(menuParent && { parent: menuParent }), - }, - }; - } - // Add related links if present in article fields - if ( - article.fields.related && - Array.isArray(article.fields.related) && - article.fields.related.length > 0 - ) { - frontmatter.related = article.fields.related; - } - // Add OpenAPI tags if present in article fields (for frontmatter metadata) - if ( - article.fields.apiTags && - Array.isArray(article.fields.apiTags) && - article.fields.apiTags.length > 0 - ) { - frontmatter.api_tags = article.fields.apiTags; - } - const pageContent = `--- + // Generate a page for each article + for (const article of data.articles) { + const pagePath = path.join(contentPath, article.path); + const pageFile = path.join(pagePath, '_index.md'); + // Create directory if needed + if (!fs.existsSync(pagePath)) { + fs.mkdirSync(pagePath, { recursive: true }); + } + // Build frontmatter object + // Use menuName for display (actual endpoint path like /health) + // Fall back to name or path if menuName is not set + const displayName = article.fields.menuName || article.fields.name || article.path; + const frontmatter = { + title: displayName, + description: `API reference for ${displayName}`, + type: 'api', + // Use explicit layout to override Hugo's default section template lookup + // (Hugo's section lookup ignores `type`, so we need `layout` for the 3-column API layout) + layout: 'list', + staticFilePath: article.fields.staticFilePath, + weight: 100, + }; + // Add menu entry if menuKey is provided + // Use menuName for menu display (shows actual endpoint path like /health) + if (menuKey) { + frontmatter.menu = { + [menuKey]: { + name: displayName, + ...(menuParent && { parent: menuParent }), + }, + }; + } + // Add related links if present in article fields + if (article.fields.related && + Array.isArray(article.fields.related) && + article.fields.related.length > 0) { + frontmatter.related = article.fields.related; + } + // Add OpenAPI tags if present in article fields (for frontmatter metadata) + if (article.fields.apiTags && + Array.isArray(article.fields.apiTags) && + article.fields.apiTags.length > 0) { + frontmatter.api_tags = article.fields.apiTags; + } + const pageContent = `--- ${yaml.dump(frontmatter)}--- `; - fs.writeFileSync(pageFile, pageContent); - } - console.log( - `✓ Generated ${data.articles.length} content pages in ${contentPath}` - ); + fs.writeFileSync(pageFile, pageContent); + } + console.log(`✓ Generated ${data.articles.length} content pages in ${contentPath}`); } /** * Generate Hugo content pages from tag-based article data @@ -279,119 +242,105 @@ ${yaml.dump(frontmatter)}--- * @param options - Generation options */ function generateTagPagesFromArticleData(options) { - const { - articlesPath, - contentPath, - menuKey, - menuParent, - productDescription, - skipParentMenu, - } = options; - const yaml = require('js-yaml'); - const articlesFile = path.join(articlesPath, 'articles.yml'); - if (!fs.existsSync(articlesFile)) { - console.warn(`⚠️ Articles file not found: ${articlesFile}`); - return; - } - // Read articles data - const articlesContent = fs.readFileSync(articlesFile, 'utf8'); - const data = yaml.load(articlesContent); - if (!data.articles || !Array.isArray(data.articles)) { - console.warn(`⚠️ No articles found in ${articlesFile}`); - return; - } - // Ensure content directory exists - if (!fs.existsSync(contentPath)) { - fs.mkdirSync(contentPath, { recursive: true }); - } - // Generate parent _index.md for the API section - const apiParentDir = path.join(contentPath, 'api'); - const parentIndexFile = path.join(apiParentDir, '_index.md'); - if (!fs.existsSync(apiParentDir)) { - fs.mkdirSync(apiParentDir, { recursive: true }); - } - if (!fs.existsSync(parentIndexFile)) { - const parentFrontmatter = { - title: menuParent || 'HTTP API', - description: - productDescription || - 'API reference documentation for all available endpoints.', - weight: 104, - }; - // Add menu entry for parent page (unless skipParentMenu is true) - if (menuKey && !skipParentMenu) { - parentFrontmatter.menu = { - [menuKey]: { - name: menuParent || 'HTTP API', - }, - }; + const { articlesPath, contentPath, menuKey, menuParent, productDescription, skipParentMenu, } = options; + const yaml = require('js-yaml'); + const articlesFile = path.join(articlesPath, 'articles.yml'); + if (!fs.existsSync(articlesFile)) { + console.warn(`⚠️ Articles file not found: ${articlesFile}`); + return; } - const parentContent = `--- + // Read articles data + const articlesContent = fs.readFileSync(articlesFile, 'utf8'); + const data = yaml.load(articlesContent); + if (!data.articles || !Array.isArray(data.articles)) { + console.warn(`⚠️ No articles found in ${articlesFile}`); + return; + } + // Ensure content directory exists + if (!fs.existsSync(contentPath)) { + fs.mkdirSync(contentPath, { recursive: true }); + } + // Generate parent _index.md for the API section + const apiParentDir = path.join(contentPath, 'api'); + const parentIndexFile = path.join(apiParentDir, '_index.md'); + if (!fs.existsSync(apiParentDir)) { + fs.mkdirSync(apiParentDir, { recursive: true }); + } + if (!fs.existsSync(parentIndexFile)) { + const parentFrontmatter = { + title: menuParent || 'HTTP API', + description: productDescription || + 'API reference documentation for all available endpoints.', + weight: 104, + }; + // Add menu entry for parent page (unless skipParentMenu is true) + if (menuKey && !skipParentMenu) { + parentFrontmatter.menu = { + [menuKey]: { + name: menuParent || 'HTTP API', + }, + }; + } + const parentContent = `--- ${yaml.dump(parentFrontmatter)}--- `; - fs.writeFileSync(parentIndexFile, parentContent); - console.log(`✓ Generated parent index at ${parentIndexFile}`); - } - // Generate a page for each article (tag) - for (const article of data.articles) { - const pagePath = path.join(contentPath, article.path); - const pageFile = path.join(pagePath, '_index.md'); - // Create directory if needed - if (!fs.existsSync(pagePath)) { - fs.mkdirSync(pagePath, { recursive: true }); + fs.writeFileSync(parentIndexFile, parentContent); + console.log(`✓ Generated parent index at ${parentIndexFile}`); } - // Build frontmatter object - const title = article.fields.title || article.fields.name || article.path; - const isConceptual = article.fields.isConceptual === true; - const frontmatter = { - title, - description: article.fields.description || `API reference for ${title}`, - type: 'api', - layout: isConceptual ? 'single' : 'list', - staticFilePath: article.fields.staticFilePath, - weight: 100, - // Tag-based fields - tag: article.fields.tag, - isConceptual, - menuGroup: article.fields.menuGroup, - }; - // Add operations for TOC generation (only for non-conceptual pages) - if ( - !isConceptual && - article.fields.operations && - article.fields.operations.length > 0 - ) { - frontmatter.operations = article.fields.operations; - } - // Add tag description for conceptual pages - if (isConceptual && article.fields.tagDescription) { - frontmatter.tagDescription = article.fields.tagDescription; - } - // Note: We deliberately don't add menu entries for tag-based API pages. - // The API sidebar navigation (api/sidebar-nav.html) handles navigation - // for API reference pages, avoiding conflicts with existing menu items - // like "Query data" and "Write data" that exist in the main sidebar. - // Add related links if present in article fields - if ( - article.fields.related && - Array.isArray(article.fields.related) && - article.fields.related.length > 0 - ) { - frontmatter.related = article.fields.related; - } - const pageContent = `--- + // Generate a page for each article (tag) + for (const article of data.articles) { + const pagePath = path.join(contentPath, article.path); + const pageFile = path.join(pagePath, '_index.md'); + // Create directory if needed + if (!fs.existsSync(pagePath)) { + fs.mkdirSync(pagePath, { recursive: true }); + } + // Build frontmatter object + const title = article.fields.title || article.fields.name || article.path; + const isConceptual = article.fields.isConceptual === true; + const frontmatter = { + title, + description: article.fields.description || `API reference for ${title}`, + type: 'api', + layout: isConceptual ? 'single' : 'list', + staticFilePath: article.fields.staticFilePath, + weight: 100, + // Tag-based fields + tag: article.fields.tag, + isConceptual, + menuGroup: article.fields.menuGroup, + }; + // Add operations for TOC generation (only for non-conceptual pages) + if (!isConceptual && + article.fields.operations && + article.fields.operations.length > 0) { + frontmatter.operations = article.fields.operations; + } + // Add tag description for conceptual pages + if (isConceptual && article.fields.tagDescription) { + frontmatter.tagDescription = article.fields.tagDescription; + } + // Note: We deliberately don't add menu entries for tag-based API pages. + // The API sidebar navigation (api/sidebar-nav.html) handles navigation + // for API reference pages, avoiding conflicts with existing menu items + // like "Query data" and "Write data" that exist in the main sidebar. + // Add related links if present in article fields + if (article.fields.related && + Array.isArray(article.fields.related) && + article.fields.related.length > 0) { + frontmatter.related = article.fields.related; + } + const pageContent = `--- ${yaml.dump(frontmatter)}--- `; - fs.writeFileSync(pageFile, pageContent); - } - console.log( - `✓ Generated ${data.articles.length} tag-based content pages in ${contentPath}` - ); - // Generate individual operation pages for standalone URLs - generateOperationPages({ - articlesPath, - contentPath, - }); + fs.writeFileSync(pageFile, pageContent); + } + console.log(`✓ Generated ${data.articles.length} tag-based content pages in ${contentPath}`); + // Generate individual operation pages for standalone URLs + generateOperationPages({ + articlesPath, + contentPath, + }); } /** * Convert API path to URL-safe slug @@ -403,8 +352,8 @@ ${yaml.dump(frontmatter)}--- * @returns URL-safe path slug (e.g., "write", "api/v3/write_lp") */ function apiPathToSlug(apiPath) { - // Remove leading slash, keep underscores (they're URL-safe) - return apiPath.replace(/^\//, ''); + // Remove leading slash, keep underscores (they're URL-safe) + return apiPath.replace(/^\//, ''); } /** * Generate standalone Hugo content pages for each API operation @@ -415,76 +364,75 @@ function apiPathToSlug(apiPath) { * @param options - Generation options */ function generateOperationPages(options) { - const { articlesPath, contentPath } = options; - const yaml = require('js-yaml'); - const articlesFile = path.join(articlesPath, 'articles.yml'); - if (!fs.existsSync(articlesFile)) { - console.warn(`⚠️ Articles file not found: ${articlesFile}`); - return; - } - // Read articles data - const articlesContent = fs.readFileSync(articlesFile, 'utf8'); - const data = yaml.load(articlesContent); - if (!data.articles || !Array.isArray(data.articles)) { - console.warn(`⚠️ No articles found in ${articlesFile}`); - return; - } - let operationCount = 0; - // Process each article (tag) and generate pages for its operations - for (const article of data.articles) { - // Skip conceptual articles (they don't have operations) - if (article.fields.isConceptual) { - continue; + const { articlesPath, contentPath } = options; + const yaml = require('js-yaml'); + const articlesFile = path.join(articlesPath, 'articles.yml'); + if (!fs.existsSync(articlesFile)) { + console.warn(`⚠️ Articles file not found: ${articlesFile}`); + return; } - const operations = article.fields.operations || []; - const tagSpecFile = article.fields.staticFilePath; - const tagName = article.fields.tag || article.fields.name || ''; - for (const op of operations) { - // Build operation page path: api/{path}/{method}/ - // e.g., /write -> api/write/post/ - // e.g., /api/v3/write_lp -> api/api/v3/write_lp/post/ - const pathSlug = apiPathToSlug(op.path); - const method = op.method.toLowerCase(); - const operationDir = path.join(contentPath, 'api', pathSlug, method); - const operationFile = path.join(operationDir, '_index.md'); - // Create directory if needed - if (!fs.existsSync(operationDir)) { - fs.mkdirSync(operationDir, { recursive: true }); - } - // Build frontmatter - const title = op.summary || `${op.method} ${op.path}`; - const frontmatter = { - title, - description: `API reference for ${op.method} ${op.path}`, - type: 'api-operation', - layout: 'operation', - // RapiDoc Mini configuration - specFile: tagSpecFile, - matchPaths: `${method} ${op.path}`, - // Operation metadata - operationId: op.operationId, - method: op.method, - apiPath: op.path, - tag: tagName, - }; - // Add compatibility version if present - if (op.compatVersion) { - frontmatter.compatVersion = op.compatVersion; - } - // Add related links from operation's externalDocs - if (op.externalDocs?.url) { - frontmatter.related = [op.externalDocs.url]; - } - const pageContent = `--- + // Read articles data + const articlesContent = fs.readFileSync(articlesFile, 'utf8'); + const data = yaml.load(articlesContent); + if (!data.articles || !Array.isArray(data.articles)) { + console.warn(`⚠️ No articles found in ${articlesFile}`); + return; + } + let operationCount = 0; + // Process each article (tag) and generate pages for its operations + for (const article of data.articles) { + // Skip conceptual articles (they don't have operations) + if (article.fields.isConceptual) { + continue; + } + const operations = article.fields.operations || []; + const tagSpecFile = article.fields.staticFilePath; + const tagName = article.fields.tag || article.fields.name || ''; + for (const op of operations) { + // Build operation page path: api/{path}/{method}/ + // e.g., /write -> api/write/post/ + // e.g., /api/v3/write_lp -> api/api/v3/write_lp/post/ + const pathSlug = apiPathToSlug(op.path); + const method = op.method.toLowerCase(); + const operationDir = path.join(contentPath, 'api', pathSlug, method); + const operationFile = path.join(operationDir, '_index.md'); + // Create directory if needed + if (!fs.existsSync(operationDir)) { + fs.mkdirSync(operationDir, { recursive: true }); + } + // Build frontmatter + const title = op.summary || `${op.method} ${op.path}`; + const frontmatter = { + title, + description: `API reference for ${op.method} ${op.path}`, + type: 'api-operation', + layout: 'operation', + // RapiDoc Mini configuration + specFile: tagSpecFile, + // RapiDoc match-paths format: "method /path" (e.g., "post /write") + matchPaths: `${method} ${op.path}`, + // Operation metadata + operationId: op.operationId, + method: op.method, + apiPath: op.path, + tag: tagName, + }; + // Add compatibility version if present + if (op.compatVersion) { + frontmatter.compatVersion = op.compatVersion; + } + // Add related links from operation's externalDocs + if (op.externalDocs?.url) { + frontmatter.related = [op.externalDocs.url]; + } + const pageContent = `--- ${yaml.dump(frontmatter)}--- `; - fs.writeFileSync(operationFile, pageContent); - operationCount++; + fs.writeFileSync(operationFile, pageContent); + operationCount++; + } } - } - console.log( - `✓ Generated ${operationCount} operation pages in ${contentPath}/api/` - ); + console.log(`✓ Generated ${operationCount} operation pages in ${contentPath}/api/`); } /** * Product configurations for all InfluxDB editions @@ -492,71 +440,62 @@ ${yaml.dump(frontmatter)}--- * Maps product identifiers to their OpenAPI specs and content directories */ const productConfigs = { - // TODO: v2 products (cloud-v2, oss-v2) are disabled for now because they - // have existing Redoc-based API reference at /reference/api/ - // Uncomment when ready to migrate v2 products to Scalar - // 'cloud-v2': { - // specFile: path.join(API_DOCS_ROOT, 'influxdb/cloud/v2/ref.yml'), - // pagesDir: path.join(DOCS_ROOT, 'content/influxdb/cloud/api'), - // description: 'InfluxDB Cloud (v2 API)', - // menuKey: 'influxdb_cloud', - // }, - // 'oss-v2': { - // specFile: path.join(API_DOCS_ROOT, 'influxdb/v2/v2/ref.yml'), - // pagesDir: path.join(DOCS_ROOT, 'content/influxdb/v2/api'), - // description: 'InfluxDB OSS v2', - // menuKey: 'influxdb_v2', - // }, - // InfluxDB 3 products use tag-based generation for better UX - // Keys use underscores to match Hugo data directory structure - influxdb3_core: { - specFile: path.join(API_DOCS_ROOT, 'influxdb3/core/v3/ref.yml'), - pagesDir: path.join(DOCS_ROOT, 'content/influxdb3/core'), - description: 'InfluxDB 3 Core', - menuKey: 'influxdb3_core', - useTagBasedGeneration: true, - }, - influxdb3_enterprise: { - specFile: path.join(API_DOCS_ROOT, 'influxdb3/enterprise/v3/ref.yml'), - pagesDir: path.join(DOCS_ROOT, 'content/influxdb3/enterprise'), - description: 'InfluxDB 3 Enterprise', - menuKey: 'influxdb3_enterprise', - useTagBasedGeneration: true, - }, - // Note: Cloud Dedicated, Serverless, and Clustered use management APIs - // with paths like /accounts/{accountId}/... so we put them under /api/ - // These products have existing /reference/api/ pages with menu entries, - // so we skip adding menu entries to the generated parent pages. - 'cloud-dedicated': { - specFile: path.join( - API_DOCS_ROOT, - 'influxdb3/cloud-dedicated/management/openapi.yml' - ), - pagesDir: path.join(DOCS_ROOT, 'content/influxdb3/cloud-dedicated/api'), - description: 'InfluxDB Cloud Dedicated', - menuKey: 'influxdb3_cloud_dedicated', - skipParentMenu: true, - }, - 'cloud-serverless': { - specFile: path.join( - API_DOCS_ROOT, - 'influxdb3/cloud-serverless/management/openapi.yml' - ), - pagesDir: path.join(DOCS_ROOT, 'content/influxdb3/cloud-serverless/api'), - description: 'InfluxDB Cloud Serverless', - menuKey: 'influxdb3_cloud_serverless', - skipParentMenu: true, - }, - clustered: { - specFile: path.join( - API_DOCS_ROOT, - 'influxdb3/clustered/management/openapi.yml' - ), - pagesDir: path.join(DOCS_ROOT, 'content/influxdb3/clustered/api'), - description: 'InfluxDB Clustered', - menuKey: 'influxdb3_clustered', - skipParentMenu: true, - }, + // TODO: v2 products (cloud-v2, oss-v2) are disabled for now because they + // have existing Redoc-based API reference at /reference/api/ + // Uncomment when ready to migrate v2 products to Scalar + // 'cloud-v2': { + // specFile: path.join(API_DOCS_ROOT, 'influxdb/cloud/v2/ref.yml'), + // pagesDir: path.join(DOCS_ROOT, 'content/influxdb/cloud/api'), + // description: 'InfluxDB Cloud (v2 API)', + // menuKey: 'influxdb_cloud', + // }, + // 'oss-v2': { + // specFile: path.join(API_DOCS_ROOT, 'influxdb/v2/v2/ref.yml'), + // pagesDir: path.join(DOCS_ROOT, 'content/influxdb/v2/api'), + // description: 'InfluxDB OSS v2', + // menuKey: 'influxdb_v2', + // }, + // InfluxDB 3 products use tag-based generation for better UX + // Keys use underscores to match Hugo data directory structure + influxdb3_core: { + specFile: path.join(API_DOCS_ROOT, 'influxdb3/core/v3/ref.yml'), + pagesDir: path.join(DOCS_ROOT, 'content/influxdb3/core'), + description: 'InfluxDB 3 Core', + menuKey: 'influxdb3_core', + useTagBasedGeneration: true, + }, + influxdb3_enterprise: { + specFile: path.join(API_DOCS_ROOT, 'influxdb3/enterprise/v3/ref.yml'), + pagesDir: path.join(DOCS_ROOT, 'content/influxdb3/enterprise'), + description: 'InfluxDB 3 Enterprise', + menuKey: 'influxdb3_enterprise', + useTagBasedGeneration: true, + }, + // Note: Cloud Dedicated, Serverless, and Clustered use management APIs + // with paths like /accounts/{accountId}/... so we put them under /api/ + // These products have existing /reference/api/ pages with menu entries, + // so we skip adding menu entries to the generated parent pages. + 'cloud-dedicated': { + specFile: path.join(API_DOCS_ROOT, 'influxdb3/cloud-dedicated/management/openapi.yml'), + pagesDir: path.join(DOCS_ROOT, 'content/influxdb3/cloud-dedicated/api'), + description: 'InfluxDB Cloud Dedicated', + menuKey: 'influxdb3_cloud_dedicated', + skipParentMenu: true, + }, + 'cloud-serverless': { + specFile: path.join(API_DOCS_ROOT, 'influxdb3/cloud-serverless/management/openapi.yml'), + pagesDir: path.join(DOCS_ROOT, 'content/influxdb3/cloud-serverless/api'), + description: 'InfluxDB Cloud Serverless', + menuKey: 'influxdb3_cloud_serverless', + skipParentMenu: true, + }, + clustered: { + specFile: path.join(API_DOCS_ROOT, 'influxdb3/clustered/management/openapi.yml'), + pagesDir: path.join(DOCS_ROOT, 'content/influxdb3/clustered/api'), + description: 'InfluxDB Clustered', + menuKey: 'influxdb3_clustered', + skipParentMenu: true, + }, }; exports.productConfigs = productConfigs; /** @@ -566,140 +505,128 @@ exports.productConfigs = productConfigs; * @param config - Product configuration */ function processProduct(productKey, config) { - console.log('\n' + '='.repeat(80)); - console.log(`Processing ${config.description || productKey}`); - console.log('='.repeat(80)); - const staticPath = path.join(DOCS_ROOT, 'static/openapi'); - const staticDirName = getStaticDirName(productKey); - const staticSpecPath = path.join(staticPath, `${staticDirName}.yml`); - const staticJsonSpecPath = path.join(staticPath, `${staticDirName}.json`); - const staticPathsPath = path.join(staticPath, `${staticDirName}/paths`); - const articlesPath = path.join( - DOCS_ROOT, - `data/article_data/influxdb/${productKey}` - ); - // Check if spec file exists - if (!fs.existsSync(config.specFile)) { - console.warn(`⚠️ Spec file not found: ${config.specFile}`); - console.log('Skipping this product. Run getswagger.sh first if needed.\n'); - return; - } - try { - // Step 1: Execute the getswagger.sh script to fetch/bundle the spec - // Note: getswagger.sh must run from api-docs/ because it uses relative paths - const getswaggerScript = path.join(API_DOCS_ROOT, 'getswagger.sh'); - if (fs.existsSync(getswaggerScript)) { - execCommand( - `cd ${API_DOCS_ROOT} && ./getswagger.sh ${productKey} -B`, - `Fetching OpenAPI spec for ${productKey}` - ); - } else { - console.log(`⚠️ getswagger.sh not found, skipping fetch step`); + console.log('\n' + '='.repeat(80)); + console.log(`Processing ${config.description || productKey}`); + console.log('='.repeat(80)); + const staticPath = path.join(DOCS_ROOT, 'static/openapi'); + const staticDirName = getStaticDirName(productKey); + const staticSpecPath = path.join(staticPath, `${staticDirName}.yml`); + const staticJsonSpecPath = path.join(staticPath, `${staticDirName}.json`); + const staticPathsPath = path.join(staticPath, `${staticDirName}/paths`); + const articlesPath = path.join(DOCS_ROOT, `data/article_data/influxdb/${productKey}`); + // Check if spec file exists + if (!fs.existsSync(config.specFile)) { + console.warn(`⚠️ Spec file not found: ${config.specFile}`); + console.log('Skipping this product. Run getswagger.sh first if needed.\n'); + return; } - // Step 2: Ensure static directory exists - if (!fs.existsSync(staticPath)) { - fs.mkdirSync(staticPath, { recursive: true }); + try { + // Step 1: Execute the getswagger.sh script to fetch/bundle the spec + // Note: getswagger.sh must run from api-docs/ because it uses relative paths + const getswaggerScript = path.join(API_DOCS_ROOT, 'getswagger.sh'); + if (fs.existsSync(getswaggerScript)) { + execCommand(`cd ${API_DOCS_ROOT} && ./getswagger.sh ${productKey} -B`, `Fetching OpenAPI spec for ${productKey}`); + } + else { + console.log(`⚠️ getswagger.sh not found, skipping fetch step`); + } + // Step 2: Ensure static directory exists + if (!fs.existsSync(staticPath)) { + fs.mkdirSync(staticPath, { recursive: true }); + } + // Step 3: Copy the generated OpenAPI spec to static folder (YAML) + if (fs.existsSync(config.specFile)) { + fs.copyFileSync(config.specFile, staticSpecPath); + console.log(`✓ Copied spec to ${staticSpecPath}`); + // Step 4: Generate JSON version of the spec + try { + const yaml = require('js-yaml'); + const specContent = fs.readFileSync(config.specFile, 'utf8'); + const specObject = yaml.load(specContent); + fs.writeFileSync(staticJsonSpecPath, JSON.stringify(specObject, null, 2)); + console.log(`✓ Generated JSON spec at ${staticJsonSpecPath}`); + } + catch (jsonError) { + console.warn(`⚠️ Could not generate JSON spec: ${jsonError}`); + } + } + // Step 5: Generate Hugo data from OpenAPI spec + if (config.useTagBasedGeneration) { + // Tag-based generation: group operations by OpenAPI tag + const staticTagsPath = path.join(staticPath, `${staticDirName}/tags`); + console.log(`\n📋 Using tag-based generation for ${productKey}...`); + openapiPathsToHugo.generateHugoDataByTag({ + specFile: config.specFile, + dataOutPath: staticTagsPath, + articleOutPath: articlesPath, + includePaths: true, // Also generate path-based files for backwards compatibility + }); + // Step 6: Generate Hugo content pages from tag-based article data + generateTagPagesFromArticleData({ + articlesPath, + contentPath: config.pagesDir, + menuKey: config.menuKey, + menuParent: 'InfluxDB HTTP API', + skipParentMenu: config.skipParentMenu, + }); + } + else { + // Path-based generation: group paths by URL prefix (legacy) + generateDataFromOpenAPI(config.specFile, staticPathsPath, articlesPath); + // Step 6: Generate Hugo content pages from path-based article data + generatePagesFromArticleData({ + articlesPath, + contentPath: config.pagesDir, + menuKey: config.menuKey, + menuParent: 'InfluxDB HTTP API', + skipParentMenu: config.skipParentMenu, + }); + } + console.log(`\n✅ Successfully processed ${config.description || productKey}\n`); } - // Step 3: Copy the generated OpenAPI spec to static folder (YAML) - if (fs.existsSync(config.specFile)) { - fs.copyFileSync(config.specFile, staticSpecPath); - console.log(`✓ Copied spec to ${staticSpecPath}`); - // Step 4: Generate JSON version of the spec - try { - const yaml = require('js-yaml'); - const specContent = fs.readFileSync(config.specFile, 'utf8'); - const specObject = yaml.load(specContent); - fs.writeFileSync( - staticJsonSpecPath, - JSON.stringify(specObject, null, 2) - ); - console.log(`✓ Generated JSON spec at ${staticJsonSpecPath}`); - } catch (jsonError) { - console.warn(`⚠️ Could not generate JSON spec: ${jsonError}`); - } + catch (error) { + console.error(`\n❌ Error processing ${productKey}:`, error); + process.exit(1); } - // Step 5: Generate Hugo data from OpenAPI spec - if (config.useTagBasedGeneration) { - // Tag-based generation: group operations by OpenAPI tag - const staticTagsPath = path.join(staticPath, `${staticDirName}/tags`); - console.log(`\n📋 Using tag-based generation for ${productKey}...`); - openapiPathsToHugo.generateHugoDataByTag({ - specFile: config.specFile, - dataOutPath: staticTagsPath, - articleOutPath: articlesPath, - includePaths: true, // Also generate path-based files for backwards compatibility - }); - // Step 6: Generate Hugo content pages from tag-based article data - generateTagPagesFromArticleData({ - articlesPath, - contentPath: config.pagesDir, - menuKey: config.menuKey, - menuParent: 'InfluxDB HTTP API', - skipParentMenu: config.skipParentMenu, - }); - } else { - // Path-based generation: group paths by URL prefix (legacy) - generateDataFromOpenAPI(config.specFile, staticPathsPath, articlesPath); - // Step 6: Generate Hugo content pages from path-based article data - generatePagesFromArticleData({ - articlesPath, - contentPath: config.pagesDir, - menuKey: config.menuKey, - menuParent: 'InfluxDB HTTP API', - skipParentMenu: config.skipParentMenu, - }); - } - console.log( - `\n✅ Successfully processed ${config.description || productKey}\n` - ); - } catch (error) { - console.error(`\n❌ Error processing ${productKey}:`, error); - process.exit(1); - } } /** * Main execution function */ function main() { - const args = process.argv.slice(2); - // Determine which products to process - let productsToProcess; - if (args.length === 0) { - // No arguments: process all products - productsToProcess = Object.keys(productConfigs); - console.log('\n📋 Processing all products...\n'); - } else { - // Arguments provided: process only specified products - productsToProcess = args; - console.log( - `\n📋 Processing specified products: ${productsToProcess.join(', ')}\n` - ); - } - // Validate product keys - const invalidProducts = productsToProcess.filter( - (key) => !productConfigs[key] - ); - if (invalidProducts.length > 0) { - console.error( - `\n❌ Invalid product identifier(s): ${invalidProducts.join(', ')}` - ); - console.error('\nValid products:'); - Object.keys(productConfigs).forEach((key) => { - console.error(` - ${key}: ${productConfigs[key].description}`); + const args = process.argv.slice(2); + // Determine which products to process + let productsToProcess; + if (args.length === 0) { + // No arguments: process all products + productsToProcess = Object.keys(productConfigs); + console.log('\n📋 Processing all products...\n'); + } + else { + // Arguments provided: process only specified products + productsToProcess = args; + console.log(`\n📋 Processing specified products: ${productsToProcess.join(', ')}\n`); + } + // Validate product keys + const invalidProducts = productsToProcess.filter((key) => !productConfigs[key]); + if (invalidProducts.length > 0) { + console.error(`\n❌ Invalid product identifier(s): ${invalidProducts.join(', ')}`); + console.error('\nValid products:'); + Object.keys(productConfigs).forEach((key) => { + console.error(` - ${key}: ${productConfigs[key].description}`); + }); + process.exit(1); + } + // Process each product + productsToProcess.forEach((productKey) => { + const config = productConfigs[productKey]; + processProduct(productKey, config); }); - process.exit(1); - } - // Process each product - productsToProcess.forEach((productKey) => { - const config = productConfigs[productKey]; - processProduct(productKey, config); - }); - console.log('\n' + '='.repeat(80)); - console.log('✅ All products processed successfully!'); - console.log('='.repeat(80) + '\n'); + console.log('\n' + '='.repeat(80)); + console.log('✅ All products processed successfully!'); + console.log('='.repeat(80) + '\n'); } // Execute if run directly if (require.main === module) { - main(); + main(); } -//# sourceMappingURL=generate-openapi-articles.js.map +//# sourceMappingURL=generate-openapi-articles.js.map \ No newline at end of file diff --git a/api-docs/scripts/dist/openapi-paths-to-hugo-data/index.js b/api-docs/scripts/dist/openapi-paths-to-hugo-data/index.js index f6a4e2e97..9ca311816 100644 --- a/api-docs/scripts/dist/openapi-paths-to-hugo-data/index.js +++ b/api-docs/scripts/dist/openapi-paths-to-hugo-data/index.js @@ -1,4 +1,4 @@ -'use strict'; +"use strict"; /** * OpenAPI to Hugo Data Converter * @@ -7,68 +7,45 @@ * * @module openapi-paths-to-hugo-data */ -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; +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 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; + 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 }); +})(); +Object.defineProperty(exports, "__esModule", { value: true }); exports.generateHugoDataByTag = generateHugoDataByTag; exports.generateHugoData = generateHugoData; -const yaml = __importStar(require('js-yaml')); -const fs = __importStar(require('fs')); -const path = __importStar(require('path')); +const yaml = __importStar(require("js-yaml")); +const fs = __importStar(require("fs")); +const path = __importStar(require("path")); /** * Read a YAML file and parse it * @@ -77,8 +54,8 @@ const path = __importStar(require('path')); * @returns Parsed YAML content */ function readFile(filepath, encoding = 'utf8') { - const content = fs.readFileSync(filepath, encoding); - return yaml.load(content); + const content = fs.readFileSync(filepath, encoding); + return yaml.load(content); } /** * Write data to a YAML file @@ -87,7 +64,7 @@ function readFile(filepath, encoding = 'utf8') { * @param outputTo - Output file path */ function writeDataFile(data, outputTo) { - fs.writeFileSync(outputTo, yaml.dump(data)); + fs.writeFileSync(outputTo, yaml.dump(data)); } /** * Write data to a JSON file @@ -96,22 +73,22 @@ function writeDataFile(data, outputTo) { * @param outputTo - Output file path */ function writeJsonFile(data, outputTo) { - fs.writeFileSync(outputTo, JSON.stringify(data, null, 2)); + fs.writeFileSync(outputTo, JSON.stringify(data, null, 2)); } /** * OpenAPI utility functions */ const openapiUtils = { - /** - * Check if a path fragment is a placeholder (e.g., {id}) - * - * @param str - Path fragment to check - * @returns True if the fragment is a placeholder - */ - isPlaceholderFragment(str) { - const placeholderRegex = /^\{.*\}$/; - return placeholderRegex.test(str); - }, + /** + * Check if a path fragment is a placeholder (e.g., {id}) + * + * @param str - Path fragment to check + * @returns True if the fragment is a placeholder + */ + isPlaceholderFragment(str) { + const placeholderRegex = /^\{.*\}$/; + return placeholderRegex.test(str); + }, }; /** * Convert tag name to URL-friendly slug @@ -120,35 +97,35 @@ const openapiUtils = { * @returns URL-friendly slug (e.g., "write-data", "processing-engine") */ function slugifyTag(tagName) { - return tagName - .toLowerCase() - .replace(/[^a-z0-9]+/g, '-') - .replace(/^-|-$/g, ''); + return tagName + .toLowerCase() + .replace(/[^a-z0-9]+/g, '-') + .replace(/^-|-$/g, ''); } /** * Menu group mappings for tag-based navigation * Maps OpenAPI tags to sidebar groups */ const TAG_MENU_GROUPS = { - // Concepts group - 'Quick start': 'Concepts', - Authentication: 'Concepts', - 'Headers and parameters': 'Concepts', - 'Response codes': 'Concepts', - // Data Operations group - 'Write data': 'Data Operations', - 'Query data': 'Data Operations', - 'Cache data': 'Data Operations', - // Administration group - Database: 'Administration', - Table: 'Administration', - Token: 'Administration', - // Processing Engine group - 'Processing engine': 'Processing Engine', - // Server group - 'Server information': 'Server', - // Compatibility group - 'Compatibility endpoints': 'Compatibility', + // Concepts group + 'Quick start': 'Concepts', + Authentication: 'Concepts', + 'Headers and parameters': 'Concepts', + 'Response codes': 'Concepts', + // Data Operations group + 'Write data': 'Data Operations', + 'Query data': 'Data Operations', + 'Cache data': 'Data Operations', + // Administration group + Database: 'Administration', + Table: 'Administration', + Token: 'Administration', + // Processing Engine group + 'Processing engine': 'Processing Engine', + // Server group + 'Server information': 'Server', + // Compatibility group + 'Compatibility endpoints': 'Compatibility', }; /** * Get menu group for a tag @@ -157,20 +134,20 @@ const TAG_MENU_GROUPS = { * @returns Menu group name or 'Other' if not mapped */ function getMenuGroupForTag(tagName) { - return TAG_MENU_GROUPS[tagName] || 'Other'; + return TAG_MENU_GROUPS[tagName] || 'Other'; } /** * HTTP methods to check for operations */ const HTTP_METHODS = [ - 'get', - 'post', - 'put', - 'patch', - 'delete', - 'options', - 'head', - 'trace', + 'get', + 'post', + 'put', + 'patch', + 'delete', + 'options', + 'head', + 'trace', ]; /** * Extract all operations from an OpenAPI document grouped by tag @@ -179,40 +156,40 @@ const HTTP_METHODS = [ * @returns Map of tag name to operations with that tag */ function extractOperationsByTag(openapi) { - const tagOperations = new Map(); - Object.entries(openapi.paths).forEach(([pathKey, pathItem]) => { - HTTP_METHODS.forEach((method) => { - const operation = pathItem[method]; - if (operation) { - const opMeta = { - operationId: operation.operationId || `${method}-${pathKey}`, - method: method.toUpperCase(), - path: pathKey, - summary: operation.summary || '', - tags: operation.tags || [], - }; - // Extract compatibility version if present - if (operation['x-compatibility-version']) { - opMeta.compatVersion = operation['x-compatibility-version']; - } - // Extract externalDocs if present - if (operation.externalDocs) { - opMeta.externalDocs = { - description: operation.externalDocs.description || '', - url: operation.externalDocs.url, - }; - } - // Add operation to each of its tags - (operation.tags || []).forEach((tag) => { - if (!tagOperations.has(tag)) { - tagOperations.set(tag, []); - } - tagOperations.get(tag).push(opMeta); + const tagOperations = new Map(); + Object.entries(openapi.paths).forEach(([pathKey, pathItem]) => { + HTTP_METHODS.forEach((method) => { + const operation = pathItem[method]; + if (operation) { + const opMeta = { + operationId: operation.operationId || `${method}-${pathKey}`, + method: method.toUpperCase(), + path: pathKey, + summary: operation.summary || '', + tags: operation.tags || [], + }; + // Extract compatibility version if present + if (operation['x-compatibility-version']) { + opMeta.compatVersion = operation['x-compatibility-version']; + } + // Extract externalDocs if present + if (operation.externalDocs) { + opMeta.externalDocs = { + description: operation.externalDocs.description || '', + url: operation.externalDocs.url, + }; + } + // Add operation to each of its tags + (operation.tags || []).forEach((tag) => { + if (!tagOperations.has(tag)) { + tagOperations.set(tag, []); + } + tagOperations.get(tag).push(opMeta); + }); + } }); - } }); - }); - return tagOperations; + return tagOperations; } /** * Write OpenAPI specs grouped by tag to separate files @@ -223,81 +200,83 @@ function extractOperationsByTag(openapi) { * @param outPath - Output directory path */ function writeTagOpenapis(openapi, prefix, outPath) { - const tagOperations = extractOperationsByTag(openapi); - // Process each tag - tagOperations.forEach((operations, tagName) => { - // Deep copy openapi - const doc = JSON.parse(JSON.stringify(openapi)); - // Filter paths to only include those with operations for this tag - const filteredPaths = {}; - Object.entries(openapi.paths).forEach(([pathKey, pathItem]) => { - const filteredPathItem = {}; - let hasOperations = false; - HTTP_METHODS.forEach((method) => { - const operation = pathItem[method]; - if (operation?.tags?.includes(tagName)) { - filteredPathItem[method] = operation; - hasOperations = true; + const tagOperations = extractOperationsByTag(openapi); + // Process each tag + tagOperations.forEach((operations, tagName) => { + // Deep copy openapi + const doc = JSON.parse(JSON.stringify(openapi)); + // Filter paths to only include those with operations for this tag + const filteredPaths = {}; + Object.entries(openapi.paths).forEach(([pathKey, pathItem]) => { + const filteredPathItem = {}; + let hasOperations = false; + HTTP_METHODS.forEach((method) => { + const operation = pathItem[method]; + if (operation?.tags?.includes(tagName)) { + // Clone the operation and restrict tags to only this tag + // This prevents RapiDoc from rendering the operation multiple times + // (once per tag) when an operation belongs to multiple tags + const filteredOperation = { ...operation, tags: [tagName] }; + filteredPathItem[method] = filteredOperation; + hasOperations = true; + } + }); + // Include path-level parameters if we have operations + if (hasOperations) { + if (pathItem.parameters) { + filteredPathItem.parameters = pathItem.parameters; + } + filteredPaths[pathKey] = filteredPathItem; + } + }); + doc.paths = filteredPaths; + // Filter tags to only include this tag (and trait tags for context) + if (doc.tags) { + doc.tags = doc.tags.filter((tag) => tag.name === tagName || tag['x-traitTag']); } - }); - // Include path-level parameters if we have operations - if (hasOperations) { - if (pathItem.parameters) { - filteredPathItem.parameters = pathItem.parameters; + // Update info + const tagSlug = slugifyTag(tagName); + doc.info.title = tagName; + doc.info.description = `API reference for ${tagName}`; + doc['x-tagGroup'] = tagName; + try { + if (!fs.existsSync(outPath)) { + fs.mkdirSync(outPath, { recursive: true }); + } + const baseFilename = `${prefix}${tagSlug}`; + const yamlPath = path.resolve(outPath, `${baseFilename}.yaml`); + const jsonPath = path.resolve(outPath, `${baseFilename}.json`); + writeDataFile(doc, yamlPath); + writeJsonFile(doc, jsonPath); + console.log(`Generated tag spec: ${baseFilename}.yaml (${Object.keys(filteredPaths).length} paths, ${operations.length} operations)`); + } + catch (err) { + console.error(`Error writing tag group ${tagName}:`, err); + } + }); + // Also create specs for conceptual tags (x-traitTag) without operations + (openapi.tags || []).forEach((tag) => { + if (tag['x-traitTag'] && !tagOperations.has(tag.name)) { + const doc = JSON.parse(JSON.stringify(openapi)); + doc.paths = {}; + doc.tags = [tag]; + doc.info.title = tag.name; + doc.info.description = tag.description || `API reference for ${tag.name}`; + doc['x-tagGroup'] = tag.name; + const tagSlug = slugifyTag(tag.name); + try { + const baseFilename = `${prefix}${tagSlug}`; + const yamlPath = path.resolve(outPath, `${baseFilename}.yaml`); + const jsonPath = path.resolve(outPath, `${baseFilename}.json`); + writeDataFile(doc, yamlPath); + writeJsonFile(doc, jsonPath); + console.log(`Generated conceptual tag spec: ${baseFilename}.yaml`); + } + catch (err) { + console.error(`Error writing conceptual tag ${tag.name}:`, err); + } } - filteredPaths[pathKey] = filteredPathItem; - } }); - doc.paths = filteredPaths; - // Filter tags to only include this tag (and trait tags for context) - if (doc.tags) { - doc.tags = doc.tags.filter( - (tag) => tag.name === tagName || tag['x-traitTag'] - ); - } - // Update info - const tagSlug = slugifyTag(tagName); - doc.info.title = tagName; - doc.info.description = `API reference for ${tagName}`; - doc['x-tagGroup'] = tagName; - try { - if (!fs.existsSync(outPath)) { - fs.mkdirSync(outPath, { recursive: true }); - } - const baseFilename = `${prefix}${tagSlug}`; - const yamlPath = path.resolve(outPath, `${baseFilename}.yaml`); - const jsonPath = path.resolve(outPath, `${baseFilename}.json`); - writeDataFile(doc, yamlPath); - writeJsonFile(doc, jsonPath); - console.log( - `Generated tag spec: ${baseFilename}.yaml (${Object.keys(filteredPaths).length} paths, ${operations.length} operations)` - ); - } catch (err) { - console.error(`Error writing tag group ${tagName}:`, err); - } - }); - // Also create specs for conceptual tags (x-traitTag) without operations - (openapi.tags || []).forEach((tag) => { - if (tag['x-traitTag'] && !tagOperations.has(tag.name)) { - const doc = JSON.parse(JSON.stringify(openapi)); - doc.paths = {}; - doc.tags = [tag]; - doc.info.title = tag.name; - doc.info.description = tag.description || `API reference for ${tag.name}`; - doc['x-tagGroup'] = tag.name; - const tagSlug = slugifyTag(tag.name); - try { - const baseFilename = `${prefix}${tagSlug}`; - const yamlPath = path.resolve(outPath, `${baseFilename}.yaml`); - const jsonPath = path.resolve(outPath, `${baseFilename}.json`); - writeDataFile(doc, yamlPath); - writeJsonFile(doc, jsonPath); - console.log(`Generated conceptual tag spec: ${baseFilename}.yaml`); - } catch (err) { - console.error(`Error writing conceptual tag ${tag.name}:`, err); - } - } - }); } /** * Write OpenAPI specs grouped by path to separate files @@ -308,80 +287,79 @@ function writeTagOpenapis(openapi, prefix, outPath) { * @param outPath - Output directory path */ function writePathOpenapis(openapi, prefix, outPath) { - const pathGroups = {}; - // Group paths by their base path (first 3-4 segments, excluding placeholders) - Object.keys(openapi.paths) - .sort() - .forEach((p) => { - const delimiter = '/'; - let key = p.split(delimiter); - // Check if this is an item path (ends with a placeholder) - let isItemPath = openapiUtils.isPlaceholderFragment(key[key.length - 1]); - if (isItemPath) { - key = key.slice(0, -1); - } - // Take first 4 segments - key = key.slice(0, 4); - // Check if the last segment is still a placeholder - isItemPath = openapiUtils.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]; - }); - // Write each path group to separate YAML and JSON files - Object.keys(pathGroups).forEach((pg) => { - // Deep copy openapi - const doc = JSON.parse(JSON.stringify(openapi)); - doc.paths = pathGroups[pg]; - // Collect tags used by operations in this path group - const usedTags = new Set(); - Object.values(doc.paths).forEach((pathItem) => { - const httpMethods = [ - 'get', - 'post', - 'put', - 'patch', - 'delete', - 'options', - 'head', - 'trace', - ]; - httpMethods.forEach((method) => { - const operation = pathItem[method]; - if (operation?.tags) { - operation.tags.forEach((tag) => usedTags.add(tag)); + const pathGroups = {}; + // Group paths by their base path (first 3-4 segments, excluding placeholders) + Object.keys(openapi.paths) + .sort() + .forEach((p) => { + const delimiter = '/'; + let key = p.split(delimiter); + // Check if this is an item path (ends with a placeholder) + let isItemPath = openapiUtils.isPlaceholderFragment(key[key.length - 1]); + if (isItemPath) { + key = key.slice(0, -1); + } + // Take first 4 segments + key = key.slice(0, 4); + // Check if the last segment is still a placeholder + isItemPath = openapiUtils.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]; + }); + // Write each path group to separate YAML and JSON files + Object.keys(pathGroups).forEach((pg) => { + // Deep copy openapi + const doc = JSON.parse(JSON.stringify(openapi)); + doc.paths = pathGroups[pg]; + // Collect tags used by operations in this path group + const usedTags = new Set(); + Object.values(doc.paths).forEach((pathItem) => { + const httpMethods = [ + 'get', + 'post', + 'put', + 'patch', + 'delete', + 'options', + 'head', + 'trace', + ]; + httpMethods.forEach((method) => { + const operation = pathItem[method]; + if (operation?.tags) { + operation.tags.forEach((tag) => usedTags.add(tag)); + } + }); + }); + // Filter tags to only include those used by operations in this path group + // Exclude x-traitTag tags (supplementary documentation tags) + if (doc.tags) { + doc.tags = doc.tags.filter((tag) => usedTags.has(tag.name) && !tag['x-traitTag']); + } + // Simplify info for path-specific docs + doc.info.title = pg; + doc.info.description = `API reference for ${pg}`; + doc['x-pathGroup'] = pg; + try { + if (!fs.existsSync(outPath)) { + fs.mkdirSync(outPath, { recursive: true }); + } + const baseFilename = `${prefix}${pg.replaceAll('/', '-').replace(/^-/, '')}`; + const yamlPath = path.resolve(outPath, `${baseFilename}.yaml`); + const jsonPath = path.resolve(outPath, `${baseFilename}.json`); + // Write both YAML and JSON versions + writeDataFile(doc, yamlPath); + writeJsonFile(doc, jsonPath); + console.log(`Generated: ${baseFilename}.yaml and ${baseFilename}.json`); + } + catch (err) { + console.error(`Error writing path group ${pg}:`, err); } - }); }); - // Filter tags to only include those used by operations in this path group - // Exclude x-traitTag tags (supplementary documentation tags) - if (doc.tags) { - doc.tags = doc.tags.filter( - (tag) => usedTags.has(tag.name) && !tag['x-traitTag'] - ); - } - // Simplify info for path-specific docs - doc.info.title = pg; - doc.info.description = `API reference for ${pg}`; - doc['x-pathGroup'] = pg; - try { - if (!fs.existsSync(outPath)) { - fs.mkdirSync(outPath, { recursive: true }); - } - const baseFilename = `${prefix}${pg.replaceAll('/', '-').replace(/^-/, '')}`; - const yamlPath = path.resolve(outPath, `${baseFilename}.yaml`); - const jsonPath = path.resolve(outPath, `${baseFilename}.json`); - // Write both YAML and JSON versions - writeDataFile(doc, yamlPath); - writeJsonFile(doc, jsonPath); - console.log(`Generated: ${baseFilename}.yaml and ${baseFilename}.json`); - } catch (err) { - console.error(`Error writing path group ${pg}:`, err); - } - }); } /** * Create article metadata for a path group @@ -390,119 +368,107 @@ function writePathOpenapis(openapi, prefix, outPath) { * @returns Article metadata object */ function createArticleDataForPathGroup(openapi) { - const article = { - path: '', - fields: { - name: openapi['x-pathGroup'] || '', - describes: Object.keys(openapi.paths), - }, - }; - /** - * Convert OpenAPI path to Hugo-friendly article path - * Legacy endpoints (without /api/ prefix) go under api/ directly - * Versioned endpoints (with /api/vN/) keep their structure - * - * @param p - Path to convert (e.g., '/health', '/api/v3/query_sql') - * @returns Path suitable for Hugo content directory (e.g., 'api/health', 'api/v3/query_sql') - */ - const toHugoPath = (p) => { - if (!p) { - return ''; - } - // If path doesn't start with /api/, it's a legacy endpoint - // Place it directly under api/ to avoid collision with /api/v1/* paths - if (!p.startsWith('/api/')) { - // /health -> api/health - // /write -> api/write - return `api${p}`; - } - // /api/v1/health -> api/v1/health - // /api/v2/write -> api/v2/write - // /api/v3/query_sql -> api/v3/query_sql - return p.replace(/^\//, ''); - }; - /** - * Convert path to tag-friendly format (dashes instead of slashes) - * - * @param p - Path to convert - * @returns Tag-friendly path - */ - const toTagPath = (p) => { - if (!p) { - return ''; - } - return p.replace(/^\//, '').replaceAll('/', '-'); - }; - const pathGroup = openapi['x-pathGroup'] || ''; - article.path = toHugoPath(pathGroup); - // Store original path for menu display (shows actual endpoint path) - article.fields.menuName = pathGroup; - article.fields.title = openapi.info?.title; - article.fields.description = openapi.description; - const pathGroupFrags = path.parse(openapi['x-pathGroup'] || ''); - article.fields.tags = [pathGroupFrags?.dir, pathGroupFrags?.name] - .filter(Boolean) - .map((t) => toTagPath(t)); - // Extract x-relatedLinks and OpenAPI tags from path items or operations - const relatedLinks = []; - const apiTags = []; - const httpMethods = [ - 'get', - 'post', - 'put', - 'patch', - 'delete', - 'options', - 'head', - 'trace', - ]; - Object.values(openapi.paths).forEach((pathItem) => { - // Check path-level x-relatedLinks - if ( - pathItem['x-relatedLinks'] && - Array.isArray(pathItem['x-relatedLinks']) - ) { - relatedLinks.push( - ...pathItem['x-relatedLinks'].filter( - (link) => !relatedLinks.includes(link) - ) - ); - } - // Check operation-level x-relatedLinks and tags - httpMethods.forEach((method) => { - const operation = pathItem[method]; - if (operation) { - // Extract x-relatedLinks - if ( - operation['x-relatedLinks'] && - Array.isArray(operation['x-relatedLinks']) - ) { - relatedLinks.push( - ...operation['x-relatedLinks'].filter( - (link) => !relatedLinks.includes(link) - ) - ); + const article = { + path: '', + fields: { + name: openapi['x-pathGroup'] || '', + describes: Object.keys(openapi.paths), + }, + }; + /** + * Convert OpenAPI path to Hugo-friendly article path + * Legacy endpoints (without /api/ prefix) go under api/ directly + * Versioned endpoints (with /api/vN/) keep their structure + * + * @param p - Path to convert (e.g., '/health', '/api/v3/query_sql') + * @returns Path suitable for Hugo content directory (e.g., 'api/health', 'api/v3/query_sql') + */ + const toHugoPath = (p) => { + if (!p) { + return ''; } - // Extract OpenAPI tags from operation - if (operation.tags && Array.isArray(operation.tags)) { - operation.tags.forEach((tag) => { - if (!apiTags.includes(tag)) { - apiTags.push(tag); + // If path doesn't start with /api/, it's a legacy endpoint + // Place it directly under api/ to avoid collision with /api/v1/* paths + if (!p.startsWith('/api/')) { + // /health -> api/health + // /write -> api/write + return `api${p}`; + } + // /api/v1/health -> api/v1/health + // /api/v2/write -> api/v2/write + // /api/v3/query_sql -> api/v3/query_sql + return p.replace(/^\//, ''); + }; + /** + * Convert path to tag-friendly format (dashes instead of slashes) + * + * @param p - Path to convert + * @returns Tag-friendly path + */ + const toTagPath = (p) => { + if (!p) { + return ''; + } + return p.replace(/^\//, '').replaceAll('/', '-'); + }; + const pathGroup = openapi['x-pathGroup'] || ''; + article.path = toHugoPath(pathGroup); + // Store original path for menu display (shows actual endpoint path) + article.fields.menuName = pathGroup; + article.fields.title = openapi.info?.title; + article.fields.description = openapi.description; + const pathGroupFrags = path.parse(openapi['x-pathGroup'] || ''); + article.fields.tags = [pathGroupFrags?.dir, pathGroupFrags?.name] + .filter(Boolean) + .map((t) => toTagPath(t)); + // Extract x-relatedLinks and OpenAPI tags from path items or operations + const relatedLinks = []; + const apiTags = []; + const httpMethods = [ + 'get', + 'post', + 'put', + 'patch', + 'delete', + 'options', + 'head', + 'trace', + ]; + Object.values(openapi.paths).forEach((pathItem) => { + // Check path-level x-relatedLinks + if (pathItem['x-relatedLinks'] && + Array.isArray(pathItem['x-relatedLinks'])) { + relatedLinks.push(...pathItem['x-relatedLinks'].filter((link) => !relatedLinks.includes(link))); + } + // Check operation-level x-relatedLinks and tags + httpMethods.forEach((method) => { + const operation = pathItem[method]; + if (operation) { + // Extract x-relatedLinks + if (operation['x-relatedLinks'] && + Array.isArray(operation['x-relatedLinks'])) { + relatedLinks.push(...operation['x-relatedLinks'].filter((link) => !relatedLinks.includes(link))); + } + // Extract OpenAPI tags from operation + if (operation.tags && Array.isArray(operation.tags)) { + operation.tags.forEach((tag) => { + if (!apiTags.includes(tag)) { + apiTags.push(tag); + } + }); + } } - }); - } - } + }); }); - }); - // Only add related if there are links - if (relatedLinks.length > 0) { - article.fields.related = relatedLinks; - } - // Add OpenAPI tags from operations (for Hugo frontmatter) - if (apiTags.length > 0) { - article.fields.apiTags = apiTags; - } - return article; + // Only add related if there are links + if (relatedLinks.length > 0) { + article.fields.related = relatedLinks; + } + // Add OpenAPI tags from operations (for Hugo frontmatter) + if (apiTags.length > 0) { + article.fields.apiTags = apiTags; + } + return article; } /** * Write OpenAPI article metadata to Hugo data files @@ -513,50 +479,49 @@ function createArticleDataForPathGroup(openapi) { * @param opts - Options including file pattern filter */ function writeOpenapiArticleData(sourcePath, targetPath, opts) { - /** - * Check if path is a file - */ - const isFile = (filePath) => { - return fs.lstatSync(filePath).isFile(); - }; - /** - * Check if filename matches pattern - */ - const matchesPattern = (filePath) => { - return opts.filePattern - ? path.parse(filePath).name.startsWith(opts.filePattern) - : true; - }; - try { - const articles = fs - .readdirSync(sourcePath) - .map((fileName) => path.join(sourcePath, fileName)) - .filter(matchesPattern) - .filter(isFile) - .filter( - (filePath) => filePath.endsWith('.yaml') || filePath.endsWith('.yml') - ) // Only process YAML files - .map((filePath) => { - const openapi = readFile(filePath); - const article = createArticleDataForPathGroup(openapi); - article.fields.source = filePath; - // Hugo omits "/static" from the URI when serving files stored in "./static" - article.fields.staticFilePath = filePath.replace(/^static\//, '/'); - return article; - }); - if (!fs.existsSync(targetPath)) { - fs.mkdirSync(targetPath, { recursive: true }); + /** + * Check if path is a file + */ + const isFile = (filePath) => { + return fs.lstatSync(filePath).isFile(); + }; + /** + * Check if filename matches pattern + */ + const matchesPattern = (filePath) => { + return opts.filePattern + ? path.parse(filePath).name.startsWith(opts.filePattern) + : true; + }; + try { + const articles = fs + .readdirSync(sourcePath) + .map((fileName) => path.join(sourcePath, fileName)) + .filter(matchesPattern) + .filter(isFile) + .filter((filePath) => filePath.endsWith('.yaml') || filePath.endsWith('.yml')) // Only process YAML files + .map((filePath) => { + const openapi = readFile(filePath); + const article = createArticleDataForPathGroup(openapi); + article.fields.source = filePath; + // Hugo omits "/static" from the URI when serving files stored in "./static" + article.fields.staticFilePath = filePath.replace(/^static\//, '/'); + return article; + }); + if (!fs.existsSync(targetPath)) { + fs.mkdirSync(targetPath, { recursive: true }); + } + const articleCollection = { articles }; + // Write both YAML and JSON versions + const yamlPath = path.resolve(targetPath, 'articles.yml'); + const jsonPath = path.resolve(targetPath, 'articles.json'); + writeDataFile(articleCollection, yamlPath); + writeJsonFile(articleCollection, jsonPath); + console.log(`Generated ${articles.length} articles in ${targetPath}`); + } + catch (e) { + console.error('Error writing article data:', e); } - const articleCollection = { articles }; - // Write both YAML and JSON versions - const yamlPath = path.resolve(targetPath, 'articles.yml'); - const jsonPath = path.resolve(targetPath, 'articles.json'); - writeDataFile(articleCollection, yamlPath); - writeJsonFile(articleCollection, jsonPath); - console.log(`Generated ${articles.length} articles in ${targetPath}`); - } catch (e) { - console.error('Error writing article data:', e); - } } /** * Create article data for a tag-based grouping @@ -567,54 +532,53 @@ function writeOpenapiArticleData(sourcePath, targetPath, opts) { * @returns Article metadata object */ function createArticleDataForTag(openapi, operations, tagMeta) { - const tagName = openapi['x-tagGroup'] || ''; - const tagSlug = slugifyTag(tagName); - const isConceptual = tagMeta?.['x-traitTag'] === true; - const article = { - path: `api/${tagSlug}`, - fields: { - name: tagName, - describes: Object.keys(openapi.paths), - title: tagName, - description: - tagMeta?.description || - openapi.info?.description || - `API reference for ${tagName}`, - tag: tagName, - isConceptual, - menuGroup: getMenuGroupForTag(tagName), - operations: operations.map((op) => ({ - operationId: op.operationId, - method: op.method, - path: op.path, - summary: op.summary, - tags: op.tags, - ...(op.compatVersion && { compatVersion: op.compatVersion }), - ...(op.externalDocs && { externalDocs: op.externalDocs }), - })), - }, - }; - // Add tag description for conceptual pages - if (tagMeta?.description) { - article.fields.tagDescription = tagMeta.description; - } - // Aggregate unique externalDocs URLs from operations into article-level related - // This populates Hugo frontmatter `related` field for "Related content" links - const relatedUrls = new Set(); - // First check tag-level externalDocs - if (tagMeta?.externalDocs?.url) { - relatedUrls.add(tagMeta.externalDocs.url); - } - // Then aggregate from operations - operations.forEach((op) => { - if (op.externalDocs?.url) { - relatedUrls.add(op.externalDocs.url); + const tagName = openapi['x-tagGroup'] || ''; + const tagSlug = slugifyTag(tagName); + const isConceptual = tagMeta?.['x-traitTag'] === true; + const article = { + path: `api/${tagSlug}`, + fields: { + name: tagName, + describes: Object.keys(openapi.paths), + title: tagName, + description: tagMeta?.description || + openapi.info?.description || + `API reference for ${tagName}`, + tag: tagName, + isConceptual, + menuGroup: getMenuGroupForTag(tagName), + operations: operations.map((op) => ({ + operationId: op.operationId, + method: op.method, + path: op.path, + summary: op.summary, + tags: op.tags, + ...(op.compatVersion && { compatVersion: op.compatVersion }), + ...(op.externalDocs && { externalDocs: op.externalDocs }), + })), + }, + }; + // Add tag description for conceptual pages + if (tagMeta?.description) { + article.fields.tagDescription = tagMeta.description; } - }); - if (relatedUrls.size > 0) { - article.fields.related = Array.from(relatedUrls); - } - return article; + // Aggregate unique externalDocs URLs from operations into article-level related + // This populates Hugo frontmatter `related` field for "Related content" links + const relatedUrls = new Set(); + // First check tag-level externalDocs + if (tagMeta?.externalDocs?.url) { + relatedUrls.add(tagMeta.externalDocs.url); + } + // Then aggregate from operations + operations.forEach((op) => { + if (op.externalDocs?.url) { + relatedUrls.add(op.externalDocs.url); + } + }); + if (relatedUrls.size > 0) { + article.fields.related = Array.from(relatedUrls); + } + return article; } /** * Write tag-based OpenAPI article metadata to Hugo data files @@ -626,85 +590,77 @@ function createArticleDataForTag(openapi, operations, tagMeta) { * @param opts - Options including file pattern filter */ function writeOpenapiTagArticleData(sourcePath, targetPath, openapi, opts) { - const isFile = (filePath) => { - return fs.lstatSync(filePath).isFile(); - }; - const matchesPattern = (filePath) => { - return opts.filePattern - ? path.parse(filePath).name.startsWith(opts.filePattern) - : true; - }; - // Create tag metadata lookup - const tagMetaMap = new Map(); - (openapi.tags || []).forEach((tag) => { - tagMetaMap.set(tag.name, tag); - }); - try { - const articles = fs - .readdirSync(sourcePath) - .map((fileName) => path.join(sourcePath, fileName)) - .filter(matchesPattern) - .filter(isFile) - .filter( - (filePath) => filePath.endsWith('.yaml') || filePath.endsWith('.yml') - ) - .map((filePath) => { - const tagOpenapi = readFile(filePath); - const tagName = - tagOpenapi['x-tagGroup'] || tagOpenapi.info?.title || ''; - const tagMeta = tagMetaMap.get(tagName); - // Extract operations from the tag-filtered spec - const operations = []; - Object.entries(tagOpenapi.paths).forEach(([pathKey, pathItem]) => { - HTTP_METHODS.forEach((method) => { - const operation = pathItem[method]; - if (operation) { - const opMeta = { - operationId: operation.operationId || `${method}-${pathKey}`, - method: method.toUpperCase(), - path: pathKey, - summary: operation.summary || '', - tags: operation.tags || [], - }; - // Extract compatibility version if present - if (operation['x-compatibility-version']) { - opMeta.compatVersion = operation['x-compatibility-version']; - } - // Extract externalDocs if present - if (operation.externalDocs) { - opMeta.externalDocs = { - description: operation.externalDocs.description || '', - url: operation.externalDocs.url, - }; - } - operations.push(opMeta); - } - }); + const isFile = (filePath) => { + return fs.lstatSync(filePath).isFile(); + }; + const matchesPattern = (filePath) => { + return opts.filePattern + ? path.parse(filePath).name.startsWith(opts.filePattern) + : true; + }; + // Create tag metadata lookup + const tagMetaMap = new Map(); + (openapi.tags || []).forEach((tag) => { + tagMetaMap.set(tag.name, tag); + }); + try { + const articles = fs + .readdirSync(sourcePath) + .map((fileName) => path.join(sourcePath, fileName)) + .filter(matchesPattern) + .filter(isFile) + .filter((filePath) => filePath.endsWith('.yaml') || filePath.endsWith('.yml')) + .map((filePath) => { + const tagOpenapi = readFile(filePath); + const tagName = tagOpenapi['x-tagGroup'] || tagOpenapi.info?.title || ''; + const tagMeta = tagMetaMap.get(tagName); + // Extract operations from the tag-filtered spec + const operations = []; + Object.entries(tagOpenapi.paths).forEach(([pathKey, pathItem]) => { + HTTP_METHODS.forEach((method) => { + const operation = pathItem[method]; + if (operation) { + const opMeta = { + operationId: operation.operationId || `${method}-${pathKey}`, + method: method.toUpperCase(), + path: pathKey, + summary: operation.summary || '', + tags: operation.tags || [], + }; + // Extract compatibility version if present + if (operation['x-compatibility-version']) { + opMeta.compatVersion = operation['x-compatibility-version']; + } + // Extract externalDocs if present + if (operation.externalDocs) { + opMeta.externalDocs = { + description: operation.externalDocs.description || '', + url: operation.externalDocs.url, + }; + } + operations.push(opMeta); + } + }); + }); + const article = createArticleDataForTag(tagOpenapi, operations, tagMeta); + article.fields.source = filePath; + article.fields.staticFilePath = filePath.replace(/^static\//, '/'); + return article; }); - const article = createArticleDataForTag( - tagOpenapi, - operations, - tagMeta - ); - article.fields.source = filePath; - article.fields.staticFilePath = filePath.replace(/^static\//, '/'); - return article; - }); - if (!fs.existsSync(targetPath)) { - fs.mkdirSync(targetPath, { recursive: true }); + if (!fs.existsSync(targetPath)) { + fs.mkdirSync(targetPath, { recursive: true }); + } + const articleCollection = { articles }; + // Write both YAML and JSON versions + const yamlPath = path.resolve(targetPath, 'articles.yml'); + const jsonPath = path.resolve(targetPath, 'articles.json'); + writeDataFile(articleCollection, yamlPath); + writeJsonFile(articleCollection, jsonPath); + console.log(`Generated ${articles.length} tag-based articles in ${targetPath}`); + } + catch (e) { + console.error('Error writing tag article data:', e); } - const articleCollection = { articles }; - // Write both YAML and JSON versions - const yamlPath = path.resolve(targetPath, 'articles.yml'); - const jsonPath = path.resolve(targetPath, 'articles.json'); - writeDataFile(articleCollection, yamlPath); - writeJsonFile(articleCollection, jsonPath); - console.log( - `Generated ${articles.length} tag-based articles in ${targetPath}` - ); - } catch (e) { - console.error('Error writing tag article data:', e); - } } /** * Generate Hugo data files from an OpenAPI specification grouped by tag @@ -718,28 +674,24 @@ function writeOpenapiTagArticleData(sourcePath, targetPath, openapi, opts) { * @param options - Generation options */ function generateHugoDataByTag(options) { - const filenamePrefix = `${path.parse(options.specFile).name}-`; - const sourceFile = readFile(options.specFile, 'utf8'); - // Optionally generate path-based files for backwards compatibility - if (options.includePaths) { - console.log( - `\nGenerating OpenAPI path files in ${options.dataOutPath}....` - ); - writePathOpenapis(sourceFile, filenamePrefix, options.dataOutPath); - } - // Generate tag-based files - const tagOutPath = options.includePaths - ? path.join(options.dataOutPath, 'tags') - : options.dataOutPath; - console.log(`\nGenerating OpenAPI tag files in ${tagOutPath}....`); - writeTagOpenapis(sourceFile, filenamePrefix, tagOutPath); - console.log( - `\nGenerating OpenAPI tag article data in ${options.articleOutPath}...` - ); - writeOpenapiTagArticleData(tagOutPath, options.articleOutPath, sourceFile, { - filePattern: filenamePrefix, - }); - console.log('\nTag-based generation complete!\n'); + const filenamePrefix = `${path.parse(options.specFile).name}-`; + const sourceFile = readFile(options.specFile, 'utf8'); + // Optionally generate path-based files for backwards compatibility + if (options.includePaths) { + console.log(`\nGenerating OpenAPI path files in ${options.dataOutPath}....`); + writePathOpenapis(sourceFile, filenamePrefix, options.dataOutPath); + } + // Generate tag-based files + const tagOutPath = options.includePaths + ? path.join(options.dataOutPath, 'tags') + : options.dataOutPath; + console.log(`\nGenerating OpenAPI tag files in ${tagOutPath}....`); + writeTagOpenapis(sourceFile, filenamePrefix, tagOutPath); + console.log(`\nGenerating OpenAPI tag article data in ${options.articleOutPath}...`); + writeOpenapiTagArticleData(tagOutPath, options.articleOutPath, sourceFile, { + filePattern: filenamePrefix, + }); + console.log('\nTag-based generation complete!\n'); } /** * Generate Hugo data files from an OpenAPI specification @@ -753,21 +705,19 @@ function generateHugoDataByTag(options) { * @param options - Generation options */ function generateHugoData(options) { - const filenamePrefix = `${path.parse(options.specFile).name}-`; - const sourceFile = readFile(options.specFile, 'utf8'); - console.log(`\nGenerating OpenAPI path files in ${options.dataOutPath}....`); - writePathOpenapis(sourceFile, filenamePrefix, options.dataOutPath); - console.log( - `\nGenerating OpenAPI article data in ${options.articleOutPath}...` - ); - writeOpenapiArticleData(options.dataOutPath, options.articleOutPath, { - filePattern: filenamePrefix, - }); - console.log('\nGeneration complete!\n'); + const filenamePrefix = `${path.parse(options.specFile).name}-`; + const sourceFile = readFile(options.specFile, 'utf8'); + console.log(`\nGenerating OpenAPI path files in ${options.dataOutPath}....`); + writePathOpenapis(sourceFile, filenamePrefix, options.dataOutPath); + console.log(`\nGenerating OpenAPI article data in ${options.articleOutPath}...`); + writeOpenapiArticleData(options.dataOutPath, options.articleOutPath, { + filePattern: filenamePrefix, + }); + console.log('\nGeneration complete!\n'); } // CommonJS export for backward compatibility module.exports = { - generateHugoData, - generateHugoDataByTag, + generateHugoData, + generateHugoDataByTag, }; -//# sourceMappingURL=index.js.map +//# sourceMappingURL=index.js.map \ No newline at end of file diff --git a/api-docs/scripts/generate-openapi-articles.ts b/api-docs/scripts/generate-openapi-articles.ts index 1dca01e96..12db38fea 100644 --- a/api-docs/scripts/generate-openapi-articles.ts +++ b/api-docs/scripts/generate-openapi-articles.ts @@ -587,6 +587,7 @@ function generateOperationPages(options: GenerateOperationPagesOptions): void { layout: 'operation', // RapiDoc Mini configuration specFile: tagSpecFile, + // RapiDoc match-paths format: "method /path" (e.g., "post /write") matchPaths: `${method} ${op.path}`, // Operation metadata operationId: op.operationId, diff --git a/api-docs/scripts/openapi-paths-to-hugo-data/index.ts b/api-docs/scripts/openapi-paths-to-hugo-data/index.ts index 72bb051cc..8492230b6 100644 --- a/api-docs/scripts/openapi-paths-to-hugo-data/index.ts +++ b/api-docs/scripts/openapi-paths-to-hugo-data/index.ts @@ -498,7 +498,11 @@ function writeTagOpenapis( HTTP_METHODS.forEach((method) => { const operation = pathItem[method] as Operation | undefined; if (operation?.tags?.includes(tagName)) { - filteredPathItem[method] = operation; + // Clone the operation and restrict tags to only this tag + // This prevents RapiDoc from rendering the operation multiple times + // (once per tag) when an operation belongs to multiple tags + const filteredOperation = { ...operation, tags: [tagName] }; + filteredPathItem[method] = filteredOperation; hasOperations = true; } }); diff --git a/assets/js/components/rapidoc-mini.ts b/assets/js/components/rapidoc-mini.ts index d4b4bc0f5..4a49eea9f 100644 --- a/assets/js/components/rapidoc-mini.ts +++ b/assets/js/components/rapidoc-mini.ts @@ -166,6 +166,7 @@ function createRapiDocElement( // Core attributes element.setAttribute('spec-url', specUrl); + // matchPaths format: "method /path" (e.g., "post /write") if (matchPaths) { element.setAttribute('match-paths', matchPaths); } diff --git a/data/article_data/influxdb/influxdb3_core/articles.json b/data/article_data/influxdb/influxdb3_core/articles.json index 5d98cf414..ff6c0f9c0 100644 --- a/data/article_data/influxdb/influxdb3_core/articles.json +++ b/data/article_data/influxdb/influxdb3_core/articles.json @@ -1,5 +1,63 @@ { "articles": [ + { + "path": "api/auth-token", + "fields": { + "name": "Auth token", + "describes": [ + "/api/v3/configure/token/admin", + "/api/v3/configure/token/admin/regenerate", + "/api/v3/configure/token", + "/api/v3/configure/token/named_admin" + ], + "title": "Auth token", + "description": "Manage tokens for authentication and authorization", + "tag": "Auth token", + "isConceptual": false, + "menuGroup": "Other", + "operations": [ + { + "operationId": "PostCreateAdminToken", + "method": "POST", + "path": "/api/v3/configure/token/admin", + "summary": "Create admin token", + "tags": [ + "Auth token" + ] + }, + { + "operationId": "PostRegenerateAdminToken", + "method": "POST", + "path": "/api/v3/configure/token/admin/regenerate", + "summary": "Regenerate admin token", + "tags": [ + "Auth token" + ] + }, + { + "operationId": "DeleteToken", + "method": "DELETE", + "path": "/api/v3/configure/token", + "summary": "Delete token", + "tags": [ + "Auth token" + ] + }, + { + "operationId": "PostCreateNamedAdminToken", + "method": "POST", + "path": "/api/v3/configure/token/named_admin", + "summary": "Create named admin token", + "tags": [ + "Auth token" + ] + } + ], + "tagDescription": "Manage tokens for authentication and authorization", + "source": "static/openapi/influxdb3-core/tags/tags/ref-auth-token.yaml", + "staticFilePath": "/openapi/influxdb3-core/tags/tags/ref-auth-token.yaml" + } + }, { "path": "api/authentication", "fields": { @@ -22,8 +80,7 @@ "path": "/api/v3/configure/token/admin", "summary": "Create admin token", "tags": [ - "Authentication", - "Token" + "Authentication" ] }, { @@ -32,8 +89,7 @@ "path": "/api/v3/configure/token/admin/regenerate", "summary": "Regenerate admin token", "tags": [ - "Authentication", - "Token" + "Authentication" ] }, { @@ -42,8 +98,7 @@ "path": "/api/v3/configure/token", "summary": "Delete token", "tags": [ - "Authentication", - "Token" + "Authentication" ] }, { @@ -52,8 +107,7 @@ "path": "/api/v3/configure/token/named_admin", "summary": "Create named admin token", "tags": [ - "Authentication", - "Token" + "Authentication" ] } ], @@ -82,8 +136,7 @@ "path": "/api/v3/configure/distinct_cache", "summary": "Create distinct cache", "tags": [ - "Cache data", - "Table" + "Cache data" ] }, { @@ -92,8 +145,7 @@ "path": "/api/v3/configure/distinct_cache", "summary": "Delete distinct cache", "tags": [ - "Cache data", - "Table" + "Cache data" ] }, { @@ -102,8 +154,7 @@ "path": "/api/v3/configure/last_cache", "summary": "Create last cache", "tags": [ - "Cache data", - "Table" + "Cache data" ] }, { @@ -112,8 +163,7 @@ "path": "/api/v3/configure/last_cache", "summary": "Delete last cache", "tags": [ - "Cache data", - "Table" + "Cache data" ] } ], @@ -514,9 +564,7 @@ "fields": { "name": "Table", "describes": [ - "/api/v3/configure/table", - "/api/v3/configure/distinct_cache", - "/api/v3/configure/last_cache" + "/api/v3/configure/table" ], "title": "Table", "description": "Manage table schemas and data", @@ -541,46 +589,6 @@ "tags": [ "Table" ] - }, - { - "operationId": "PostConfigureDistinctCache", - "method": "POST", - "path": "/api/v3/configure/distinct_cache", - "summary": "Create distinct cache", - "tags": [ - "Cache data", - "Table" - ] - }, - { - "operationId": "DeleteConfigureDistinctCache", - "method": "DELETE", - "path": "/api/v3/configure/distinct_cache", - "summary": "Delete distinct cache", - "tags": [ - "Cache data", - "Table" - ] - }, - { - "operationId": "PostConfigureLastCache", - "method": "POST", - "path": "/api/v3/configure/last_cache", - "summary": "Create last cache", - "tags": [ - "Cache data", - "Table" - ] - }, - { - "operationId": "DeleteConfigureLastCache", - "method": "DELETE", - "path": "/api/v3/configure/last_cache", - "summary": "Delete last cache", - "tags": [ - "Cache data", - "Table" - ] } ], "tagDescription": "Manage table schemas and data", @@ -588,68 +596,6 @@ "staticFilePath": "/openapi/influxdb3-core/tags/tags/ref-table.yaml" } }, - { - "path": "api/token", - "fields": { - "name": "Token", - "describes": [ - "/api/v3/configure/token/admin", - "/api/v3/configure/token/admin/regenerate", - "/api/v3/configure/token", - "/api/v3/configure/token/named_admin" - ], - "title": "Token", - "description": "Manage tokens for authentication and authorization", - "tag": "Token", - "isConceptual": false, - "menuGroup": "Administration", - "operations": [ - { - "operationId": "PostCreateAdminToken", - "method": "POST", - "path": "/api/v3/configure/token/admin", - "summary": "Create admin token", - "tags": [ - "Authentication", - "Token" - ] - }, - { - "operationId": "PostRegenerateAdminToken", - "method": "POST", - "path": "/api/v3/configure/token/admin/regenerate", - "summary": "Regenerate admin token", - "tags": [ - "Authentication", - "Token" - ] - }, - { - "operationId": "DeleteToken", - "method": "DELETE", - "path": "/api/v3/configure/token", - "summary": "Delete token", - "tags": [ - "Authentication", - "Token" - ] - }, - { - "operationId": "PostCreateNamedAdminToken", - "method": "POST", - "path": "/api/v3/configure/token/named_admin", - "summary": "Create named admin token", - "tags": [ - "Authentication", - "Token" - ] - } - ], - "tagDescription": "Manage tokens for authentication and authorization", - "source": "static/openapi/influxdb3-core/tags/tags/ref-token.yaml", - "staticFilePath": "/openapi/influxdb3-core/tags/tags/ref-token.yaml" - } - }, { "path": "api/write-data", "fields": { diff --git a/data/article_data/influxdb/influxdb3_core/articles.yml b/data/article_data/influxdb/influxdb3_core/articles.yml index 3c7b297e1..c8f317c95 100644 --- a/data/article_data/influxdb/influxdb3_core/articles.yml +++ b/data/article_data/influxdb/influxdb3_core/articles.yml @@ -1,4 +1,45 @@ articles: + - path: api/auth-token + fields: + name: Auth token + describes: + - /api/v3/configure/token/admin + - /api/v3/configure/token/admin/regenerate + - /api/v3/configure/token + - /api/v3/configure/token/named_admin + title: Auth token + description: Manage tokens for authentication and authorization + tag: Auth token + isConceptual: false + menuGroup: Other + operations: + - operationId: PostCreateAdminToken + method: POST + path: /api/v3/configure/token/admin + summary: Create admin token + tags: + - Auth token + - operationId: PostRegenerateAdminToken + method: POST + path: /api/v3/configure/token/admin/regenerate + summary: Regenerate admin token + tags: + - Auth token + - operationId: DeleteToken + method: DELETE + path: /api/v3/configure/token + summary: Delete token + tags: + - Auth token + - operationId: PostCreateNamedAdminToken + method: POST + path: /api/v3/configure/token/named_admin + summary: Create named admin token + tags: + - Auth token + tagDescription: Manage tokens for authentication and authorization + source: static/openapi/influxdb3-core/tags/tags/ref-auth-token.yaml + staticFilePath: /openapi/influxdb3-core/tags/tags/ref-auth-token.yaml - path: api/authentication fields: name: Authentication @@ -38,28 +79,24 @@ articles: summary: Create admin token tags: - Authentication - - Token - operationId: PostRegenerateAdminToken method: POST path: /api/v3/configure/token/admin/regenerate summary: Regenerate admin token tags: - Authentication - - Token - operationId: DeleteToken method: DELETE path: /api/v3/configure/token summary: Delete token tags: - Authentication - - Token - operationId: PostCreateNamedAdminToken method: POST path: /api/v3/configure/token/named_admin summary: Create named admin token tags: - Authentication - - Token tagDescription: > Depending on your workflow, use one of the following schemes to authenticate to the InfluxDB 3 API: @@ -162,28 +199,24 @@ articles: summary: Create distinct cache tags: - Cache data - - Table - operationId: DeleteConfigureDistinctCache method: DELETE path: /api/v3/configure/distinct_cache summary: Delete distinct cache tags: - Cache data - - Table - operationId: PostConfigureLastCache method: POST path: /api/v3/configure/last_cache summary: Create last cache tags: - Cache data - - Table - operationId: DeleteConfigureLastCache method: DELETE path: /api/v3/configure/last_cache summary: Delete last cache tags: - Cache data - - Table tagDescription: > Manage the in-memory cache. @@ -817,8 +850,6 @@ articles: name: Table describes: - /api/v3/configure/table - - /api/v3/configure/distinct_cache - - /api/v3/configure/last_cache title: Table description: Manage table schemas and data tag: Table @@ -837,82 +868,9 @@ articles: summary: Delete a table tags: - Table - - operationId: PostConfigureDistinctCache - method: POST - path: /api/v3/configure/distinct_cache - summary: Create distinct cache - tags: - - Cache data - - Table - - operationId: DeleteConfigureDistinctCache - method: DELETE - path: /api/v3/configure/distinct_cache - summary: Delete distinct cache - tags: - - Cache data - - Table - - operationId: PostConfigureLastCache - method: POST - path: /api/v3/configure/last_cache - summary: Create last cache - tags: - - Cache data - - Table - - operationId: DeleteConfigureLastCache - method: DELETE - path: /api/v3/configure/last_cache - summary: Delete last cache - tags: - - Cache data - - Table tagDescription: Manage table schemas and data source: static/openapi/influxdb3-core/tags/tags/ref-table.yaml staticFilePath: /openapi/influxdb3-core/tags/tags/ref-table.yaml - - path: api/token - fields: - name: Token - describes: - - /api/v3/configure/token/admin - - /api/v3/configure/token/admin/regenerate - - /api/v3/configure/token - - /api/v3/configure/token/named_admin - title: Token - description: Manage tokens for authentication and authorization - tag: Token - isConceptual: false - menuGroup: Administration - operations: - - operationId: PostCreateAdminToken - method: POST - path: /api/v3/configure/token/admin - summary: Create admin token - tags: - - Authentication - - Token - - operationId: PostRegenerateAdminToken - method: POST - path: /api/v3/configure/token/admin/regenerate - summary: Regenerate admin token - tags: - - Authentication - - Token - - operationId: DeleteToken - method: DELETE - path: /api/v3/configure/token - summary: Delete token - tags: - - Authentication - - Token - - operationId: PostCreateNamedAdminToken - method: POST - path: /api/v3/configure/token/named_admin - summary: Create named admin token - tags: - - Authentication - - Token - tagDescription: Manage tokens for authentication and authorization - source: static/openapi/influxdb3-core/tags/tags/ref-token.yaml - staticFilePath: /openapi/influxdb3-core/tags/tags/ref-token.yaml - path: api/write-data fields: name: Write data diff --git a/data/article_data/influxdb/influxdb3_enterprise/articles.json b/data/article_data/influxdb/influxdb3_enterprise/articles.json index 834e5452a..7d9b658d4 100644 --- a/data/article_data/influxdb/influxdb3_enterprise/articles.json +++ b/data/article_data/influxdb/influxdb3_enterprise/articles.json @@ -1,5 +1,73 @@ { "articles": [ + { + "path": "api/auth-token", + "fields": { + "name": "Auth token", + "describes": [ + "/api/v3/configure/enterprise/token", + "/api/v3/configure/token/admin", + "/api/v3/configure/token/admin/regenerate", + "/api/v3/configure/token", + "/api/v3/configure/token/named_admin" + ], + "title": "Auth token", + "description": "Manage tokens for authentication and authorization", + "tag": "Auth token", + "isConceptual": false, + "menuGroup": "Other", + "operations": [ + { + "operationId": "PostCreateResourceToken", + "method": "POST", + "path": "/api/v3/configure/enterprise/token", + "summary": "Create a resource token", + "tags": [ + "Auth token" + ] + }, + { + "operationId": "PostCreateAdminToken", + "method": "POST", + "path": "/api/v3/configure/token/admin", + "summary": "Create admin token", + "tags": [ + "Auth token" + ] + }, + { + "operationId": "PostRegenerateAdminToken", + "method": "POST", + "path": "/api/v3/configure/token/admin/regenerate", + "summary": "Regenerate admin token", + "tags": [ + "Auth token" + ] + }, + { + "operationId": "DeleteToken", + "method": "DELETE", + "path": "/api/v3/configure/token", + "summary": "Delete token", + "tags": [ + "Auth token" + ] + }, + { + "operationId": "PostCreateNamedAdminToken", + "method": "POST", + "path": "/api/v3/configure/token/named_admin", + "summary": "Create named admin token", + "tags": [ + "Auth token" + ] + } + ], + "tagDescription": "Manage tokens for authentication and authorization", + "source": "static/openapi/influxdb3-enterprise/tags/tags/ref-auth-token.yaml", + "staticFilePath": "/openapi/influxdb3-enterprise/tags/tags/ref-auth-token.yaml" + } + }, { "path": "api/authentication", "fields": { @@ -23,8 +91,7 @@ "path": "/api/v3/configure/enterprise/token", "summary": "Create a resource token", "tags": [ - "Authentication", - "Token" + "Authentication" ] }, { @@ -33,8 +100,7 @@ "path": "/api/v3/configure/token/admin", "summary": "Create admin token", "tags": [ - "Authentication", - "Token" + "Authentication" ] }, { @@ -43,8 +109,7 @@ "path": "/api/v3/configure/token/admin/regenerate", "summary": "Regenerate admin token", "tags": [ - "Authentication", - "Token" + "Authentication" ] }, { @@ -53,8 +118,7 @@ "path": "/api/v3/configure/token", "summary": "Delete token", "tags": [ - "Authentication", - "Token" + "Authentication" ] }, { @@ -63,8 +127,7 @@ "path": "/api/v3/configure/token/named_admin", "summary": "Create named admin token", "tags": [ - "Authentication", - "Token" + "Authentication" ] } ], @@ -93,8 +156,7 @@ "path": "/api/v3/configure/distinct_cache", "summary": "Create distinct cache", "tags": [ - "Cache data", - "Table" + "Cache data" ] }, { @@ -103,8 +165,7 @@ "path": "/api/v3/configure/distinct_cache", "summary": "Delete distinct cache", "tags": [ - "Cache data", - "Table" + "Cache data" ] }, { @@ -113,8 +174,7 @@ "path": "/api/v3/configure/last_cache", "summary": "Create last cache", "tags": [ - "Cache data", - "Table" + "Cache data" ] }, { @@ -123,8 +183,7 @@ "path": "/api/v3/configure/last_cache", "summary": "Delete last cache", "tags": [ - "Cache data", - "Table" + "Cache data" ] } ], @@ -545,9 +604,7 @@ "fields": { "name": "Table", "describes": [ - "/api/v3/configure/table", - "/api/v3/configure/distinct_cache", - "/api/v3/configure/last_cache" + "/api/v3/configure/table" ], "title": "Table", "description": "Manage table schemas and data", @@ -581,46 +638,6 @@ "tags": [ "Table" ] - }, - { - "operationId": "PostConfigureDistinctCache", - "method": "POST", - "path": "/api/v3/configure/distinct_cache", - "summary": "Create distinct cache", - "tags": [ - "Cache data", - "Table" - ] - }, - { - "operationId": "DeleteConfigureDistinctCache", - "method": "DELETE", - "path": "/api/v3/configure/distinct_cache", - "summary": "Delete distinct cache", - "tags": [ - "Cache data", - "Table" - ] - }, - { - "operationId": "PostConfigureLastCache", - "method": "POST", - "path": "/api/v3/configure/last_cache", - "summary": "Create last cache", - "tags": [ - "Cache data", - "Table" - ] - }, - { - "operationId": "DeleteConfigureLastCache", - "method": "DELETE", - "path": "/api/v3/configure/last_cache", - "summary": "Delete last cache", - "tags": [ - "Cache data", - "Table" - ] } ], "tagDescription": "Manage table schemas and data", @@ -628,79 +645,6 @@ "staticFilePath": "/openapi/influxdb3-enterprise/tags/tags/ref-table.yaml" } }, - { - "path": "api/token", - "fields": { - "name": "Token", - "describes": [ - "/api/v3/configure/enterprise/token", - "/api/v3/configure/token/admin", - "/api/v3/configure/token/admin/regenerate", - "/api/v3/configure/token", - "/api/v3/configure/token/named_admin" - ], - "title": "Token", - "description": "Manage tokens for authentication and authorization", - "tag": "Token", - "isConceptual": false, - "menuGroup": "Administration", - "operations": [ - { - "operationId": "PostCreateResourceToken", - "method": "POST", - "path": "/api/v3/configure/enterprise/token", - "summary": "Create a resource token", - "tags": [ - "Authentication", - "Token" - ] - }, - { - "operationId": "PostCreateAdminToken", - "method": "POST", - "path": "/api/v3/configure/token/admin", - "summary": "Create admin token", - "tags": [ - "Authentication", - "Token" - ] - }, - { - "operationId": "PostRegenerateAdminToken", - "method": "POST", - "path": "/api/v3/configure/token/admin/regenerate", - "summary": "Regenerate admin token", - "tags": [ - "Authentication", - "Token" - ] - }, - { - "operationId": "DeleteToken", - "method": "DELETE", - "path": "/api/v3/configure/token", - "summary": "Delete token", - "tags": [ - "Authentication", - "Token" - ] - }, - { - "operationId": "PostCreateNamedAdminToken", - "method": "POST", - "path": "/api/v3/configure/token/named_admin", - "summary": "Create named admin token", - "tags": [ - "Authentication", - "Token" - ] - } - ], - "tagDescription": "Manage tokens for authentication and authorization", - "source": "static/openapi/influxdb3-enterprise/tags/tags/ref-token.yaml", - "staticFilePath": "/openapi/influxdb3-enterprise/tags/tags/ref-token.yaml" - } - }, { "path": "api/write-data", "fields": { diff --git a/data/article_data/influxdb/influxdb3_enterprise/articles.yml b/data/article_data/influxdb/influxdb3_enterprise/articles.yml index 2d502b7e9..acc7d4262 100644 --- a/data/article_data/influxdb/influxdb3_enterprise/articles.yml +++ b/data/article_data/influxdb/influxdb3_enterprise/articles.yml @@ -1,4 +1,52 @@ articles: + - path: api/auth-token + fields: + name: Auth token + describes: + - /api/v3/configure/enterprise/token + - /api/v3/configure/token/admin + - /api/v3/configure/token/admin/regenerate + - /api/v3/configure/token + - /api/v3/configure/token/named_admin + title: Auth token + description: Manage tokens for authentication and authorization + tag: Auth token + isConceptual: false + menuGroup: Other + operations: + - operationId: PostCreateResourceToken + method: POST + path: /api/v3/configure/enterprise/token + summary: Create a resource token + tags: + - Auth token + - operationId: PostCreateAdminToken + method: POST + path: /api/v3/configure/token/admin + summary: Create admin token + tags: + - Auth token + - operationId: PostRegenerateAdminToken + method: POST + path: /api/v3/configure/token/admin/regenerate + summary: Regenerate admin token + tags: + - Auth token + - operationId: DeleteToken + method: DELETE + path: /api/v3/configure/token + summary: Delete token + tags: + - Auth token + - operationId: PostCreateNamedAdminToken + method: POST + path: /api/v3/configure/token/named_admin + summary: Create named admin token + tags: + - Auth token + tagDescription: Manage tokens for authentication and authorization + source: static/openapi/influxdb3-enterprise/tags/tags/ref-auth-token.yaml + staticFilePath: /openapi/influxdb3-enterprise/tags/tags/ref-auth-token.yaml - path: api/authentication fields: name: Authentication @@ -37,35 +85,30 @@ articles: summary: Create a resource token tags: - Authentication - - Token - operationId: PostCreateAdminToken method: POST path: /api/v3/configure/token/admin summary: Create admin token tags: - Authentication - - Token - operationId: PostRegenerateAdminToken method: POST path: /api/v3/configure/token/admin/regenerate summary: Regenerate admin token tags: - Authentication - - Token - operationId: DeleteToken method: DELETE path: /api/v3/configure/token summary: Delete token tags: - Authentication - - Token - operationId: PostCreateNamedAdminToken method: POST path: /api/v3/configure/token/named_admin summary: Create named admin token tags: - Authentication - - Token tagDescription: > Depending on your workflow, use one of the following schemes to authenticate to the InfluxDB 3 API: @@ -159,28 +202,24 @@ articles: summary: Create distinct cache tags: - Cache data - - Table - operationId: DeleteConfigureDistinctCache method: DELETE path: /api/v3/configure/distinct_cache summary: Delete distinct cache tags: - Cache data - - Table - operationId: PostConfigureLastCache method: POST path: /api/v3/configure/last_cache summary: Create last cache tags: - Cache data - - Table - operationId: DeleteConfigureLastCache method: DELETE path: /api/v3/configure/last_cache summary: Delete last cache tags: - Cache data - - Table tagDescription: > Manage the in-memory cache. @@ -787,8 +826,6 @@ articles: name: Table describes: - /api/v3/configure/table - - /api/v3/configure/distinct_cache - - /api/v3/configure/last_cache title: Table description: Manage table schemas and data tag: Table @@ -813,90 +850,9 @@ articles: summary: Delete a table tags: - Table - - operationId: PostConfigureDistinctCache - method: POST - path: /api/v3/configure/distinct_cache - summary: Create distinct cache - tags: - - Cache data - - Table - - operationId: DeleteConfigureDistinctCache - method: DELETE - path: /api/v3/configure/distinct_cache - summary: Delete distinct cache - tags: - - Cache data - - Table - - operationId: PostConfigureLastCache - method: POST - path: /api/v3/configure/last_cache - summary: Create last cache - tags: - - Cache data - - Table - - operationId: DeleteConfigureLastCache - method: DELETE - path: /api/v3/configure/last_cache - summary: Delete last cache - tags: - - Cache data - - Table tagDescription: Manage table schemas and data source: static/openapi/influxdb3-enterprise/tags/tags/ref-table.yaml staticFilePath: /openapi/influxdb3-enterprise/tags/tags/ref-table.yaml - - path: api/token - fields: - name: Token - describes: - - /api/v3/configure/enterprise/token - - /api/v3/configure/token/admin - - /api/v3/configure/token/admin/regenerate - - /api/v3/configure/token - - /api/v3/configure/token/named_admin - title: Token - description: Manage tokens for authentication and authorization - tag: Token - isConceptual: false - menuGroup: Administration - operations: - - operationId: PostCreateResourceToken - method: POST - path: /api/v3/configure/enterprise/token - summary: Create a resource token - tags: - - Authentication - - Token - - operationId: PostCreateAdminToken - method: POST - path: /api/v3/configure/token/admin - summary: Create admin token - tags: - - Authentication - - Token - - operationId: PostRegenerateAdminToken - method: POST - path: /api/v3/configure/token/admin/regenerate - summary: Regenerate admin token - tags: - - Authentication - - Token - - operationId: DeleteToken - method: DELETE - path: /api/v3/configure/token - summary: Delete token - tags: - - Authentication - - Token - - operationId: PostCreateNamedAdminToken - method: POST - path: /api/v3/configure/token/named_admin - summary: Create named admin token - tags: - - Authentication - - Token - tagDescription: Manage tokens for authentication and authorization - source: static/openapi/influxdb3-enterprise/tags/tags/ref-token.yaml - staticFilePath: /openapi/influxdb3-enterprise/tags/tags/ref-token.yaml - path: api/write-data fields: name: Write data diff --git a/docs/plans/2024-12-10-standalone-operation-pages-design.md b/docs/plans/2024-12-10-standalone-operation-pages-design.md new file mode 100644 index 000000000..70e619af2 --- /dev/null +++ b/docs/plans/2024-12-10-standalone-operation-pages-design.md @@ -0,0 +1,171 @@ +# Standalone API Operation Pages Design + +## Overview + +Create individual pages for each API operation with path-based URLs, rendered using RapiDoc Mini with existing tag-level OpenAPI specs. + +## Goals + +- **SEO/discoverability**: Each operation indexable with its own URL and metadata +- **Deep linking**: Reliable bookmarkable/shareable URLs for specific operations +- **Navigation UX**: Sidebar links navigate to actual pages (not hash fragments) +- **Content customization**: Foundation for adding operation-specific guides and examples + +## URL Structure + +Path-based URLs with HTTP method as the final segment: + +| Operation | API Path | Page URL | +| ------------------ | ----------------------------------- | ------------------------------------ | +| PostV1Write | `POST /write` | `/api/write/post/` | +| PostV2Write | `POST /api/v2/write` | `/api/v2/write/post/` | +| PostWriteLP | `POST /api/v3/write_lp` | `/api/v3/write_lp/post/` | +| GetV1ExecuteQuery | `GET /query` | `/api/query/get/` | +| PostExecuteV1Query | `POST /query` | `/api/query/post/` | +| GetExecuteQuerySQL | `GET /api/v3/query_sql` | `/api/v3/query_sql/get/` | +| GetDatabases | `GET /api/v3/configure/database` | `/api/v3/configure/database/get/` | +| DeleteDatabase | `DELETE /api/v3/configure/database` | `/api/v3/configure/database/delete/` | + +## Architecture + +### Existing Components (unchanged) + +- **Tag pages**: `/api/write-data/`, `/api/query-data/` etc. remain as landing pages +- **Tag-level specs**: `static/openapi/influxdb-{product}/tags/tags/ref-{tag}.yaml` (\~30-50KB each) +- **Sidebar structure**: Tag-based groups with operation summaries as link text + +### New Components + +1. **Operation page content files**: Generated at path-based locations +2. **Operation page template**: Hugo layout using RapiDoc Mini +3. **Updated sidebar links**: Point to path-based URLs instead of hash fragments + +## Content File Structure + +Generated operation pages at `content/influxdb3/{product}/api/{path}/{method}/_index.md`: + +```yaml +--- +title: Write line protocol (v1-compatible) +description: Write data using InfluxDB v1-compatible line protocol endpoint +type: api-operation +layout: operation +# RapiDoc Mini configuration +specFile: /openapi/influxdb-influxdb3_core/tags/tags/ref-write-data.yaml +matchPaths: post /write +# Operation metadata +operationId: PostV1Write +method: POST +apiPath: /write +tag: Write data +compatVersion: v1 +# Links +related: + - /influxdb3/core/write-data/http-api/compatibility-apis/ +--- +``` + +## Hugo Template + +New layout `layouts/api-operation/operation.html`: + +```html +{{ define "main" }} +
+
+

{{ .Title }}

+
+ {{ .Params.method }} + {{ .Params.apiPath }} + {{ with .Params.compatVersion }} + {{ . }} + {{ end }} +
+
+ + + + + {{ with .Params.related }} + + {{ end }} +
+{{ end }} +``` + +## Sidebar Navigation Changes + +Update `layouts/partials/sidebar/api-menu-items.html` to generate path-based URLs: + +**Before:** + +```go +{{ $fragment := printf "#operation/%s" .operationId }} +{{ $fullUrl := printf "%s%s" $tagPageUrl $fragment }} +``` + +**After:** + +```go +{{ $apiPath := .path }} +{{ $method := lower .method }} +{{ $pathSlug := $apiPath | replaceRE "^/" "" }} +{{ $operationUrl := printf "/%s/%s/api/%s/%s/" $product $version $pathSlug $method | relURL }} +``` + +## Generator Changes + +Update `api-docs/scripts/openapi-paths-to-hugo-data/index.ts`: + +1. Add new function `generateOperationPages()` that creates content files for each operation +2. Include operation metadata: specFile path, matchPaths filter, tag association +3. Call from `generateHugoDataByTag()` after generating tag-based articles + +## File Generation Summary + +For InfluxDB 3 Core (\~43 operations), this creates: + +- \~43 new content files at `content/influxdb3/core/api/{path}/{method}/_index.md` +- No new spec files (reuses existing tag-level specs) + +## Data Flow + +``` +OpenAPI Spec + ↓ +Generator extracts operations + ↓ +Creates content files with frontmatter (specFile, matchPaths, metadata) + ↓ +Hugo builds pages using api-operation/operation.html template + ↓ +RapiDoc Mini loads tag-level spec, filters to single operation client-side +``` + +## Testing Plan + +1. Generate operation pages for Core product +2. Verify URLs resolve correctly +3. Verify RapiDoc Mini renders single operation +4. Verify sidebar links navigate to operation pages +5. Test deep linking (direct URL access) +6. Check page titles and meta descriptions for SEO + +## Future Improvements + +- Generate operation-level specs for smaller payloads (if performance issues arise) +- Add custom content sections per operation +- Implement operation search/filtering on tag pages diff --git a/layouts/api/single.html b/layouts/api/single.html index b53a1a249..362acbc8d 100644 --- a/layouts/api/single.html +++ b/layouts/api/single.html @@ -46,12 +46,14 @@

{{ .Title }}

{{ end }} - {{/* Summary/Description */}} - {{ with .Params.summary }} -

{{ . | markdownify }}

- {{ else }} - {{ with .Description }} + {{/* Summary/Description - skip for conceptual pages (shown in content section) */}} + {{ if not (.Params.isConceptual | default false) }} + {{ with .Params.summary }}

{{ . | markdownify }}

+ {{ else }} + {{ with .Description }} +

{{ . | markdownify }}

+ {{ end }} {{ end }} {{ end }} diff --git a/layouts/partials/api/rapidoc-mini.html b/layouts/partials/api/rapidoc-mini.html index c29b82351..2d7251077 100644 --- a/layouts/partials/api/rapidoc-mini.html +++ b/layouts/partials/api/rapidoc-mini.html @@ -95,8 +95,9 @@ rapi-doc::part(section-tag) { } /* Fix parameter table layout - reduce indentation from empty td cells */ +/* Match site's body font size (16px) for consistency */ rapi-doc { - --font-size-regular: 14px; + --font-size-regular: 16px; } /* Fix auth schemes at narrow widths - ensure content is scrollable */ diff --git a/layouts/partials/sidebar/api-menu-items.html b/layouts/partials/sidebar/api-menu-items.html index 3bde1d9b2..204f5b924 100644 --- a/layouts/partials/sidebar/api-menu-items.html +++ b/layouts/partials/sidebar/api-menu-items.html @@ -133,7 +133,12 @@ {{ if and (reflect.IsMap $fields) (isset $fields "operations") }} {{ $operations = index $fields "operations" }} {{ end }} - {{ $hasOperations := gt (len $operations) 0 }} + {{/* Don't show operations for conceptual/traitTag pages (e.g., Authentication) */}} + {{ $isConceptual := false }} + {{ if and (reflect.IsMap $fields) (isset $fields "isConceptual") }} + {{ $isConceptual = index $fields "isConceptual" }} + {{ end }} + {{ $hasOperations := and (gt (len $operations) 0) (not $isConceptual) }}