Skip to content

Commit f553b3c

Browse files
authored
feat(api): update aip field filter types (#4301)
1 parent cbd4e2a commit f553b3c

21 files changed

Lines changed: 2392 additions & 786 deletions

File tree

api/spec/Makefile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ generate: format ## Generate OpenAPI spec
1616
pnpm --filter @openmeter/api-spec-aip exec openapi bundle output/definitions/metering-and-billing/v3/openapi.OpenMeter.yaml -o ../../../v3/openapi.yaml
1717
cp packages/legacy/output/openapi.OpenMeter.yaml ../openapi.yaml
1818
cp packages/legacy/output/openapi.OpenMeterCloud.yaml ../openapi.cloud.yaml
19+
cp packages/aip/output/definitions/metering-and-billing/v3/openapi.Test.yaml ../v3/test/openapi.test.yaml
1920

2021
.PHONY: lint
2122
lint: ## Lint OpenAPI spec

api/spec/packages/aip/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"version": "0.1.0",
44
"type": "module",
55
"scripts": {
6-
"generate": "tsp compile --config tspconfig.yaml ./src && node ./scripts/flatten-allof.mjs ./output/definitions/metering-and-billing/v3/openapi.MeteringAndBilling.yaml",
6+
"generate": "tsp compile --config tspconfig.yaml ./src && node ./scripts/flatten-allof.mjs ./output/definitions/metering-and-billing/v3/openapi.MeteringAndBilling.yaml && node ./scripts/seal-object-schemas.mjs ./output/definitions/metering-and-billing/v3/*.yaml",
77
"bundle": "openapi bundle output/definitions/metering-and-billing/v3/openapi.OpenMeter.yaml",
88
"watch": "tsp compile --watch --config tspconfig.yaml ./src",
99
"format": "prettier --ignore-path ../../.prettierignore --list-different --find-config-path --write .",
Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
#!/usr/bin/env node
2+
3+
import fs from 'node:fs/promises'
4+
import process from 'node:process'
5+
import YAML from 'yaml'
6+
7+
const YAML_OPTIONS = {
8+
indent: 2,
9+
lineWidth: 0,
10+
}
11+
12+
function isPlainObject(value) {
13+
return value != null && typeof value === 'object' && !Array.isArray(value)
14+
}
15+
16+
// `@typespec/openapi3` with `seal-object-schemas: true` emits
17+
// `additionalProperties: { not: {} }` to forbid extra properties.
18+
// kin-openapi's deepObject decoder cannot pick the matching branch of a
19+
// `oneOf` when the branch carries that form, so every nested-object
20+
// query (e.g. `filter[boolean][eq]=true`) becomes "path is not
21+
// convertible to primitive". `additionalProperties: false` is
22+
// semantically equivalent and accepted by kin-openapi, so rewrite it.
23+
function isNotEmptyObject(value) {
24+
if (!isPlainObject(value)) {
25+
return false
26+
}
27+
28+
const keys = Object.keys(value)
29+
if (keys.length !== 1 || keys[0] !== 'not') {
30+
return false
31+
}
32+
33+
return isPlainObject(value.not) && Object.keys(value.not).length === 0
34+
}
35+
36+
function rewriteAdditionalProperties(node) {
37+
if (Array.isArray(node)) {
38+
let changed = false
39+
40+
for (const item of node) {
41+
changed = rewriteAdditionalProperties(item) || changed
42+
}
43+
44+
return changed
45+
}
46+
47+
if (!isPlainObject(node)) {
48+
return false
49+
}
50+
51+
let changed = false
52+
53+
if (isNotEmptyObject(node.additionalProperties)) {
54+
node.additionalProperties = false
55+
changed = true
56+
}
57+
58+
for (const value of Object.values(node)) {
59+
changed = rewriteAdditionalProperties(value) || changed
60+
}
61+
62+
return changed
63+
}
64+
65+
async function pathExists(path) {
66+
try {
67+
await fs.access(path)
68+
return true
69+
} catch {
70+
return false
71+
}
72+
}
73+
74+
async function validateInputFile(filePath) {
75+
if (!(await pathExists(filePath))) {
76+
throw new Error(`file not found: ${filePath}`)
77+
}
78+
79+
const stat = await fs.stat(filePath)
80+
if (!stat.isFile()) {
81+
throw new Error(`not a file: ${filePath}`)
82+
}
83+
}
84+
85+
async function readYamlFile(filePath) {
86+
const raw = await fs.readFile(filePath, 'utf8')
87+
88+
try {
89+
return YAML.parse(raw)
90+
} catch (error) {
91+
throw new Error(`parse error: ${error.message}`)
92+
}
93+
}
94+
95+
async function writeYamlFile(filePath, document) {
96+
const output = YAML.stringify(document, YAML_OPTIONS)
97+
const normalized = output.endsWith('\n') ? output : `${output}\n`
98+
99+
await fs.writeFile(filePath, normalized, 'utf8')
100+
}
101+
102+
function printUsage() {
103+
process.stderr.write(
104+
'Usage: seal-object-schemas.mjs <openapi.yaml-or-glob> [<openapi.yaml-or-glob> ...]\n',
105+
)
106+
}
107+
108+
async function processFile(filePath) {
109+
await validateInputFile(filePath)
110+
111+
const parsed = await readYamlFile(filePath)
112+
const changed = rewriteAdditionalProperties(parsed)
113+
114+
if (changed) {
115+
await writeYamlFile(filePath, parsed)
116+
}
117+
}
118+
119+
function looksLikeGlob(pattern) {
120+
return /[*?[]/.test(pattern)
121+
}
122+
123+
async function expandPattern(pattern) {
124+
if (!looksLikeGlob(pattern)) {
125+
return [pattern]
126+
}
127+
128+
const matches = []
129+
for await (const match of fs.glob(pattern)) {
130+
matches.push(match)
131+
}
132+
matches.sort()
133+
134+
if (matches.length === 0) {
135+
throw new Error(`no files matched pattern: ${pattern}`)
136+
}
137+
138+
return matches
139+
}
140+
141+
async function main() {
142+
const patterns = process.argv.slice(2)
143+
144+
if (patterns.length === 0) {
145+
printUsage()
146+
process.exitCode = 1
147+
return
148+
}
149+
150+
try {
151+
const seen = new Set()
152+
for (const pattern of patterns) {
153+
const filePaths = await expandPattern(pattern)
154+
for (const filePath of filePaths) {
155+
if (seen.has(filePath)) {
156+
continue
157+
}
158+
seen.add(filePath)
159+
await processFile(filePath)
160+
}
161+
}
162+
} catch (error) {
163+
process.stderr.write(`seal-object-schemas: ${error.message}\n`)
164+
process.exitCode = 1
165+
}
166+
}
167+
168+
await main()

0 commit comments

Comments
 (0)