94 lines
5.8 KiB
JavaScript
94 lines
5.8 KiB
JavaScript
import fs from 'node:fs'
|
|
import path from 'node:path'
|
|
import crypto from 'node:crypto'
|
|
|
|
const sourcePath = 'contracts/api/kbx.api.json'
|
|
const sourceText = fs.readFileSync(sourcePath, 'utf8')
|
|
const source = JSON.parse(sourceText)
|
|
const sha = crypto.createHash('sha256').update(sourceText).digest('hex')
|
|
const operations = source.operations ?? []
|
|
|
|
const ids = new Set()
|
|
const routes = new Set()
|
|
for (const op of operations) {
|
|
if (!op.id || !op.method || !op.path) throw new Error(`Invalid API operation: ${JSON.stringify(op)}`)
|
|
if (ids.has(op.id)) throw new Error(`Duplicate operation id: ${op.id}`)
|
|
ids.add(op.id)
|
|
const routeKey = `${op.method} ${op.path}`
|
|
if (routes.has(routeKey)) throw new Error(`Duplicate API route: ${routeKey}`)
|
|
routes.add(routeKey)
|
|
if (!['GET','POST','PUT','PATCH','DELETE'].includes(op.method)) throw new Error(`Unsupported method: ${op.method}`)
|
|
if (op.idempotency === 'required' && op.method === 'GET') throw new Error(`GET cannot require idempotency: ${op.id}`)
|
|
}
|
|
|
|
const manifest = {
|
|
schemaVersion: source.schemaVersion,
|
|
apiVersion: source.apiVersion,
|
|
sourceSha256: sha,
|
|
operationCount: operations.length,
|
|
operations: operations.map(({ source: _source, ...op }) => op),
|
|
}
|
|
fs.mkdirSync('generated', { recursive: true })
|
|
fs.writeFileSync('generated/api-manifest.json', JSON.stringify(manifest, null, 2) + '\n')
|
|
|
|
const problemSchemaText = fs.readFileSync('contracts/problems/kbx.problem.schema.json','utf8')
|
|
const problemSchemaSha = crypto.createHash('sha256').update(problemSchemaText).digest('hex')
|
|
const problemSchema = JSON.parse(problemSchemaText)
|
|
const problemTypes = ['validation','businessRule','conflict','permission','notFound','integration','system']
|
|
.map(key => problemSchema.$defs?.[key]?.allOf?.[1]?.properties?.type?.const)
|
|
.filter(Boolean)
|
|
fs.writeFileSync('generated/problem-manifest.json', JSON.stringify({
|
|
schemaVersion: '1.0', sourceSha256: problemSchemaSha, types: problemTypes,
|
|
}, null, 2) + '\n')
|
|
|
|
const tsOps = operations.map(op => ` ${JSON.stringify(op.id)}: ${JSON.stringify({
|
|
id: op.id, method: op.method, path: op.path, permission: op.permission ?? null, kind: op.kind,
|
|
idempotency: op.idempotency, successStatuses: op.successStatuses ?? [200],
|
|
...(op.contentType ? { contentType: op.contentType } : {}),
|
|
...(op.responseType ? { responseType: op.responseType } : {}),
|
|
})},`).join('\n')
|
|
const ts = `// Generated from ${sourcePath}. Do not edit.\nimport type { KbxApiOperationDefinition } from '../api'\n\nexport const kbxApiSourceSha256 = '${sha}' as const\nexport const kbxApiCatalog = {\n${tsOps}\n} as const satisfies Record<string, KbxApiOperationDefinition>\n\nexport type KbxApiOperationId = keyof typeof kbxApiCatalog\nexport type KbxApiOperation = (typeof kbxApiCatalog)[KbxApiOperationId]\n`
|
|
fs.mkdirSync('packages/kbx-contracts/src/generated', { recursive: true })
|
|
fs.writeFileSync('packages/kbx-contracts/src/generated/apiCatalog.ts', ts)
|
|
|
|
const csItems = operations.map(op => ` new(${JSON.stringify(op.id)}, ${JSON.stringify(op.method)}, ${JSON.stringify(op.path)}, ${op.permission ? JSON.stringify(op.permission) : 'null'}, ${JSON.stringify(op.kind)}, ${JSON.stringify(op.idempotency)}, new[] { ${(op.successStatuses ?? [200]).join(', ')} })`).join(',\n')
|
|
const cs = `// Generated from ${sourcePath}. Do not edit.\nnamespace Shared.Contracts.Generated;\n\npublic sealed record KbxApiOperationContract(\n string Id, string Method, string Path, string? Permission, string Kind, string Idempotency, IReadOnlyList<int> SuccessStatuses);\n\npublic static class KbxApiCatalog\n{\n public const string SourceSha256 = "${sha}";\n public static readonly IReadOnlyList<KbxApiOperationContract> All = new KbxApiOperationContract[]\n {\n${csItems}\n };\n}\n`
|
|
fs.mkdirSync('backend/Shared/Contracts/Generated', { recursive: true })
|
|
fs.writeFileSync('backend/Shared/Contracts/Generated/KbxApiCatalog.g.cs', cs)
|
|
|
|
const paths = {}
|
|
for (const op of operations) {
|
|
const normalizedPath = op.path.replace(/\{([^}:]+):[^}]+\}/g, '{$1}')
|
|
const method = op.method.toLowerCase()
|
|
paths[normalizedPath] ??= {}
|
|
const parameters = [...normalizedPath.matchAll(/\{([^}]+)\}/g)].map(m => ({
|
|
name: m[1], in: 'path', required: true, schema: { type: 'string' }
|
|
}))
|
|
paths[normalizedPath][method] = {
|
|
operationId: op.id,
|
|
tags: [op.id.split('.')[0].toUpperCase()],
|
|
parameters,
|
|
responses: Object.fromEntries([
|
|
...(op.successStatuses ?? [200]).map(status => [String(status), { description: 'Success' }]),
|
|
['400', { description: 'Validation problem', content: { 'application/json': { schema: { '$ref': '#/components/schemas/KbxProblem' } } } }],
|
|
['403', { description: 'Permission problem', content: { 'application/json': { schema: { '$ref': '#/components/schemas/KbxProblem' } } } }],
|
|
['404', { description: 'Not found problem', content: { 'application/json': { schema: { '$ref': '#/components/schemas/KbxProblem' } } } }],
|
|
['409', { description: 'Conflict problem', content: { 'application/json': { schema: { '$ref': '#/components/schemas/KbxProblem' } } } }],
|
|
['422', { description: 'Business problem', content: { 'application/json': { schema: { '$ref': '#/components/schemas/KbxProblem' } } } }],
|
|
['500', { description: 'System problem', content: { 'application/json': { schema: { '$ref': '#/components/schemas/KbxProblem' } } } }],
|
|
]),
|
|
'x-kbx-permission': op.permission ?? null,
|
|
'x-kbx-idempotency': op.idempotency,
|
|
'x-kbx-kind': op.kind,
|
|
}
|
|
}
|
|
const openapi = {
|
|
openapi: '3.1.0',
|
|
info: { title: 'KBX API Contract', version: source.apiVersion },
|
|
paths,
|
|
components: { schemas: { KbxProblem: problemSchema } },
|
|
'x-kbx-source-sha256': sha,
|
|
}
|
|
fs.writeFileSync('contracts/api/openapi.kbx.json', JSON.stringify(openapi, null, 2) + '\n')
|
|
console.log(`API contracts generated: ${operations.length} operations, SHA ${sha.slice(0,12)}`)
|