Skip to content

feat(server): add OpenAPI spec generation#2498

Open
ymc9 wants to merge 6 commits intodevfrom
feat/server-openapi-spec
Open

feat(server): add OpenAPI spec generation#2498
ymc9 wants to merge 6 commits intodevfrom
feat/server-openapi-spec

Conversation

@ymc9
Copy link
Member

@ymc9 ymc9 commented Mar 19, 2026

Summary

  • Extract OpenAPI spec generation from REST and RPC handlers into dedicated RestApiSpecGenerator and RPCApiSpecGenerator classes
  • Add shared spec-utils.ts with helpers for model/operation/field/filter-kind inclusion checks
  • Support queryOptions.slicing.fields with includedFilterKinds/excludedFilterKinds to control which filter operators appear in generated OpenAPI specs
    • REST: Controls individual filter[field][op] query parameters per filter kind (Equality, Like, Range, List)
    • RPC: Controls per-operator properties (equals, contains, lt, etc.) in WhereInput schemas, with oneOf shorthand when Equality is included
  • Add queryOptions support to handler constructors (with Zod validation) for omit and slicing configuration
  • Migrate OpenAPI v3.1 tests and baselines from zenstack-v2
  • 76 OpenAPI tests passing (REST + RPC + migrated)

Test plan

  • All 76 OpenAPI tests pass (npx vitest run packages/server/test/openapi/)
  • Verify REST filter params respect excludedFilterKinds / includedFilterKinds
  • Verify RPC WhereInput schemas reflect per-operator filter kind control
  • Verify $all field-level slicing applies to all fields with field-specific overrides
  • Verify migrated v3.1 baseline tests still pass

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added OpenAPI 3.1 spec generation for REST and RPC handlers, emitting paths, tags, parameters, components and procedure routes with JSON:API-style wrappers; handlers expose a generateSpec capability.
    • Spec generation honors runtime queryOptions to include/exclude models, operations, fields, and filter kinds; supports custom title/version/description/summary.
  • Tests

    • Added comprehensive tests and YAML baselines validating generated OpenAPI output across many schema shapes, types, procedures, and queryOptions scenarios.
  • Chores

    • Added OpenAPI typing dependency.

…pport

Refactor REST and RPC API handlers to extract OpenAPI spec generation into
dedicated classes (RestApiSpecGenerator, RPCApiSpecGenerator). Add support
for queryOptions.slicing.fields with includedFilterKinds/excludedFilterKinds
to control which filter operators appear in generated specs. REST generator
controls individual filter query parameters; RPC generator controls per-operator
properties in WhereInput schemas.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 19, 2026

📝 Walkthrough

Walkthrough

Adds OpenAPI 3.1 generation for REST and RPC handlers: new spec generators, shared types and spec utilities, package dependency, comprehensive tests, and baseline OpenAPI YAML fixtures. Generation respects schema and queryOptions-driven slicing.

Changes

Cohort / File(s) Summary
Core dependency & shared types
packages/server/package.json, packages/server/src/api/common/types.ts, packages/server/src/api/index.ts
Added openapi-types dependency; introduced CommonHandlerOptions, OpenApiSpecOptions, and OpenApiSpecGenerator; re-exported new types from API index.
Spec utilities
packages/server/src/api/common/spec-utils.ts
New helpers implementing queryOptions-driven slicing: model/operation/procedure inclusion, field omission, included-model enumeration, filter-kind inclusion, and extraction of @meta description.
REST handler integration & generator
packages/server/src/api/rest/index.ts, packages/server/src/api/rest/openapi.ts
Extended RestApiHandler options to accept queryOptions and implement OpenApiSpecGenerator; added generateSpec() and RestApiSpecGenerator producing OpenAPI 3.1 documents (paths, tags, components, schemas, parameters, relation and procedure routes, model name mapping).
RPC handler integration & generator
packages/server/src/api/rpc/index.ts, packages/server/src/api/rpc/openapi.ts
Extended RPCApiHandler options to include queryOptions, implemented OpenApiSpecGenerator, added generateSpec() and RPCApiSpecGenerator producing OpenAPI 3.1 docs (RPC CRUD endpoints, transaction endpoint, procedures, Args/Where schemas, filter operator handling).
Baselines / fixtures
packages/server/test/openapi/baseline/rest-type-coverage-3.1.0.baseline.yaml, packages/server/test/openapi/migrated/baseline/rest-type-coverage-3.1.0.baseline.yaml
Added OpenAPI 3.1 baseline YAML fixtures describing JSON:API-style REST contracts, shared components, parameters, and model request/response schemas.
REST OpenAPI tests
packages/server/test/openapi/rest-openapi.test.ts, packages/server/test/openapi/migrated/rest-migrated.test.ts
Added Vitest suites exercising REST spec generation: path/method assertions, schema/parameter generation, model-name mapping, compound IDs, extensive type/format coverage, and queryOptions slicing behavior; includes baseline comparisons (non-fatal if missing).
RPC OpenAPI tests
packages/server/test/openapi/rpc-openapi.test.ts, packages/server/test/openapi/migrated/rpc-migrated.test.ts
Added Vitest suites for RPC spec generation: CRUD RPC endpoints, transaction/procedure routes, component schemas (Args/Where/Responses), type/format mapping, queryOptions slicing tests, enums, and baseline comparisons.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Poem

🐇 I hopped through specs with nimble feet,

spun routes and schemas tidy and neat,
REST and RPC now sing in YAML light,
tests hum softly through the night,
carrots for CI — a tasty treat! 🥕

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main feature added: OpenAPI spec generation for REST and RPC handlers with supporting utilities.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/server-openapi-spec
📝 Coding Plan
  • Generate coding plan for human review comments

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@claude
Copy link
Contributor

claude bot commented Mar 19, 2026

Claude finished @ymc9's task —— View job


Code Review for OpenAPI Spec Generation PR

I've conducted a comprehensive review of this PR that adds OpenAPI spec generation with filter kind slicing support. Here's my detailed feedback:

Review Tasks:

  • Analyze architectural changes and new class structure
  • Review filter kind slicing implementation
  • Examine test coverage and quality
  • Check for potential bugs and edge cases
  • Review security considerations
  • Assess performance implications
  • Verify migration quality from zenstack-v2

Strengths

Excellent Architecture

  • Clean separation of concerns: Extracting OpenAPI spec generation into dedicated RestApiSpecGenerator and RPCApiSpecGenerator classes is a solid architectural decision that improves maintainability
  • Shared utilities: The spec-utils.ts module provides reusable functions for inclusion/exclusion logic across REST and RPC handlers
  • Interface-based design: OpenApiSpecGenerator interface enables extensibility and testing

Robust Filter Kind Slicing Implementation

  • Well-designed inclusion/exclusion logic: isFilterKindIncluded function properly handles both includedFilterKinds and excludedFilterKinds with correct precedence (exclusion takes priority)
  • Proper fallback hierarchy: Model-specific → $all model → global defaults works as expected
  • Comprehensive filter kind coverage: Supports all four filter kinds (Equality, Like, Range, List) with appropriate type constraints

Strong Test Coverage

  • Comprehensive testing: 76 OpenAPI tests covering both REST and RPC approaches
  • Migration validation: Proper migration from zenstack-v2 with baseline comparisons
  • Edge case testing: Tests for filter kind exclusions, field-level slicing, and type coverage

⚠️ Areas for Improvement

Security Considerations

  1. Insufficient queryOptions validation:

    // Current validation is too permissive
    queryOptions: z.object().optional()

    Recommendation: Add stricter Zod schema validation for slicing, omit, and their nested properties to prevent malformed configurations.

  2. Type safety gaps: Filter kind strings ('Equality', 'Like', etc.) are magic strings that could benefit from enum/const assertions:

    // Suggest adding:
    export const FILTER_KINDS = ['Equality', 'Like', 'Range', 'List'] as const;
    export type FilterKind = typeof FILTER_KINDS[number];

Performance Considerations

  1. Repeated schema lookups: In packages/server/src/api/rest/openapi.ts:77-98, the code performs multiple model lookups that could be cached
  2. String operations in loops: lowerCaseFirst(modelName) is called repeatedly in filter generation loops - consider memoization

Potential Edge Cases

  1. Empty filter configurations: No validation for empty includedFilterKinds arrays, which could lead to unexpected behavior
  2. Circular model references: Spec generation might not handle deeply nested self-referential models gracefully
  3. Field name collisions: No validation for field names that might conflict with OpenAPI reserved words

Code Quality Issues

  1. Magic numbers: Line 408 in rest/openapi.ts hardcodes 'id' field - should use schema introspection
  2. Error handling gaps: No specific error handling for malformed queryOptions.slicing configurations
  3. Documentation: Missing JSDoc for complex filter kind logic in spec generators

🔧 Specific Recommendations

packages/server/src/api/common/spec-utils.ts

  • Line 88: Consider adding validation that $all doesn't conflict with specific field names
  • Lines 91-95: Add input sanitization for filter kind strings

packages/server/src/api/rest/openapi.ts

  • Lines 424-458: Extract filter parameter generation into separate methods for better readability
  • Line 408: Replace hardcoded 'id' with dynamic ID field detection

packages/server/src/api/rpc/openapi.ts

  • Lines 697-737: The oneOf shorthand logic could be more explicit about when it applies

📋 Migration Quality

The migration from zenstack-v2 appears thorough:

  • ✅ Baseline files properly copied and validated
  • ✅ Test structure maintains consistency
  • ✅ OpenAPI v3.1 compliance maintained

🎯 Overall Assessment

This is a high-quality PR that significantly improves the codebase architecture while adding valuable functionality. The filter kind slicing feature is well-implemented and the testing is comprehensive.

Recommended Actions:

  1. Add stricter queryOptions validation schema
  2. Implement filter kind constants/types for type safety
  3. Add performance optimizations for repeated lookups
  4. Enhance error handling for edge cases

The code is ready for merge with minor improvements to address the security and performance considerations mentioned above.

@ymc9 ymc9 changed the title feat(server): add OpenAPI spec generation with filter kind slicing feat(server): add OpenAPI spec generation Mar 19, 2026
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 14

🧹 Nitpick comments (4)
packages/server/test/openapi/migrated/rest-migrated.test.ts (1)

198-208: Consider using it.skip() or test.skipIf() for clearer test reporting.

When baseline files are missing, the test returns early with a console.warn. This passes the test silently rather than marking it as skipped in test reports. Using Vitest's skip mechanism would provide better visibility.

♻️ Alternative using skipIf
it.skipIf(!fs.existsSync(path.join(__dirname, 'baseline', 'rest-3.1.0.baseline.yaml')))(
    'baseline comparison - rest-3.1.0',
    async () => {
        const baselinePath = path.join(__dirname, 'baseline', 'rest-3.1.0.baseline.yaml');
        const baseline = YAML.parse(fs.readFileSync(baselinePath, 'utf-8'));
        expect(baseline.openapi).toBe('3.1.0');
        expect(baseline.components?.schemas).toBeDefined();
        expect(baseline.paths).toBeDefined();
    }
);

Alternatively, keep the current approach if you prefer tests to pass when baselines are intentionally absent during development.

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

In `@packages/server/test/openapi/migrated/rest-migrated.test.ts` around lines 198
- 208, The test "baseline comparison - rest-3.1.0" currently uses an early
return after checking fs.existsSync(path.join(...)) which silently passes when
the baseline is missing; change this to use Vitest's skip mechanism (e.g.,
it.skip or it.skipIf) so the test is explicitly marked skipped instead of
returning early—remove the console.warn and early return, and wrap or declare
the test using it.skipIf(!fs.existsSync(path.join(__dirname, 'baseline',
'rest-3.1.0.baseline.yaml'))) while keeping the body that reads
YAML.parse(fs.readFileSync(...)) and the existing expects intact.
packages/server/src/api/index.ts (1)

1-3: Consider exporting the concrete spec generator classes.

The RestApiSpecGenerator and RPCApiSpecGenerator classes are exported from ./rest and ./rpc respectively (per context snippets), but not re-exported from this main barrel module. If external consumers should have direct access to these generators (independent of the handlers), consider adding them here for API surface consistency.

🔧 Optional: Add generator class exports
 export { RestApiHandler, type RestApiHandlerOptions } from './rest';
 export { RPCApiHandler, type RPCApiHandlerOptions } from './rpc';
 export type { OpenApiSpecGenerator, OpenApiSpecOptions } from './common/types';
