Files
qqq-cli/scripts/gen-operations.mjs
james.maesandClaude Fable 5.1 1bc4ddd0be release: qqq 1.0.0 (#11)
qqq 1.0.0: the command-line client and the full-screen terminal UI (qqq ui) for QQQ applications over the UI API (/qqq/v1), with @qrun-io/qqq-cli 1.0.0, @qrun-io/qqq-core 1.0.0, @qrun-io/qqq-ui-api-client 1.8.0 and @qrun-io/qqq-ui-api-conformance 1.6.0 going final together. Squash of the release/1.0.0 branch at 79ba68e (the branch is kept with the full signed history; the tags v1.8.0 and cli-v1.0.0 point at that commit, which is what the released binaries were built from). See CHANGELOG.md [1.0.0] and docs/release-notes/1.0.0.md.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-11-01 12:49:06 -06: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}`)