Skip to content
Merged
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
5 changes: 5 additions & 0 deletions .changeset/brave-dryers-play.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@kopai/app": minor
---

Fix ugly warning displayed at /documentation
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,8 @@
},
"lint-staged": {
"*.{ts,tsx,js,jsx,mjs}": [
"eslint --fix",
"prettier --write"
"pnpm exec eslint --fix",
"pnpm exec prettier --write"
],
"*.{json,md,yaml,yml}": [
"prettier --write"
Expand Down
51 changes: 50 additions & 1 deletion packages/app/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,21 @@ import { printStartupBanner } from "./startup-banner.js";

const __dirname = dirname(fileURLToPath(import.meta.url));

function hasComponentSchemas(
doc: unknown
): doc is { components: { schemas: Record<string, object> } } {
return (
typeof doc === "object" &&
doc !== null &&
"components" in doc &&
typeof doc.components === "object" &&
doc.components !== null &&
"schemas" in doc.components &&
typeof doc.components.schemas === "object" &&
doc.components.schemas !== null
);
}

const apiServer = fastify({
logger: { level: "warn" },
});
Expand All @@ -46,7 +61,41 @@ apiServer.register(fastifySwagger, {
if (uiRoutes.includes(url)) return { schema: { hide: true }, url };
return jsonSchemaTransform({ schema, url, ...rest });
},
transformObject: jsonSchemaTransformObject,
transformObject: (input) => {
const result = jsonSchemaTransformObject(input);
// Fix: z.lazy() recursive schemas generate $ref to schema0
// but fastify-type-provider-zod doesn't define it in components.
// Inject the missing schema definition and rename schema0 → AttributeValue.
const raw = JSON.stringify(result);
const renamed = raw.replaceAll(
"#/components/schemas/schema0",
"#/components/schemas/AttributeValue"
);
Comment on lines +70 to +73
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find all z.lazy() usages to identify unregistered lazy schemas
rg -n --type ts 'z\.lazy\(' -A 3

Repository: kopai-app/kopai-mono

Length of output: 11038


🏁 Script executed:

#!/bin/bash
# Get context around lines 70-73 in server.ts to understand the full patch
cat -n packages/app/src/server.ts | sed -n '50,90p'

Repository: kopai-app/kopai-mono

Length of output: 1713


🏁 Script executed:

#!/bin/bash
# Search for schema0, schema1, schema2 references in the codebase to understand naming patterns
rg -n 'schema[0-9]' --type ts --type json

Repository: kopai-app/kopai-mono

Length of output: 336


schema0 is an unstable internal implementation detail — coupling to library internals risks silent breakage.

Three concrete concerns with this approach:

  1. Multiple lazy schemas: The codebase contains many z.lazy() schemas across multiple files. While only schema0 appears in the current Swagger output, if any route uses an unregistered z.lazy() that generates schema1, schema2, etc., those dangling references remain unpatched, producing Swagger validation warnings.

  2. Library version fragility: schema0 is not part of fastify-type-provider-zod's public API. A patch release could change the naming convention (e.g., to a content-hash or UUID), silently breaking this fix with no type error or test failure.

  3. Overly broad string replacement: replaceAll on the full JSON string searches the entire serialized document. While #/components/schemas/schema0 is fairly specific, it would incorrectly rewrite any occurrence in a description or example field that happens to contain this exact string.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/app/src/server.ts` around lines 70 - 73, The current naive string
replacement using raw.replaceAll("#/components/schemas/schema0",
"#/components/schemas/AttributeValue") is fragile (ties to internal schema
names, misses schema1/schema2, and can replace occurrences in
descriptions/examples); instead parse the Swagger JSON into an object (use
JSON.parse on raw), recursively walk the object to find all $ref values that
match the pattern /^#\/components\/schemas\/schema\d+$/ and remap only those
reference nodes to the intended public schema name
("#/components/schemas/AttributeValue"), then serialize back to string
(JSON.stringify) and assign to renamed; update the logic around the renamed
variable and the replace step to use this structured transformation so changes
in lazy schema numbering or incidental text won't break the output.

const patched: unknown = JSON.parse(renamed);
if (
hasComponentSchemas(patched) &&
!patched.components.schemas.AttributeValue
) {
patched.components.schemas.AttributeValue = {
anyOf: [
{ type: "string" },
{ type: "number" },
{ type: "boolean" },
{
type: "array",
items: { $ref: "#/components/schemas/AttributeValue" },
},
{
type: "object",
additionalProperties: {
$ref: "#/components/schemas/AttributeValue",
},
},
],
};
Comment on lines +79 to +95
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find the AttributeValue Zod schema definition
rg -n 'AttributeValue' --type ts -B 3 -A 10 packages/app/src/

Repository: kopai-app/kopai-mono

Length of output: 2348


🏁 Script executed:

#!/bin/bash
# Search for the Zod schema definition of AttributeValue
# Look for z.union, z.discriminatedUnion, z.lazy patterns
rg -n 'z\.(lazy|union|discriminatedUnion)' --type ts -B 2 -A 15 | grep -A 15 -B 2 -i attribute

Repository: kopai-app/kopai-mono

Length of output: 29488


🏁 Script executed:

#!/bin/bash
# Alternative: search for where AttributeValue Zod type is defined
# Look for const or type declarations with AttributeValue
rg -n 'const.*AttributeValue|type.*AttributeValue' --type ts -A 10

Repository: kopai-app/kopai-mono

Length of output: 4753


🏁 Script executed:

#!/bin/bash
# Verify the mapping by checking both schemas side-by-side
echo "=== Actual Zod schema in denormalized-signals-zod.ts ==="
sed -n '10,17p' packages/core/src/denormalized-signals-zod.ts

echo ""
echo "=== Hardcoded anyOf in server.ts ==="
sed -n '80,94p' packages/app/src/server.ts

Repository: kopai-app/kopai-mono

Length of output: 760


Hardcoded anyOf definition creates a maintenance risk if the Zod schema changes.

The hardcoded schema here currently matches the attributeValue Zod definition in packages/core/src/denormalized-signals-zod.ts (string, number, boolean, array, and object types). However, because it is manually authored rather than derived from the Zod definition, any future schema changes will silently break OpenAPI docs without compile-time or runtime warnings.

Consider deriving this schema from the Zod definition instead of maintaining it manually, or add a comment explicitly linking it to the source Zod schema to flag this coupling.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/app/src/server.ts` around lines 79 - 95, The hardcoded
patched.components.schemas.AttributeValue should be derived from the canonical
Zod schema to avoid drift: import the attributeValue Zod schema from
packages/core/src/denormalized-signals-zod.ts and convert it to an OpenAPI/JSON
Schema (using your project’s Zod-to-OpenAPI converter utility such as
zodToJsonSchema/zodToOpenAPI or a small adapter) and assign that result to
patched.components.schemas.AttributeValue instead of the manual anyOf object; if
the conversion utility isn't available, at minimum replace the literal block
with a clear comment referencing the source symbol (attributeValue in
denormalized-signals-zod.ts) and add a TODO to convert it programmatically.

}
return patched as ReturnType<typeof jsonSchemaTransformObject>;
},
});

apiServer.register(fastifySwaggerUI, {
Expand Down