Skip to content

Conversation

@ymc9
Copy link
Member

@ymc9 ymc9 commented Oct 24, 2025

Summary by CodeRabbit

  • Breaking Changes

    • Removed check() ORM API
    • Policy rejection now surfaces as NotFoundError
    • Non-optional to-one relations no longer auto-filter parent reads during policy evaluation
  • New Features

    • New server package with RPC API handler and Express middleware for CRUD/aggregation
    • Safe JSON stringify utility that preserves BigInt
  • Bug Fixes

    • Validation and error messages now include model names
    • Defaults added for foreign-key delete/update actions
    • Improved handling of hex-encoded PostgreSQL bytea
  • Chores

    • Dependency and packaging updates; new tests and configs added

Copilot AI review requested due to automatic review settings October 24, 2025 01:20
@coderabbitai
Copy link

coderabbitai bot commented Oct 24, 2025

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

Walkthrough

New @zenstackhq/server package added (RPC handler, Express middleware, types, utilities, tests). Runtime and tooling changes: safe JSON stringify utility added/removed in places, Postgres bytea hex handling, FK default actions, error constructors now include model, policy error construction updated, decimal.js moved to catalog, and BREAKINGCHANGES updated (check() removed).

Changes

Cohort / File(s) Summary
Breaking Changes & Docs
BREAKINGCHANGES.md
Removed check() ORM API entry; reordered items and added the check() removal note.
Project TODOs
TODO.md
Marked "Benchmark" task completed.
Common Helpers
packages/common-helpers/src/index.ts, packages/common-helpers/src/safe-json-stringify.ts
Added and re-exported safeJSONStringify(value: unknown); handles bigint by converting to strings during JSON.stringify.
Runtime Error Types & Validation
packages/runtime/src/client/errors.ts, packages/runtime/src/client/crud/validator/index.ts
InputValidationError and NotFoundError constructors now include a model parameter; validator errors include model in messages.
Policy Handler
packages/plugins/policy/src/policy-handler.ts
RejectedByPolicyError now constructed with RejectedByPolicyReason.NO_ACCESS for pre- and post-mutation rejections.
Postgres Bytes & Query Utils
packages/runtime/src/client/crud/dialects/postgresql.ts, packages/runtime/src/client/query-utils.ts
transformOutputBytes handles hex-encoded \x... bytea strings -> Uint8Array; removed safeJSONStringify export from query-utils.
Schema DB Pusher (FK defaults)
packages/runtime/src/client/helpers/schema-db-pusher.ts
Default FK actions when unspecified: onDelete'set null' if optional else 'restrict'; onUpdate'cascade'.
Dependency Catalog Migration
packages/runtime/package.json, packages/sdk/package.json, tests/e2e/package.json, tests/regression/package.json, pnpm-workspace.yaml
Migrated decimal.js dependency spec from ^10.4.3 to catalog: in manifests and workspace catalog.
Server Package Manifests & Configs
packages/server/package.json, packages/server/tsconfig.json, packages/server/tsup.config.ts, packages/server/vitest.config.ts, packages/server/eslint.config.js
Added @zenstackhq/server package manifest and TS/build/test/ESLint configs with exports for /api and /express.
Server Types & Utilities
packages/server/src/types.ts, packages/server/src/api/utils.ts
Added LogLevel, Logger, LogConfig, RequestContext, ApiHandler types; added log() and registerCustomSerializers() (SuperJSON Decimal and optional Buffer).
Server RPC API Handler
packages/server/src/api/index.ts, packages/server/src/api/rpc/index.ts
Implemented RPCApiHandler with routing, SuperJSON (de)serialization, standardized error→HTTP mappings, and logging.
Server Express Adapter
packages/server/src/express/index.ts, packages/server/src/express/middleware.ts
Added ZenStackMiddleware factory with configurable getClient, optional sendResponse, and integration with ApiHandler.
Server Tests & Utilities
packages/server/test/utils.ts, packages/server/test/api/rpc.test.ts, packages/server/test/adapter/express.test.ts
Added test schema and URL helper; comprehensive RPC handler tests and Express adapter E2E tests covering CRUD, filters, aggregation, groupBy, pagination, ordering, serialization, and policy scenarios.
Language Package Scripts
packages/language/package.json
Added test script: "vitest run".
Testtools
packages/testtools/src/client.ts
getTestDbName uses a nullish fallback for test name (?? 'unnamed') and removes invariant assertion.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

🐰
I hopped through code with tiny paws,
Routed RPCs and stitched the laws,
Bigints now wear stringy coats so bright,
Bytea hex turns bytes to light,
Middleware hums — the server's right!

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The PR title "feat(server): migrate rpc api handler and express adapter" accurately reflects the primary objective of this changeset. The bulk of the changes introduce a new server package with comprehensive implementations of an RPC API handler (RPCApiHandler class) and Express middleware (ZenStackMiddleware), along with supporting infrastructure like type definitions, utilities, and test suites. While the PR also includes secondary changes such as utility reorganization (safeJSONStringify), error handling enhancements, and dependency management updates, the title appropriately emphasizes the main feature being delivered. The title is specific, clear, and concise without vague terminology or noise.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/rpc-adapter

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

claude bot commented Oct 24, 2025

Claude finished @ymc9's task —— View job


Code Review: RPC API Handler and Express Adapter Migration

I've completed a comprehensive review of this PR that introduces the new @zenstackhq/server package. Here's my detailed feedback:

🟢 Strengths

Architecture & Design:

  • Clean separation of concerns with framework-agnostic ApiHandler interface
  • Well-structured package with proper TypeScript exports and peer dependency configuration
  • Comprehensive test coverage with both unit tests (529 lines) and integration tests
  • Good use of SuperJSON for handling complex data types (BigInt, Decimal, Buffer, Date)

Code Quality:

  • Proper error handling with specific error types (NotFoundError, InputValidationError, RejectedByPolicyError)
  • HTTP status codes correctly mapped to error types (403 for policy violations, 422 for validation errors)
  • Type safety maintained throughout with generic schema constraints
  • Good logging system with configurable levels

🟡 Security Considerations

Input Validation:

  • ✅ JSON parsing is properly wrapped in try-catch blocks (packages/server/src/api/rpc/index.ts:221-224)
  • ✅ Path validation prevents invalid routes (packages/server/src/api/rpc/index.ts:39-41)
  • ✅ Method validation enforces correct HTTP methods per operation
  • ✅ Model name validation against schema prevents arbitrary model access (packages/server/src/api/rpc/index.ts:157-159)

Potential Security Issues:

  • ⚠️ Dynamic client method invocation: Line 128 uses (client as any)[model][op](processedArgs) which could be risky if model/operation validation fails
  • ⚠️ Query parameter handling: The Express middleware reconstructs query parameters from URL, but this could potentially be exploited with malformed URLs
  • ⚠️ Error information leakage: Error responses include detailed validation messages that might expose internal structure

🔴 Code Issues & Bugs

RPC Handler (packages/server/src/api/rpc/index.ts):

  1. Case sensitivity inconsistency (Line 158):
return Object.keys(client.$schema.models).some((m) => m.toLowerCase() === model.toLowerCase());

Model comparison is case-insensitive, but the actual client call uses the exact path segment. This could cause runtime errors.

  1. Generic error handling too broad (Line 151-153):
} else {
    return this.makeGenericErrorResponse(err);
}

All non-ZenStack errors return 400 status, but some might warrant 500 (internal server errors).

Express Middleware (packages/server/src/express/middleware.ts):

  1. Inconsistent error handling (Lines 39-45):
if (sendResponse === false && !client) {
    throw new Error('unable to get ZenStackClient from request context');
}

if (!client) {
    return response.status(500).json({ message: 'unable to get ZenStackClient from request context' });
}

Same error condition handled differently based on sendResponse flag.

  1. URL reconstruction vulnerability (Line 50):
const url = request.protocol + '://' + request.get('host') + request.originalUrl;

Uses user-supplied Host header without validation, potential for Host header injection.

🔍 Performance Considerations

Positive:

  • SuperJSON serialization only applied when metadata is present
  • Efficient model validation using key existence check
  • Lazy evaluation of log messages with function callbacks

Potential Improvements:

  • URL reconstruction in Express middleware happens on every request
  • JSON parsing/stringifying happens multiple times per request for complex data types
  • No request size limits implemented at handler level

📋 Test Coverage Assessment

Excellent coverage includes:

  • ✅ All CRUD operations (create, read, update, delete)
  • ✅ Complex data types (BigInt, Decimal, Buffer, Date)
  • ✅ Policy violations and validation errors
  • ✅ Pagination and ordering
  • ✅ SuperJSON serialization/deserialization
  • ✅ Express middleware integration

Missing test cases:

  • ❌ Host header injection scenarios
  • ❌ Large payload handling
  • ❌ Concurrent request handling
  • ❌ Error boundary testing for malformed SuperJSON metadata

📝 Recommendations

  1. Fix case sensitivity bug in model validation
  2. Implement Host header validation in Express middleware
  3. Add request size limits configuration
  4. Enhance error classification (distinguish between 400/500 errors)
  5. Add input sanitization tests for edge cases
  6. Consider rate limiting support for production deployments