+export { RestApiSpecGenerator } from './rest';
+export { RPCApiSpecGenerator } from './rpc';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/server/src/api/index.ts` around lines 1 - 3, Add re-exports for the
concrete spec generator classes so external consumers can import them from the
main barrel; specifically export RestApiSpecGenerator and RPCApiSpecGenerator
from the module by adding "export { RestApiSpecGenerator } from './rest';" and
"export { RPCApiSpecGenerator } from './rpc';" alongside the existing handler
and type exports (keeping the existing RestApiHandler, RPCApiHandler, and
OpenApiSpecGenerator/OpenApiSpecOptions exports).
packages/server/test/openapi/migrated/rpc-migrated.test.ts (2)

165-197: Same recommendation: consider using it.skip() for missing baselines.

Consistent with the REST test file feedback, using Vitest's skip mechanism would provide clearer test reporting when baseline files are absent.

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

In `@packages/server/test/openapi/migrated/rpc-migrated.test.ts` around lines 165
- 197, The three tests ('baseline comparison - rpc-3.1.0', 'baseline comparison
- rpc-3.1.0-omit', 'baseline comparison - rpc-type-coverage-3.1.0') currently
early-return with console.warn when baseline files are missing; replace that
pattern with Vitest's skip so missing baselines show as skipped tests. For each
test, compute baselinePath and then do: if (!fs.existsSync(baselinePath)) {
it.skip('<same test title>', () => {}); return; } or better yet choose the test
function dynamically (const maybeIt = fs.existsSync(baselinePath) ? it :
it.skip) and call maybeIt('<same test title>', async () => { const baseline =
YAML.parse(...); expect(...); }); ensuring the test name and assertions
(YAML.parse and expect lines) remain unchanged.

128-129: Remove unnecessary as any type assertions.

The OpenAPIV3_1.InfoObject type from openapi-types includes both description and summary as optional fields (per OpenAPI 3.1 spec). The as any casts on lines 128-129 are unnecessary and can be removed:

expect(spec.info.description).toBe('A custom API');
expect(spec.info.summary).toBe('awesome api');
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/server/test/openapi/migrated/rpc-migrated.test.ts` around lines 128
- 129, Remove the unnecessary `as any` casts on the OpenAPI info assertions:
replace `expect((spec.info as any).description).toBe('A custom API')` and
`expect((spec.info as any).summary).toBe('awesome api')` with
`expect(spec.info.description).toBe('A custom API')` and
`expect(spec.info.summary).toBe('awesome api')`; ensure `spec` is typed as an
OpenAPIV3_1.Document (or otherwise that `spec.info` is recognized) so the
`description` and `summary` properties on `spec.info` are accepted by TypeScript
without casting.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/server/src/api/common/spec-utils.ts`:
- Around line 24-39: The isOperationIncluded function treats a model-specific
slicing block as a full replacement of $all (using (slicing.models[modelKey] ??
slicing.models.$all)), which drops global rules; instead merge the $all block
with the model-specific block so each property (excludedOperations,
includedOperations) considers defaults from $all and overrides from
models[modelKey] before evaluating includes/excludes. Update isOperationIncluded
to compute a combined modelSlicing = { ...slicing.models.$all,
...slicing.models[modelKey] } (or equivalent property-level merge) and then
check modelSlicing.excludedOperations and modelSlicing.includedOperations; apply
the same merge-fix to the analogous logic later in the file (the field-slicing
check around lines 75-97).

In `@packages/server/src/api/rest/handler.ts`:
- Around line 1267-1298: processUpdate is manually constructing relation
payloads (set/connect) causing numeric IDs to remain strings and compound IDs to
be formatted differently than processCreate/processRelationshipCRUD; replace the
manual constructions with calls to makeIdConnect so relation payloads are
normalized. Concretely, inside the relationships loop where
updatePayload.data[key] is set for collection and non-collection branches, call
makeIdConnect(relationInfo.idFields, item.id) for each item (for collections use
set: enumerate(...).map(idObj => ({ [makeDefaultIdKey?]?: idObj }) if
makeIdConnect returns the nested unique object adjust accordingly) and for
single relations use connect: makeIdConnect(relationInfo.idFields,
data.data.id); keep updatePayload.include assignment but ensure IDs produced
match the shape used by processCreate/processRelationshipCRUD. Use the existing
symbols processUpdate, makeIdConnect, makeDefaultIdKey, processCreate,
processRelationshipCRUD, relationInfo, updatePayload and relationships to locate
and update the code.
- Around line 1817-1903: The bug is that item, curr and currType are created
once per query key and reused across enumerate(value) entries causing repeated
filter params (e.g. ?filter[id]=1&filter[id]=2) to overwrite the same object;
fix it by moving the creation/initialization of item, curr and currType inside
the for (const filterValue of enumerate(value)) loop so each filterValue builds
a fresh item object, reset curr = item and currType = typeInfo at the start of
each iteration, and then continue using existing logic (getIdFields,
getModelInfo, makeFilterValue, and pushing into items) so each value produces
its own independent filter object.
- Around line 510-512: handleGenericError currently appends err.stack into the
response body which leaks internals; change handleGenericError to only return a
generic error message via makeError('unknownError', err instanceof Error ?
err.message : 'Unknown error') and ensure the full error and stack are logged
internally (using the existing logger on the class or console.error) instead of
exposing the stack in the HTTP response; keep references to the
handleGenericError method and makeError call so the response content is
sanitized while retaining internal logs.

In `@packages/server/src/api/rest/openapi.ts`:
- Around line 73-89: generatePaths is currently adding collection and
single-resource paths unconditionally, causing operations like DELETE to be
included despite excludedOperations; update generatePaths to check
isOperationIncluded(modelName, operation) before adding each operation and when
calling buildCollectionPath and buildSinglePath so only allowed operations are
emitted—specifically, in generatePaths use isOperationIncluded for collection
ops (e.g., 'list','create') before assigning paths[`/${modelPath}`] and for
single-resource ops (e.g., 'read','update','delete') before assigning
paths[`/${modelPath}/{id}`]; also ensure the lower-level builders
(buildCollectionPath and buildSinglePath) accept or consult operation filters
(or are called selectively) so lines in the broader generation block (the later
code around 136-250) follow the same inclusion checks.
- Around line 191-250: The OpenAPI generator (buildSinglePath) is missing
operations that the runtime serves: add a PUT operation for /{model}/{id}
(operationId `replace<Model>` or `put<Model>` to match naming) with the same
requestBody schema as PATCH (`${modelName}UpdateRequest`) and appropriate
200/400/404 responses, and ensure relationship DELETE paths are generated for
/{model}/{id}/relationships/{field} (operationId like
`delete${modelName}Relationship${Field}`) returning 200/404; mirror the runtime
behavior found in RestApiHandler.handleRequest (lines handling PUT and DELETE
relationship) when creating these entries so the OpenAPI spec matches the live
endpoints.
- Around line 139-400: The responses currently reference
components/schemas/_errorResponse directly (invalid because Response Object refs
must target components/responses and Response Objects need a description);
update every occurrence inside buildCollectionPath (list/create),
buildSinglePath (get/patch/delete), buildFetchRelatedPath (get),
buildRelationshipPath (get/put/patch/post) and buildProcedureOperation to either
reference a Response Object at '#/components/responses/_errorResponse' or
replace the inline entry with a proper Response Object that includes a
description and content whose schema points to
'#/components/schemas/_errorResponse' (e.g. '400': { $ref:
'#/components/responses/_errorResponse' } or '400': { description: 'Error',
content: { 'application/vnd.api+json': { schema: { $ref:
'#/components/schemas/_errorResponse' } } } }); ensure every response entry that
previously used the schema $ref is converted accordingly.

In `@packages/server/src/api/rpc/openapi.ts`:
- Around line 73-98: The writeOps list includes ops createManyAndReturn and
updateManyAndReturn that cause references to
`${modelName}CreateManyAndReturnArgs` / `${modelName}UpdateManyAndReturnArgs`
but generateSchemas() never registers those schemas; update the generation logic
so either (A) remove those ops from writeOps or (B) add schema generation for
those args by extending opToArgsSchema/op mapping and ensure generateSchemas()
creates `${modelName}CreateManyAndReturnArgs` and
`${modelName}UpdateManyAndReturnArgs` (or maps them to existing
CreateMany/UpdateMany args) so buildPostOperation/buildPatchOperation produce
valid request-body component refs and no dangling refs. Ensure to touch
writeOps, opToArgsSchema, generateSchemas, and where
buildPostOperation/buildPatchOperation are invoked to keep names consistent.
- Around line 477-495: The recursive filter refs in the logical combinators
(properties['AND'], properties['OR'], properties['NOT']) point to
`#/components/schemas/_${_modelName}WhereInput` which doesn't match what
generateSchemas() emits (`${modelName}WhereInput`), causing dangling refs;
update those three $ref strings to reference the actual schema name produced by
generateSchemas (e.g., `#/components/schemas/${modelName}WhereInput`) or
otherwise make generateSchemas() emit the underscored name so the names match
(modify the ref strings in openapi.ts around the AND/OR/NOT blocks or adjust
generateSchemas() schema names accordingly).
- Around line 171-181: The response entries currently use a response-level $ref
to a Schema Object (e.g. { $ref: '#/components/schemas/_ErrorResponse' }) which
is invalid; update the responses in the OpenAPI generation (the responses object
built alongside modelName and op in packages/server/src/api/rpc/openapi.ts) to
either reference a Response Object (change to { $ref:
'#/components/responses/_ErrorResponse' } and add a
components.responses._ErrorResponse entry that points to the error schema) or
replace the direct $ref with a proper Response Object (include description and
content: application/json with schema: { $ref:
'#/components/schemas/_ErrorResponse' }); apply this change for all occurrences
of the error response generation so validators and generators target
`#/components/responses/` instead of `#/components/schemas/`.
- Around line 63-99: The generated OpenAPI paths always use the single-model
response schema `${modelName}Response`, causing client deserialization errors
for operations that return arrays, counts, aggregates or booleans; update the
path-building loop so each operation uses the correct response schema instead of
`${modelName}Response`. Add an op→response-schema mapping (e.g., findMany ->
`${modelName}[]` or `${modelName}ArrayResponse`,
count/createMany/updateMany/deleteMany -> `${modelName}BatchResponse` or a count
schema, aggregate -> `${modelName}AggregateResponse`, groupBy ->
`${modelName}GroupByResponse`, exists -> `${modelName}ExistsResponse`), and pass
that schema name into the builders (or extend
buildGetOperation/buildPostOperation/buildPatchOperation/buildDeleteOperation to
accept a responseSchemaName). Use the existing symbols opToArgsSchema,
buildGetOperation, buildPostOperation, buildPatchOperation,
buildDeleteOperation, isOperationIncluded and queryOptions to locate where to
inject the response schema selection and adjust the builders to reference the
provided response schema when constructing responses.

In
`@packages/server/test/openapi/migrated/baseline/rest-type-coverage-3.1.0.baseline.yaml`:
- Line 549: Fix the typo in the OpenAPI baseline description under the _links
schema: change the description string "Link for refetching the curent results"
to "Link for refetching the current results" so the word "curent" is corrected
to "current" in the description field.
- Around line 609-610: The YAML description for the schema property named
"zodErrors" has an unintended line break so the word "failure" is split onto its
own line; fix it by merging the lines into a single properly quoted or block
scalar YAML value (e.g., use a single-line string or a pipe/greater-than block)
so the description reads continuously as "Zod validation errors if the error is
due to data validation failure", and update the "description" value for the
zodErrors entry accordingly.

---

Nitpick comments:
In `@packages/server/src/api/index.ts`:
- Around line 1-3: Add re-exports for the concrete spec generator classes so
external consumers can import them from the main barrel; specifically export
RestApiSpecGenerator and RPCApiSpecGenerator from the module by adding "export {
RestApiSpecGenerator } from './rest';" and "export { RPCApiSpecGenerator } from
'./rpc';" alongside the existing handler and type exports (keeping the existing
RestApiHandler, RPCApiHandler, and OpenApiSpecGenerator/OpenApiSpecOptions
exports).

In `@packages/server/test/openapi/migrated/rest-migrated.test.ts`:
- Around line 198-208: The test "baseline comparison - rest-3.1.0" currently
uses an early return after checking fs.existsSync(path.join(...)) which silently
passes when the baseline is missing; change this to use Vitest's skip mechanism
(e.g., it.skip or it.skipIf) so the test is explicitly marked skipped instead of
returning early—remove the console.warn and early return, and wrap or declare
the test using it.skipIf(!fs.existsSync(path.join(__dirname, 'baseline',
'rest-3.1.0.baseline.yaml'))) while keeping the body that reads
YAML.parse(fs.readFileSync(...)) and the existing expects intact.

