Files
qqq-cli/scripts/gen-operations.mjs
T
james.maesandClaude Fable 5.1 2a2325bc6f client: merge every extension document and tag each operation with its extension
The client document is now the sample server's live document (core plus esb, UI API 1.8.0) merged
with the extension documents of every qbit checked out beside this repository: health (qqq), alerts,
assistant, collaboration, data-analysis, db-performance, edi, qa-capture, quick-search,
release-notes, secured-data and sql-workbench. 202 operations, 136 of them from extensions.

- scripts/merge-spec.mjs records each extension's path prefix and operationIds in
  info.x-qqq-extensions, and accepts an extension the core input already carries when its
  operations are identical (the live document's esb), instead of failing on the duplicate.
- scripts/merge-extensions.mjs (npm run spec:merge-extensions) finds the documents, merges them and
  writes openapi/extension-sources.json with each document's version, branch and commit.
- scripts/gen-operations.mjs adds `extension` to OperationInfo (from x-qqq-extensions, else the
  /qbit/{name} or established prefix) and exports DOCUMENT_EXTENSIONS.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-31 17:43:58 -05:00

103 lines
4.1 KiB
JavaScript

/*
* Copyright 2026 QRun.IO, Inc.
* Licensed under the Apache License, Version 2.0 (see LICENSE).
*/
/**
* Writes packages/core/src/api/operations.ts from the merged OpenAPI document: one entry per
* operation (id, method, path, path and query parameters, body content types), so the kernel can
* call any operation by id without the generated types, and the CLI can complete operation ids.
*
* Usage: node scripts/gen-operations.mjs [spec.json] [out.ts]
*/
import { readFileSync, writeFileSync, mkdirSync } from 'node:fs'
import { dirname } from 'node:path'
const [specPath = 'packages/ui-api-client/openapi/qqq-v1.json', outPath = 'packages/core/src/api/operations.ts'] = process.argv.slice(2)
const spec = JSON.parse(readFileSync(specPath, 'utf8'))
const extensions = spec.info['x-qqq-extensions'] ?? []
const extensionByOperation = new Map()
for (const e of extensions) for (const id of e.operations ?? []) extensionByOperation.set(id, e.name)
const ESTABLISHED_PREFIXES = { esb: 'esb', alerts: 'alerts', assistant: 'assistant', 'sql-workbench': 'sql-workbench', health: 'health' }
/** The extension an operation belongs to: x-qqq-extensions, else its /qbit/{name} or established path prefix. */
function extensionOf(id, path) {
if (extensionByOperation.has(id)) return extensionByOperation.get(id)
const rest = path.replace(/^\/qqq\/v1/, '').split('/')
if (rest[1] === 'qbit' && rest[2]) return rest[2]
return ESTABLISHED_PREFIXES[rest[1]]
}
const operations = []
for (const [path, item] of Object.entries(spec.paths)) {
for (const method of ['get', 'post', 'put', 'patch', 'delete']) {
const op = item[method]
if (!op) continue
const params = [...(item.parameters ?? []), ...(op.parameters ?? [])]
operations.push({
id: op.operationId,
method: method.toUpperCase(),
path,
summary: (op.summary ?? op.description ?? '').split('\n')[0].slice(0, 120),
tag: (op.tags ?? [])[0] ?? '',
pathParams: params.filter((p) => p.in === 'path').map((p) => p.name),
queryParams: params.filter((p) => p.in === 'query').map((p) => p.name),
bodyTypes: Object.keys(op.requestBody?.content ?? {}),
...(extensionOf(op.operationId, path) ? { extension: extensionOf(op.operationId, path) } : {}),
})
}
}
operations.sort((a, b) => a.id.localeCompare(b.id))
const lines = [
'/*',
' * Copyright 2026 QRun.IO, Inc.',
' * Licensed under the Apache License, Version 2.0 (see LICENSE).',
' */',
'',
`// Generated by scripts/gen-operations.mjs from the UI API ${spec.info.version} document (core and ${extensions.length} extensions). Do not edit.`,
'',
'export interface OperationInfo {',
' id: string',
" method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'",
' path: string',
' summary: string',
' tag: string',
' pathParams: string[]',
' queryParams: string[]',
' bodyTypes: string[]',
' /** The extension (qbit or module) that serves this operation; absent for core operations. */',
' extension?: string',
'}',
'',
'/** An extension document merged into the client document (info.x-qqq-extensions). */',
'export interface DocumentExtension {',
' name: string',
' version: string',
' pathPrefix?: string',
' description?: string',
'}',
'',
`export const UI_API_DOCUMENT_VERSION = ${JSON.stringify(spec.info.version)}`,
'',
'/** The extensions merged into the client document, with the versions it was generated from. */',
'export const DOCUMENT_EXTENSIONS: DocumentExtension[] = [',
...extensions.map((e) => ` ${JSON.stringify({ name: e.name, version: e.version, pathPrefix: e.pathPrefix, description: (e.description ?? '').split('\n')[0].slice(0, 160) })},`),
']',
'',
'export const OPERATIONS: OperationInfo[] = [',
...operations.map((o) => ` ${JSON.stringify(o)},`),
']',
'',
'const byId = new Map(OPERATIONS.map((o) => [o.id, o]))',
'',
'/** The operation with this id, or undefined. */',
'export function operation(id: string): OperationInfo | undefined {',
' return byId.get(id)',
'}',
'',
]
mkdirSync(dirname(outPath), { recursive: true })
writeFileSync(outPath, lines.join('\n'))
console.log(`${operations.length} operations -> ${outPath}`)