🎯 Overall Assessment

This is a well-architected and thoroughly tested implementation that successfully migrates RPC functionality to a dedicated server package. The code demonstrates strong TypeScript practices, comprehensive error handling, and good separation of concerns. The main concerns are around security hardening and some edge case handling that should be addressed before production use.

Recommendation: Approve with minor fixes for the identified security and bug issues.

--- • Branch: feat/rpc-adapter

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull Request Overview

This PR migrates the RPC API handler and Express adapter to a new @zenstackhq/server package. The migration standardizes dependency management by moving decimal.js to the workspace catalog, implements a framework-agnostic API handler with Express middleware support, and consolidates helper utilities.

Key Changes:

  • Created new @zenstackhq/server package with RPC API handler and Express middleware
  • Centralized decimal.js dependency to workspace catalog
  • Moved safeJSONStringify utility to @zenstackhq/common-helpers package
  • Enhanced error handling with model context in validation and policy errors

Reviewed Changes

Copilot reviewed 29 out of 30 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
packages/server/package.json New package configuration with peer dependencies for Express
packages/server/src/api/rpc/index.ts RPC-style API handler implementing framework-agnostic request handling
packages/server/src/express/middleware.ts Express adapter middleware for the API handler
packages/server/test/api/rpc.test.ts Comprehensive test suite for RPC handler functionality
packages/server/test/adapter/express.test.ts Integration tests for Express middleware
pnpm-workspace.yaml Added decimal.js to catalog for centralized version management
packages/runtime/src/client/errors.ts Enhanced error classes with model context
packages/common-helpers/src/safe-json-stringify.ts Extracted JSON stringify utility for reuse
Files not reviewed (1)
  • pnpm-lock.yaml: Language not supported

Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

@claude
Copy link

claude bot commented Oct 24, 2025

Claude finished @ymc9's task —— View job


Code Review: RPC API Handler and Express Adapter Migration

Todo List:

  • Analyze new server package architecture and design
  • Review security implications of RPC handler and Express middleware
  • Assess test coverage and identify potential gaps
  • Check for bugs, performance issues, and best practices
  • Provide comprehensive feedback and recommendations

🟢 Strengths

Architecture & Design:

  • Clean separation of concerns with framework-agnostic ApiHandler interface
  • Well-structured package with proper TypeScript exports and peer dependency configuration
  • Comprehensive test coverage with both unit tests (529 lines) and integration tests
  • Good use of SuperJSON for handling complex data types (BigInt, Decimal, Buffer, Date)

Code Quality:

  • Proper error handling with specific error types (NotFoundError, InputValidationError, RejectedByPolicyError)
  • HTTP status codes correctly mapped to error types (403 for policy violations, 422 for validation errors)
  • Type safety maintained throughout with generic schema constraints
  • Good logging system with configurable levels

🟡 Security Considerations

Input Validation (✅ Good):

  • JSON parsing properly wrapped in try-catch blocks (packages/server/src/api/rpc/index.ts:221-224)
  • Path validation prevents invalid routes (packages/server/src/api/rpc/index.ts:39-41)
  • Method validation enforces correct HTTP methods per operation
  • Model name validation against schema (packages/server/src/api/rpc/index.ts:157-159)

Security Concerns (⚠️ Needs Attention):

  • Case sensitivity bug: Model comparison is case-insensitive but client call uses exact path segment (packages/server/src/api/rpc/index.ts:158 vs line 128)
  • Dynamic method invocation: (client as any)[model][op](processedArgs) at line 128 could be risky if validation fails
  • Host header injection: Express middleware uses unvalidated Host header for URL reconstruction (packages/server/src/express/middleware.ts:50)
  • Error information leakage: Detailed validation messages might expose internal structure

🔴 Critical Issues

1. Case Sensitivity Bug (packages/server/src/api/rpc/index.ts:158)

return Object.keys(client.$schema.models).some((m) => m.toLowerCase() === model.toLowerCase());

Model validation is case-insensitive, but the actual client call uses the exact path segment. This could cause runtime errors.

2. Inconsistent Error Handling (packages/server/src/express/middleware.ts:39-45)

if (sendResponse === false && !client) {
    throw new Error('unable to get ZenStackClient from request context');
}
if (!client) {
    return response.status(500).json({ message: 'unable to get ZenStackClient from request context' });
}

Same error condition handled differently based on sendResponse flag.

3. Host Header Vulnerability (packages/server/src/express/middleware.ts:50)

const url = request.protocol + '://' + request.get('host') + request.originalUrl;

Uses user-supplied Host header without validation - potential for Host header injection.

🟢 Test Coverage Assessment

Excellent Coverage:

  • All CRUD operations, complex data types, policy violations, validation errors
  • Pagination, ordering, aggregation, Express middleware integration
  • Edge cases: invalid paths, malformed JSON

Minor Gaps:

  • Host header injection scenarios, large payload handling, concurrent requests

📝 Recommendations

  1. Fix case sensitivity bug by using exact model name matching
  2. Implement Host header validation in Express middleware
  3. Add request size limits configuration
  4. Enhance error classification (distinguish between 400/500 errors)
  5. Add security-focused tests for edge cases

🎯 Overall Assessment

This is a well-architected and thoroughly tested implementation that successfully migrates RPC functionality to a dedicated server package. The code demonstrates strong TypeScript practices and comprehensive error handling. The main concerns are around security hardening that should be addressed before production use.

Recommendation: Approve with minor fixes for the identified security and bug issues.

--- • Branch: feat/rpc-adapter

@claude
Copy link

claude bot commented Oct 24, 2025

Claude finished @ymc9's task —— View job


Code Review: RPC API Handler and Express Adapter Migration

Todo List:

  • Analyze new server package architecture and design
  • Review security implications of RPC handler and Express middleware
  • Assess test coverage and identify potential gaps
  • Check for bugs, performance issues, and best practices
  • Provide comprehensive feedback and recommendations

🟢 Architecture & Design Strengths

Clean Framework-Agnostic Design:

  • Excellent separation with ApiHandler interface allowing multiple framework implementations
  • Well-structured package exports (./api and ./express) with proper TypeScript support
  • Good use of SuperJSON for complex data type serialization (BigInt, Decimal, Buffer, Date)
  • Comprehensive error handling with specific error types mapped to appropriate HTTP status codes

Package Structure:

  • Proper peer dependency configuration for Express (optional via peerDependenciesMeta)
  • Clean TypeScript configuration with both ESM and CommonJS support
  • Well-organized test structure with comprehensive coverage (527 lines RPC tests, 176 lines Express tests)

🔴 Critical Issues Found

1. Package Export Configuration Bug (packages/server/package.json:26-50)

The package exports reference non-existent files. The tsup.config.ts only builds api and express entry points, but the exports point to ./dist/index.d.ts and ./dist/index.js which don't exist.

Current exports are correct - they properly point to api.js and express.js which match the tsup config. The Copilot review comment appears to be incorrect here.

2. Case Sensitivity Security Issue (packages/server/src/api/rpc/index.ts:158)

return Object.keys(client.$schema.models).some((m) => m.toLowerCase() === model.toLowerCase());

Model validation uses case-insensitive comparison, but the actual client call at line 128 uses the exact path segment:

const clientResult = await (client as any)[model][op](processedArgs);

This could cause runtime errors if a user provides a differently-cased model name that passes validation but fails at execution.

3. Host Header Injection Vulnerability (packages/server/src/express/middleware.ts:50)

const url = request.protocol + '://' + request.get('host') + request.originalUrl;

Uses user-supplied Host header without validation, creating potential for Host header injection attacks.

4. Inconsistent Error Handling (packages/server/src/express/middleware.ts:39-45)

Same error condition handled differently based on sendResponse flag:

if (sendResponse === false && !client) {
    throw new Error('unable to get ZenStackClient from request context');
}
if (!client) {
    return response.status(500).json({ message: 'unable to get ZenStackClient from request context' });
}

🟡 Security Considerations

Input Validation - Good Coverage:

  • ✅ JSON parsing properly wrapped in try-catch blocks (rpc/index.ts:221-224)
  • ✅ Path validation prevents invalid routes (rpc/index.ts:39-41)
  • ✅ Method validation enforces correct HTTP methods per operation
  • ✅ Model name validation against schema prevents arbitrary model access

Potential Security Concerns:

  • ⚠️ Dynamic method invocation: Line 128 uses (client as any)[model][op](processedArgs) - relies on validation not being bypassed
  • ⚠️ Error information disclosure: Detailed validation error messages could expose internal schema structure
  • ⚠️ No request size limits: Missing protection against large payload attacks

🔍 Performance & Code Quality

Performance Positives:

  • SuperJSON serialization only applied when metadata is present
  • Efficient model validation using key existence check
  • Lazy evaluation of log messages with function callbacks

Potential Improvements:

  • URL reconstruction in Express middleware happens on every request
  • JSON parsing/stringifying occurs multiple times for complex data types
  • Generic error handling too broad (line 151-153) - all non-ZenStack errors return 400 status