In `@packages/server/test/openapi/migrated/rpc-migrated.test.ts`:
- Around line 165-197: The three tests ('baseline comparison - rpc-3.1.0',
'baseline comparison - rpc-3.1.0-omit', 'baseline comparison -
rpc-type-coverage-3.1.0') currently early-return with console.warn when baseline
files are missing; replace that pattern with Vitest's skip so missing baselines
show as skipped tests. For each test, compute baselinePath and then do: if
(!fs.existsSync(baselinePath)) { it.skip('<same test title>', () => {}); return;
} or better yet choose the test function dynamically (const maybeIt =
fs.existsSync(baselinePath) ? it : it.skip) and call maybeIt('<same test
title>', async () => { const baseline = YAML.parse(...); expect(...); });
ensuring the test name and assertions (YAML.parse and expect lines) remain
unchanged.
- Around line 128-129: Remove the unnecessary `as any` casts on the OpenAPI info
assertions: replace `expect((spec.info as any).description).toBe('A custom
API')` and `expect((spec.info as any).summary).toBe('awesome api')` with
`expect(spec.info.description).toBe('A custom API')` and
`expect(spec.info.summary).toBe('awesome api')`; ensure `spec` is typed as an
OpenAPIV3_1.Document (or otherwise that `spec.info` is recognized) so the
`description` and `summary` properties on `spec.info` are accepted by TypeScript
without casting.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: f75a29d7-1077-45f0-bb75-c14f1dc714ee

📥 Commits

Reviewing files that changed from the base of the PR and between 00768de and 59e3c3a.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (24)
  • packages/server/package.json
  • packages/server/src/api/common/spec-utils.ts
  • packages/server/src/api/common/types.ts
  • packages/server/src/api/index.ts
  • packages/server/src/api/rest/handler.ts
  • packages/server/src/api/rest/index.ts
  • packages/server/src/api/rest/openapi.ts
  • packages/server/src/api/rpc/handler.ts
  • packages/server/src/api/rpc/index.ts
  • packages/server/src/api/rpc/openapi.ts
  • packages/server/test/openapi/baseline/rest-3.1.0.baseline.yaml
  • packages/server/test/openapi/baseline/rest-type-coverage-3.1.0.baseline.yaml
  • packages/server/test/openapi/baseline/rpc-3.1.0-omit.baseline.yaml
  • packages/server/test/openapi/baseline/rpc-3.1.0.baseline.yaml
  • packages/server/test/openapi/baseline/rpc-type-coverage-3.1.0.baseline.yaml
  • packages/server/test/openapi/migrated/baseline/rest-3.1.0.baseline.yaml
  • packages/server/test/openapi/migrated/baseline/rest-type-coverage-3.1.0.baseline.yaml
  • packages/server/test/openapi/migrated/baseline/rpc-3.1.0-omit.baseline.yaml
  • packages/server/test/openapi/migrated/baseline/rpc-3.1.0.baseline.yaml
  • packages/server/test/openapi/migrated/baseline/rpc-type-coverage-3.1.0.baseline.yaml
  • packages/server/test/openapi/migrated/rest-migrated.test.ts
  • packages/server/test/openapi/migrated/rpc-migrated.test.ts
  • packages/server/test/openapi/rest-openapi.test.ts
  • packages/server/test/openapi/rpc-openapi.test.ts

Comment on lines +510 to +512
private handleGenericError(err: unknown): Response | PromiseLike<Response> {
return this.makeError('unknownError', err instanceof Error ? `${err.message}\n${err.stack}` : 'Unknown error');
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Don't return stack traces in 500 responses.

handleGenericError() concatenates err.stack into the response body. That leaks internals on unexpected failures; keep the stack in logs only.

🔒 Suggested fix
 private handleGenericError(err: unknown): Response | PromiseLike<Response> {
-    return this.makeError('unknownError', err instanceof Error ? `${err.message}\n${err.stack}` : 'Unknown error');
+    log(this.log, 'error', 'unexpected REST handler error', err);
+    return this.makeError('unknownError', err instanceof Error ? err.message : 'Unknown error');
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/server/src/api/rest/handler.ts` around lines 510 - 512,
handleGenericError currently appends err.stack into the response body which
leaks internals; change handleGenericError to only return a generic error
message via makeError('unknownError', err instanceof Error ? err.message :
'Unknown error') and ensure the full error and stack are logged internally
(using the existing logger on the class or console.error) instead of exposing
the stack in the HTTP response; keep references to the handleGenericError method
and makeError call so the response content is sanitized while retaining internal
logs.

Comment on lines +1267 to +1298
// turn relationships into query payload
if (relationships) {
for (const [key, data] of Object.entries<any>(relationships)) {
if (!data?.data) {
return this.makeError('invalidRelationData');
}

const relationInfo = typeInfo.relationships[key];
if (!relationInfo) {
return this.makeUnsupportedRelationshipError(type, key, 400);
}

if (relationInfo.isCollection) {
updatePayload.data[key] = {
set: enumerate(data.data).map((item: any) => ({
[this.makeDefaultIdKey(relationInfo.idFields)]: item.id,
})),
};
} else {
if (typeof data.data !== 'object') {
return this.makeError('invalidRelationData');
}
updatePayload.data[key] = {
connect: {
[this.makeDefaultIdKey(relationInfo.idFields)]: data.data.id,
},
};
}
updatePayload.include = {
...updatePayload.include,
[key]: { select: { [this.makeDefaultIdKey(relationInfo.idFields)]: true } },
};
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

processUpdate() should reuse makeIdConnect() for relation payloads.

This branch manually builds set/connect inputs, unlike processCreate() and processRelationshipCRUD(). Numeric IDs stay as strings, and compound IDs are passed as the joined token instead of the nested unique object the other paths already build.

🛠️ Proposed fix
                 if (relationInfo.isCollection) {
                     updatePayload.data[key] = {
-                        set: enumerate(data.data).map((item: any) => ({
-                            [this.makeDefaultIdKey(relationInfo.idFields)]: item.id,
-                        })),
+                        set: enumerate(data.data).map((item: any) =>
+                            this.makeIdConnect(relationInfo.idFields, item.id),
+                        ),
                     };
                 } else {
                     if (typeof data.data !== 'object') {
                         return this.makeError('invalidRelationData');
                     }
                     updatePayload.data[key] = {
-                        connect: {
-                            [this.makeDefaultIdKey(relationInfo.idFields)]: data.data.id,
-                        },
+                        connect: this.makeIdConnect(relationInfo.idFields, data.data.id),
                     };
                 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/server/src/api/rest/handler.ts` around lines 1267 - 1298,
processUpdate is manually constructing relation payloads (set/connect) causing
numeric IDs to remain strings and compound IDs to be formatted differently than
processCreate/processRelationshipCRUD; replace the manual constructions with
calls to makeIdConnect so relation payloads are normalized. Concretely, inside
the relationships loop where updatePayload.data[key] is set for collection and
non-collection branches, call makeIdConnect(relationInfo.idFields, item.id) for
each item (for collections use set: enumerate(...).map(idObj => ({
[makeDefaultIdKey?]?: idObj }) if makeIdConnect returns the nested unique object
adjust accordingly) and for single relations use connect:
makeIdConnect(relationInfo.idFields, data.data.id); keep updatePayload.include
assignment but ensure IDs produced match the shape used by
processCreate/processRelationshipCRUD. Use the existing symbols processUpdate,
makeIdConnect, makeDefaultIdKey, processCreate, processRelationshipCRUD,
relationInfo, updatePayload and relationships to locate and update the code.

Comment on lines +1817 to +1903
for (const [key, value] of Object.entries(query)) {
if (!value) {
continue;
}

// try matching query parameter key as "filter[x][y]..."
const match = key.match(this.filterParamPattern);
if (!match || !match.groups || !match.groups['match']) {
continue;
}

const filterKeys = match.groups['match']
.replaceAll(/[[\]]/g, ' ')
.split(' ')
.filter((i) => i);

if (!filterKeys.length) {
continue;
}

// turn filter into a nested query object

const item: any = {};
let curr = item;
let currType = typeInfo;

for (const filterValue of enumerate(value)) {
for (let i = 0; i < filterKeys.length; i++) {
// extract filter operation from (optional) trailing $op
let filterKey = filterKeys[i]!;
let filterOp: FilterOperationType | undefined;
const pos = filterKey.indexOf('$');
if (pos > 0) {
filterOp = filterKey.substring(pos + 1) as FilterOperationType;
filterKey = filterKey.substring(0, pos);
}

if (!!filterOp && !FilterOperations.includes(filterOp)) {
return {
filter: undefined,
error: this.makeError('invalidFilter', `invalid filter operation: ${filterOp}`),
};
}

const idFields = this.getIdFields(currType.name);
const fieldDef =
filterKey === 'id'
? Object.values(currType.fields).find((f) => idFields.some((idf) => idf.name === f.name))
: currType.fields[filterKey];
if (!fieldDef) {
return { filter: undefined, error: this.makeError('invalidFilter') };
}

if (!fieldDef.relation) {
// regular field
if (i !== filterKeys.length - 1) {
// must be the last segment of a filter
return { filter: undefined, error: this.makeError('invalidFilter') };
}
curr[fieldDef.name] = this.makeFilterValue(fieldDef, filterValue, filterOp);
} else {
// relation field
if (i === filterKeys.length - 1) {
curr[fieldDef.name] = this.makeFilterValue(fieldDef, filterValue, filterOp);
} else {
// keep going
if (fieldDef.array) {
// collection filtering implies "some" operation
curr[fieldDef.name] = { some: {} };
curr = curr[fieldDef.name].some;
} else {
curr = curr[fieldDef.name] = {};
}
currType = this.getModelInfo(fieldDef.type)!;
}
}
}
items.push(item);
}
}

if (items.length === 0) {
return { filter: undefined, error: undefined };
} else {
// combine filters with AND
return { filter: items.length === 1 ? items[0] : { AND: items }, error: undefined };
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Repeated filter params collapse to the last value.

item, curr, and currType are created once per query key, then reused for every entry in enumerate(value). A request like ?filter[id]=1&filter[id]=2 overwrites the first filter and pushes the same object twice.

🐛 Proposed fix
-            const item: any = {};
-            let curr = item;
-            let currType = typeInfo;
-
             for (const filterValue of enumerate(value)) {
+                const item: any = {};
+                let curr = item;
+                let currType = typeInfo;
+
                 for (let i = 0; i < filterKeys.length; i++) {
                     // extract filter operation from (optional) trailing $op
                     let filterKey = filterKeys[i]!;
@@
                 }
                 items.push(item);
             }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/server/src/api/rest/handler.ts` around lines 1817 - 1903, The bug is
that item, curr and currType are created once per query key and reused across
enumerate(value) entries causing repeated filter params (e.g.
?filter[id]=1&filter[id]=2) to overwrite the same object; fix it by moving the
creation/initialization of item, curr and currType inside the for (const
filterValue of enumerate(value)) loop so each filterValue builds a fresh item
object, reset curr = item and currType = typeInfo at the start of each
iteration, and then continue using existing logic (getIdFields, getModelInfo,
makeFilterValue, and pushing into items) so each value produces its own
independent filter object.

Comment on lines +73 to +89
private generatePaths(): OpenAPIV3_1.PathsObject {
const paths: OpenAPIV3_1.PathsObject = {};

for (const modelName of getIncludedModels(this.schema, this.queryOptions)) {
const modelDef = this.schema.models[modelName]!;
const idFields = this.getIdFields(modelDef);
if (idFields.length === 0) continue;

const modelPath = this.getModelPath(modelName);
const tag = lowerCaseFirst(modelName);

// Collection: GET (list) + POST (create)
paths[`/${modelPath}`] = this.buildCollectionPath(modelName, modelDef, tag) as any;

// Single resource: GET + PATCH + DELETE
paths[`/${modelPath}/{id}`] = this.buildSinglePath(modelName, tag) as any;

Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

REST path generation is ignoring includedOperations / excludedOperations.

Collection and single-resource methods are emitted unconditionally, so a sliced config like excludedOperations: ['delete'] still leaves DELETE /model/{id} in the spec. This needs to go through isOperationIncluded() the same way the RPC generator already does.

Also applies to: 136-250

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

In `@packages/server/src/api/rest/openapi.ts` around lines 73 - 89, generatePaths
is currently adding collection and single-resource paths unconditionally,
causing operations like DELETE to be included despite excludedOperations; update
generatePaths to check isOperationIncluded(modelName, operation) before adding
each operation and when calling buildCollectionPath and buildSinglePath so only
allowed operations are emitted—specifically, in generatePaths use
isOperationIncluded for collection ops (e.g., 'list','create') before assigning
paths[`/${modelPath}`] and for single-resource ops (e.g.,
'read','update','delete') before assigning paths[`/${modelPath}/{id}`]; also
ensure the lower-level builders (buildCollectionPath and buildSinglePath) accept
or consult operation filters (or are called selectively) so lines in the broader
generation block (the later code around 136-250) follow the same inclusion
checks.

Comment on lines +73 to +98
// Write operations
const writeOps: Array<{ op: string; method: 'post' | 'patch' | 'delete' }> = [
{ op: 'create', method: 'post' },
{ op: 'createMany', method: 'post' },
{ op: 'createManyAndReturn', method: 'post' },
{ op: 'upsert', method: 'post' },
{ op: 'update', method: 'patch' },
{ op: 'updateMany', method: 'patch' },
{ op: 'updateManyAndReturn', method: 'patch' },
{ op: 'delete', method: 'delete' },
{ op: 'deleteMany', method: 'delete' },
];

for (const { op, method } of writeOps) {
if (!isOperationIncluded(modelName, op, this.queryOptions)) continue;
const argsSchemaName = `${modelName}${this.opToArgsSchema(op)}`;
const buildOp =
method === 'post'
? this.buildPostOperation
: method === 'patch'
? this.buildPatchOperation
: this.buildDeleteOperation;
paths[`/${modelPath}/${op}`] = {
[method]: buildOp.call(this, modelName, op, tag, argsSchemaName),
};
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

createManyAndReturn and updateManyAndReturn reference components that are never generated.

These paths point at ${modelName}CreateManyAndReturnArgs / ${modelName}UpdateManyAndReturnArgs, but generateSchemas() never registers either schema. The emitted OpenAPI will contain dangling request-body refs for both endpoints.

Also applies to: 341-364

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

In `@packages/server/src/api/rpc/openapi.ts` around lines 73 - 98, The writeOps
list includes ops createManyAndReturn and updateManyAndReturn that cause
references to `${modelName}CreateManyAndReturnArgs` /
`${modelName}UpdateManyAndReturnArgs` but generateSchemas() never registers
those schemas; update the generation logic so either (A) remove those ops from
writeOps or (B) add schema generation for those args by extending
opToArgsSchema/op mapping and ensure generateSchemas() creates
`${modelName}CreateManyAndReturnArgs` and `${modelName}UpdateManyAndReturnArgs`
(or maps them to existing CreateMany/UpdateMany args) so
buildPostOperation/buildPatchOperation produce valid request-body component refs
and no dangling refs. Ensure to touch writeOps, opToArgsSchema, generateSchemas,
and where buildPostOperation/buildPatchOperation are invoked to keep names
consistent.

Comment on lines +171 to +181
responses: {
'200': {
description: `Result of ${op}`,
content: {
'application/json': {
schema: { $ref: `#/components/schemas/${modelName}Response` },
},
},
},
'400': { $ref: '#/components/schemas/_ErrorResponse' },
},
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

cat -n packages/server/src/api/rpc/openapi.ts | sed -n '160,200p'

Repository: zenstackhq/zenstack

Length of output: 1732


🏁 Script executed:

cat -n packages/server/src/api/rpc/openapi.ts | sed -n '195,220p'

Repository: zenstackhq/zenstack

Length of output: 1058


🏁 Script executed:

cat -n packages/server/src/api/rpc/openapi.ts | sed -n '225,250p'

Repository: zenstackhq/zenstack

Length of output: 1124


🏁 Script executed:

cat -n packages/server/src/api/rpc/openapi.ts | sed -n '260,295p'

Repository: zenstackhq/zenstack

Length of output: 1587


🌐 Web query:

OpenAPI 3.0 specification response $ref schema component validation

💡 Result:

In OpenAPI 3.0, when a response schema uses $ref to reference a schema component (e.g., responses: { "200": { content: { application/json: { schema: { $ref: '#/components/schemas/User' } } } }), the $ref is fully valid and supported. The official specification explicitly allows Reference Objects (containing $ref) in place of Schema Objects, including in response content schemas. Key validation aspects: - Document validation: OpenAPI documents are validated against the official JSON Schema for OAS 3.0 (available at spec.openapis.org), which permits $ref in schema fields of Media Type Objects within Response Objects. Tools like oascomply resolve $refs during parsing and validate that targets exist and are of correct type (e.g., a schema $ref must point to a Schema Object). - Resolution rules: $refs follow JSON Reference (RFC 3986), resolved relative to the current document. In OAS 3.0, a $ref object cannot have sibling properties (they are ignored), and it logically replaces itself with the target during resolution. - Runtime/data validation: The referenced schema (e.g., User) defines validation rules for actual response payloads. Tools like AJV (used in many OpenAPI validators) resolve $refs and apply the full schema (including discriminators, oneOf, etc.) to validate JSON payloads against the response schema. - Common pitfalls: $ref only allowed where spec permits (e.g., schema fields); invalid targets or circular refs cause validation errors in conformance tools. Examples from spec: - responses: { "200": { content: { application/json: { schema: { $ref: '#/components/schemas/Pet' } } } } This is standard and passes official validation.

Citations:


Response-level $ref must target a Response Object, not a Schema Object.

At lines 180, 212, 244, 276, and 288, response definitions use { $ref: '#/components/schemas/_ErrorResponse' } directly at the response level. Per OpenAPI 3.0 specification, a response-level $ref must reference #/components/responses/, not #/components/schemas/. Validators and client generators will reject the document because the target is a Schema Object, not a Response Object.

Either create a Response Object in #/components/responses/ and reference it, or wrap the schema reference in a proper Response Object with description and content:

Fix pattern
- '400': { $ref: '#/components/schemas/_ErrorResponse' },
+ '400': {
+     description: 'Bad request',
+     content: {
+         'application/json': {
+             schema: { $ref: '#/components/schemas/_ErrorResponse' },
+         },
+     },
+ },
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
responses: {
'200': {
description: `Result of ${op}`,
content: {
'application/json': {
schema: { $ref: `#/components/schemas/${modelName}Response` },
},
},
},
'400': { $ref: '#/components/schemas/_ErrorResponse' },
},
responses: {
'200': {
description: `Result of ${op}`,
content: {
'application/json': {
schema: { $ref: `#/components/schemas/${modelName}Response` },
},
},
},
'400': {
description: 'Bad request',
content: {
'application/json': {
schema: { $ref: '#/components/schemas/_ErrorResponse' },
},
},
},
},
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/server/src/api/rpc/openapi.ts` around lines 171 - 181, The response
entries currently use a response-level $ref to a Schema Object (e.g. { $ref:
'#/components/schemas/_ErrorResponse' }) which is invalid; update the responses
in the OpenAPI generation (the responses object built alongside modelName and op
in packages/server/src/api/rpc/openapi.ts) to either reference a Response Object
(change to { $ref: '#/components/responses/_ErrorResponse' } and add a
components.responses._ErrorResponse entry that points to the error schema) or
replace the direct $ref with a proper Response Object (include description and
content: application/json with schema: { $ref:
'#/components/schemas/_ErrorResponse' }); apply this change for all occurrences
of the error response generation so validators and generators target
`#/components/responses/` instead of `#/components/schemas/`.

Comment on lines +477 to +495
// Logical combinators
properties['AND'] = {
oneOf: [
{ $ref: `#/components/schemas/_${_modelName}WhereInput` },
{ type: 'array', items: { $ref: `#/components/schemas/_${_modelName}WhereInput` } },
],
};
properties['OR'] = {
type: 'array',
items: { $ref: `#/components/schemas/_${_modelName}WhereInput` },
};
properties['NOT'] = {
oneOf: [
{ $ref: `#/components/schemas/_${_modelName}WhereInput` },
{ type: 'array', items: { $ref: `#/components/schemas/_${_modelName}WhereInput` } },
],
};

return { type: 'object', properties };
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

The recursive WhereInput refs never resolve.

AND/OR/NOT point to #/components/schemas/_${modelName}WhereInput, but generateSchemas() only creates ${modelName}WhereInput. Every recursive filter schema therefore contains dangling refs.

🐛 Proposed fix
- { $ref: `#/components/schemas/_${_modelName}WhereInput` },
- { type: 'array', items: { $ref: `#/components/schemas/_${_modelName}WhereInput` } },
+ { $ref: `#/components/schemas/${_modelName}WhereInput` },
+ { type: 'array', items: { $ref: `#/components/schemas/${_modelName}WhereInput` } },
...
- items: { $ref: `#/components/schemas/_${_modelName}WhereInput` },
+ items: { $ref: `#/components/schemas/${_modelName}WhereInput` },
...
- { $ref: `#/components/schemas/_${_modelName}WhereInput` },
- { type: 'array', items: { $ref: `#/components/schemas/_${_modelName}WhereInput` } },
+ { $ref: `#/components/schemas/${_modelName}WhereInput` },
+ { type: 'array', items: { $ref: `#/components/schemas/${_modelName}WhereInput` } },
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Logical combinators
properties['AND'] = {
oneOf: [
{ $ref: `#/components/schemas/_${_modelName}WhereInput` },
{ type: 'array', items: { $ref: `#/components/schemas/_${_modelName}WhereInput` } },
],
};
properties['OR'] = {
type: 'array',
items: { $ref: `#/components/schemas/_${_modelName}WhereInput` },
};
properties['NOT'] = {
oneOf: [
{ $ref: `#/components/schemas/_${_modelName}WhereInput` },
{ type: 'array', items: { $ref: `#/components/schemas/_${_modelName}WhereInput` } },
],
};
return { type: 'object', properties };
// Logical combinators
properties['AND'] = {
oneOf: [
{ $ref: `#/components/schemas/${_modelName}WhereInput` },
{ type: 'array', items: { $ref: `#/components/schemas/${_modelName}WhereInput` } },
],
};
properties['OR'] = {
type: 'array',
items: { $ref: `#/components/schemas/${_modelName}WhereInput` },
};
properties['NOT'] = {
oneOf: [
{ $ref: `#/components/schemas/${_modelName}WhereInput` },
{ type: 'array', items: { $ref: `#/components/schemas/${_modelName}WhereInput` } },
],
};
return { type: 'object', properties };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/server/src/api/rpc/openapi.ts` around lines 477 - 495, The recursive
filter refs in the logical combinators (properties['AND'], properties['OR'],
properties['NOT']) point to `#/components/schemas/_${_modelName}WhereInput`
which doesn't match what generateSchemas() emits (`${modelName}WhereInput`),
causing dangling refs; update those three $ref strings to reference the actual
schema name produced by generateSchemas (e.g.,
`#/components/schemas/${modelName}WhereInput`) or otherwise make
generateSchemas() emit the underscored name so the names match (modify the ref
strings in openapi.ts around the AND/OR/NOT blocks or adjust generateSchemas()
schema names accordingly).

properties:
self:
type: string
description: Link for refetching the curent results
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Typo: "curent" should be "current".

Minor typo in the _links schema description.

✏️ Fix typo
                 self:
                     type: string
-                    description: Link for refetching the curent results
+                    description: Link for refetching the current results
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
description: Link for refetching the curent results
self:
type: string
description: Link for refetching the current results
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/server/test/openapi/migrated/baseline/rest-type-coverage-3.1.0.baseline.yaml`
at line 549, Fix the typo in the OpenAPI baseline description under the _links
schema: change the description string "Link for refetching the curent results"
to "Link for refetching the current results" so the word "curent" is corrected
to "current" in the description field.

Comment on lines +609 to +610
description: Zod validation errors if the error is due to data validation
failure
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Fix YAML multiline formatting in description.

The description for zodErrors appears to have an unintended line break causing "failure" to appear on a separate line.

✏️ Fix formatting
                     zodErrors:
                         type: object
                         additionalProperties: true
-                        description: Zod validation errors if the error is due to data validation
-                            failure
+                        description: Zod validation errors if the error is due to data validation failure
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
description: Zod validation errors if the error is due to data validation
failure
zodErrors:
type: object
additionalProperties: true
description: Zod validation errors if the error is due to data validation failure
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/server/test/openapi/migrated/baseline/rest-type-coverage-3.1.0.baseline.yaml`
around lines 609 - 610, The YAML description for the schema property named
"zodErrors" has an unintended line break so the word "failure" is split onto its
own line; fix it by merging the lines into a single properly quoted or block
scalar YAML value (e.g., use a single-line string or a pipe/greater-than block)
so the description reads continuously as "Zod validation errors if the error is
due to data validation failure", and update the "description" value for the
zodErrors entry accordingly.

@claude
Copy link
Contributor

claude bot commented Mar 19, 2026

Claude finished @ymc9's task —— View job


Code Review for OpenAPI Spec Generation PR

I'm conducting a comprehensive review of this PR that adds OpenAPI spec generation with filter kind slicing support. Here's my review progress:

Review Tasks:

  • Analyze architectural changes and new class structure
  • Review filter kind slicing implementation
  • Examine test coverage and quality
  • Check for potential bugs and edge cases
  • Review security considerations
  • Assess performance implications
  • Verify OpenAPI specification compliance

Major Strengths

Excellent Architecture & Design

  • Clean separation of concerns: Extracting OpenAPI spec generation into dedicated RestApiSpecGenerator and RPCApiSpecGenerator classes is a solid architectural decision
  • Shared utilities: The spec-utils.ts module provides excellent reusable functions for inclusion/exclusion logic
  • Interface-based design: OpenApiSpecGenerator interface enables extensibility and proper testing
  • Proper Zod validation: Good input validation for handler options

Robust Filter Kind Slicing Implementation

  • Well-designed inclusion/exclusion logic: isFilterKindIncluded function properly handles both includedFilterKinds and excludedFilterKinds with correct precedence
  • Proper fallback hierarchy: Model-specific → $all model → global defaults works as expected
  • Comprehensive filter coverage: Supports all four filter kinds (Equality, Like, Range, List) with appropriate type constraints

Strong Test Coverage

  • Comprehensive test suite: 4 test files with 10 baseline YAML fixtures
  • Migration validation: Proper migration from zenstack-v2 with baseline comparisons
  • Good test scenarios: Tests cover both REST and RPC approaches with various configurations

⚠️ Critical Security Issues

🔴 CRITICAL: Stack Trace Exposure

Location: packages/server/src/api/rest/index.ts:511

private handleGenericError(err: unknown): Response | PromiseLike<Response> {
    return this.makeError('unknownError', err instanceof Error ? `${err.message}\n${err.stack}` : 'Unknown error');
}

Risk: This exposes full stack traces in 500 responses, leaking internal server paths, code structure, and potentially sensitive information to clients.

Impact: High - Information disclosure vulnerability

Fix Required: Remove stack traces from responses, log them internally only.


🔴 Critical OpenAPI Specification Violations

1. Invalid Response References in RPC Generator

Location: packages/server/src/api/rpc/openapi.ts:180, 212, 244, 276, 288

'400': { $ref: '#/components/schemas/_ErrorResponse' }

Issue: Per OpenAPI 3.1 spec, response-level $ref must target components/responses, not components/schemas. Response Objects also require a description field.

2. Broken Self-References in WhereInput Schemas

Location: packages/server/src/api/rpc/openapi.ts:480-492

properties['AND'] = {
    oneOf: [
        { $ref: `#/components/schemas/_${_modelName}WhereInput` },  // ❌ Wrong name

Issue: References _${modelName}WhereInput but generateSchemas() creates ${modelName}WhereInput, causing dangling references.

3. Missing Schema Definitions

Location: packages/server/src/api/rpc/openapi.ts:77, 81
Issue: createManyAndReturn and updateManyAndReturn operations reference ${modelName}CreateManyAndReturnArgs / ${modelName}UpdateManyAndReturnArgs but these schemas are never generated in generateSchemas().

4. Wrong Response Schema Types

Location: packages/server/src/api/rpc/openapi.ts:176
Issue: All operations use ${modelName}Response schema, but operations like findMany, count, aggregate return arrays/batch objects, not single models. This will cause client deserialization failures.


🟠 Major Logic Issues

1. Filter Kind Slicing Logic Bug

Location: packages/server/src/api/common/spec-utils.ts:29, 85

const modelSlicing = (slicing.models as Record<string, any>)[modelKey] ?? (slicing.models as any).$all;

Issue: Uses ?? operator which completely replaces $all rules when model-specific config exists. Global $all rules should be merged with model-specific overrides, not replaced.

Impact: Users lose global configuration when any model-specific settings are applied.

2. REST Generator Ignores Operation Filtering

Location: packages/server/src/api/rest/openapi.ts:84-88
Issue: Collection and single-resource paths are generated unconditionally, ignoring excludedOperations/includedOperations from queryOptions.slicing.

3. Inconsistent ID Handling in processUpdate

Location: packages/server/src/api/rest/index.ts:1280-1293
Issue: Manual construction of relation payloads instead of using existing makeIdConnect() function, causing numeric IDs to remain as strings and compound IDs to use different formats.


🟡 Minor Issues

1. Missing Documentation

  • No JSDoc for complex filter kind logic in spec generators
  • Missing exports in main barrel module for concrete generator classes

2. Typos in Baseline Files

  • Line 549: "curent" → "current" in REST type coverage baseline
  • Lines 609-610: Broken YAML multiline description for "zodErrors"

3. Test Improvements Needed

  • Tests use early returns instead of it.skip() for missing baselines
  • Unnecessary as any casts in RPC test assertions

🎯 Performance Considerations

Good:

  • Proper caching of serializers and model metadata
  • Efficient schema generation with reusable components

Areas for Optimization:

  • Repeated model lookups in OpenAPI generation could be cached
  • String operations in filter generation loops could benefit from memoization

🔒 Other Security Considerations

Input Validation:

  • ✅ Good Zod validation for handler options
  • ⚠️ queryOptions validation could be more specific (currently z.object().optional())

Type Safety:

  • ⚠️ Filter kind strings are magic strings - could benefit from const assertions
  • ✅ Good use of TypeScript for schema definitions

📋 Recommendations

Must Fix (Critical):

  1. Remove stack traces from error responses (security risk)
  2. Fix OpenAPI spec violations - wrong response references, missing schemas, broken refs
  3. Implement operation filtering in REST generator
  4. Fix filter kind slicing logic to properly merge $all with model-specific rules

Should Fix (Major):

  1. Use makeIdConnect() consistently for relation payloads
  2. Add missing response schema variants for different operation types
  3. Improve input validation specificity

Nice to Have:

  1. Add JSDoc documentation for complex logic
  2. Export concrete generator classes from main module
  3. Fix typos and test improvements

Overall Assessment

This is a significant and valuable addition to the codebase with excellent architectural decisions. The filter kind slicing feature is well-designed and the test coverage is comprehensive. However, several critical issues must be addressed before merging:

  • Security: Stack trace exposure is a significant risk
  • Specification Compliance: Multiple OpenAPI violations will break client generation
  • Logic Correctness: Filter kind slicing and operation filtering bugs affect core functionality

Recommended Action: Address critical and major issues before merge. The foundation is solid but needs these important fixes.


@claude
Copy link
Contributor

claude bot commented Mar 19, 2026

Claude finished @ymc9's task —— View job


Code Review for OpenAPI Spec Generation PR

I've conducted a comprehensive review of this PR that adds OpenAPI spec generation with filter kind slicing support. Here's my detailed feedback:

Review Tasks:

  • Analyze architectural changes and new class structure
  • Review filter kind slicing implementation
  • Examine test coverage and quality
  • Check for potential bugs and edge cases
  • Review security considerations
  • Assess performance implications
  • Verify OpenAPI specification compliance

Major Strengths

Excellent Architecture & Design

  • Clean separation of concerns: Extracting OpenAPI spec generation into dedicated RestApiSpecGenerator and RPCApiSpecGenerator classes is a solid architectural decision
  • Shared utilities: The spec-utils.ts module provides excellent reusable functions for inclusion/exclusion logic
  • Interface-based design: OpenApiSpecGenerator interface enables extensibility and proper testing
  • Proper Zod validation: Good input validation for handler options

Robust Filter Kind Slicing Implementation

  • Well-designed inclusion/exclusion logic: isFilterKindIncluded function properly handles both includedFilterKinds and excludedFilterKinds with correct precedence
  • Proper fallback hierarchy: Model-specific → $all model → global defaults works as expected
  • Comprehensive filter coverage: Supports all four filter kinds (Equality, Like, Range, List) with appropriate type constraints

Strong Test Coverage

  • Comprehensive test suite: 4 test files with 10 baseline YAML fixtures
  • Migration validation: Proper migration from zenstack-v2 with baseline comparisons
  • Good test scenarios: Tests cover both REST and RPC approaches with various configurations

⚠️ Critical Security Issues

🔴 CRITICAL: Stack Trace Exposure

Location: packages/server/src/api/rest/index.ts:511

private handleGenericError(err: unknown): Response | PromiseLike<Response> {
    return this.makeError('unknownError', err instanceof Error ? `${err.message}\n${err.stack}` : 'Unknown error');
}

Risk: This exposes full stack traces in 500 responses, leaking internal server paths, code structure, and potentially sensitive information to clients.

Impact: High - Information disclosure vulnerability

Fix Required: Remove stack traces from responses, log them internally only.


🔴 Critical OpenAPI Specification Violations

1. Invalid Response References in RPC Generator

Location: packages/server/src/api/rpc/openapi.ts:180, 212, 244, 276, 288

'400': { $ref: '#/components/schemas/_ErrorResponse' }

Issue: Per OpenAPI 3.1 spec, response-level $ref must target components/responses, not components/schemas. Response Objects also require a description field.

2. Broken Self-References in WhereInput Schemas

Location: packages/server/src/api/rpc/openapi.ts:480-492

properties['AND'] = {
    oneOf: [
        { $ref: `#/components/schemas/_${_modelName}WhereInput` },  // ❌ Wrong name

Issue: References _${modelName}WhereInput but generateSchemas() creates ${modelName}WhereInput, causing dangling references.

3. Missing Schema Definitions

Location: packages/server/src/api/rpc/openapi.ts:77, 81
Issue: createManyAndReturn and updateManyAndReturn operations reference ${modelName}CreateManyAndReturnArgs / ${modelName}UpdateManyAndReturnArgs but these schemas are never generated in generateSchemas().

4. Wrong Response Schema Types

Location: packages/server/src/api/rpc/openapi.ts:176
Issue: All operations use ${modelName}Response schema, but operations like findMany, count, aggregate return arrays/batch objects, not single models. This will cause client deserialization failures.


🟠 Major Logic Issues

1. Filter Kind Slicing Logic Bug

Location: packages/server/src/api/common/spec-utils.ts:29, 85

const modelSlicing = (slicing.models as Record<string, any>)[modelKey] ?? (slicing.models as any).$all;

Issue: Uses ?? operator which completely replaces $all rules when model-specific config exists. Global $all rules should be merged with model-specific overrides, not replaced.

Impact: Users lose global configuration when any model-specific settings are applied.

2. REST Generator Ignores Operation Filtering

Location: packages/server/src/api/rest/openapi.ts:84-88
Issue: Collection and single-resource paths are generated unconditionally, ignoring excludedOperations/includedOperations from queryOptions.slicing.

3. Inconsistent ID Handling in processUpdate

Location: packages/server/src/api/rest/index.ts:1280-1293
Issue: Manual construction of relation payloads instead of using existing makeIdConnect() function, causing numeric IDs to remain as strings and compound IDs to use different formats.


🟡 Minor Issues

1. Missing Documentation

  • No JSDoc for complex filter kind logic in spec generators
  • Missing exports in main barrel module for concrete generator classes

2. Typos in Baseline Files

  • Line 549: "curent" → "current" in REST type coverage baseline
  • Lines 609-610: Broken YAML multiline description for "zodErrors"

3. Test Improvements Needed

  • Tests use early returns instead of it.skip() for missing baselines
  • Unnecessary as any casts in RPC test assertions

🎯 Performance Considerations

Good:

  • Proper caching of serializers and model metadata
  • Efficient schema generation with reusable components

Areas for Optimization:

  • Repeated model lookups in OpenAPI generation could be cached
  • String operations in filter generation loops could benefit from memoization

🔒 Other Security Considerations

Input Validation:

  • ✅ Good Zod validation for handler options
  • ⚠️ queryOptions validation could be more specific (currently z.object().optional())

Type Safety:

  • ⚠️ Filter kind strings are magic strings - could benefit from const assertions
  • ✅ Good use of TypeScript for schema definitions

📋 Recommendations

Must Fix (Critical):

  1. Remove stack traces from error responses (security risk)
  2. Fix OpenAPI spec violations - wrong response references, missing schemas, broken refs
  3. Implement operation filtering in REST generator
  4. Fix filter kind slicing logic to properly merge $all with model-specific rules

Should Fix (Major):

  1. Use makeIdConnect() consistently for relation payloads
  2. Add missing response schema variants for different operation types
  3. Improve input validation specificity

Nice to Have:

  1. Add JSDoc documentation for complex logic
  2. Export concrete generator classes from main module
  3. Fix typos and test improvements

Overall Assessment

This is a significant and valuable addition to the codebase with excellent architectural decisions. The filter kind slicing feature is well-designed and the test coverage is comprehensive. However, several critical issues must be addressed before merging:

  • Security: Stack trace exposure is a significant risk
  • Specification Compliance: Multiple OpenAPI violations will break client generation
  • Logic Correctness: Filter kind slicing and operation filtering bugs affect core functionality

Recommended Action: Address critical and major issues before merge. The foundation is solid but needs these important fixes.


Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 6

♻️ Duplicate comments (3)
packages/server/src/api/rest/openapi.ts (3)

73-89: ⚠️ Potential issue | 🟠 Major

Honor operation slicing before publishing REST methods.

generatePaths() always assigns both collection and single-resource path items, and the builders always return every method, so includedOperations / excludedOperations can't remove get, post, patch, or delete from the emitted spec. Gate each method with isOperationIncluded(...) and skip empty path items.

Also applies to: 136-188, 191-250

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

In `@packages/server/src/api/rest/openapi.ts` around lines 73 - 89, generatePaths
currently unconditionally adds collection and single-resource PathItems by
calling buildCollectionPath and buildSinglePath which themselves always include
all methods; modify generatePaths and the builders (buildCollectionPath,
buildSinglePath) to check isOperationIncluded(modelName, operation) before
adding each operation (e.g., "get", "post", "patch", "delete") to the PathItem
so omitted operations are not emitted, and ensure generatePaths skips adding a
PathItem if it ends up empty after filtering; update logic used by
getIncludedModels/getModelPath as needed so only included operations produce
entries in the final PathsObject.

150-160: ⚠️ Potential issue | 🔴 Critical

Use Response Objects for error statuses.

Every 400 / 404 entry here points directly at #/components/schemas/_errorResponse. Under OpenAPI 3.1, a status-code response must be a Response Object or a Reference Object to components/responses; schema refs only belong under content.*.schema. As written, these entries are invalid and also omit the required description.

#!/bin/bash
set -euo pipefail

file="packages/server/src/api/rest/openapi.ts"

echo "Direct schema refs under response status codes:"
rg -n "'(400|404)':\s*\{\s*\$ref:\s*'#/components/schemas/_errorResponse'" "$file"

echo
echo "Expected: no matches after wrapping these as Response Objects or moving them to components.responses."

Also applies to: 175-185, 199-209, 225-236, 244-247, 278-281, 308-314, 325-328, 339-342, 360-363, 375-378

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

In `@packages/server/src/api/rest/openapi.ts` around lines 150 - 160, The response
entries for error status codes (e.g., the '400' and '404' entries next to
modelName in openapi.ts) incorrectly reference a schema
(`#/components/schemas/_errorResponse`) directly instead of using a Response
Object; update each such status-code entry to be either a Reference Object to
components.responses (e.g., $ref: '#/components/responses/_errorResponse') or a
full Response Object that includes a required description and a content block
whose schema references `#/components/schemas/_errorResponse`, and apply the same
change to all occurrences flagged (near the blocks that construct responses for
modelNameListResponse and other endpoints). Ensure each response entry includes
description when not using a $ref and replace direct schema refs under the
status keys with the proper responses structure.

91-113: ⚠️ Potential issue | 🟠 Major

Relation slicing still leaks endpoints and dangling refs.

generatePaths() never checks isFieldOmitted(modelName, fieldName, this.queryOptions), so omitted relation fields still get /{field} and /relationships/{field} paths. Separately, buildModelReadSchema() can still emit $ref: '#/components/schemas/${fieldDef.type}' for a kept relation whose target model was sliced out, which leaves a dangling ref because that component is never generated.

Also applies to: 675-680

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

In `@packages/server/src/api/rest/openapi.ts` around lines 91 - 113,
generatePaths() currently adds relation endpoints without checking
isFieldOmitted and can refer to relation target schemas that were sliced out;
update the loop over modelDef.fields in generatePaths() to skip any field where
isFieldOmitted(modelName, fieldName, this.queryOptions) returns true and also
ensure that before adding a relation path you verify the relation target model
still exists in this.schema.models (reuse the existing relModelDef check) and
that relModelDef was not sliced out; additionally, update buildModelReadSchema()
to avoid emitting $ref to fieldDef.type when that target model has been omitted
(check this.schema.models[fieldDef.type] or a slice/keep predicate) and emit an
appropriate inline schema or remove the relation property instead to prevent
dangling $ref; change references in generatePaths() to only call
buildFetchRelatedPath and buildRelationshipPath when the field is not omitted
and the target model is present.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/server/src/api/rest/openapi.ts`:
- Around line 253-284: In buildFetchRelatedPath, the 200 response currently only
has a description; update the responses object so the '200' entry includes a
proper schema $ref: use `#/components/schemas/${fieldDef.type}ListResponse` when
fieldDef.array is true (collection) and
`#/components/schemas/${fieldDef.type}Response` when false (singular). Locate
the method buildFetchRelatedPath and modify the returned get.responses['200'] to
point at the correct schema based on isCollection (using
modelName/fieldName/fieldDef to determine the type).
- Around line 671-691: buildModelReadSchema currently emits fields at the top
level which causes the later allOf merge with the _resource wrapper to put
required attributes and relation refs at the wrong level; update
buildModelReadSchema so it returns an object with type: 'object' and two
properties: attributes (an object whose properties are the non-relation scalar
fields produced via fieldToSchema, with required moved into attributes.required)
and relationships (an object whose properties are the relation fields as
ReferenceObject refs to related model schemas), keeping existing checks
isFieldOmitted, fieldDef.omit, and this.queryOptions; ensure any array/optional
logic remains but that required only lists attribute names under
attributes.required and relation fields are only referenced under relationships
rather than at the top level.
- Around line 694-757: The builders (buildCreateRequestSchema and the similar
Update builder) currently skip fields only by fieldDef.omit but don't respect
per-request field slicing; modify buildCreateRequestSchema (and the Update
counterpart) to call isFieldOmitted(_modelName, fieldName, modelDef) for each
field and skip the field when it returns true (in addition to existing checks
like fieldDef.omit/updatedAt/foreignKeyFor/id with default); ensure you actually
use the _modelName parameter passed into buildCreateRequestSchema and mirror the
same change in the update schema builder so fields removed via queryOptions are
not included in CreateRequest/UpdateRequest.

In `@packages/server/src/api/rpc/openapi.ts`:
- Around line 401-412: The loop over modelDef.fields used by the
CreateInput/UpdateInput builders calls typeToSchema(fieldDef.type) directly,
which loses nullability and array shape; update the logic in the
CreateInput/UpdateInput builders (the block that fills properties and required)
to: 1) detect when fieldDef.type represents an array and set
properties[fieldName] to an OpenAPI array schema with items produced by
typeToSchema(baseType) instead of passing the array type through unchanged; 2)
preserve nullable by setting properties[fieldName].nullable = true when the
field type allows null (or by merging/using the nullable flag returned from
typeToSchema for the inner/base type); and 3) adjust the required calculation to
include non-optional array fields (remove the current !fieldDef.array guard) so
list columns that are required remain required; ensure you reference and update
the same symbols used now (typeToSchema, modelDef.fields, properties, required)
so CreateInput and UpdateInput both get the fix.
- Around line 380-386: The generator is marking non-array, non-optional relation
fields as required which is incorrect; update the required-pushing logic in the
loop over modelDef.fields (the block using isFieldOmitted, fieldToSchema and
this.queryOptions) to skip pushing relation fields into required—i.e., add a
relation-detection check (e.g., detect relation/object-kind fields via
fieldDef.kind/type/relationship flag used in your schema representation) and
change the condition to: if (!fieldDef.optional && !fieldDef.array &&
!isRelationField(fieldDef)) { required.push(fieldName); } so relation payloads
remain optional unless explicitly selected/included.
- Around line 688-735: buildFieldFilterSchema currently uses baseSchema =
this.typeToSchema(fieldDef.type) which strips array and optional wrappers so
filters like equals/in/notIn accept only the element type and cannot represent
null or arrays; change it to build a wrapped schema that respects fieldDef.array
and fieldDef.optional (e.g. if fieldDef.array wrap baseSchema into an array
schema for equals/in/notIn where appropriate, and if fieldDef.optional mark the
schema as nullable or include null in the type) and use that wrapped schema for
filterProps entries such as 'equals', 'not', 'in', 'notIn' (while keeping
element-level schemas for list operators like 'has'/'hasEvery'/'hasSome' if
intended); update references to this.typeToSchema, fieldDef.array, and
fieldDef.optional inside buildFieldFilterSchema to produce and use
baseSchemaWithWrappers instead of the bare baseSchema.

---

Duplicate comments:
In `@packages/server/src/api/rest/openapi.ts`:
- Around line 73-89: generatePaths currently unconditionally adds collection and
single-resource PathItems by calling buildCollectionPath and buildSinglePath
which themselves always include all methods; modify generatePaths and the
builders (buildCollectionPath, buildSinglePath) to check
isOperationIncluded(modelName, operation) before adding each operation (e.g.,
"get", "post", "patch", "delete") to the PathItem so omitted operations are not
emitted, and ensure generatePaths skips adding a PathItem if it ends up empty
after filtering; update logic used by getIncludedModels/getModelPath as needed
so only included operations produce entries in the final PathsObject.
- Around line 150-160: The response entries for error status codes (e.g., the
'400' and '404' entries next to modelName in openapi.ts) incorrectly reference a
schema (`#/components/schemas/_errorResponse`) directly instead of using a
Response Object; update each such status-code entry to be either a Reference
Object to components.responses (e.g., $ref:
'#/components/responses/_errorResponse') or a full Response Object that includes
a required description and a content block whose schema references
`#/components/schemas/_errorResponse`, and apply the same change to all
occurrences flagged (near the blocks that construct responses for
modelNameListResponse and other endpoints). Ensure each response entry includes
description when not using a $ref and replace direct schema refs under the
status keys with the proper responses structure.
- Around line 91-113: generatePaths() currently adds relation endpoints without
checking isFieldOmitted and can refer to relation target schemas that were
sliced out; update the loop over modelDef.fields in generatePaths() to skip any
field where isFieldOmitted(modelName, fieldName, this.queryOptions) returns true
and also ensure that before adding a relation path you verify the relation
target model still exists in this.schema.models (reuse the existing relModelDef
check) and that relModelDef was not sliced out; additionally, update
buildModelReadSchema() to avoid emitting $ref to fieldDef.type when that target
model has been omitted (check this.schema.models[fieldDef.type] or a slice/keep
predicate) and emit an appropriate inline schema or remove the relation property
instead to prevent dangling $ref; change references in generatePaths() to only
call buildFetchRelatedPath and buildRelationshipPath when the field is not
omitted and the target model is present.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: d8ec89b2-c972-4703-b6c5-f4719db96185

📥 Commits

Reviewing files that changed from the base of the PR and between ff5735f and 8eea293.

📒 Files selected for processing (2)
  • packages/server/src/api/rest/openapi.ts
  • packages/server/src/api/rpc/openapi.ts

Comment on lines +253 to +284
private buildFetchRelatedPath(
modelName: string,
fieldName: string,
fieldDef: FieldDef,
tag: string,
): Record<string, any> {
const isCollection = !!fieldDef.array;
const params: any[] = [{ $ref: '#/components/parameters/id' }, { $ref: '#/components/parameters/include' }];

if (isCollection && this.schema.models[fieldDef.type]) {
const relModelDef = this.schema.models[fieldDef.type]!;
params.push(
{ $ref: '#/components/parameters/sort' },
{ $ref: '#/components/parameters/pageOffset' },
{ $ref: '#/components/parameters/pageLimit' },
...this.buildFilterParams(fieldDef.type, relModelDef),
);
}

return {
get: {
tags: [tag],
summary: `Fetch related ${fieldDef.type} for ${modelName}`,
operationId: `get${modelName}_${fieldName}`,
parameters: params,
responses: {
'200': { description: `Related ${fieldDef.type} resource(s)` },
'404': { $ref: '#/components/schemas/_errorResponse' },
},
},
};
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Document the 200 body for related-resource GETs.

buildFetchRelatedPath() knows both the related model and whether the relation is singular or plural, but the 200 response only has a description. That leaves every GET /{model}/{id}/{field} endpoint without a usable response schema. Point it at ${fieldDef.type}Response or ${fieldDef.type}ListResponse.

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

In `@packages/server/src/api/rest/openapi.ts` around lines 253 - 284, In
buildFetchRelatedPath, the 200 response currently only has a description; update
the responses object so the '200' entry includes a proper schema $ref: use
`#/components/schemas/${fieldDef.type}ListResponse` when fieldDef.array is true
(collection) and `#/components/schemas/${fieldDef.type}Response` when false
(singular). Locate the method buildFetchRelatedPath and modify the returned
get.responses['200'] to point at the correct schema based on isCollection (using
modelName/fieldName/fieldDef to determine the type).

Comment on lines +671 to +691
private buildModelReadSchema(modelName: string, modelDef: ModelDef): SchemaObject {
const properties: Record<string, SchemaObject | ReferenceObject> = {};
const required: string[] = [];

for (const [fieldName, fieldDef] of Object.entries(modelDef.fields)) {
if (fieldDef.omit) continue;
if (isFieldOmitted(modelName, fieldName, this.queryOptions)) continue;

const schema = this.fieldToSchema(fieldDef);
properties[fieldName] = schema;

if (!fieldDef.optional && !fieldDef.array) {
required.push(fieldName);
}
}

const result: SchemaObject = { type: 'object', properties };
if (required.length > 0) {
result.required = required;
}
return result;
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

data currently requires the wrong JSON:API shape.

buildModelReadSchema() emits raw scalar/relation fields, then the response wrappers allOf-merge that object with _resource. The result puts required attributes at the top level and relation fields as nested model refs, instead of under attributes / relationships.

Also applies to: 816-848

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

In `@packages/server/src/api/rest/openapi.ts` around lines 671 - 691,
buildModelReadSchema currently emits fields at the top level which causes the
later allOf merge with the _resource wrapper to put required attributes and
relation refs at the wrong level; update buildModelReadSchema so it returns an
object with type: 'object' and two properties: attributes (an object whose
properties are the non-relation scalar fields produced via fieldToSchema, with
required moved into attributes.required) and relationships (an object whose
properties are the relation fields as ReferenceObject refs to related model
schemas), keeping existing checks isFieldOmitted, fieldDef.omit, and
this.queryOptions; ensure any array/optional logic remains but that required
only lists attribute names under attributes.required and relation fields are
only referenced under relationships rather than at the top level.

Comment on lines +694 to +757
private buildCreateRequestSchema(_modelName: string, modelDef: ModelDef): SchemaObject {
const idFieldNames = new Set(modelDef.idFields);
const attributes: Record<string, SchemaObject | ReferenceObject> = {};
const attrRequired: string[] = [];
const relationships: Record<string, SchemaObject | ReferenceObject> = {};

for (const [fieldName, fieldDef] of Object.entries(modelDef.fields)) {
if (fieldDef.omit) continue;
if (fieldDef.updatedAt) continue;
if (fieldDef.foreignKeyFor) continue;
// Skip auto-generated id fields
if (idFieldNames.has(fieldName) && fieldDef.default !== undefined) continue;

if (fieldDef.relation) {
relationships[fieldName] = fieldDef.array
? {
type: 'object',
properties: {
data: {
type: 'array',
items: { $ref: '#/components/schemas/_resourceIdentifier' },
},
},
}
: {
type: 'object',
properties: {
data: { $ref: '#/components/schemas/_resourceIdentifier' },
},
};
} else {
attributes[fieldName] = this.fieldToSchema(fieldDef);
if (!fieldDef.optional && fieldDef.default === undefined && !fieldDef.array) {
attrRequired.push(fieldName);
}
}
}

const dataProperties: Record<string, SchemaObject | ReferenceObject> = {
type: { type: 'string' },
};

if (Object.keys(attributes).length > 0) {
const attrSchema: SchemaObject = { type: 'object', properties: attributes };
if (attrRequired.length > 0) attrSchema.required = attrRequired;
dataProperties['attributes'] = attrSchema;
}

if (Object.keys(relationships).length > 0) {
dataProperties['relationships'] = { type: 'object', properties: relationships };
}

return {
type: 'object',
properties: {
data: {
type: 'object',
properties: dataProperties,
required: ['type'],
},
},
required: ['data'],
};
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Field-level slicing is only applied to read schemas.

These builders skip fieldDef.omit, but they never call isFieldOmitted(...), so fields removed via queryOptions still show up in CreateRequest and UpdateRequest. _modelName being unused is the giveaway.

Minimal fix shape
-    private buildCreateRequestSchema(_modelName: string, modelDef: ModelDef): SchemaObject {
+    private buildCreateRequestSchema(modelName: string, modelDef: ModelDef): SchemaObject {
         const idFieldNames = new Set(modelDef.idFields);
         const attributes: Record<string, SchemaObject | ReferenceObject> = {};
         const attrRequired: string[] = [];
         const relationships: Record<string, SchemaObject | ReferenceObject> = {};

         for (const [fieldName, fieldDef] of Object.entries(modelDef.fields)) {
-            if (fieldDef.omit) continue;
+            if (fieldDef.omit || isFieldOmitted(modelName, fieldName, this.queryOptions)) continue;
             if (fieldDef.updatedAt) continue;
             if (fieldDef.foreignKeyFor) continue;
             // Skip auto-generated id fields
             if (idFieldNames.has(fieldName) && fieldDef.default !== undefined) continue;
-            schemas[`${modelName}UpdateRequest`] = this.buildUpdateRequestSchema(modelDef);
+            schemas[`${modelName}UpdateRequest`] = this.buildUpdateRequestSchema(modelName, modelDef);
-    private buildUpdateRequestSchema(modelDef: ModelDef): SchemaObject {
+    private buildUpdateRequestSchema(modelName: string, modelDef: ModelDef): SchemaObject {
         const attributes: Record<string, SchemaObject | ReferenceObject> = {};
         const relationships: Record<string, SchemaObject | ReferenceObject> = {};

         for (const [fieldName, fieldDef] of Object.entries(modelDef.fields)) {
-            if (fieldDef.omit) continue;
+            if (fieldDef.omit || isFieldOmitted(modelName, fieldName, this.queryOptions)) continue;
             if (fieldDef.updatedAt) continue;
             if (fieldDef.foreignKeyFor) continue;

Also applies to: 759-814

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

In `@packages/server/src/api/rest/openapi.ts` around lines 694 - 757, The builders
(buildCreateRequestSchema and the similar Update builder) currently skip fields
only by fieldDef.omit but don't respect per-request field slicing; modify
buildCreateRequestSchema (and the Update counterpart) to call
isFieldOmitted(_modelName, fieldName, modelDef) for each field and skip the
field when it returns true (in addition to existing checks like
fieldDef.omit/updatedAt/foreignKeyFor/id with default); ensure you actually use
the _modelName parameter passed into buildCreateRequestSchema and mirror the
same change in the update schema builder so fields removed via queryOptions are
not included in CreateRequest/UpdateRequest.

Comment on lines +380 to +386
for (const [fieldName, fieldDef] of Object.entries(modelDef.fields)) {
if (fieldDef.omit) continue;
if (isFieldOmitted(modelName, fieldName, this.queryOptions)) continue;
properties[fieldName] = this.fieldToSchema(fieldDef);
if (!fieldDef.optional && !fieldDef.array) {
required.push(fieldName);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Don't require relation fields in the shared model schema.

This generator already models select/include on the args side, so relation payloads are conditional. Marking every non-array, non-optional relation as required makes ordinary RPC responses invalid unless the caller explicitly requested that relation.

Suggested fix
         for (const [fieldName, fieldDef] of Object.entries(modelDef.fields)) {
             if (fieldDef.omit) continue;
             if (isFieldOmitted(modelName, fieldName, this.queryOptions)) continue;
             properties[fieldName] = this.fieldToSchema(fieldDef);
-            if (!fieldDef.optional && !fieldDef.array) {
+            if (!fieldDef.optional && !fieldDef.array && !fieldDef.relation) {
                 required.push(fieldName);
             }
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/server/src/api/rpc/openapi.ts` around lines 380 - 386, The generator
is marking non-array, non-optional relation fields as required which is
incorrect; update the required-pushing logic in the loop over modelDef.fields
(the block using isFieldOmitted, fieldToSchema and this.queryOptions) to skip
pushing relation fields into required—i.e., add a relation-detection check
(e.g., detect relation/object-kind fields via fieldDef.kind/type/relationship
flag used in your schema representation) and change the condition to: if
(!fieldDef.optional && !fieldDef.array && !isRelationField(fieldDef)) {
required.push(fieldName); } so relation payloads remain optional unless
explicitly selected/included.

Comment on lines +401 to +412
for (const [fieldName, fieldDef] of Object.entries(modelDef.fields)) {
if (fieldDef.relation) continue;
if (fieldDef.foreignKeyFor) continue;
if (fieldDef.omit) continue;
if (fieldDef.updatedAt) continue;
// Skip auto-generated id fields
if (idFieldNames.has(fieldName) && fieldDef.default !== undefined) continue;

properties[fieldName] = this.typeToSchema(fieldDef.type);
if (!fieldDef.optional && fieldDef.default === undefined && !fieldDef.array) {
required.push(fieldName);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

CreateInput and UpdateInput drop nullability and list shape.

Both builders go straight through typeToSchema(fieldDef.type), so nullable scalars are emitted as non-null and scalar-list columns are emitted as single values. CreateInput also never marks required list fields as required because of the !fieldDef.array guard.

Suggested fix
-            properties[fieldName] = this.typeToSchema(fieldDef.type);
-            if (!fieldDef.optional && fieldDef.default === undefined && !fieldDef.array) {
+            const fieldSchema = fieldDef.array
+                ? { type: 'array', items: this.typeToSchema(fieldDef.type) }
+                : this.typeToSchema(fieldDef.type);
+            properties[fieldName] = fieldDef.optional ? { oneOf: [fieldSchema, { type: 'null' }] } : fieldSchema;
+            if (!fieldDef.optional && fieldDef.default === undefined) {
                 required.push(fieldName);
             }
...
-            properties[fieldName] = this.typeToSchema(fieldDef.type);
+            const fieldSchema = fieldDef.array
+                ? { type: 'array', items: this.typeToSchema(fieldDef.type) }
+                : this.typeToSchema(fieldDef.type);
+            properties[fieldName] = fieldDef.optional ? { oneOf: [fieldSchema, { type: 'null' }] } : fieldSchema;

Also applies to: 425-432

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

In `@packages/server/src/api/rpc/openapi.ts` around lines 401 - 412, The loop over
modelDef.fields used by the CreateInput/UpdateInput builders calls
typeToSchema(fieldDef.type) directly, which loses nullability and array shape;
update the logic in the CreateInput/UpdateInput builders (the block that fills
properties and required) to: 1) detect when fieldDef.type represents an array
and set properties[fieldName] to an OpenAPI array schema with items produced by
typeToSchema(baseType) instead of passing the array type through unchanged; 2)
preserve nullable by setting properties[fieldName].nullable = true when the
field type allows null (or by merging/using the nullable flag returned from
typeToSchema for the inner/base type); and 3) adjust the required calculation to
include non-optional array fields (remove the current !fieldDef.array guard) so
list columns that are required remain required; ensure you reference and update
the same symbols used now (typeToSchema, modelDef.fields, properties, required)
so CreateInput and UpdateInput both get the fix.

Comment on lines +688 to +735
private buildFieldFilterSchema(modelName: string, fieldName: string, fieldDef: FieldDef): SchemaObject | undefined {
const baseSchema = this.typeToSchema(fieldDef.type);
const filterProps: Record<string, SchemaObject | ReferenceObject> = {};
const type = fieldDef.type;

// Equality operators
if (isFilterKindIncluded(modelName, fieldName, 'Equality', this.queryOptions)) {
filterProps['equals'] = baseSchema;
filterProps['not'] = baseSchema;
filterProps['in'] = { type: 'array', items: baseSchema };
filterProps['notIn'] = { type: 'array', items: baseSchema };
}

// Range operators (numeric/datetime types)
if (
(type === 'Int' || type === 'Float' || type === 'BigInt' || type === 'Decimal' || type === 'DateTime') &&
isFilterKindIncluded(modelName, fieldName, 'Range', this.queryOptions)
) {
filterProps['lt'] = baseSchema;
filterProps['lte'] = baseSchema;
filterProps['gt'] = baseSchema;
filterProps['gte'] = baseSchema;
}

// Like operators (String type)
if (type === 'String' && isFilterKindIncluded(modelName, fieldName, 'Like', this.queryOptions)) {
filterProps['contains'] = { type: 'string' };
filterProps['startsWith'] = { type: 'string' };
filterProps['endsWith'] = { type: 'string' };
filterProps['mode'] = { type: 'string', enum: ['default', 'insensitive'] };
}

// List operators (array fields)
if (fieldDef.array && isFilterKindIncluded(modelName, fieldName, 'List', this.queryOptions)) {
filterProps['has'] = baseSchema;
filterProps['hasEvery'] = { type: 'array', items: baseSchema };
filterProps['hasSome'] = { type: 'array', items: baseSchema };
filterProps['isEmpty'] = { type: 'boolean' };
}

if (Object.keys(filterProps).length === 0) return undefined;

const filterObject: SchemaObject = { type: 'object', properties: filterProps };

// If Equality is included, allow shorthand (direct value) via oneOf
if (isFilterKindIncluded(modelName, fieldName, 'Equality', this.queryOptions)) {
return { oneOf: [baseSchema, filterObject] };
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Filter values should mirror the field shape, not just the element type.

buildFieldFilterSchema() starts from typeToSchema(fieldDef.type), which strips both fieldDef.array and fieldDef.optional. That makes list equals/shorthand filters accept a single element and prevents null comparisons on nullable fields.

Suggested fix
-        const baseSchema = this.typeToSchema(fieldDef.type);
+        const elementSchema = this.typeToSchema(fieldDef.type);
+        const valueSchema = fieldDef.optional
+            ? {
+                  oneOf: [
+                      fieldDef.array ? { type: 'array', items: elementSchema } : elementSchema,
+                      { type: 'null' },
+                  ],
+              }
+            : fieldDef.array
+              ? { type: 'array', items: elementSchema }
+              : elementSchema;
         const filterProps: Record<string, SchemaObject | ReferenceObject> = {};
         const type = fieldDef.type;
 
         // Equality operators
         if (isFilterKindIncluded(modelName, fieldName, 'Equality', this.queryOptions)) {
-            filterProps['equals'] = baseSchema;
-            filterProps['not'] = baseSchema;
-            filterProps['in'] = { type: 'array', items: baseSchema };
-            filterProps['notIn'] = { type: 'array', items: baseSchema };
+            filterProps['equals'] = valueSchema;
+            filterProps['not'] = valueSchema;
+            filterProps['in'] = { type: 'array', items: valueSchema };
+            filterProps['notIn'] = { type: 'array', items: valueSchema };
         }
 
         // Range operators (numeric/datetime types)
         if (
             (type === 'Int' || type === 'Float' || type === 'BigInt' || type === 'Decimal' || type === 'DateTime') &&
             isFilterKindIncluded(modelName, fieldName, 'Range', this.queryOptions)
         ) {
-            filterProps['lt'] = baseSchema;
-            filterProps['lte'] = baseSchema;
-            filterProps['gt'] = baseSchema;
-            filterProps['gte'] = baseSchema;
+            filterProps['lt'] = elementSchema;
+            filterProps['lte'] = elementSchema;
+            filterProps['gt'] = elementSchema;
+            filterProps['gte'] = elementSchema;
         }
 ...
         if (fieldDef.array && isFilterKindIncluded(modelName, fieldName, 'List', this.queryOptions)) {
-            filterProps['has'] = baseSchema;
-            filterProps['hasEvery'] = { type: 'array', items: baseSchema };
-            filterProps['hasSome'] = { type: 'array', items: baseSchema };
+            filterProps['has'] = elementSchema;
+            filterProps['hasEvery'] = { type: 'array', items: elementSchema };
+            filterProps['hasSome'] = { type: 'array', items: elementSchema };
             filterProps['isEmpty'] = { type: 'boolean' };
         }
 ...
-            return { oneOf: [baseSchema, filterObject] };
+            return { oneOf: [valueSchema, filterObject] };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/server/src/api/rpc/openapi.ts` around lines 688 - 735,
buildFieldFilterSchema currently uses baseSchema =
this.typeToSchema(fieldDef.type) which strips array and optional wrappers so
filters like equals/in/notIn accept only the element type and cannot represent
null or arrays; change it to build a wrapped schema that respects fieldDef.array
and fieldDef.optional (e.g. if fieldDef.array wrap baseSchema into an array
schema for equals/in/notIn where appropriate, and if fieldDef.optional mark the
schema as nullable or include null in the type) and use that wrapped schema for
filterProps entries such as 'equals', 'not', 'in', 'notIn' (while keeping
element-level schemas for list operators like 'has'/'hasEvery'/'hasSome' if
intended); update references to this.typeToSchema, fieldDef.array, and
fieldDef.optional inside buildFieldFilterSchema to produce and use
baseSchemaWithWrappers instead of the bare baseSchema.

…n OpenAPI

Extract model description from @@meta("description", "...") attribute and
apply it to the generated schema object in both REST and RPC OpenAPI generators.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@claude
Copy link
Contributor

claude bot commented Mar 19, 2026

Claude encountered an error —— View job


I'll analyze this and get back to you.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (7)
packages/server/src/api/rpc/openapi.ts (7)

482-497: ⚠️ Potential issue | 🔴 Critical

Recursive WhereInput refs never resolve.

generateSchemas() only registers ${modelName}WhereInput, but AND/OR/NOT point at #/components/schemas/_${modelName}WhereInput. Every recursive filter schema will contain dangling refs.

Suggested fix
-                { $ref: `#/components/schemas/_${_modelName}WhereInput` },
-                { type: 'array', items: { $ref: `#/components/schemas/_${_modelName}WhereInput` } },
+                { $ref: `#/components/schemas/${_modelName}WhereInput` },
+                { type: 'array', items: { $ref: `#/components/schemas/${_modelName}WhereInput` } },
...
-            items: { $ref: `#/components/schemas/_${_modelName}WhereInput` },
+            items: { $ref: `#/components/schemas/${_modelName}WhereInput` },
...
-                { $ref: `#/components/schemas/_${_modelName}WhereInput` },
-                { type: 'array', items: { $ref: `#/components/schemas/_${_modelName}WhereInput` } },
+                { $ref: `#/components/schemas/${_modelName}WhereInput` },
+                { type: 'array', items: { $ref: `#/components/schemas/${_modelName}WhereInput` } },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/server/src/api/rpc/openapi.ts` around lines 482 - 497, The recursive
WhereInput refs are pointing to `#/components/schemas/_`${modelName}WhereInput
while generateSchemas() registers ${modelName}WhereInput, leaving dangling refs;
update the schema registration in generateSchemas() to also register the
underscored name (register a schema under _${modelName}WhereInput) or change the
AND/OR/NOT refs in the block that sets properties['AND'], properties['OR'],
properties['NOT'] to point to `#/components/schemas/`${modelName}WhereInput so the
references match—locate generateSchemas() and the properties['AND'|'OR'|'NOT']
lines to make the names consistent.

75-98: ⚠️ Potential issue | 🔴 Critical

createManyAndReturn and updateManyAndReturn reference request components that are never generated.

opToArgsSchema() resolves these ops to ${modelName}CreateManyAndReturnArgs and ${modelName}UpdateManyAndReturnArgs, but generateSchemas() never registers either component. The resulting OpenAPI will contain dangling request-body refs for both endpoints unless these ops are mapped to existing args schemas or dedicated schemas are emitted.

Also applies to: 155-157, 350-354


127-130: ⚠️ Potential issue | 🔴 Critical

Response-level $ref is pointing at the wrong component type.

These 400 entries sit inside a Responses Object, so a $ref here has to resolve to a Response Object, not #/components/schemas/_ErrorResponse. Validators and codegen will reject the document until this is wrapped as a real response or moved under components.responses.

Suggested fix pattern
- '400': { $ref: '#/components/schemas/_ErrorResponse' },
+ '400': {
+     description: 'Bad request',
+     content: {
+         'application/json': {
+             schema: { $ref: '#/components/schemas/_ErrorResponse' },
+         },
+     },
+ },
In OpenAPI 3.1, when a response entry inside `responses` uses `$ref`, can it target `#/components/schemas/...`, or must it reference a Response Object under `#/components/responses/...`?

Also applies to: 172-182, 204-214, 236-246, 268-278, 287-290

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

In `@packages/server/src/api/rpc/openapi.ts` around lines 127 - 130, The responses
objects currently use a $ref that points to a schema (e.g. '400': { $ref:
'#/components/schemas/_ErrorResponse' }) which is invalid because response-level
$ref must resolve to a Response Object; update each offending entry (the '400'
response refs and the other similar entries around the other occurrences) to
either reference a Response Object under `#/components/responses` (e.g.
'#/components/responses/_ErrorResponse') or replace the $ref with an inline
Response Object that includes description and content referencing the schema
(e.g. description plus content: application/json with schema: { $ref:
'#/components/schemas/_ErrorResponse' }); ensure you apply this change for every
responses block that currently points directly at the schema.

414-416: ⚠️ Potential issue | 🟠 Major

CreateInput and UpdateInput drop nullable and list field shape.

These builders go straight through typeToSchema(fieldDef.type), so nullable columns are emitted as non-null and list columns are emitted as single values. Line 415 also skips required scalar-list fields because of !fieldDef.array.

Also applies to: 436-436

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

In `@packages/server/src/api/rpc/openapi.ts` around lines 414 - 416,
CreateInput/UpdateInput builders currently call typeToSchema(fieldDef.type)
directly, which loses nullable and array shapes and skips required array fields;
update the property generation so that if fieldDef.array is true you wrap the
element schema into an array schema (type: "array", items:
typeToSchema(fieldDef.type)), and if fieldDef.nullable is true you mark the
schema as nullable or allow null (e.g. set schema.nullable = true or use anyOf
with {type:null}); keep the required push logic independent of fieldDef.array so
non-optional array fields are included in required, and continue to respect
fieldDef.default and fieldDef.optional when deciding required. Ensure these
changes are applied where CreateInput/UpdateInput properties are produced and
where typeToSchema is currently called.

64-99: ⚠️ Potential issue | 🔴 Critical

Use operation-specific response schemas.

All CRUD builders still hardcode ${modelName}Response, whose data field is a single-model ref. That is wrong for findMany, createManyAndReturn, updateManyAndReturn, count, exists, aggregate, groupBy, and the batch ops, so the emitted spec will generate incorrect client types. Plumb a response schema name per op and generate the matching response components instead of reusing the single-model wrapper everywhere.

Also applies to: 172-178, 205-210, 237-242, 269-274, 672-678

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

In `@packages/server/src/api/rpc/openapi.ts` around lines 64 - 99, The code always
uses a single `${modelName}Response` wrapper which yields incorrect types for
multi-item, count, exists, aggregate, groupBy and batch-returning ops; compute
an operation-specific response schema name (e.g., via a new helper like
opToResponseSchema(op, modelName) or a map for ops -> schema names) and pass
that responseSchemaName into the OpenAPI builders instead of relying on
`${modelName}Response`. Concretely, update the loop callers (the GET loop and
the writeOps loop) to compute responseSchemaName and call
this.buildGetOperation(this, ...) / this.buildPostOperation /
this.buildPatchOperation / this.buildDeleteOperation with the extra
responseSchemaName argument, then modify those builder methods
(buildGetOperation, buildPostOperation, buildPatchOperation,
buildDeleteOperation) to accept and use the provided responseSchemaName to
reference and emit correct response components for
findMany/createManyAndReturn/updateManyAndReturn/count/exists/aggregate/groupBy
and batch ops. Ensure the response component generator creates
list/number/boolean/aggregate/grouped shapes as appropriate for each op.

381-386: ⚠️ Potential issue | 🟠 Major

Don't require relation fields in the shared model schema.

select/include make relation payloads conditional, but Line 385 still marks every non-optional relation as required. That makes ordinary RPC responses invalid unless the caller explicitly included the relation.

Suggested fix
-            if (!fieldDef.optional && !fieldDef.array) {
+            if (!fieldDef.optional && !fieldDef.relation) {
                 required.push(fieldName);
             }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/server/src/api/rpc/openapi.ts` around lines 381 - 386, The loop over
modelDef.fields is currently marking every non-optional, non-array field as
required; change the condition that pushes into required (in the block using
fieldDef, modelDef.fields, fieldToSchema, and isFieldOmitted) to also skip
relation fields so relation/select/include-driven payloads are not required;
implement this by detecting relation descriptors on fieldDef (e.g., a relation
flag/property such as fieldDef.relation, fieldDef.targetModel,
fieldDef.isRelation, or other project-specific relation marker) and only push
fieldName into required when the field is non-optional, non-array, and not a
relation.

693-740: ⚠️ Potential issue | 🟠 Major

Filter operators should mirror the field's value schema.

buildFieldFilterSchema() starts from bare typeToSchema(fieldDef.type), so equality/shorthand filters on list fields accept a single element and nullable fields cannot compare against null. Keep an element schema for has/range-style operators, but use a wrapped value schema for equals/not/in/notIn and the shorthand oneOf.

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

In `@packages/server/src/api/rpc/openapi.ts` around lines 693 - 740,
buildFieldFilterSchema currently uses baseSchema everywhere so
equality/shorthand filters treat array fields as single elements and nullable
fields cannot be compared to null; fix by deriving a valueSchema that represents
the field's full value (if fieldDef.array then valueSchema = { type: 'array',
items: baseSchema } else valueSchema = baseSchema) and if fieldDef.nullable wrap
valueSchema to allow null (e.g., oneOf [valueSchema, { type: 'null' }]); then
use valueSchema for equals and not, use { type: 'array', items: valueSchema }
for in and notIn, keep element-based baseSchema for list operators
(has/hasEvery/hasSome), and return the shorthand oneOf as { oneOf: [valueSchema,
filterObject] } in buildFieldFilterSchema.
🧹 Nitpick comments (2)
packages/server/test/openapi/rest-openapi.test.ts (2)

78-79: Consider using a proper OpenAPI type for spec.

Using any loses type safety and IDE support. If an OpenAPI type is available from a package like openapi-types, it would help catch assertion errors at compile time.

+import type { OpenAPIV3_1 } from 'openapi-types';
+
 describe('REST OpenAPI spec generation', () => {
     let handler: RestApiHandler;
-    let spec: any;
+    let spec: OpenAPIV3_1.Document;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/server/test/openapi/rest-openapi.test.ts` around lines 78 - 79, The
test declares spec as type any which loses type safety; change spec's type to a
proper OpenAPI type (e.g., OpenAPIV3.Document from the openapi-types package) by
importing the type (import type { OpenAPIV3 } from 'openapi-types') and updating
the declaration (let spec: OpenAPIV3.Document) so assertions against spec in
this file and usage with RestApiHandler are type-checked by TypeScript.

191-203: Consider extracting the parameter name extraction into a helper.

The pattern listOp.parameters.map((p: any) => ('name' in p ? p.name : p.$ref)) is repeated 6 times throughout the file. Extracting it would improve readability and maintainability.

♻️ Proposed helper function

Add near the top of the file:

const getParamNames = (params: any[]) => 
    params.map((p: any) => ('name' in p ? p.name : p.$ref));

Then replace usages:

-const paramNames = listOp.parameters.map((p: any) => ('name' in p ? p.name : p.$ref));
+const paramNames = getParamNames(listOp.parameters);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/server/test/openapi/rest-openapi.test.ts` around lines 191 - 203,
The tests repeat the extraction logic listOp.parameters.map((p: any) => ('name'
in p ? p.name : p.$ref)) so add a small helper (e.g., getParamNames) and replace
direct uses with getParamNames(listOp.parameters) to improve readability;
implement the helper near the top of the test file and update each occurrence
(references: getParamNames, listOp.parameters) to call it.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/server/src/api/rpc/openapi.ts`:
- Around line 406-410: The loops that build
CreateInput/UpdateInput/WhereUniqueInput/WhereInput currently only check
fieldDef.omit and should instead use the same visibility logic as
buildModelOutputSchema; replace checks like "if (fieldDef.omit) continue" with a
call to isFieldOmitted(modelDef, fieldName, queryOptions) (or the existing
isFieldOmitted signature used by buildModelOutputSchema) so that
queryOptions.omit/slicing are honored for request and filter schemas; update the
loops that iterate modelDef.fields (the blocks constructing CreateInput,
UpdateInput, WhereUniqueInput, WhereInput) to call isFieldOmitted before adding
a field.

---

Duplicate comments:
In `@packages/server/src/api/rpc/openapi.ts`:
- Around line 482-497: The recursive WhereInput refs are pointing to
`#/components/schemas/_`${modelName}WhereInput while generateSchemas() registers
${modelName}WhereInput, leaving dangling refs; update the schema registration in
generateSchemas() to also register the underscored name (register a schema under
_${modelName}WhereInput) or change the AND/OR/NOT refs in the block that sets
properties['AND'], properties['OR'], properties['NOT'] to point to
`#/components/schemas/`${modelName}WhereInput so the references match—locate
generateSchemas() and the properties['AND'|'OR'|'NOT'] lines to make the names
consistent.
- Around line 127-130: The responses objects currently use a $ref that points to
a schema (e.g. '400': { $ref: '#/components/schemas/_ErrorResponse' }) which is
invalid because response-level $ref must resolve to a Response Object; update
each offending entry (the '400' response refs and the other similar entries
around the other occurrences) to either reference a Response Object under
`#/components/responses` (e.g. '#/components/responses/_ErrorResponse') or replace
the $ref with an inline Response Object that includes description and content
referencing the schema (e.g. description plus content: application/json with
schema: { $ref: '#/components/schemas/_ErrorResponse' }); ensure you apply this
change for every responses block that currently points directly at the schema.
- Around line 414-416: CreateInput/UpdateInput builders currently call
typeToSchema(fieldDef.type) directly, which loses nullable and array shapes and
skips required array fields; update the property generation so that if
fieldDef.array is true you wrap the element schema into an array schema (type:
"array", items: typeToSchema(fieldDef.type)), and if fieldDef.nullable is true
you mark the schema as nullable or allow null (e.g. set schema.nullable = true
or use anyOf with {type:null}); keep the required push logic independent of
fieldDef.array so non-optional array fields are included in required, and
continue to respect fieldDef.default and fieldDef.optional when deciding
required. Ensure these changes are applied where CreateInput/UpdateInput
properties are produced and where typeToSchema is currently called.
- Around line 64-99: The code always uses a single `${modelName}Response`
wrapper which yields incorrect types for multi-item, count, exists, aggregate,
groupBy and batch-returning ops; compute an operation-specific response schema
name (e.g., via a new helper like opToResponseSchema(op, modelName) or a map for
ops -> schema names) and pass that responseSchemaName into the OpenAPI builders
instead of relying on `${modelName}Response`. Concretely, update the loop
callers (the GET loop and the writeOps loop) to compute responseSchemaName and
call this.buildGetOperation(this, ...) / this.buildPostOperation /
this.buildPatchOperation / this.buildDeleteOperation with the extra
responseSchemaName argument, then modify those builder methods
(buildGetOperation, buildPostOperation, buildPatchOperation,
buildDeleteOperation) to accept and use the provided responseSchemaName to
reference and emit correct response components for
findMany/createManyAndReturn/updateManyAndReturn/count/exists/aggregate/groupBy
and batch ops. Ensure the response component generator creates
list/number/boolean/aggregate/grouped shapes as appropriate for each op.
- Around line 381-386: The loop over modelDef.fields is currently marking every
non-optional, non-array field as required; change the condition that pushes into
required (in the block using fieldDef, modelDef.fields, fieldToSchema, and
isFieldOmitted) to also skip relation fields so relation/select/include-driven
payloads are not required; implement this by detecting relation descriptors on
fieldDef (e.g., a relation flag/property such as fieldDef.relation,
fieldDef.targetModel, fieldDef.isRelation, or other project-specific relation
marker) and only push fieldName into required when the field is non-optional,
non-array, and not a relation.
- Around line 693-740: buildFieldFilterSchema currently uses baseSchema
everywhere so equality/shorthand filters treat array fields as single elements
and nullable fields cannot be compared to null; fix by deriving a valueSchema
that represents the field's full value (if fieldDef.array then valueSchema = {
type: 'array', items: baseSchema } else valueSchema = baseSchema) and if
fieldDef.nullable wrap valueSchema to allow null (e.g., oneOf [valueSchema, {
type: 'null' }]); then use valueSchema for equals and not, use { type: 'array',
items: valueSchema } for in and notIn, keep element-based baseSchema for list
operators (has/hasEvery/hasSome), and return the shorthand oneOf as { oneOf:
[valueSchema, filterObject] } in buildFieldFilterSchema.

---

Nitpick comments:
In `@packages/server/test/openapi/rest-openapi.test.ts`:
- Around line 78-79: The test declares spec as type any which loses type safety;
change spec's type to a proper OpenAPI type (e.g., OpenAPIV3.Document from the
openapi-types package) by importing the type (import type { OpenAPIV3 } from
'openapi-types') and updating the declaration (let spec: OpenAPIV3.Document) so
assertions against spec in this file and usage with RestApiHandler are
type-checked by TypeScript.
- Around line 191-203: The tests repeat the extraction logic
listOp.parameters.map((p: any) => ('name' in p ? p.name : p.$ref)) so add a
small helper (e.g., getParamNames) and replace direct uses with
getParamNames(listOp.parameters) to improve readability; implement the helper
near the top of the test file and update each occurrence (references:
getParamNames, listOp.parameters) to call it.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: e241c9a7-4616-40e5-9a80-757113f92040

📥 Commits

Reviewing files that changed from the base of the PR and between 8eea293 and 1853a04.

📒 Files selected for processing (5)
  • packages/server/src/api/common/spec-utils.ts
  • packages/server/src/api/rest/openapi.ts
  • packages/server/src/api/rpc/openapi.ts
  • packages/server/test/openapi/rest-openapi.test.ts
  • packages/server/test/openapi/rpc-openapi.test.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • packages/server/test/openapi/rpc-openapi.test.ts
  • packages/server/src/api/common/spec-utils.ts
  • packages/server/src/api/rest/openapi.ts

Comment on lines +406 to +410
for (const [fieldName, fieldDef] of Object.entries(modelDef.fields)) {
if (fieldDef.relation) continue;
if (fieldDef.foreignKeyFor) continue;
if (fieldDef.omit) continue;
if (fieldDef.updatedAt) continue;
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Apply isFieldOmitted to request and filter schemas too.

buildModelOutputSchema() honors query-time field slicing, but CreateInput, UpdateInput, WhereUniqueInput, and WhereInput only check fieldDef.omit. With queryOptions.omit/slicing, hidden fields still show up in request/filter components, so the RPC spec advertises inputs the handler is supposed to hide.

Also applies to: 430-434, 445-460, 473-476

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

In `@packages/server/src/api/rpc/openapi.ts` around lines 406 - 410, The loops
that build CreateInput/UpdateInput/WhereUniqueInput/WhereInput currently only
check fieldDef.omit and should instead use the same visibility logic as
buildModelOutputSchema; replace checks like "if (fieldDef.omit) continue" with a
call to isFieldOmitted(modelDef, fieldName, queryOptions) (or the existing
isFieldOmitted signature used by buildModelOutputSchema) so that
queryOptions.omit/slicing are honored for request and filter schemas; update the
loops that iterate modelDef.fields (the blocks constructing CreateInput,
UpdateInput, WhereUniqueInput, WhereInput) to call isFieldOmitted before adding
a field.

@claude
Copy link
Contributor

claude bot commented Mar 19, 2026

Claude finished @ymc9's task —— View job


Code Review for OpenAPI Spec Generation PR

I've conducted a comprehensive review of this PR that adds OpenAPI spec generation with filter kind slicing support. Here's my detailed feedback:

Review Tasks:

  • Analyze architectural changes and new class structure
  • Review filter kind slicing implementation
  • Examine test coverage and quality
  • Check for potential bugs and edge cases
  • Review security considerations
  • Assess performance implications
  • Verify OpenAPI specification compliance

Major Strengths

Excellent Architecture & Design

  • Clean separation of concerns: Extracting OpenAPI spec generation into dedicated RestApiSpecGenerator and RPCApiSpecGenerator classes is a solid architectural decision
  • Shared utilities: The spec-utils.ts module provides excellent reusable functions for inclusion/exclusion logic
  • Interface-based design: OpenApiSpecGenerator interface enables extensibility and proper testing
  • Proper Zod validation: Good input validation for handler options

Robust Filter Kind Slicing Implementation

  • Well-designed inclusion/exclusion logic: isFilterKindIncluded function properly handles both includedFilterKinds and excludedFilterKinds with correct precedence
  • Proper fallback hierarchy: Model-specific → $all model → global defaults works as expected
  • Comprehensive filter coverage: Supports all four filter kinds (Equality, Like, Range, List) with appropriate type constraints

Strong Test Coverage

  • Comprehensive test suite: 4 test files with 5 baseline YAML fixtures
  • Migration validation: Proper migration from zenstack-v2 with baseline comparisons
  • Good test scenarios: Tests cover both REST and RPC approaches with various configurations

⚠️ Critical Security Issues

🔴 CRITICAL: Stack Trace Exposure

Location: packages/server/src/api/rest/index.ts:511

private handleGenericError(err: unknown): Response | PromiseLike<Response> {
    return this.makeError('unknownError', err instanceof Error ? `${err.message}\n${err.stack}` : 'Unknown error');
}

Risk: This exposes full stack traces in 500 responses, leaking internal server paths, code structure, and potentially sensitive information to clients.

Impact: High - Information disclosure vulnerability

Fix Required: Remove stack traces from responses, log them internally only.


🔴 Critical OpenAPI Specification Violations

1. Invalid Response References in RPC Generator

Location: packages/server/src/api/rpc/openapi.ts:181, 213, 245, 277, 289

'400': { $ref: '#/components/schemas/_ErrorResponse' }

Issue: Per OpenAPI 3.1 spec, response-level $ref must target components/responses, not components/schemas. Response Objects also require a description field.

2. Broken Self-References in WhereInput Schemas

Location: packages/server/src/api/rpc/openapi.ts:485-496

properties['AND'] = {
    oneOf: [
        { $ref: `#/components/schemas/_${_modelName}WhereInput` },  // ❌ Wrong name

Issue: References _${modelName}WhereInput but generateSchemas() creates ${modelName}WhereInput, causing dangling references.

3. Missing Schema Definitions

Location: packages/server/src/api/rpc/openapi.ts:78, 82
Issue: createManyAndReturn and updateManyAndReturn operations reference ${modelName}CreateManyAndReturnArgs / ${modelName}UpdateManyAndReturnArgs but these schemas are never generated in generateSchemas().

4. Wrong Response Schema Types

Location: packages/server/src/api/rpc/openapi.ts:177
Issue: All operations use ${modelName}Response schema, but operations like findMany, count, aggregate return arrays/batch objects, not single models. This will cause client deserialization failures.


🟠 Major Logic Issues

1. Filter Kind Slicing Logic Bug

Location: packages/server/src/api/common/spec-utils.ts:29, 85

const modelSlicing = (slicing.models as Record<string, any>)[modelKey] ?? (slicing.models as any).$all;

Issue: Uses ?? operator which completely replaces $all rules when model-specific config exists. Global $all rules should be merged with model-specific overrides, not replaced.

Impact: Users lose global configuration when any model-specific settings are applied.

2. REST Generator Ignores Operation Filtering

Location: packages/server/src/api/rest/openapi.ts:91-94
Issue: Collection and single-resource paths are generated unconditionally, ignoring excludedOperations/includedOperations from queryOptions.slicing.

3. Inconsistent Field Slicing in Request Schemas

Location: packages/server/src/api/rest/openapi.ts:704, 769
Issue: buildCreateRequestSchema and buildUpdateRequestSchema check fieldDef.omit but never call isFieldOmitted(), so fields excluded via queryOptions still appear in request schemas.


🟡 Minor Issues

1. Missing Documentation

  • No JSDoc for complex filter kind logic in spec generators
  • Missing exports in main barrel module for concrete generator classes

2. Type Safety Improvements

  • Filter kind strings are magic strings - could benefit from const assertions
  • Some test files use any types that could be more specific

3. Performance Considerations

  • Repeated model lookups in OpenAPI generation could be cached
  • String operations in filter generation loops could benefit from memoization

🎯 Performance Assessment

Good:

  • Proper caching of serializers and model metadata
  • Efficient schema generation with reusable components

Areas for Optimization:

  • Model lookups are performed repeatedly during spec generation
  • Filter parameter generation could be memoized for repeated calls

🔒 Other Security Considerations

Input Validation:

  • ✅ Good Zod validation for handler options
  • ⚠️ queryOptions validation could be more specific (currently z.object().optional())

Type Safety:

  • ⚠️ Filter kind strings are magic strings - could benefit from const assertions
  • ✅ Good use of TypeScript for schema definitions

📋 Recommendations

Must Fix (Critical):

  1. Remove stack traces from error responses (security risk)
  2. Fix OpenAPI spec violations - wrong response references, missing schemas, broken refs
  3. Implement operation filtering in REST generator
  4. Fix filter kind slicing logic to properly merge $all with model-specific rules

Should Fix (Major):

  1. Apply isFieldOmitted() consistently in request schema builders
  2. Add missing response schema variants for different operation types
  3. Improve input validation specificity

Nice to Have:

  1. Add JSDoc documentation for complex logic
  2. Export concrete generator classes from main module
  3. Improve type safety with const assertions for filter kinds

Overall Assessment

This is a significant and valuable addition to the codebase with excellent architectural decisions. The filter kind slicing feature is well-designed and the test coverage is comprehensive. However, several critical issues must be addressed before merging:

  • Security: Stack trace exposure is a significant risk
  • Specification Compliance: Multiple OpenAPI violations will break client generation
  • Logic Correctness: Filter kind slicing and operation filtering bugs affect core functionality

Recommended Action: Address critical and major issues before merge. The foundation is solid but needs these important fixes.


Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant