mirror of
https://github.com/langgenius/dify.git
synced 2026-05-25 19:00:43 -04:00
chore: move API readiness reporting to terminal output (#36433)
This commit is contained in:
@@ -1,40 +0,0 @@
|
||||
# Contracts
|
||||
|
||||
## API OpenAPI Readiness
|
||||
|
||||
<!-- api-openapi-readiness:start -->
|
||||
|
||||
<!-- This section is auto-generated by scripts/generate-api-readiness-readme.mjs. Do not edit between the markers. -->
|
||||
|
||||
Snapshot generated from `packages/contracts/generated/api/readiness.json` after running `pnpm -C packages/contracts gen-api-contract-from-openapi`.
|
||||
|
||||
Are we OpenAPI ready? **No.** Current generated API contracts are **35.4% ready**.
|
||||
|
||||
| Surface | Ready | Not ready | Total | Ready % |
|
||||
| --------- | ------: | --------: | ------: | --------: |
|
||||
| console | 205 | 383 | 588 | 34.9% |
|
||||
| service | 28 | 60 | 88 | 31.8% |
|
||||
| web | 21 | 20 | 41 | 51.2% |
|
||||
| **total** | **254** | **463** | **717** | **35.4%** |
|
||||
|
||||
Readiness here means the generated contract operation is not marked with:
|
||||
|
||||
> Generated contract types may be inaccurate because backend OpenAPI annotations are incomplete. Do not migrate callers until the generated contract is accurate.
|
||||
|
||||
Operations marked with that warning should not be migrated to blindly. Prefer fixing backend OpenAPI annotations first so the generated contract has accurate request and response types, then migrate callers endpoint by endpoint.
|
||||
|
||||
The current heuristic marks an operation as not ready when a request body or success response that should have a body contains a loose object type, when a mutating controller reads a JSON body that is not documented as a request body, or when an operation has no documented 2xx response. 204, 205, and 304 responses are treated as bodyless when the request type is otherwise accurate.
|
||||
|
||||
<!-- api-openapi-readiness:end -->
|
||||
|
||||
## How to Improve Readiness
|
||||
|
||||
Improve the ready percentage by fixing the backend annotations that produce loose generated types, then regenerating the contracts.
|
||||
|
||||
- Add accurate request body schemas for endpoints that currently generate loose object types.
|
||||
- Add accurate 2xx response schemas for endpoints that return JSON payloads.
|
||||
- Use 204 responses for endpoints that intentionally return no body.
|
||||
- Avoid untyped dictionaries, raw objects, or `additionalProperties: true` responses unless the API really returns an arbitrary object.
|
||||
- Regenerate with `pnpm -C packages/contracts gen-api-contract` and use this README to verify the updated percentage.
|
||||
|
||||
Do not remove the generated warning just to increase the number. The warning should disappear because the backend OpenAPI output became accurate enough for callers to migrate safely.
|
||||
@@ -1,17 +0,0 @@
|
||||
{
|
||||
"surfaces": {
|
||||
"console": {
|
||||
"notReady": 383,
|
||||
"total": 588
|
||||
},
|
||||
"service": {
|
||||
"notReady": 60,
|
||||
"total": 88
|
||||
},
|
||||
"web": {
|
||||
"notReady": 20,
|
||||
"total": 41
|
||||
}
|
||||
},
|
||||
"warning": "Generated contract types may be inaccurate because backend OpenAPI annotations are incomplete. Do not migrate callers until the generated contract is accurate."
|
||||
}
|
||||
@@ -90,7 +90,6 @@ type ApiOperationContext = {
|
||||
|
||||
const currentDir = path.dirname(fileURLToPath(import.meta.url))
|
||||
const apiOpenApiDir = path.resolve(currentDir, 'openapi')
|
||||
const apiReadinessStatsPath = path.resolve(currentDir, 'generated/api/readiness.json')
|
||||
const apiControllersDir = path.resolve(currentDir, '../../api/controllers')
|
||||
|
||||
const operationMethods = new Set(['delete', 'get', 'patch', 'post', 'put'])
|
||||
@@ -750,6 +749,10 @@ const recordApiReadiness = (surface: string, isReady: boolean) => {
|
||||
stats.notReady += 1
|
||||
}
|
||||
|
||||
const formatPercent = (ready: number, total: number) => {
|
||||
return total === 0 ? '0.0%' : `${((ready / total) * 100).toFixed(1)}%`
|
||||
}
|
||||
|
||||
const normalizeOperations = (document: SwaggerDocument, surface: string) => {
|
||||
const definitions = document.definitions ??= {}
|
||||
|
||||
@@ -792,18 +795,29 @@ const normalizeApiSwagger = (document: SwaggerDocument, surface: string) => {
|
||||
return document
|
||||
}
|
||||
|
||||
const writeApiReadinessStats = () => {
|
||||
const printApiReadinessStats = () => {
|
||||
const sortedSurfaces = Object.entries(apiReadinessStats)
|
||||
.sort(([left], [right]) => left.localeCompare(right))
|
||||
|
||||
fs.mkdirSync(path.dirname(apiReadinessStatsPath), { recursive: true })
|
||||
fs.writeFileSync(
|
||||
apiReadinessStatsPath,
|
||||
`${JSON.stringify({
|
||||
surfaces: Object.fromEntries(sortedSurfaces),
|
||||
warning: inaccurateGeneratedContractDescription,
|
||||
}, null, 2)}\n`,
|
||||
const totals = sortedSurfaces.reduce(
|
||||
(summary, [, stats]) => {
|
||||
summary.notReady += stats.notReady
|
||||
summary.total += stats.total
|
||||
return summary
|
||||
},
|
||||
{ notReady: 0, total: 0 },
|
||||
)
|
||||
const totalReady = totals.total - totals.notReady
|
||||
const rows = sortedSurfaces.map(([surface, stats]) => {
|
||||
const ready = stats.total - stats.notReady
|
||||
return ` ${surface}: ${ready}/${stats.total} ready (${formatPercent(ready, stats.total)}), ${stats.notReady} not ready`
|
||||
})
|
||||
|
||||
console.log([
|
||||
'API OpenAPI readiness:',
|
||||
...rows,
|
||||
` total: ${totalReady}/${totals.total} ready (${formatPercent(totalReady, totals.total)}), ${totals.notReady} not ready`,
|
||||
].join('\n'))
|
||||
}
|
||||
|
||||
const topLevelPathSegment = (routePath: string) => {
|
||||
@@ -933,7 +947,7 @@ const createApiJobs = (spec: ApiSpec): ApiJob[] => {
|
||||
}
|
||||
|
||||
const apiJobs = apiSpecs.flatMap(createApiJobs)
|
||||
writeApiReadinessStats()
|
||||
printApiReadinessStats()
|
||||
|
||||
const createApiConfig = (job: ApiJob): UserConfig => ({
|
||||
input: job.document,
|
||||
|
||||
@@ -15,9 +15,8 @@
|
||||
},
|
||||
"scripts": {
|
||||
"gen-api-contract": "pnpm gen-api-openapi && pnpm gen-api-contract-from-openapi",
|
||||
"gen-api-contract-from-openapi": "node -e \"fs.rmSync('generated/api', { recursive: true, force: true })\" && openapi-ts -f openapi-ts.api.config.ts && vp fmt generated/api && eslint --fix generated/api && pnpm gen-api-readiness-readme",
|
||||
"gen-api-contract-from-openapi": "node -e \"fs.rmSync('generated/api', { recursive: true, force: true })\" && openapi-ts -f openapi-ts.api.config.ts && vp fmt generated/api && eslint --fix generated/api",
|
||||
"gen-api-openapi": "uv run --project ../../api ../../api/dev/generate_swagger_specs.py --output-dir openapi",
|
||||
"gen-api-readiness-readme": "node scripts/generate-api-readiness-readme.mjs && eslint --fix README.md",
|
||||
"gen-enterprise-contract": "openapi-ts -f openapi-ts.enterprise.config.ts",
|
||||
"type-check": "tsgo"
|
||||
},
|
||||
|
||||
@@ -1,94 +0,0 @@
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
const currentDir = path.dirname(fileURLToPath(import.meta.url))
|
||||
const packageDir = path.resolve(currentDir, '..')
|
||||
const readinessStatsPath = path.resolve(packageDir, 'generated/api/readiness.json')
|
||||
const readmePath = path.resolve(packageDir, 'README.md')
|
||||
|
||||
const readinessStartMarker = '<!-- api-openapi-readiness:start -->'
|
||||
const readinessEndMarker = '<!-- api-openapi-readiness:end -->'
|
||||
|
||||
const formatPercent = (ready, total) => {
|
||||
return total === 0 ? '0.0%' : `${((ready / total) * 100).toFixed(1)}%`
|
||||
}
|
||||
|
||||
const collectStats = () => {
|
||||
if (!fs.existsSync(readinessStatsPath)) {
|
||||
throw new Error(
|
||||
`Missing API readiness stats: ${readinessStatsPath}. Run "pnpm -C packages/contracts gen-api-contract-from-openapi" first.`,
|
||||
)
|
||||
}
|
||||
|
||||
return JSON.parse(fs.readFileSync(readinessStatsPath, 'utf8'))
|
||||
}
|
||||
|
||||
const tableRow = (surface, ready, notReady, total) => {
|
||||
return `| ${surface} | ${ready} | ${notReady} | ${total} | ${formatPercent(ready, total)} |`
|
||||
}
|
||||
|
||||
const renderReadinessSection = (stats) => {
|
||||
const rows = Object.entries(stats.surfaces)
|
||||
.sort(([left], [right]) => left.localeCompare(right))
|
||||
.map(([surface, stat]) => tableRow(surface, stat.total - stat.notReady, stat.notReady, stat.total))
|
||||
|
||||
const totals = Object.values(stats.surfaces).reduce(
|
||||
(summary, stat) => {
|
||||
summary.notReady += stat.notReady
|
||||
summary.total += stat.total
|
||||
return summary
|
||||
},
|
||||
{ notReady: 0, total: 0 },
|
||||
)
|
||||
const totalReady = totals.total - totals.notReady
|
||||
|
||||
if (totals.total === 0)
|
||||
throw new Error(`No API readiness stats found in ${readinessStatsPath}`)
|
||||
|
||||
return `${readinessStartMarker}
|
||||
|
||||
<!-- This section is auto-generated by scripts/generate-api-readiness-readme.mjs. Do not edit between the markers. -->
|
||||
|
||||
Snapshot generated from \`packages/contracts/generated/api/readiness.json\` after running \`pnpm -C packages/contracts gen-api-contract-from-openapi\`.
|
||||
|
||||
Are we OpenAPI ready? **No.** Current generated API contracts are **${formatPercent(totalReady, totals.total)} ready**.
|
||||
|
||||
| Surface | Ready | Not ready | Total | Ready % |
|
||||
| --- | ---: | ---: | ---: | ---: |
|
||||
${rows.join('\n')}
|
||||
| **total** | **${totalReady}** | **${totals.notReady}** | **${totals.total}** | **${formatPercent(totalReady, totals.total)}** |
|
||||
|
||||
Readiness here means the generated contract operation is not marked with:
|
||||
|
||||
> ${stats.warning}
|
||||
|
||||
Operations marked with that warning should not be migrated to blindly. Prefer fixing backend OpenAPI annotations first so the generated contract has accurate request and response types, then migrate callers endpoint by endpoint.
|
||||
|
||||
The current heuristic marks an operation as not ready when a request body or success response that should have a body contains a loose object type, when a mutating controller reads a JSON body that is not documented as a request body, or when an operation has no documented 2xx response. 204, 205, and 304 responses are treated as bodyless when the request type is otherwise accurate.
|
||||
|
||||
${readinessEndMarker}
|
||||
`
|
||||
}
|
||||
|
||||
const updateReadme = (readinessSection) => {
|
||||
const readme = fs.readFileSync(readmePath, 'utf8')
|
||||
const startIndex = readme.indexOf(readinessStartMarker)
|
||||
const endIndex = readme.indexOf(readinessEndMarker)
|
||||
|
||||
if (startIndex === -1 || endIndex === -1 || endIndex < startIndex) {
|
||||
throw new Error(
|
||||
`Missing readiness markers in ${readmePath}. Expected ${readinessStartMarker} and ${readinessEndMarker}.`,
|
||||
)
|
||||
}
|
||||
|
||||
const nextReadme = [
|
||||
readme.slice(0, startIndex),
|
||||
readinessSection,
|
||||
readme.slice(endIndex + readinessEndMarker.length),
|
||||
].join('')
|
||||
|
||||
fs.writeFileSync(readmePath, nextReadme)
|
||||
}
|
||||
|
||||
updateReadme(renderReadinessSection(collectStats()))
|
||||
Reference in New Issue
Block a user