Code Quality:

  • Good TypeScript practices with proper type safety
  • Comprehensive logging system with configurable levels
  • Clean separation of concerns between handler and middleware

📋 Test Coverage Assessment

Excellent Coverage Includes:

  • ✅ All CRUD operations (create, read, update, delete, upsert)
  • ✅ Complex data types (BigInt, Decimal, Buffer, Date) via SuperJSON
  • ✅ Policy violations (403) and validation errors (422)
  • ✅ Pagination, ordering, and query operations
  • ✅ Express middleware integration and error handling
  • ✅ Various HTTP methods and error scenarios

Missing Edge Cases:

  • ❌ Host header injection scenarios
  • ❌ Large payload handling
  • ❌ Malformed SuperJSON metadata handling
  • ❌ Case sensitivity edge cases in model names

📝 Recommendations

High Priority Fixes:

  1. Fix case sensitivity bug - Make model validation consistent with execution
  2. Implement Host header validation in Express middleware
  3. Enhance error classification - distinguish between 400/500 errors properly

Security Hardening:

  1. Add request size limits configuration option
  2. Consider input sanitization for edge cases
  3. Add security-focused test cases for injection scenarios

Performance & Reliability:

  1. Cache URL reconstruction results where possible
  2. Add retry/timeout configurations for production deployments
  3. Implement structured error codes for better client error handling

🎯 Overall Assessment

This is a well-architected and thoroughly tested implementation that successfully migrates RPC functionality to a dedicated server package. The code demonstrates strong TypeScript practices, comprehensive error handling, and good separation of concerns.

Main Strengths:

  • Clean framework-agnostic design with proper abstractions
  • Comprehensive test coverage (703 total test lines)
  • Good security foundations with input validation
  • Proper error handling with appropriate HTTP status codes

Primary Concerns:

  • Case sensitivity bug could cause runtime failures
  • Host header injection vulnerability needs addressing
  • Some inconsistent error handling patterns

Recommendation: Approve with minor security fixes for the identified case sensitivity and Host header issues. The architecture is solid and the implementation is production-ready after addressing these specific concerns.

--- • Branch: feat/rpc-adapter

Copy link

@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

🧹 Nitpick comments (12)
TODO.md (1)

89-89: Address markdown linting issue: adjust list indentation.

The static analysis tool flagged an indentation inconsistency (MD007). The file uses 4-space indentation for nested list items, but the linter expects 2-space indentation. Consider aligning with the linter's expectations for consistency across the codebase.

- [ ] ORM
-    - [x] generate
+    - [x] generate

(Apply this pattern throughout nested lists in the file.)

packages/server/src/api/utils.ts (1)

6-24: LGTM! Consider enhancing error formatting.

The log function implementation is solid with good support for lazy message evaluation and multiple logger types.

One minor suggestion: Line 22 converts the error to a string using template literal interpolation (${error}), which may not provide detailed stack traces. Consider using error instanceof Error ? error.stack : String(error) for more informative error logging.

Apply this diff if you want to include stack traces:

-        logFn(`@zenstackhq/server: [${level}] ${getMessage()}${error ? `\n${error}` : ''}`);
+        logFn(`@zenstackhq/server: [${level}] ${getMessage()}${error ? `\n${error instanceof Error ? error.stack : String(error)}` : ''}`);
packages/common-helpers/src/safe-json-stringify.ts (1)

1-12: LGTM! Function correctly handles bigint serialization.

The implementation properly converts bigint values to strings during JSON stringification. The JSDoc comment accurately describes the specific handling of bigint values.

Optional: If you plan to add more safety features in the future (e.g., circular reference detection, other special type handling), consider renaming to bigIntSafeJSONStringify to be more explicit about its current scope, or keep the current name if you intend to expand its capabilities.

packages/server/vitest.config.ts (1)

1-4: Merge is redundant; export base directly.

No local overrides. Simplify to reduce indirection.

-import base from '@zenstackhq/vitest-config/base';
-import { defineConfig, mergeConfig } from 'vitest/config';
-
-export default mergeConfig(base, defineConfig({}));
+import base from '@zenstackhq/vitest-config/base';
+export default base;
packages/server/package.json (1)

1-64: Minor package polish.

Optional tweaks for DX and safety.

  • Add engines: { "node": ">=18" }.
  • Consider "sideEffects": false for better tree-shaking.
  • Add top-level "types": "./dist/index.d.ts" for editor tooling.
packages/server/src/types.ts (2)

49-62: Avoid naming collision with global Response.

Rename to reduce confusion with DOM/Fetch Response.

