Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
109 changes: 80 additions & 29 deletions valhalla/jawn/fix_swagger_operators.py
Original file line number Diff line number Diff line change
@@ -1,55 +1,106 @@
"""
Repair empty filter-operator schemas in the tsoa-generated swagger files.
Repair empty filter-operator models in the tsoa-generated artifacts.

tsoa's type resolver intermittently fails to expand Partial<Record<..., string>>
aliases from @helicone-package/filters (the result depends on the order in which
the compiler happens to visit the referencing controllers), leaving schemas like
Partial_TextOperators_ as an empty object. An empty schema type-checks as
Record<string, never> downstream and breaks every filter body in the web client.
tsoa's type resolver intermittently fails to expand the operator aliases from
@helicone-package/filters (the result depends on the order in which the
compiler happens to visit the referencing controllers), leaving models such as
Partial_TextOperators_ as an empty object. That breaks two things:

* swagger.json -> the web client types every text filter body as
Record<string, never>, which fails the web build.
* routes.ts -> TSOA's *runtime* validation (noImplicitAdditionalProperties
= throw-on-extras) rejects every filter that uses a text operator with
'"..." is an excess property', which broke request filtering in prod on
2026-08-31 when only swagger.json was being repaired.

Until tsoa resolves these reliably (see https://github.com/lukeautry/tsoa/issues/911
for the underlying Record-alias handling), patch the known operator schemas with
their true expansions, which mirror packages/filters/filterDefs.ts.
for the underlying alias handling), patch the known operator models in BOTH
artifacts with their true expansions, which mirror packages/filters/filterDefs.ts,
and fail the build if any of them is still empty afterwards.
"""

import json
import re
import sys

TEXT_OPERATOR_KEYS = ["not-equals", "equals", "like", "ilike", "contains", "not-contains"]
VECTOR_OPERATOR_KEYS = ["contains"]

REPAIRS = {
"Partial_TextOperators_": {
"properties": {key: {"type": "string"} for key in TEXT_OPERATOR_KEYS},
"type": "object",
"description": "Make all properties in T optional",
},
"Partial_VectorOperators_": {
"properties": {key: {"type": "string"} for key in VECTOR_OPERATOR_KEYS},
"type": "object",
"description": "Make all properties in T optional",
},
# name -> (property keys, swagger type, tsoa dataType)
OPERATORS = {
"Partial_TextOperators_": (
["not-equals", "equals", "like", "ilike", "contains", "not-contains"],
"string",
"string",
),
"Partial_VectorOperators_": (["contains"], "string", "string"),
"Partial_NumberOperators_": (
["not-equals", "equals", "gte", "lte", "lt", "gt"],
"number",
"double",
),
"Partial_BooleanOperators_": (["equals"], "boolean", "boolean"),
"Partial_TimestampOperators_": (
["equals", "gte", "lte", "lt", "gt"],
"string",
"string",
),
}


def repair(path: str) -> None:
def repair_swagger(path: str) -> None:
with open(path) as f:
spec = json.load(f)

schemas = spec.get("components", {}).get("schemas", {})
repaired = []
for name, replacement in REPAIRS.items():
for name, (keys, swagger_type, _) in OPERATORS.items():
schema = schemas.get(name)
if schema is not None and not schema.get("properties"):
schemas[name] = replacement
schemas[name] = {
"properties": {k: {"type": swagger_type} for k in keys},
"type": "object",
"description": "Make all properties in T optional",
}
repaired.append(name)

if repaired:
with open(path, "w") as f:
json.dump(spec, f, indent="\t")
print(f"{path}: repaired {', '.join(repaired)}")


def repair_routes(path: str) -> None:
with open(path) as f:
src = f.read()
repaired = []
for name, (keys, _, tsoa_type) in OPERATORS.items():
# tsoa emits: "Partial_X_": {\n "dataType": "refAlias",\n "type": {"dataType":"nestedObjectLiteral","nestedProperties":{},"validators":{}},
pattern = re.compile(
r'("' + re.escape(name) + r'":\s*\{\s*"dataType":\s*"refAlias",\s*"type":\s*\{"dataType":"nestedObjectLiteral","nestedProperties":)\{\}'
)
props = json.dumps({k: {"dataType": tsoa_type} for k in keys}, separators=(",", ":"))
src, n = pattern.subn(lambda m: m.group(1) + props, src)
if n:
repaired.append(name)
if repaired:
with open(path, "w") as f:
f.write(src)
print(f"{path}: repaired {', '.join(repaired)}")
# Fail loudly if a known operator model is still empty.
still_empty = [
name
for name in OPERATORS
if re.search(
r'"' + re.escape(name) + r'":\s*\{\s*"dataType":\s*"refAlias",\s*"type":\s*\{"dataType":"nestedObjectLiteral","nestedProperties":\{\}',
src,
)
]
if still_empty:
print(f"{path}: ERROR operator models still empty after repair: {', '.join(still_empty)}", file=sys.stderr)
sys.exit(1)


if __name__ == "__main__":
for swagger_path in sys.argv[1:]:
repair(swagger_path)
for p in sys.argv[1:]:
if p.endswith(".json"):
repair_swagger(p)
elif p.endswith(".ts"):
repair_routes(p)
else:
print(f"skipping unknown artifact {p}", file=sys.stderr)
2 changes: 1 addition & 1 deletion valhalla/jawn/src/tsoa-build/public/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -610,7 +610,7 @@ const models: TsoaRoute.Models = {
// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
"Partial_TextOperators_": {
"dataType": "refAlias",
"type": {"dataType":"nestedObjectLiteral","nestedProperties":{},"validators":{}},
"type": {"dataType":"nestedObjectLiteral","nestedProperties":{"not-equals":{"dataType":"string"},"equals":{"dataType":"string"},"like":{"dataType":"string"},"ilike":{"dataType":"string"},"contains":{"dataType":"string"},"not-contains":{"dataType":"string"}},"validators":{}},
},
// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
"Partial_NumberOperators_": {
Expand Down
2 changes: 1 addition & 1 deletion valhalla/jawn/tsoa_run.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,5 @@ set -e
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
npx tsoa spec-and-routes -c tsoa-private.json
npx tsoa spec-and-routes -c tsoa-public.json
python3 "$SCRIPT_DIR/fix_swagger_operators.py" src/tsoa-build/public/swagger.json src/tsoa-build/private/swagger.json
python3 "$SCRIPT_DIR/fix_swagger_operators.py" src/tsoa-build/public/swagger.json src/tsoa-build/private/swagger.json src/tsoa-build/public/routes.ts src/tsoa-build/private/routes.ts
cp src/tsoa-build/public/swagger.json "$SCRIPT_DIR/../../docs/swagger.json"
Loading