-export type Response = {
+export type ApiResponse = {
     status: number;
     body: unknown;
 };

And update usages accordingly (e.g., ApiHandler.handleRequest return type).


28-41: Narrow method and harden query typing.

Small ergonomics improvement.

-    method: string;
+    method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
-    query?: Record<string, string | string[]>;
+    query?: Readonly<Record<string, string | string[]>>;
packages/server/test/adapter/express.test.ts (1)

10-92: Add tests for SuperJSON GET path and error cases.

Increase confidence in RPC handler.

  • SuperJSON: use makeUrl with useSuperJson=true and include a Date in filter to verify meta round-trip.
  • Error handling: assert 400 for method mismatch (e.g., POST /findMany) and unknown model path.
    Example snippet:
// superjson GET with Date
const date = new Date();
let r = await request(app).get(makeUrl('/api/post/findMany', { where: { createdAt: { gte: date } } }, true));
expect(r.status).toBe(200);

// method mismatch
r = await request(app).post('/api/post/findMany');
expect(r.status).toBe(400);

// unknown model
r = await request(app).get(makeUrl('/api/unknown/findMany'));
expect(r.status).toBe(400);
packages/server/src/api/rpc/index.ts (4)

84-86: Tiny grammar fix

Message reads “only PUT AND PATCH are supported”. Prefer “PUT or PATCH”.

-                    return this.makeBadInputErrorResponse('invalid request method, only PUT AND PATCH are supported');
+                    return this.makeBadInputErrorResponse('invalid request method, only PUT or PATCH are supported');

170-177: Map unexpected errors to 500 Internal Server Error

Generic, non-ZenStack errors should be server faults (500), not client faults (400).

-        const resp = {
-            status: 400,
+        const resp = {
+            status: 500,
             body: { error: { message: (err as Error).message || 'unknown error' } },
         };

205-216: Optional: drop unnecessary async

processRequestPayload doesn’t await anything; remove async to simplify.

-    private async processRequestPayload(args: any) {
+    private processRequestPayload(args: any) {

131-139: Note on serialization of falsy results

When clientResult is falsy (e.g., 0 for count), serialization is skipped. That’s fine for current needs, but if you ever return falsy structured values, consider serializing unconditionally.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 68e0072 and 3fe9732.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (29)
  • BREAKINGCHANGES.md (1 hunks)
  • TODO.md (1 hunks)
  • packages/common-helpers/src/index.ts (1 hunks)
  • packages/common-helpers/src/safe-json-stringify.ts (1 hunks)
  • packages/plugins/policy/src/policy-handler.ts (2 hunks)
  • packages/runtime/package.json (1 hunks)
  • packages/runtime/src/client/crud/dialects/postgresql.ts (1 hunks)
  • packages/runtime/src/client/crud/validator/index.ts (1 hunks)
  • packages/runtime/src/client/errors.ts (2 hunks)
  • packages/runtime/src/client/helpers/schema-db-pusher.ts (1 hunks)
  • packages/runtime/src/client/query-utils.ts (0 hunks)
  • packages/sdk/package.json (1 hunks)
  • packages/server/eslint.config.js (1 hunks)
  • packages/server/package.json (1 hunks)
  • packages/server/src/api/index.ts (1 hunks)
  • packages/server/src/api/rpc/index.ts (1 hunks)
  • packages/server/src/api/utils.ts (1 hunks)
  • packages/server/src/express/index.ts (1 hunks)
  • packages/server/src/express/middleware.ts (1 hunks)
  • packages/server/src/types.ts (1 hunks)
  • packages/server/test/adapter/express.test.ts (1 hunks)
  • packages/server/test/api/rpc.test.ts (1 hunks)
  • packages/server/test/utils.ts (1 hunks)
  • packages/server/tsconfig.json (1 hunks)
  • packages/server/tsup.config.ts (1 hunks)
  • packages/server/vitest.config.ts (1 hunks)
  • pnpm-workspace.yaml (1 hunks)
  • tests/e2e/package.json (1 hunks)
  • tests/regression/package.json (1 hunks)
💤 Files with no reviewable changes (1)
  • packages/runtime/src/client/query-utils.ts
🧰 Additional context used
📓 Path-based instructions (3)
{packages,samples,tests}/**

📄 CodeRabbit inference engine (CLAUDE.md)

Place packages only under packages/, samples/, or tests/

Files:

  • packages/server/vitest.config.ts
  • packages/sdk/package.json
  • packages/runtime/src/client/crud/dialects/postgresql.ts
  • tests/e2e/package.json
  • packages/server/src/api/utils.ts
  • packages/server/test/api/rpc.test.ts
  • packages/common-helpers/src/index.ts
  • packages/runtime/package.json
  • packages/server/src/api/index.ts
  • packages/server/src/express/index.ts
  • packages/server/test/utils.ts
  • packages/server/tsconfig.json
  • packages/server/src/express/middleware.ts
  • packages/common-helpers/src/safe-json-stringify.ts
  • packages/server/tsup.config.ts
  • packages/runtime/src/client/helpers/schema-db-pusher.ts
  • packages/runtime/src/client/crud/validator/index.ts
  • packages/server/test/adapter/express.test.ts
  • packages/plugins/policy/src/policy-handler.ts
  • packages/server/src/types.ts
  • packages/server/package.json
  • packages/runtime/src/client/errors.ts
  • tests/regression/package.json
  • packages/server/eslint.config.js
  • packages/server/src/api/rpc/index.ts
tests/e2e/**

📄 CodeRabbit inference engine (CLAUDE.md)

End-to-end tests must live under tests/e2e/

Files:

  • tests/e2e/package.json
pnpm-workspace.yaml

📄 CodeRabbit inference engine (CLAUDE.md)

Use pnpm workspaces for the monorepo

Files:

  • pnpm-workspace.yaml
🧬 Code graph analysis (8)
packages/server/src/api/utils.ts (2)
packages/server/src/types.ts (2)
  • LogConfig (17-17)
  • LogLevel (7-7)
packages/runtime/src/client/executor/zenstack-driver.ts (1)
  • error (172-179)
packages/server/test/api/rpc.test.ts (4)
packages/runtime/src/client/contract.ts (1)
  • ClientContract (50-170)
packages/testtools/src/client.ts (2)
  • createPolicyTestClient (179-190)
  • createTestClient (52-169)
packages/server/src/api/rpc/index.ts (3)
  • schema (30-32)
  • handleRequest (34-155)
  • RPCApiHandler (27-241)
packages/server/test/utils.ts (1)
  • schema (3-29)
packages/server/test/utils.ts (2)
packages/runtime/src/client/crud/validator/index.ts (1)
  • schema (60-62)
packages/server/src/api/rpc/index.ts (1)
  • schema (30-32)
packages/server/src/express/middleware.ts (3)
packages/sdk/src/schema/schema.ts (1)
  • SchemaDef (10-18)
packages/server/src/types.ts (2)
  • ApiHandler (67-77)
  • Response (52-62)
packages/runtime/src/client/contract.ts (1)
  • ClientContract (50-170)
packages/server/test/adapter/express.test.ts (3)
packages/testtools/src/client.ts (1)
  • createPolicyTestClient (179-190)
packages/server/src/api/rpc/index.ts (2)
  • schema (30-32)
  • RPCApiHandler (27-241)
packages/server/test/utils.ts (2)
  • schema (3-29)
  • makeUrl (31-33)
packages/plugins/policy/src/policy-handler.ts (1)
packages/runtime/src/client/errors.ts (1)
  • RejectedByPolicyError (68-76)
packages/server/src/types.ts (2)
packages/sdk/src/schema/schema.ts (1)
  • SchemaDef (10-18)
packages/runtime/src/client/contract.ts (1)
  • ClientContract (50-170)
packages/server/src/api/rpc/index.ts (6)
packages/server/src/api/utils.ts (2)
  • registerCustomSerializers (29-50)
  • log (6-24)
packages/sdk/src/schema/schema.ts (1)
  • SchemaDef (10-18)
packages/server/src/types.ts (4)
  • LogConfig (17-17)
  • ApiHandler (67-77)
  • RequestContext (22-47)
  • Response (52-62)
packages/common-helpers/src/safe-json-stringify.ts (1)
  • safeJSONStringify (4-12)
packages/runtime/src/client/errors.ts (4)
  • ZenStackError (4-4)
  • NotFoundError (36-43)
  • InputValidationError (9-17)
  • RejectedByPolicyError (68-76)
packages/runtime/src/client/contract.ts (1)
  • ClientContract (50-170)
🪛 markdownlint-cli2 (0.18.1)
TODO.md

89-89: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)

🔇 Additional comments (24)
packages/runtime/src/client/helpers/schema-db-pusher.ts (1)

295-307: LGTM! Sensible foreign key constraint defaults.

The default behaviors are well-chosen and align with database best practices:

  • onDelete: 'set null' for optional relations allows parent deletion without orphaning children
  • onDelete: 'restrict' for required relations prevents deletion that would violate referential integrity
  • onUpdate: 'cascade' maintains consistency when primary keys are updated

These defaults will improve the developer experience by reducing boilerplate while maintaining data integrity.

packages/runtime/src/client/crud/dialects/postgresql.ts (1)

113-120: Code implementation is correct; verify test coverage for hex-encoded bytea handling.

The transformOutputBytes method correctly handles the three cases: Buffer objects, hex-encoded bytea strings prefixed with \x, and other values. The implementation properly parses the hex string by removing the prefix and converting to Uint8Array.

Existing tests in packages/server/test/api/rpc.test.ts exercise Bytes fields with PostgreSQL, but the specific hex-encoded bytea case (when node-pg embeds bytea in JSON responses) cannot be confirmed as explicitly tested. Verify that your test suite exercises this code path, particularly when bytea fields are returned in JSON contexts from the database.

tests/regression/package.json (1)

12-12: Catalog-based decimal.js resolution is properly aligned.

The migration to catalog: is consistent with the catalog definition in pnpm-workspace.yaml line 19, which specifies ^10.4.3. No issues detected.

packages/server/tsconfig.json (1)

1-4: TypeScript configuration is correctly structured.

The configuration properly extends the workspace base config and includes the appropriate source path pattern. No issues detected.

packages/runtime/package.json (1)

67-67: Catalog-based decimal.js resolution is properly configured.

Consistent with the catalog definition in pnpm-workspace.yaml. No issues detected.

tests/e2e/package.json (1)

23-23: Catalog-based decimal.js resolution is correctly applied.

Consistent with the workspace catalog definition. No issues detected.

packages/sdk/package.json (1)

50-50: Catalog-based decimal.js resolution in devDependencies is correct.

Properly configured and consistent with the workspace catalog. No issues detected.

pnpm-workspace.yaml (1)

19-19: Decimal.js catalog entry is properly configured.

The catalog entry establishes ^10.4.3 as the centralized version referenced by all packages via catalog: syntax. Consistent and correct.

BREAKINGCHANGES.md (1)

1-3: Breaking changes documentation is clear and properly ordered.

The restructured list correctly prioritizes the update/delete policy rejection behavior, documents the new check() API removal, and maintains the to-one relation context. Clear and appropriate.

packages/server/src/api/utils.ts (1)

29-50: Verify idempotency of serializer registration.

The implementation looks correct with proper edge runtime support. However, if registerCustomSerializers is called multiple times (e.g., in different parts of the application), the same serializers will be re-registered.

Please verify that SuperJSON handles duplicate registrations gracefully, or consider adding a guard to ensure this function is only called once:

let registered = false;

export function registerCustomSerializers() {
    if (registered) return;
    registered = true;
    
    // ... rest of the implementation
}
packages/server/src/express/index.ts (1)

1-1: LGTM!

Clean re-export of the Express middleware.

packages/runtime/src/client/crud/validator/index.ts (1)

234-239: LGTM!

The updated error construction correctly passes the model as the first parameter, aligning with the new InputValidationError constructor signature.

packages/common-helpers/src/index.ts (1)

4-4: LGTM!

Clean addition to the barrel exports.

packages/server/src/api/index.ts (1)

1-1: LGTM!

Clean re-export of the RPC API handler.

packages/plugins/policy/src/policy-handler.ts (1)

107-107: LGTM!

The updated error construction correctly passes RejectedByPolicyReason.NO_ACCESS as the second parameter, aligning with the new RejectedByPolicyError constructor signature. The reason code accurately represents the policy rejection scenario in both cases.

Also applies to: 624-624

packages/runtime/src/client/errors.ts (3)

10-16: LGTM! Enhanced error context with model information.

The updated constructor signature adds the model as a public readonly field, enabling programmatic access to the model context when handling validation errors. This is a breaking change but significantly improves error handling capabilities.


37-42: LGTM! Enhanced error context with model information.

The updated constructor adds model as a public readonly field, improving error handling by providing programmatic access to the affected model.


48-75: LGTM! Well-structured policy rejection handling.

The new RejectedByPolicyReason enum provides clear, structured reason codes for different policy rejection scenarios. The updated RejectedByPolicyError constructor with a default reason of NO_ACCESS is well-designed and aligns with the usage patterns in the codebase.

packages/server/tsup.config.ts (1)

12-13: Confirm .d.cts generation for CJS “types” export.

With dts: true and dual formats, tsup may only emit .d.ts. Your package.json requires ./dist/index.d.cts for require.

Please build locally and confirm dist contains index.d.cts. If not, either:

  • Configure tsup to emit .d.cts, or
  • Simplify exports to use index.d.ts for both import/require.
packages/server/package.json (1)

45-55: body-parser v2 with v1 types; verify compatibility.

body-parser is ^2.2.0 while @types/body-parser is ^1.19.6.

  • If v2 ships its own types, remove @types/body-parser.
  • Otherwise, bump @types/body-parser to match v2 (if available) or pin body-parser to v1.x.
packages/server/eslint.config.js (1)

1-4: The suggested type Linter.FlatConfig[] is deprecated; use Linter.Config[] instead.

The review correctly identifies a type mismatch: packages/server/eslint.config.js re-exports an array from base.js but types it as singular Config. However, ESLint v9 replaced the deprecated Linter.FlatConfig with Linter.Config, so the correct fix is:

-/** @type {import("eslint").Linter.Config} */
+/** @type {import("eslint").Linter.Config[]} */
export default config;

Likely an incorrect or invalid review comment.

packages/server/test/api/rpc.test.ts (3)

14-17: Ensure clients are disconnected to avoid leaked connections

Add an afterAll to close both clients. Also disconnect the Postgres test client created in “field types”.
[ suggest_recommended_refactor ]

     beforeAll(async () => {
         client = await createPolicyTestClient(schema);
         rawClient = client.$unuseAll();
     });
+
+    afterAll(async () => {
+        try {
+            await rawClient?.$disconnect();
+            await client?.$disconnect();
+        } catch {}
+    });

And near the end of the “field types” test (before the closing brace):

         expect(r.status).toBe(200);
         expect(r.data).toBeNull();
-    });
+        await client.$disconnect();
+    });

286-291: Remove redundant handler re-initialization

handleRequest = makeHandler(); is called twice back-to-back; the second is unnecessary.
[ suggest_optional_refactor ]

-    let handleRequest = makeHandler();
-
-    handleRequest = makeHandler();
+    const handleRequest = makeHandler();

359-381: Avoid variable shadowing for readability

Local schema shadows the imported schema. Rename to a distinct identifier to reduce confusion.
[ suggest_optional_refactor ]

-        const schema = `
+        const fieldTypesSchema = `
 ...
-        const client = await createTestClient(schema, { provider: 'postgresql' });
+        const client = await createTestClient(fieldTypesSchema, { provider: 'postgresql' });

@claude
Copy link

claude bot commented Oct 24, 2025

Claude finished @ymc9's task —— View job


Code Review: RPC API Handler and Express Adapter Migration

I've completed a comprehensive review of this PR that introduces the new @zenstackhq/server package. Here's my detailed feedback:

🟢 Strengths

Architecture & Design:

  • Clean separation of concerns with framework-agnostic ApiHandler interface
  • Well-structured package with proper TypeScript exports and peer dependency configuration
  • Comprehensive test coverage with both unit tests (529 lines) and integration tests
  • Good use of SuperJSON for handling complex data types (BigInt, Decimal, Buffer, Date)

Code Quality:

  • Proper error handling with specific error types (NotFoundError, InputValidationError, RejectedByPolicyError)
  • HTTP status codes correctly mapped to error types (403 for policy violations, 422 for validation errors)
  • Type safety maintained throughout with generic schema constraints
  • Good logging system with configurable levels

🟡 Security Considerations

Input Validation (✅ Good):

  • JSON parsing properly wrapped in try-catch blocks (packages/server/src/api/rpc/index.ts:221-224)
  • Path validation prevents invalid routes (packages/server/src/api/rpc/index.ts:39-41)
  • Method validation enforces correct HTTP methods per operation
  • Model name validation against schema (packages/server/src/api/rpc/index.ts:157-159)

Security Concerns (⚠️ Needs Attention):

  • Case sensitivity bug: Model comparison is case-insensitive but client call uses exact path segment (packages/server/src/api/rpc/index.ts:158 vs line 128)
  • Dynamic method invocation: (client as any)[model][op](processedArgs) at line 128 could be risky if validation fails
  • Host header injection: Express middleware uses unvalidated Host header for URL reconstruction (packages/server/src/express/middleware.ts:50)
  • Error information leakage: Detailed validation messages might expose internal structure

🔴 Critical Issues

1. Case Sensitivity Bug (packages/server/src/api/rpc/index.ts:158)

return Object.keys(client.$schema.models).some((m) => lowerCaseFirst(m) === lowerCaseFirst(model));

Model validation is case-insensitive, but the actual client call uses the exact path segment. This could cause runtime errors.

2. Inconsistent Error Handling (packages/server/src/express/middleware.ts:39-45)

if (sendResponse === false && !client) {
    throw new Error('unable to get ZenStackClient from request context');
}
if (!client) {
    return response.status(500).json({ message: 'unable to get ZenStackClient from request context' });
}

Same error condition handled differently based on sendResponse flag.

3. Host Header Vulnerability (packages/server/src/express/middleware.ts:50)

const url = request.protocol + '://' + request.get('host') + request.originalUrl;

Uses user-supplied Host header without validation - potential for Host header injection.

🟢 Test Coverage Assessment

Excellent Coverage:

  • All CRUD operations, complex data types, policy violations, validation errors
  • Pagination, ordering, aggregation, Express middleware integration
  • Edge cases: invalid paths, malformed JSON

Minor Gaps:

  • Host header injection scenarios, large payload handling, concurrent requests

📝 Recommendations

  1. Fix case sensitivity bug by using exact model name matching
  2. Implement Host header validation in Express middleware
  3. Add request size limits configuration
  4. Enhance error classification (distinguish between 400/500 errors)
  5. Add security-focused tests for edge cases

🎯 Overall Assessment

This is a well-architected and thoroughly tested implementation that successfully migrates RPC functionality to a dedicated server package. The code demonstrates strong TypeScript practices and comprehensive error handling. The main concerns are around security hardening that should be addressed before production use.

Recommendation: Approve with minor fixes for the identified security and bug issues.

--- • Branch: feat/rpc-adapter

Copy link

@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: 2

♻️ Duplicate comments (1)
packages/server/src/api/rpc/index.ts (1)

73-80: Preserve unmarshalQ error message and handle duplicate q/meta parameters

Return the specific error from unmarshalQ and reject duplicate array-style query params. This keeps “invalid meta” vs “invalid q” distinct and avoids passing arrays into JSON.parse.

Apply to both GET and DELETE branches:

-                try {
-                    args = query?.['q']
-                        ? this.unmarshalQ(query['q'] as string, query['meta'] as string | undefined)
-                        : {};
-                } catch {
-                    return this.makeBadInputErrorResponse('invalid "q" query parameter');
-                }
+                try {
+                    const qParam = query?.['q'];
+                    const metaParam = query?.['meta'];
+                    if (Array.isArray(qParam) || Array.isArray(metaParam)) {
+                        return this.makeBadInputErrorResponse('duplicate "q" or "meta" query parameter');
+                    }
+                    args = qParam ? this.unmarshalQ(qParam as string, metaParam as string | undefined) : {};
+                } catch (err) {
+                    return this.makeBadInputErrorResponse((err as Error).message);
+                }

Also applies to: 100-106

🧹 Nitpick comments (7)
packages/server/src/api/rpc/index.ts (7)

132-139: Always SuperJSON.serialize to preserve meta for falsy values (0/false/null)

Conditional serialization skips meta when result is falsy (e.g., count = 0). Serialize unconditionally for consistency.

-            // superjson serialize response
-            if (clientResult) {
-                const { json, meta } = SuperJSON.serialize(clientResult);
-                responseBody = { data: json };
-                if (meta) {
-                    responseBody.meta = { serialization: meta };
-                }
-            }
+            // superjson serialize response (always)
+            const { json, meta } = SuperJSON.serialize(clientResult);
+            responseBody = { data: json };
+            if (meta) {
+                responseBody.meta = { serialization: meta };
+            }

123-127: Avoid logging full request args/response bodies at debug; redact or summarize to reduce PII risk

Current logs may include sensitive data. Prefer summarizing (op/model and arg keys) or add redaction.

Example tweak (minimal):

-                () => `handling "${model}.${op}" request with args: ${safeJSONStringify(processedArgs)}`,
+                () => `handling "${model}.${op}" request (arg keys: ${processedArgs && typeof processedArgs === 'object' ? Object.keys(processedArgs as any).join(',') : 'n/a'})`,
-                () => `sending response for "${model}.${op}" request: ${safeJSONStringify(response)}`,
+                () => `sending response for "${model}.${op}" request (status: ${resCode})`,

Also applies to: 141-147


60-62: Return 200 for upsert; reserve 201 for creates

Upsert may update an existing entity; 201 can mislead clients.

-                resCode = 201;
+                resCode = op === 'upsert' ? 200 : 201;

162-169: Use appropriate log levels for error responses

  • 400/403/404/422: warn
  • 500: error
-        log(this.options.log, 'debug', () => `sending error response: ${safeJSONStringify(resp)}`);
+        log(this.options.log, 'warn', () => `sending error response: ${safeJSONStringify(resp)}`);
-        log(this.options.log, 'debug', () => `sending error response: ${safeJSONStringify(resp)}`);
+        log(this.options.log, 'error', () => `sending error response: ${safeJSONStringify(resp)}`);
-        log(this.options.log, 'debug', () => `sending error response: ${safeJSONStringify(resp)}`);
+        log(this.options.log, 'warn', () => `sending error response: ${safeJSONStringify(resp)}`);

Also applies to: 171-178, 201-204


118-131: Fail fast if operation is not implemented on the client

Protects against version skew (e.g., createManyAndReturn not present) and yields a clear 400 instead of 500.

             log(
                 this.options.log,
                 'debug',
                 () => `handling "${model}.${op}" request with args: ${safeJSONStringify(processedArgs)}`,
             );

-            const clientResult = await (client as any)[model][op](processedArgs);
+            const modelApi = (client as any)[model];
+            if (!modelApi || typeof modelApi[op] !== 'function') {
+                return this.makeBadInputErrorResponse(`unsupported operation "${op}" for model "${model}"`);
+            }
+            const clientResult = await modelApi[op](processedArgs);

If you prefer, I can run a repo scan to see where these ops are relied on and confirm runtime compatibility.


14-14: Consider registering SuperJSON serializers once to avoid duplicate registration across imports

If other modules also call registerCustomSerializers, SuperJSON may throw on duplicate type IDs. Use a module-level once flag.

Example (in utils.ts):

let __serializersRegistered = false;
export function registerCustomSerializersOnce() {
    if (__serializersRegistered) return;
    __serializersRegistered = true;
    registerCustomSerializers();
}

Then here:

// replace
-registerCustomSerializers();
// with
+registerCustomSerializersOnce();

113-121: Optional: validate model before payload processing to fail fast

Minor efficiency and clearer errors for unknown models.

-        const { result: processedArgs, error } = await this.processRequestPayload(args);
-        if (error) {
-            return this.makeBadInputErrorResponse(error);
-        }
-
-        try {
-            if (!this.isValidModel(client, model)) {
-                return this.makeBadInputErrorResponse(`unknown model name: ${model}`);
-            }
+        if (!this.isValidModel(client, model)) {
+            return this.makeBadInputErrorResponse(`unknown model name: ${model}`);
+        }
+        const { result: processedArgs, error } = await this.processRequestPayload(args);
+        if (error) {
+            return this.makeBadInputErrorResponse(error);
+        }
+        try {
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8ac4d0b and 8bf71e8.

📒 Files selected for processing (3)
  • packages/language/package.json (1 hunks)
  • packages/server/package.json (1 hunks)
  • packages/server/src/api/rpc/index.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/server/package.json
🧰 Additional context used
📓 Path-based instructions (1)
{packages,samples,tests}/**

📄 CodeRabbit inference engine (CLAUDE.md)

Place packages only under packages/, samples/, or tests/

Files:

  • packages/language/package.json
  • packages/server/src/api/rpc/index.ts
🧬 Code graph analysis (1)
packages/server/src/api/rpc/index.ts (6)
packages/server/src/api/utils.ts (2)
  • registerCustomSerializers (29-50)
  • log (6-24)
packages/sdk/src/schema/schema.ts (1)
  • SchemaDef (10-18)
packages/server/src/types.ts (4)
  • LogConfig (17-17)
  • ApiHandler (67-77)
  • RequestContext (22-47)
  • Response (52-62)
packages/common-helpers/src/safe-json-stringify.ts (1)
  • safeJSONStringify (4-12)
packages/runtime/src/client/errors.ts (4)
  • ZenStackError (4-4)
  • NotFoundError (36-43)
  • InputValidationError (9-17)
  • RejectedByPolicyError (68-76)
packages/runtime/src/client/contract.ts (1)
  • ClientContract (50-170)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: build-test (20.x, postgresql)
  • GitHub Check: build-test (20.x, sqlite)
🔇 Additional comments (1)
packages/language/package.json (1)

16-16: Good alignment with repo-wide testing infrastructure.

The addition of the "test": "vitest run" script aligns the language package with the Vitest-based testing approach being introduced across the monorepo, as evidenced by the existing @zenstackhq/vitest-config dev dependency.

@claude
Copy link

claude bot commented Oct 24, 2025

Claude finished @ymc9's task —— View job


Code Review: RPC API Handler and Express Adapter Migration

Todo List:

  • Read repository guidelines and setup instructions
  • Analyze new server package architecture and design
  • Review security implications of RPC handler and Express middleware
  • Assess test coverage and identify potential gaps
  • Check for bugs, performance issues, and best practices
  • Provide comprehensive feedback and recommendations

🟢 Architecture & Design Strengths

Clean Framework-Agnostic Design:

  • Excellent separation with ApiHandler interface allowing multiple framework implementations
  • Well-structured package exports (./api and ./express) with proper TypeScript support
  • Good use of SuperJSON for complex data type serialization (BigInt, Decimal, Buffer, Date)
  • Comprehensive error handling with specific error types mapped to appropriate HTTP status codes

Package Structure:

  • Proper peer dependency configuration for Express (optional via peerDependenciesMeta)
  • Clean TypeScript configuration with both ESM and CommonJS support
  • Well-organized test structure with comprehensive coverage (527 lines RPC tests, 176 lines Express tests)

🔴 Critical Issues Found

1. Case Sensitivity Security Issue (packages/server/src/api/rpc/index.ts:161)

return Object.keys(client.$schema.models).some((m) => lowerCaseFirst(m) === lowerCaseFirst(model));

Model validation uses case-insensitive comparison, but the actual client call at line 131 uses the exact path segment:

const clientResult = await (client as any)[model][op](processedArgs);

This could cause runtime errors if a user provides a differently-cased model name that passes validation but fails at execution.

2. Host Header Injection Vulnerability (packages/server/src/express/middleware.ts:50)

const url = request.protocol + '://' + request.get('host') + request.originalUrl;

Uses user-supplied Host header without validation, creating potential for Host header injection attacks.

3. Inconsistent Error Handling (packages/server/src/express/middleware.ts:39-45)

Same error condition handled differently based on sendResponse flag:

if (sendResponse === false && !client) {
    throw new Error('unable to get ZenStackClient from request context');
}
if (!client) {
    return response.status(500).json({ message: 'unable to get ZenStackClient from request context' });
}

4. SuperJSON Test Utility Bug (packages/server/test/utils.ts:32)

The makeUrl function with useSuperJson=true only encodes the JSON part but omits the required meta parameter:

return q ? `${path}?q=${encodeURIComponent(useSuperJson ? superjson.stringify(q) : JSON.stringify(q))}` : path;

This breaks SuperJSON deserialization in the RPC handler which expects both q and meta parameters.


🟡 Security Considerations

Input Validation - Good Coverage:

  • ✅ JSON parsing properly wrapped in try-catch blocks (packages/server/src/api/rpc/index.ts:224-226)
  • ✅ Path validation prevents invalid routes (packages/server/src/api/rpc/index.ts:39-41)
  • ✅ Method validation enforces correct HTTP methods per operation
  • ✅ Model name validation against schema prevents arbitrary model access

Security Concerns:

  • ⚠️ Dynamic method invocation: Line 131 uses (client as any)[model][op](processedArgs) - relies on validation not being bypassed
  • ⚠️ Error information disclosure: Detailed validation error messages could expose internal schema structure
  • ⚠️ No request size limits: Missing protection against large payload attacks

🔍 Performance & Code Quality

Performance Positives:

  • SuperJSON serialization only applied when metadata is present
  • Efficient model validation using key existence check
  • Lazy evaluation of log messages with function callbacks

Potential Improvements:

  • URL reconstruction in Express middleware happens on every request
  • JSON parsing/stringifying occurs multiple times for complex data types
  • Generic error handling too broad (line 154-156) - all non-ZenStack errors return 500 status

Code Quality:

  • Good TypeScript practices with proper type safety
  • Comprehensive logging system with configurable levels
  • Clean separation of concerns between handler and middleware

📋 Test Coverage Assessment

Excellent Coverage Includes:

  • ✅ All CRUD operations (create, read, update, delete, upsert)
  • ✅ Complex data types (BigInt, Decimal, Buffer, Date) via SuperJSON
  • ✅ Policy violations (403) and validation errors (422)
  • ✅ Pagination, ordering, and query operations
  • ✅ Express middleware integration and error handling
  • ✅ Various HTTP methods and error scenarios

Missing Edge Cases:

  • ❌ Host header injection scenarios
  • ❌ Large payload handling
  • ❌ Malformed SuperJSON metadata handling
  • ❌ Case sensitivity edge cases in model names

📝 Recommendations

High Priority Fixes:

  1. Fix case sensitivity bug - Make model validation consistent with execution
  2. Implement Host header validation in Express middleware
  3. Fix SuperJSON test utility - Include meta parameter for proper serialization
  4. Enhance error classification - distinguish between 400/500 errors properly

Security Hardening:

  1. Add request size limits configuration option
  2. Consider input sanitization for edge cases
  3. Add security-focused test cases for injection scenarios

Performance & Reliability:

  1. Cache URL reconstruction results where possible
  2. Add retry/timeout configurations for production deployments
  3. Implement structured error codes for better client error handling

🎯 Overall Assessment

This is a well-architected and thoroughly tested implementation that successfully migrates RPC functionality to a dedicated server package. The code demonstrates strong TypeScript practices, comprehensive error handling, and good separation of concerns.

Main Strengths:

  • Clean framework-agnostic design with proper abstractions
  • Comprehensive test coverage (703 total test lines)
  • Good security foundations with input validation
  • Proper error handling with appropriate HTTP status codes

Primary Concerns:

  • Case sensitivity bug could cause runtime failures
  • Host header injection vulnerability needs addressing
  • Some inconsistent error handling patterns

Recommendation: Approve with minor security fixes for the identified case sensitivity and Host header issues. The architecture is solid and the implementation is production-ready after addressing these specific concerns.

--- • Branch: feat/rpc-adapter

@claude
Copy link

claude bot commented Oct 24, 2025

Claude finished @ymc9's task —— View job


Code Review: RPC API Handler and Express Adapter Migration

I've completed a comprehensive review of this PR that introduces the new @zenstackhq/server package. Here's my detailed feedback:

🟢 Strengths

Architecture & Design:

  • Clean separation of concerns with framework-agnostic ApiHandler interface
  • Well-structured package with proper TypeScript exports and peer dependency configuration
  • Comprehensive test coverage with both unit tests (529 lines) and integration tests
  • Good use of SuperJSON for handling complex data types (BigInt, Decimal, Buffer, Date)

Code Quality:

  • Proper error handling with specific error types (NotFoundError, InputValidationError, RejectedByPolicyError)
  • HTTP status codes correctly mapped to error types (403 for policy violations, 422 for validation errors)
  • Type safety maintained throughout with generic schema constraints
  • Good logging system with configurable levels

🟡 Security Considerations

Input Validation (✅ Good):

  • JSON parsing properly wrapped in try-catch blocks (packages/server/src/api/rpc/index.ts:221-224)
  • Path validation prevents invalid routes (packages/server/src/api/rpc/index.ts:39-41)
  • Method validation enforces correct HTTP methods per operation
  • Model name validation against schema (packages/server/src/api/rpc/index.ts:157-159)

Security Concerns (⚠️ Needs Attention):

  • Case sensitivity bug: Model comparison is case-insensitive but client call uses exact path segment (packages/server/src/api/rpc/index.ts:158 vs line 128)
  • Dynamic method invocation: (client as any)[model][op](processedArgs) at line 128 could be risky if validation fails
  • Host header injection: Express middleware uses unvalidated Host header for URL reconstruction (packages/server/src/express/middleware.ts:50)
  • Error information leakage: Detailed validation messages might expose internal structure

🔴 Critical Issues

1. Case Sensitivity Bug (packages/server/src/api/rpc/index.ts:158)

return Object.keys(client.$schema.models).some((m) => lowerCaseFirst(m) === lowerCaseFirst(model));

Model validation is case-insensitive, but the actual client call uses the exact path segment. This could cause runtime errors.

2. Inconsistent Error Handling (packages/server/src/express/middleware.ts:39-45)

if (sendResponse === false && !client) {
    throw new Error('unable to get ZenStackClient from request context');
}
if (!client) {
    return response.status(500).json({ message: 'unable to get ZenStackClient from request context' });
}

Same error condition handled differently based on sendResponse flag.

3. Host Header Vulnerability (packages/server/src/express/middleware.ts:50)

const url = request.protocol + '://' + request.get('host') + request.originalUrl;

Uses user-supplied Host header without validation - potential for Host header injection.

🟢 Test Coverage Assessment

Excellent Coverage:

  • All CRUD operations, complex data types, policy violations, validation errors
  • Pagination, ordering, aggregation, Express middleware integration
  • Edge cases: invalid paths, malformed JSON

Minor Gaps:

  • Host header injection scenarios, large payload handling, concurrent requests

📝 Recommendations

  1. Fix case sensitivity bug by using exact model name matching
  2. Implement Host header validation in Express middleware
  3. Add request size limits configuration
  4. Enhance error classification (distinguish between 400/500 errors)
  5. Add security-focused tests for edge cases

🎯 Overall Assessment

This is a well-architected and thoroughly tested implementation that successfully migrates RPC functionality to a dedicated server package. The code demonstrates strong TypeScript practices and comprehensive error handling. The main concerns are around security hardening that should be addressed before production use.

Recommendation: Approve with minor fixes for the identified security and bug issues.

--- • Branch: feat/rpc-adapter

@claude
Copy link

claude bot commented Oct 24, 2025

Claude finished @ymc9's task —— View job


Code Review: RPC API Handler and Express Adapter Migration

Todo List:

  • Read repository guidelines and setup instructions
  • Analyze new server package architecture and design
  • Review security implications of RPC handler and Express middleware
  • Assess test coverage and identify potential gaps
  • Check for bugs, performance issues, and best practices
  • Provide comprehensive feedback and recommendations

🟢 Architecture & Design Strengths

Clean Framework-Agnostic Design:

  • Excellent separation with ApiHandler interface allowing multiple framework implementations
  • Well-structured package exports (./api and ./express) with proper TypeScript support
  • Good use of SuperJSON for complex data type serialization (BigInt, Decimal, Buffer, Date)
  • Comprehensive error handling with specific error types mapped to appropriate HTTP status codes

Package Structure:

  • Proper peer dependency configuration for Express (optional via peerDependenciesMeta)
  • Clean TypeScript configuration with both ESM and CommonJS support
  • Well-organized test structure with comprehensive coverage (527 lines RPC tests, 176 lines Express tests)

🔴 Critical Issues Found

1. Case Sensitivity Security Issue (packages/server/src/api/rpc/index.ts:161)

return Object.keys(client.$schema.models).some((m) => lowerCaseFirst(m) === lowerCaseFirst(model));

Model validation uses case-insensitive comparison, but the actual client call at line 131 uses the exact path segment:

const clientResult = await (client as any)[model][op](processedArgs);

This could cause runtime errors if a user provides a differently-cased model name that passes validation but fails at execution.

2. Host Header Injection Vulnerability (packages/server/src/express/middleware.ts:50)

const url = request.protocol + '://' + request.get('host') + request.originalUrl;

Uses user-supplied Host header without validation, creating potential for Host header injection attacks.

3. Inconsistent Error Handling (packages/server/src/express/middleware.ts:39-45)

Same error condition handled differently based on sendResponse flag:

if (sendResponse === false && !client) {
    throw new Error('unable to get ZenStackClient from request context');
}
if (!client) {
    return response.status(500).json({ message: 'unable to get ZenStackClient from request context' });
}

4. SuperJSON Test Utility Bug (packages/server/test/utils.ts:32)

The makeUrl function with useSuperJson=true only encodes the JSON part but omits the required meta parameter:

return q ? `${path}?q=${encodeURIComponent(useSuperJson ? superjson.stringify(q) : JSON.stringify(q))}` : path;

This breaks SuperJSON deserialization in the RPC handler which expects both q and meta parameters.


🟡 Security Considerations

Input Validation - Good Coverage:

  • ✅ JSON parsing properly wrapped in try-catch blocks (packages/server/src/api/rpc/index.ts:224-226)
  • ✅ Path validation prevents invalid routes (packages/server/src/api/rpc/index.ts:39-41)
  • ✅ Method validation enforces correct HTTP methods per operation
  • ✅ Model name validation against schema prevents arbitrary model access

Security Concerns:

  • ⚠️ Dynamic method invocation: Line 131 uses (client as any)[model][op](processedArgs) - relies on validation not being bypassed
  • ⚠️ Error information disclosure: Detailed validation error messages could expose internal schema structure
  • ⚠️ No request size limits: Missing protection against large payload attacks

🔍 Performance & Code Quality

Performance Positives:

  • SuperJSON serialization only applied when metadata is present
  • Efficient model validation using key existence check
  • Lazy evaluation of log messages with function callbacks

Potential Improvements:

  • URL reconstruction in Express middleware happens on every request
  • JSON parsing/stringifying occurs multiple times for complex data types
  • Generic error handling too broad (line 154-156) - all non-ZenStack errors return 500 status

Code Quality:

  • Good TypeScript practices with proper type safety
  • Comprehensive logging system with configurable levels
  • Clean separation of concerns between handler and middleware

📋 Test Coverage Assessment

Excellent Coverage Includes:

  • ✅ All CRUD operations (create, read, update, delete, upsert)
  • ✅ Complex data types (BigInt, Decimal, Buffer, Date) via SuperJSON
  • ✅ Policy violations (403) and validation errors (422)
  • ✅ Pagination, ordering, and query operations
  • ✅ Express middleware integration and error handling
  • ✅ Various HTTP methods and error scenarios

Missing Edge Cases:

  • ❌ Host header injection scenarios
  • ❌ Large payload handling
  • ❌ Malformed SuperJSON metadata handling
  • ❌ Case sensitivity edge cases in model names

📝 Recommendations

High Priority Fixes:

  1. Fix case sensitivity bug - Make model validation consistent with execution
  2. Implement Host header validation in Express middleware
  3. Fix SuperJSON test utility - Include meta parameter for proper serialization
  4. Enhance error classification - distinguish between 400/500 errors properly

Security Hardening:

  1. Add request size limits configuration option
  2. Consider input sanitization for edge cases
  3. Add security-focused test cases for injection scenarios

Performance & Reliability:

  1. Cache URL reconstruction results where possible
  2. Add retry/timeout configurations for production deployments
  3. Implement structured error codes for better client error handling

🎯 Overall Assessment

This is a well-architected and thoroughly tested implementation that successfully migrates RPC functionality to a dedicated server package. The code demonstrates strong TypeScript practices, comprehensive error handling, and good separation of concerns.

Main Strengths:

  • Clean framework-agnostic design with proper abstractions
  • Comprehensive test coverage (703 total test lines)
  • Good security foundations with input validation
  • Proper error handling with appropriate HTTP status codes

Primary Concerns:

  • Case sensitivity bug could cause runtime failures
  • Host header injection vulnerability needs addressing
  • Some inconsistent error handling patterns

Recommendation: Approve with minor security fixes for the identified case sensitivity and Host header issues. The architecture is solid and the implementation is production-ready after addressing these specific concerns.

--- • Branch: feat/rpc-adapter

Copy link

@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: 0

♻️ Duplicate comments (4)
packages/server/src/api/rpc/index.ts (4)

100-108: Nice: error propagation now preserves q/meta specificity

DELETE branch now returns the specific unmarshalQ error message. LGTM.


85-87: Wording nit resolved

“PUT or PATCH” wording looks good now. Thanks for fixing.


73-79: Preserve specific unmarshalQ error (don’t always blame "q")

Propagate the actual error from unmarshalQ so “invalid meta” is not masked.

-                try {
-                    args = query?.['q']
-                        ? this.unmarshalQ(query['q'] as string, query['meta'] as string | undefined)
-                        : {};
-                } catch {
-                    return this.makeBadInputErrorResponse('invalid "q" query parameter');
-                }
+                try {
+                    args = query?.['q']
+                        ? this.unmarshalQ(query['q'] as string, query['meta'] as string | undefined)
+                        : {};
+                } catch (err) {
+                    return this.makeBadInputErrorResponse(
+                        err instanceof Error ? err.message : 'invalid "q" query parameter',
+                    );
+                }

208-219: Guard processRequestPayload against null/non-object/array to avoid TypeError and array-shape corruption

Destructuring args can throw for null/primitives and converts arrays to objects. Add shape checks before destructuring.

-    private async processRequestPayload(args: any) {
-        const { meta, ...rest } = args;
-        if (meta?.serialization) {
-            try {
-                // superjson deserialization
-                args = SuperJSON.deserialize({ json: rest, meta: meta.serialization });
-            } catch (err) {
-                return { result: undefined, error: `failed to deserialize request payload: ${(err as Error).message}` };
-            }
-        }
-        return { result: args, error: undefined };
-    }
+    private async processRequestPayload(args: any) {
+        // Non-object or null: nothing to deserialize
+        if (args === null || typeof args !== 'object') {
+            return { result: args, error: undefined };
+        }
+        // Avoid converting arrays to objects; top-level arrays are not expected to carry meta
+        if (Array.isArray(args)) {
+            return { result: args, error: undefined };
+        }
+
+        const { meta, ...rest } = args as Record<string, unknown>;
+        if (meta && typeof meta === 'object' && (meta as any).serialization) {
+            try {
+                // superjson deserialization
+                args = SuperJSON.deserialize({ json: rest, meta: (meta as any).serialization });
+            } catch (err) {
+                return { result: undefined, error: `failed to deserialize request payload: ${(err as Error).message}` };
+            }
+        }
+        return { result: args, error: undefined };
+    }
🧹 Nitpick comments (5)
packages/server/src/api/rpc/index.ts (5)

73-76: Handle string[] query params for q/meta (avoid ambiguous inputs)

RequestContext.query allows string | string[]; normalize to a single string. This prevents accidental array-to-string casts.

-                    args = query?.['q']
-                        ? this.unmarshalQ(query['q'] as string, query['meta'] as string | undefined)
+                    args = this.getSingleQueryParam(query, 'q')
+                        ? this.unmarshalQ(
+                              this.getSingleQueryParam(query, 'q')!,
+                              this.getSingleQueryParam(query, 'meta'),
+                          )
                         : {};
// Add this helper in the class (e.g., near other private helpers)
private getSingleQueryParam(
    query: Record<string, string | string[]> | undefined,
    key: string,
): string | undefined {
    const v = query?.[key];
    return Array.isArray(v) ? v[0] : (v as string | undefined);
}

Also applies to: 100-103


131-133: Guard for unsupported operations to return 400 instead of 500

Validate client[model][op] exists and is callable before invoking.

-            const clientResult = await (client as any)[model][op](processedArgs);
+            const modelApi = (client as any)[model];
+            if (!modelApi || typeof modelApi[op] !== 'function') {
+                return this.makeBadInputErrorResponse(`unsupported operation: ${model}.${op}`);
+            }
+            const clientResult = await modelApi[op](processedArgs);

49-62: Consider 200 for upsert responses (201 may be misleading when updating)

RPC style doesn’t need REST creation semantics; if kept, return 200 for upsert to avoid implying resource creation.

-            case 'create':
-            case 'createMany':
-            case 'createManyAndReturn':
-            case 'upsert':
+            case 'create':
+            case 'createMany':
+            case 'createManyAndReturn':
                 if (method !== 'POST') {
                     return this.makeBadInputErrorResponse('invalid request method, only POST is supported');
                 }
                 if (!requestBody) {
                     return this.makeBadInputErrorResponse('missing request body');
                 }
 
                 args = requestBody;
                 resCode = 201;
                 break;
+
+            case 'upsert':
+                if (method !== 'POST') {
+                    return this.makeBadInputErrorResponse('invalid request method, only POST is supported');
+                }
+                if (!requestBody) {
+                    return this.makeBadInputErrorResponse('missing request body');
+                }
+                args = requestBody;
+                resCode = 200;
+                break;

14-14: Make serializer registration idempotent (avoid duplicate SuperJSON.registerCustom)

Top‑level registerCustomSerializers() can be invoked from multiple entry points; guard it to run once or initialize at app bootstrap.


125-130: PII‑aware logging

Debug logs currently include full args and response. Consider masking sensitive fields or gating by env to avoid accidental PII leakage in prod.

Also applies to: 143-149

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8bf71e8 and e29f67c.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (5)
  • packages/language/src/constants.ts (1 hunks)
  • packages/server/package.json (1 hunks)
  • packages/server/src/api/rpc/index.ts (1 hunks)
  • packages/server/src/express/middleware.ts (1 hunks)
  • packages/testtools/src/client.ts (1 hunks)
✅ Files skipped from review due to trivial changes (2)
  • packages/language/src/constants.ts
  • packages/server/package.json
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/server/src/express/middleware.ts
🧰 Additional context used
📓 Path-based instructions (1)
{packages,samples,tests}/**

📄 CodeRabbit inference engine (CLAUDE.md)

Place packages only under packages/, samples/, or tests/

Files:

  • packages/testtools/src/client.ts
  • packages/server/src/api/rpc/index.ts
🧬 Code graph analysis (1)
packages/server/src/api/rpc/index.ts (7)
packages/server/src/api/utils.ts (2)
  • registerCustomSerializers (29-50)
  • log (6-24)
packages/sdk/src/schema/schema.ts (1)
  • SchemaDef (10-18)
packages/server/src/types.ts (4)
  • LogConfig (17-17)
  • ApiHandler (67-77)
  • RequestContext (22-47)
  • Response (52-62)
packages/common-helpers/src/lower-case-first.ts (1)
  • lowerCaseFirst (1-3)
packages/common-helpers/src/safe-json-stringify.ts (1)
  • safeJSONStringify (4-12)
packages/runtime/src/client/errors.ts (4)
  • ZenStackError (4-4)
  • NotFoundError (36-43)
  • InputValidationError (9-17)
  • RejectedByPolicyError (68-76)
packages/runtime/src/client/contract.ts (1)
  • ClientContract (50-170)
🔇 Additional comments (1)
packages/testtools/src/client.ts (1)

200-200: Good defensive improvement; verify edge case handling.

The fallback to 'unnamed' makes the function more robust by preventing failures when test state is unavailable. However, if both currentTestName and testPath (line 201) are unavailable in multiple tests, they would generate identical database names, potentially causing conflicts during parallel test execution.

Consider verifying that this edge case is either:

  • Unlikely to occur in practice (test state is consistently available), or
  • Acceptable for your test isolation strategy (tests with unavailable state run sequentially)

You can also add an incrementing counter or timestamp to the digest computation to further reduce collision risk if needed.

@ymc9 ymc9 merged commit 56b68ab into dev Oct 24, 2025
6 checks passed
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.

2 participants