Skip to content

Conversation

@mwillbanks
Copy link
Contributor

@mwillbanks mwillbanks commented Dec 30, 2025

draft of an implementation of custom procedures based on the blog article: https://zenstack.dev/blog/next-chapter-2#custom-procedures

Summary by CodeRabbit

  • New Features

    • Procedures: new $procs API across client & server with type-safe per-procedure hooks (queries & mutations), array param/return support, SuperJSON payload handling, input validation, plugin hooks, and transactional mutation semantics.
    • Client SDK & typings: generated strongly-typed procedure envelopes and per-procedure TanStack Query/Svelte/Vue hooks.
  • Tests

    • Expanded unit, typing, integration, RPC/REST and e2e tests for procedures, validation, routing, serialization, and transactional behavior.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link

coderabbitai bot commented Dec 30, 2025

📝 Walkthrough

Walkthrough

Adds first-class "procedures": grammar/validator updates, schema/type changes (array flags), SDK ts-schema emission, ORM $procs runtime with input validation and plugin hooks, server REST/RPC procedure routing and SuperJSON handling, TanStack Query per-procedure hooks, and tests/fixtures.

Changes

Cohort / File(s) Summary
Language & grammar
packages/language/src/zmodel.langium
Grammar: optional mutation token; expanded ExpressionType and RegularIDWithTypeNames (add Void, Undefined and extra primitives).
Language validators & utils
packages/language/src/validator.ts, packages/language/src/validators/procedure-validator.ts, packages/language/src/validators/common.ts, packages/language/src/validators/function-invocation-validator.ts, packages/language/src/utils.ts, packages/language/test/procedure-validation.test.ts
Added ProcedureValidator and wiring; null‑prototype map for duplicate detection; mapBuiltinTypeToExpressionType signature/logic updated; invocation validation uses mapped types; added procedure validation tests.
Schema & SDK generator / CLI test
packages/schema/src/schema.ts, packages/sdk/src/ts-schema-generator.ts, packages/cli/test/ts-schema-gen.test.ts
ProcedureParam gains array?: boolean; ProcedureDef.params → Record; ProcedureDef gains returnArray?: boolean; ts-schema-generator emits param array flags and returnArray; test added for array params/returns.
ORM types & runtime
packages/orm/src/client/crud-types.ts, packages/orm/src/client/contract.ts, packages/orm/src/client/options.ts, packages/orm/src/client/client-impl.ts, packages/orm/src/client/crud/validator/index.ts, packages/orm/src/client/index.ts, packages/orm/src/client/plugin.ts, packages/orm/test/procedures.test.ts
Added procedure type utilities (GetProcedureNames/Params/Envelope/Func/Handler), renamed public surface $procedures$procs, introduced ProcedureHandlerFunc/Ctx, added validateProcedureInput with per-param zod schema builder, added onProcedure plugin hook and pipeline, updated options/contract types, and tests.
Server common + REST/RPC
packages/server/src/api/common/procedures.ts, packages/server/src/api/common/utils.ts, packages/server/src/api/rest/index.ts, packages/server/src/api/rpc/index.ts, packages/server/test/api/*.test.ts, packages/server/test/api/procedures.e2e.test.ts
New procedure utilities (getProcedureDef, mapProcedureArgs, SuperJSON helpers), added /$procs routing and private RPC handling, method enforcement, arg mapping/validation, result serialization, error mapping, and unit/e2e tests.
TanStack Query clients & tests
packages/clients/tanstack-query/src/react.ts, packages/clients/tanstack-query/src/vue.ts, packages/clients/tanstack-query/src/svelte/index.svelte.ts, packages/clients/tanstack-query/src/common/types.ts, packages/clients/tanstack-query/test/*, packages/clients/tanstack-query/test/schemas/procedures/schema-lite.ts
Added procedure typings (ProcedureHookFn/Group/Hooks/ProcedureReturn), exposed $procs hooks at runtime (per-procedure useQuery/useMutation/useInfinite variants), merged options, and added typing tests and fixture schema.
E2E generated schema & types
tests/e2e/orm/schemas/procedures/*
Added generated schema, models, enums, type-defs, strongly-typed input/result aliases and procedure declarations (incl. arrays, Json, Void).
Samples & consumer usage
samples/orm/*
Added procedure examples and schema entries; sample usage updated to call authDb.$procs.signUp and authDb.$procs.listPublicPosts.
Tests & e2e
tests/e2e/orm/client-api/procedures.test.ts, packages/orm/test/procedures.test.ts, packages/server/test/api/*, packages/clients/tanstack-query/test/*
New integration and e2e tests covering procedure invocation, validation, plugin hook ordering, transactional semantics, REST/RPC endpoints, client hook typings, and edge cases.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

"🐇 I hopped through grammar, types, and tests today,
arrays for params and returns in play,
$procs hum softly while plugins spin the tune,
clients call, servers reply—under the moon,
a tiny rabbit cheers: procedures are in bloom."

🚥 Pre-merge checks | ✅ 1 | ❌ 2
❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The pull request title 'custom procedures' is vague and generic, lacking specificity about the main changes. Consider using a more descriptive title such as 'Add custom procedures support to client and server' or 'Implement envelope-style custom procedure invocation' to better convey the primary change.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

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

✨ Finishing touches
  • 📝 Generate docstrings

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.

@mwillbanks
Copy link
Contributor Author

@coderabbitai review

@coderabbitai
Copy link

coderabbitai bot commented Dec 30, 2025

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

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: 1

🧹 Nitpick comments (7)
packages/orm/src/client/crud/validator/index.ts (1)

74-128: Validation logic is sound, but parsed values are discarded.

The validateProcedureArgs method validates each parameter but returns the original args array rather than the Zod-parsed values. This means:

  1. Type coercion is lost: Zod transformations (e.g., z.iso.datetime() coercing strings to Date objects) won't propagate to the procedure handler.

  2. Refinement side effects: Any .transform() or .refine() results are discarded.

If this is intentional (validation-only, no coercion), consider adding a comment. Otherwise, consider accumulating parsed values:

🔎 Proposed fix to return coerced values
+        const coercedArgs: unknown[] = [];
+
         // Validate each parameter using the procedure signature. Missing trailing optional parameters are allowed.
         for (let i = 0; i < procDef.params.length; i++) {
             const param = procDef.params[i]!;
             const value = args[i];

             if (typeof value === 'undefined') {
                 // When a value is not provided, allow it only if the parameter is optional.
                 // Note: callers can still explicitly pass `undefined`.
                 if (i >= args.length) {
                     if (param.optional) {
+                        coercedArgs.push(undefined);
                         continue;
                     }
                     throw createInvalidInputError(
                         `Missing procedure argument: ${param.name}`,
                         `$procedures.${proc}`,
                     );
                 }

                 if (param.optional) {
+                    coercedArgs.push(undefined);
                     continue;
                 }

                 throw createInvalidInputError(
                     `Invalid procedure argument: ${param.name} is required`,
                     `$procedures.${proc}`,
                 );
             }

             const schema = this.makeProcedureParamSchema(param);
             const parsed = schema.safeParse(value);
             if (!parsed.success) {
                 throw createInvalidInputError(
                     `Invalid procedure argument: ${param.name}: ${formatError(parsed.error)}`,
                     `$procedures.${proc}`,
                 );
             }
+            coercedArgs.push(parsed.data);
         }

-        return args;
+        return coercedArgs;
packages/language/src/validators/procedure-validator.ts (1)

6-10: Consider expanding reserved names for JavaScript runtime safety.

The reserved names list covers critical prototype-related names, which is good for preventing prototype pollution. Consider whether additional reserved names like hasOwnProperty, toString, or valueOf should be included to prevent potential runtime conflicts when procedures are exposed as object properties.

packages/language/src/utils.ts (1)

107-131: LGTM! Consider adding exhaustiveness check.

The expanded signature correctly handles both BuiltinType and ExpressionType inputs, making the function idempotent for expression types. The switch now covers all cases from the ExpressionType union.

For future maintainability, consider adding an exhaustiveness check to catch new types at compile time:

🔎 Optional: Add exhaustiveness check
         case 'Json':
         case 'Bytes':
             return 'Any';
+        default:
+            const _exhaustive: never = type;
+            return _exhaustive;
     }
 }
packages/server/src/api/rest/index.ts (1)

548-556: Consider simplifying SuperJSON payload detection.

The condition argsPayload && typeof argsPayload === 'object' && !Array.isArray(argsPayload) && 'meta' in (argsPayload as any) is duplicated between REST and RPC handlers. The processSuperJsonRequestPayload function already handles non-object cases gracefully (returns { result: payload } unchanged).

🔎 Suggested simplification
-        let processedArgsPayload = argsPayload;
-        if (argsPayload && typeof argsPayload === 'object' && !Array.isArray(argsPayload) && 'meta' in (argsPayload as any)) {
-            const { result, error } = await processSuperJsonRequestPayload(argsPayload);
-            if (error) {
-                return this.makeProcBadInputErrorResponse(error);
-            }
-            processedArgsPayload = result;
+        const { result: processedArgsPayload, error: superJsonError } = await processSuperJsonRequestPayload(argsPayload);
+        if (superJsonError) {
+            return this.makeProcBadInputErrorResponse(superJsonError);
         }
packages/server/src/api/rpc/index.ts (1)

180-292: Consider extracting shared procedure handling logic.

The handleProcedureRequest method is nearly identical to processProcedureRequest in the REST handler. Both perform:

  1. Procedure definition lookup
  2. HTTP method validation
  3. Argument extraction (body/query)
  4. SuperJSON payload processing
  5. Argument mapping and validation
  6. Procedure invocation
  7. Response serialization

Consider extracting this to the shared procedures.ts module to reduce duplication and ensure consistent behavior.

packages/orm/src/client/client-impl.ts (1)

217-227: Consider caching the $procs object.

The $procs getter creates a new object with wrapped handlers on every access. While this ensures fresh closure over this, repeated access patterns like client.$procs.foo(); client.$procs.bar(); will create the wrapper object multiple times.

🔎 Optional: Cache the procedures object
+    private _procs: Record<string, (...args: unknown[]) => Promise<unknown>> | undefined;
+
     get $procs() {
+        if (this._procs) {
+            return this._procs;
+        }
-        return Object.keys(this.$schema.procedures ?? {}).reduce((acc, name) => {
+        this._procs = Object.keys(this.$schema.procedures ?? {}).reduce((acc, name) => {
             acc[name] = (...args: unknown[]) => this.handleProc(name, args);
             return acc;
         }, {} as any);
+        return this._procs;
     }

Note: If caching, ensure the cache is invalidated when plugins change (in $use/$unuse).

packages/clients/tanstack-query/test/schemas/procedures/schema-lite.ts (1)

5-5: Minor: Remove extra blank line.

There's an extra blank line (line 5) that could be removed for consistency.

📜 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 65388a5 and 2b09acb.

⛔ Files ignored due to path filters (2)
  • packages/language/src/generated/ast.ts is excluded by !**/generated/**
  • packages/language/src/generated/grammar.ts is excluded by !**/generated/**
📒 Files selected for processing (33)
  • packages/cli/test/ts-schema-gen.test.ts
  • packages/clients/tanstack-query/src/react.ts
  • packages/clients/tanstack-query/src/svelte/index.svelte.ts
  • packages/clients/tanstack-query/src/vue.ts
  • packages/clients/tanstack-query/test/react-typing-test.ts
  • packages/clients/tanstack-query/test/schemas/procedures/schema-lite.ts
  • packages/clients/tanstack-query/test/svelte-typing-test.ts
  • packages/clients/tanstack-query/test/vue-typing-test.ts
  • packages/language/src/utils.ts
  • packages/language/src/validator.ts
  • packages/language/src/validators/common.ts
  • packages/language/src/validators/function-invocation-validator.ts
  • packages/language/src/validators/procedure-validator.ts
  • packages/language/src/zmodel.langium
  • packages/language/test/procedure-validation.test.ts
  • packages/orm/src/client/client-impl.ts
  • packages/orm/src/client/contract.ts
  • packages/orm/src/client/crud/validator/index.ts
  • packages/orm/src/client/index.ts
  • packages/orm/src/client/plugin.ts
  • packages/orm/test/procedures.test.ts
  • packages/schema/src/schema.ts
  • packages/sdk/src/ts-schema-generator.ts
  • packages/server/src/api/common/procedures.ts
  • packages/server/src/api/rest/index.ts
  • packages/server/src/api/rpc/index.ts
  • packages/server/test/api/procedures.e2e.test.ts
  • packages/server/test/api/rest.test.ts
  • packages/server/test/api/rpc.test.ts
  • tests/e2e/orm/schemas/procedures/input.ts
  • tests/e2e/orm/schemas/procedures/models.ts
  • tests/e2e/orm/schemas/procedures/schema.ts
  • tests/e2e/orm/schemas/procedures/schema.zmodel
🧰 Additional context used
📓 Path-based instructions (2)
tests/e2e/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

E2E tests should validate real-world schema compatibility with established projects

Files:

  • tests/e2e/orm/schemas/procedures/schema.ts
  • tests/e2e/orm/schemas/procedures/input.ts
  • tests/e2e/orm/schemas/procedures/models.ts
**/*.zmodel

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.zmodel: Always run zenstack generate after modifying ZModel schemas
ZModel schema files should define database structure and policies that compile to TypeScript via zenstack generate

Files:

  • tests/e2e/orm/schemas/procedures/schema.zmodel
🧠 Learnings (9)
📓 Common learnings
Learnt from: CR
Repo: zenstackhq/zenstack-v3 PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-26T01:55:04.540Z
Learning: Applies to packages/zenstackhq/orm/**/*.{ts,tsx} : Implement plugin hooks at ORM, Kysely, and entity mutation levels for query interception and customization
Learnt from: CR
Repo: zenstackhq/zenstack-v3 PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-26T01:55:04.540Z
Learning: Applies to packages/zenstackhq/orm/**/*.{ts,tsx} : Use Kysely as the query builder interface for low-level database queries, avoiding raw SQL when possible
Learnt from: CR
Repo: zenstackhq/zenstack-v3 PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-26T01:55:04.540Z
Learning: Applies to packages/zenstackhq/orm/**/*.test.{ts,tsx} : ORM package tests should include comprehensive client API tests and policy tests
Learnt from: CR
Repo: zenstackhq/zenstack-v3 PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-26T01:55:04.540Z
Learning: Applies to **/*.zmodel : ZModel schema files should define database structure and policies that compile to TypeScript via `zenstack generate`
Learnt from: CR
Repo: zenstackhq/zenstack-v3 PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-26T01:55:04.540Z
Learning: Database migrations should use Prisma CLI under the hood via ZenStack commands
📚 Learning: 2025-11-26T01:55:04.540Z
Learnt from: CR
Repo: zenstackhq/zenstack-v3 PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-26T01:55:04.540Z
Learning: Applies to tests/e2e/**/*.{ts,tsx} : E2E tests should validate real-world schema compatibility with established projects

Applied to files:

  • packages/cli/test/ts-schema-gen.test.ts
  • packages/language/test/procedure-validation.test.ts
  • tests/e2e/orm/schemas/procedures/schema.ts
  • packages/clients/tanstack-query/test/vue-typing-test.ts
  • packages/clients/tanstack-query/test/schemas/procedures/schema-lite.ts
  • packages/orm/test/procedures.test.ts
  • packages/server/test/api/rpc.test.ts
  • packages/clients/tanstack-query/test/react-typing-test.ts
  • packages/clients/tanstack-query/test/svelte-typing-test.ts
  • packages/server/test/api/procedures.e2e.test.ts
  • packages/server/test/api/rest.test.ts
  • tests/e2e/orm/schemas/procedures/models.ts
📚 Learning: 2025-11-26T01:55:04.540Z
Learnt from: CR
Repo: zenstackhq/zenstack-v3 PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-26T01:55:04.540Z
Learning: Applies to tests/**/type*.{ts,tsx} : Ensure TypeScript inference and type coverage are validated through type coverage tests

Applied to files:

  • packages/cli/test/ts-schema-gen.test.ts
  • packages/language/test/procedure-validation.test.ts
  • packages/clients/tanstack-query/test/vue-typing-test.ts
  • packages/clients/tanstack-query/test/react-typing-test.ts
  • packages/clients/tanstack-query/test/svelte-typing-test.ts
📚 Learning: 2025-11-26T01:55:04.540Z
Learnt from: CR
Repo: zenstackhq/zenstack-v3 PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-26T01:55:04.540Z
Learning: Applies to **/*.zmodel : ZModel schema files should define database structure and policies that compile to TypeScript via `zenstack generate`

Applied to files:

  • packages/cli/test/ts-schema-gen.test.ts
  • tests/e2e/orm/schemas/procedures/schema.ts
  • packages/orm/src/client/contract.ts
  • packages/clients/tanstack-query/test/schemas/procedures/schema-lite.ts
  • tests/e2e/orm/schemas/procedures/schema.zmodel
  • tests/e2e/orm/schemas/procedures/models.ts
📚 Learning: 2025-11-26T01:55:04.540Z
Learnt from: CR
Repo: zenstackhq/zenstack-v3 PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-26T01:55:04.540Z
Learning: Applies to packages/zenstackhq/orm/**/*.test.{ts,tsx} : ORM package tests should include comprehensive client API tests and policy tests

Applied to files:

  • packages/language/test/procedure-validation.test.ts
  • packages/orm/src/client/index.ts
  • packages/clients/tanstack-query/test/vue-typing-test.ts
  • packages/orm/test/procedures.test.ts
  • packages/server/test/api/rpc.test.ts
  • packages/clients/tanstack-query/test/react-typing-test.ts
  • packages/clients/tanstack-query/test/svelte-typing-test.ts
  • packages/server/test/api/procedures.e2e.test.ts
  • packages/server/test/api/rest.test.ts
  • tests/e2e/orm/schemas/procedures/models.ts
📚 Learning: 2025-11-26T01:55:04.540Z
Learnt from: CR
Repo: zenstackhq/zenstack-v3 PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-26T01:55:04.540Z
Learning: Applies to packages/zenstackhq/orm/**/*.{ts,tsx} : Implement plugin hooks at ORM, Kysely, and entity mutation levels for query interception and customization

Applied to files:

  • packages/orm/src/client/index.ts
  • tests/e2e/orm/schemas/procedures/schema.ts
  • packages/clients/tanstack-query/src/vue.ts
  • packages/clients/tanstack-query/test/schemas/procedures/schema-lite.ts
  • packages/orm/test/procedures.test.ts
  • packages/orm/src/client/plugin.ts
  • packages/clients/tanstack-query/src/react.ts
  • packages/clients/tanstack-query/test/svelte-typing-test.ts
  • packages/clients/tanstack-query/src/svelte/index.svelte.ts
  • tests/e2e/orm/schemas/procedures/models.ts
  • packages/orm/src/client/client-impl.ts
📚 Learning: 2025-10-21T16:09:31.218Z
Learnt from: ymc9
Repo: zenstackhq/zenstack-v3 PR: 319
File: packages/runtime/src/client/executor/zenstack-query-executor.ts:63-72
Timestamp: 2025-10-21T16:09:31.218Z
Learning: In ZenStack, TypeDefs can be inherited by models. When a TypeDef contains fields with `map` attributes, those mapped field names need to be processed by the QueryNameMapper since they become part of the inheriting model's schema. Therefore, when checking if a schema has mapped names (e.g., in `schemaHasMappedNames`), both `schema.models` and `schema.typeDefs` must be inspected for `@map` and `map` attributes.

Applied to files:

  • tests/e2e/orm/schemas/procedures/schema.ts
  • packages/clients/tanstack-query/test/schemas/procedures/schema-lite.ts
📚 Learning: 2025-11-26T01:55:04.540Z
Learnt from: CR
Repo: zenstackhq/zenstack-v3 PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-26T01:55:04.540Z
Learning: Applies to packages/zenstackhq/orm/**/*.{ts,tsx} : Use Kysely as the query builder interface for low-level database queries, avoiding raw SQL when possible

Applied to files:

  • packages/clients/tanstack-query/src/vue.ts
  • packages/clients/tanstack-query/test/schemas/procedures/schema-lite.ts
  • packages/orm/test/procedures.test.ts
  • packages/clients/tanstack-query/test/react-typing-test.ts
  • packages/clients/tanstack-query/src/react.ts
  • packages/clients/tanstack-query/test/svelte-typing-test.ts
  • tests/e2e/orm/schemas/procedures/input.ts
  • tests/e2e/orm/schemas/procedures/models.ts
📚 Learning: 2025-11-26T01:55:04.540Z
Learnt from: CR
Repo: zenstackhq/zenstack-v3 PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-26T01:55:04.540Z
Learning: Applies to packages/zenstackhq/cli/**/*.test.{ts,tsx} : CLI package tests should focus on action-specific tests for each command

Applied to files:

  • packages/orm/test/procedures.test.ts
🧬 Code graph analysis (18)
packages/language/src/validators/function-invocation-validator.ts (1)
packages/language/src/utils.ts (2)
  • mapBuiltinTypeToExpressionType (107-131)
  • typeAssignable (84-102)
packages/language/src/validator.ts (2)
packages/language/src/generated/ast.ts (2)
  • Procedure (647-655)
  • Procedure (657-657)
packages/language/src/validators/procedure-validator.ts (1)
  • ProcedureValidator (15-55)
packages/orm/src/client/contract.ts (2)
packages/schema/src/schema.ts (6)
  • SchemaDef (11-19)
  • GetEnums (137-137)
  • GetEnum (139-141)
  • GetModels (122-122)
  • GetTypeDefs (143-143)
  • ProcedureDef (82-87)
packages/orm/src/utils/type-utils.ts (1)
  • OrUndefinedIf (80-80)
packages/clients/tanstack-query/test/vue-typing-test.ts (1)
packages/clients/tanstack-query/src/vue.ts (1)
  • useClientQueries (276-332)
packages/clients/tanstack-query/src/vue.ts (5)
packages/schema/src/schema.ts (1)
  • SchemaDef (11-19)
packages/orm/src/client/contract.ts (1)
  • ProcedureFunc (261-263)
packages/clients/tanstack-query/src/react.ts (5)
  • ProcedureHooks (192-199)
  • useInternalProcedureMutation (457-470)
  • useInternalProcedureInfiniteQuery (428-442)
  • useInternalQuery (588-609)
  • useInternalInfiniteQuery (634-661)
packages/clients/tanstack-query/src/svelte/index.svelte.ts (5)
  • ProcedureHooks (171-178)
  • useInternalProcedureMutation (339-352)
  • useInternalProcedureInfiniteQuery (326-337)
  • useInternalQuery (444-474)
  • useInternalInfiniteQuery (476-508)
packages/clients/tanstack-query/src/common/types.ts (2)
  • QueryContext (9-24)
  • ExtraQueryOptions (29-34)
packages/language/src/validators/common.ts (1)
packages/language/src/ast.ts (2)
  • AstNode (4-4)
  • AstNode (66-71)
packages/clients/tanstack-query/test/schemas/procedures/schema-lite.ts (2)
packages/schema/src/schema.ts (1)
  • SchemaDef (11-19)
packages/schema/src/expression-utils.ts (1)
  • ExpressionUtils (19-123)
packages/orm/test/procedures.test.ts (3)
packages/orm/src/client/index.ts (1)
  • ZenStackClient (1-1)
packages/orm/src/dialects/sqlite.ts (1)
  • SqliteDialect (1-1)
packages/orm/src/client/plugin.ts (1)
  • definePlugin (58-60)
packages/server/test/api/rpc.test.ts (2)
packages/testtools/src/client.ts (1)
  • createPolicyTestClient (258-269)
packages/server/src/api/rpc/index.ts (1)
  • RPCApiHandler (36-377)
packages/orm/src/client/plugin.ts (2)
packages/schema/src/schema.ts (1)
  • SchemaDef (11-19)
packages/orm/src/client/contract.ts (1)
  • ClientContract (65-199)
packages/clients/tanstack-query/test/react-typing-test.ts (3)
packages/clients/tanstack-query/test/schemas/procedures/schema-lite.ts (1)
  • schema (62-62)
tests/e2e/orm/schemas/procedures/schema.ts (1)
  • schema (173-173)
packages/clients/tanstack-query/test/schemas/basic/schema-lite.ts (1)
  • schema (172-172)
packages/clients/tanstack-query/src/react.ts (4)
packages/schema/src/schema.ts (1)
  • SchemaDef (11-19)
packages/orm/src/client/contract.ts (1)
  • ProcedureFunc (261-263)
packages/clients/tanstack-query/src/common/types.ts (2)
  • QueryContext (9-24)
  • ExtraQueryOptions (29-34)
packages/clients/tanstack-query/src/common/query-key.ts (1)
  • getQueryKey (25-35)
packages/clients/tanstack-query/test/svelte-typing-test.ts (2)
packages/clients/tanstack-query/src/svelte/index.svelte.ts (1)
  • useClientQueries (264-314)
packages/clients/tanstack-query/test/schemas/procedures/schema-lite.ts (1)
  • schema (62-62)
packages/language/src/utils.ts (2)
packages/schema/src/schema.ts (1)
  • BuiltinType (89-99)
packages/language/src/generated/ast.ts (2)
  • BuiltinType (103-103)
  • ExpressionType (125-125)
packages/orm/src/client/crud/validator/index.ts (4)
packages/schema/src/schema.ts (2)
  • ProcedureDef (82-87)
  • BuiltinType (89-99)
packages/common-helpers/src/tiny-invariant.ts (1)
  • invariant (4-14)
packages/orm/src/client/errors.ts (2)
  • createInvalidInputError (116-120)
  • createInternalError (147-151)
packages/orm/src/utils/zod-utils.ts (1)
  • formatError (7-9)
packages/server/test/api/procedures.e2e.test.ts (3)
packages/orm/src/client/contract.ts (1)
  • ClientContract (65-199)
packages/server/src/api/rest/index.ts (1)
  • schema (285-287)
packages/testtools/src/client.ts (1)
  • createTestClient (101-248)
packages/schema/src/schema.ts (1)
packages/language/src/generated/ast.ts (2)
  • ProcedureParam (663-669)
  • ProcedureParam (671-671)
packages/server/src/api/rpc/index.ts (2)
packages/server/src/api/common/procedures.ts (3)
  • getProcedureDef (8-14)
  • processSuperJsonRequestPayload (44-66)
  • validateProcedureArgs (154-166)
packages/orm/src/client/errors.ts (1)
  • ORMError (66-104)
🪛 GitHub Actions: Build and Test
packages/clients/tanstack-query/test/react-typing-test.ts

[error] 119-119: test:typecheck failed. TypeScript error TS2345: Argument of type '"bob"' is not assignable to parameter of type '[] | [string | undefined] | undefined'.

⏰ 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). (1)
  • GitHub Check: GitGuardian Security Checks
🔇 Additional comments (57)
packages/sdk/src/ts-schema-generator.ts (1)

1126-1204: LGTM! Array flag propagation for procedures is correctly implemented.

The changes properly emit:

  1. array: true in the parameter object literal when param.type.array is true (line 1134)
  2. An array property signature in the TypeScript tuple type (lines 1165-1174)
  3. returnArray: true on the procedure object when proc.returnType.array is true (lines 1197-1199)

The implementation follows the established patterns for optional flags and aligns with the type definitions in packages/schema/src/schema.ts.

packages/schema/src/schema.ts (1)

80-87: LGTM! Type definitions correctly extended for procedure array support.

The updates to ProcedureParam and ProcedureDef properly introduce:

  • array?: boolean flag for parameters
  • readonly modifier for params tuple (appropriate for immutable schema definitions)
  • returnArray?: boolean for return type arrays

These changes align with the TS schema generator and enable proper type inference across the codebase.

packages/cli/test/ts-schema-gen.test.ts (1)

187-209: LGTM! Good test coverage for procedure array flags.

The test case properly validates:

  • Array-typed parameters (ids: Int[]) emit array: true
  • Array return types emit returnArray: true
  • Both single-parameter and zero-parameter procedures are covered

This aligns with the learnings that TypeScript type coverage should be validated through tests.

packages/orm/src/client/contract.ts (3)

242-259: Good backward-compatible API extension with $procs and $procedures aliases.

The dual exposure pattern maintains backward compatibility while introducing the preferred $procs namespace. Documentation comments clearly indicate the preferred API.


265-287: Well-designed type mapping for procedure parameters and returns.

The type helpers correctly handle:

  • MapProcedureReturn: Array wrapping based on returnArray flag
  • MapProcedureParam: Array wrapping based on array flag with conditional undefined for optional params
  • MapProcedureParams: Recursive union construction allowing trailing optional params to be omitted

The recursive tuple prefix union in MapProcedureParams (lines 281-287) is a clever approach for modeling optional trailing parameters in function signatures.


227-240: LGTM! Enhanced type mapping with enum support.

The EnumValue type and updated MapType properly resolve enum types to their value union, enabling correct type inference for procedures returning enum values.

packages/language/src/zmodel.langium (2)

228-229: LGTM! Optional mutation flag enables query procedures.

Making mutation optional allows procedures to default to query behavior (non-mutation), which aligns with the common pattern where most procedures are read operations.


236-237: LGTM! Extended type vocabulary for procedures.

The additions of BigInt, Decimal, Json, Bytes, Void, and Undefined to both RegularIDWithTypeNames and ExpressionType enable richer type expressions in procedure signatures, consistent with the _TypeMap extensions in packages/orm/src/client/contract.ts.

Also applies to: 268-269

packages/server/src/api/common/procedures.ts (3)

1-14: LGTM! Well-structured procedure lookup utility.

The getProcedureDef function uses safe property access via Object.prototype.hasOwnProperty.call to avoid prototype pollution issues. The PROCEDURE_ROUTE_PREFIXES constant centralizes the endpoint paths for consistency.


68-152: Comprehensive argument mapping with good edge case handling.

The mapProcedureArgs function handles multiple input formats elegantly:

  • Zero-parameter procedures with empty/undefined payloads (lines 73-84)
  • All-optional parameters with undefined payload (lines 87-89)
  • Single array-typed parameter disambiguation (lines 93-96)
  • Positional array arguments (lines 91-111)
  • Single parameter with object wrapping detection (lines 113-128)
  • Named object arguments with unknown key rejection (lines 136-141)

The unknown key rejection (lines 137-140) is a good defensive measure to catch user mistakes early.


154-166: LGTM! Validation respects global client options.

The validateProcedureArgs function correctly defers to the client's validateInput option, maintaining consistency with the ORM's validation behavior.

packages/clients/tanstack-query/src/vue.ts (4)

65-83: LGTM! Procedure type utilities follow established patterns.

The type helpers (ExtractProcedures, ProcedureArgsTuple, ProcedureReturn, ProcedurePayload) correctly derive procedure types from the schema and align with the React client implementation for cross-framework consistency.


148-188: LGTM! Well-typed procedure hook group.

The ProcedureHookGroup type correctly branches based on the mutation flag:

  • Mutation procedures expose useMutation only
  • Non-mutation procedures expose useQuery and useInfiniteQuery

This mirrors the React implementation and provides a consistent API across frameworks.


300-331: LGTM! Dynamic procedure hook building.

The buildProcedureHooks function correctly iterates over schema procedures and wires:

  • useMutation for mutation procedures
  • useQuery/useInfiniteQuery for non-mutation procedures

The hooks are attached to the $procs namespace, matching the ORM contract.


362-375: LGTM! Procedure mutation hook implementation.

The useInternalProcedureMutation correctly:

  • Uses makeUrl with $procs endpoint model
  • Sends POST request with procedure data
  • Integrates with Vue's reactivity via toValue
packages/clients/tanstack-query/src/svelte/index.svelte.ts (4)

66-83: LGTM! Procedure type utilities consistent with Vue/React implementations.

The type helpers are identical across frameworks, ensuring consistent type inference for procedure arguments and returns.


147-178: LGTM! Svelte-adapted procedure hook types.

The ProcedureHookGroup correctly uses Svelte's Accessor pattern (instead of Vue's MaybeRefOrGetter) while maintaining the same mutation/non-mutation branching logic.


288-314: LGTM! Procedure hook building consistent with Vue implementation.

The dynamic hook building follows the same pattern as Vue, correctly wiring mutation and non-mutation procedures to appropriate hooks.


339-352: LGTM! Svelte procedure mutation hook.

Uses createMutation (Svelte's equivalent of useMutation) with the correct endpoint model and request format.

packages/orm/src/client/index.ts (1)

6-6: LGTM! Public API extension for procedure validation.

The export of InputValidator is a clean addition to the ORM client's public API surface, enabling procedure argument validation in REST/RPC server layers.

packages/language/src/validators/function-invocation-validator.ts (1)

23-23: LGTM! Consistent type mapping for scalar argument validation.

The addition of mapBuiltinTypeToExpressionType ensures that scalar argument types (like BigInt, Decimal, Json, Bytes) are consistently mapped to their expression equivalents (Int, Float, Any) before type compatibility checks. This aligns with the broader type resolution behavior introduced across the validator subsystem.

Also applies to: 177-179

packages/language/test/procedure-validation.test.ts (1)

1-43: LGTM! Good baseline test coverage for procedure validation.

The test file covers the essential validation scenarios:

  • Unknown parameter types
  • Unknown return types
  • Reserved procedure names

The tests appropriately use loadSchemaWithError with regex matching to verify error reporting.

packages/language/src/validators/common.ts (1)

22-27: LGTM! Important security improvement to prevent prototype pollution.

Using Object.create(null) creates a null-prototype map that prevents collisions with built-in prototype properties like __proto__ and constructor. This is particularly important for procedure validation where reserved names must be safely grouped by name.

packages/clients/tanstack-query/test/svelte-typing-test.ts (1)

3-3: LGTM! Comprehensive procedure typing tests for Svelte.

The test additions cover:

  • Query procedures (greet, greetMany) with optional arguments and data transformations
  • Mutation procedures (sum) with array arguments
  • Type safety checks for invalid usage (e.g., attempting useMutation on a query procedure)
  • Both $procs and $procedures alias paths
  • Query key validation for procedures

The tests mirror existing patterns and provide strong type coverage for the new procedure support.

Also applies to: 6-6, 112-131

packages/server/test/api/rest.test.ts (1)

3167-3401: LGTM! Comprehensive REST procedure test coverage.

The new test suite thoroughly exercises the REST procedure endpoints with excellent coverage:

  • Serialization: SuperJSON with metadata for complex types (Decimal)
  • Argument handling: Optional params, required params, array types, enums, typedefs
  • HTTP methods: GET for queries, POST for mutations
  • Error cases: Missing required args, wrong types, too many args, unknown arg keys
  • Path variants: Both /$procs and alias paths

The tests are well-structured, clear, and follow established patterns in the file.

packages/language/src/validator.ts (1)

12-12: LGTM! Clean integration of procedure validation.

The changes properly wire ProcedureValidator into the validation flow:

  • Added necessary imports
  • Registered Procedure validation check in the checks mapping
  • Implemented checkProcedure method following the established pattern

The integration is consistent with existing validators and maintains the validator architecture.

Also applies to: 24-24, 45-45, 96-98

packages/clients/tanstack-query/test/react-typing-test.ts (1)

117-136: The React test code is correct and matches the hook type signatures.

The current implementation in react-typing-test.ts line 119 (useQuery('bob')) is valid. React's procedure hooks accept ProcedurePayload<Schema, Name> directly (not wrapped), which for the greet procedure resolves to string | undefined. The test passing a string value is correct.

The differences between React, Svelte, and Vue are intentional and framework-specific:

  • React: Accepts direct values (args?: ProcedurePayload<Schema, Name>)
  • Svelte: Requires function accessors (args?: Accessor<ProcedurePayload<Schema, Name>>) due to Svelte's reactivity model
  • Vue: Accepts flexible reactive patterns (args?: MaybeRefOrGetter<ProcedurePayload<Schema, Name>>)

These patterns are not bugs but correct framework-specific implementations. No changes are needed.

packages/server/test/api/procedures.e2e.test.ts (1)

1-103: Well-structured E2E test for procedure handling.

The test suite covers essential procedure scenarios:

  • Query procedure with valid/invalid input
  • Mutation procedure with transactional rollback verification
  • HTTP method enforcement (GET for queries, POST for mutations)

A few observations:

  1. Line 86: The requestBody: ['a@a.com', 'b@b.com'] format tests array-to-positional-args mapping, which is good coverage for the RPC layer.

  2. Consider adding edge cases: The test could be extended to cover:

    • Procedure not found (404 or similar)
    • Empty/null arguments for optional parameters

Based on learnings, E2E tests should validate real-world schema compatibility. This test uses an inline schema which is acceptable for unit-style E2E tests, but consider adding tests against established project schemas in a separate suite.

packages/clients/tanstack-query/test/vue-typing-test.ts (1)

114-134: Comprehensive procedure typing tests for Vue client.

The type-level tests effectively validate:

  • Query procedures expose useQuery and useInfiniteQuery but not useMutation (Line 122-123)
  • Mutation procedures expose useMutation with correct argument tuple types (Line 126-128)
  • Both $procs and $procedures aliases are accessible (Line 121, 134)
  • Optional parameters work correctly (Line 116 with undefined)
  • queryKey access is typed (Lines 118, 120)

The @ts-expect-error guards appropriately enforce type constraints at compile time.

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

130-159: Model-typed parameters use shallow validation.

Line 139 accepts z.record(z.string(), z.unknown()) for model-typed parameters, which only validates the value is an object with string keys. This is documented inline but worth noting:

  • No field-level validation against the model schema
  • Allows extra/missing fields

This is reasonable for procedure inputs where the handler may perform custom logic, but callers should be aware that model-typed procedure arguments aren't deeply validated.

tests/e2e/orm/schemas/procedures/schema.ts (1)

1-173: Auto-generated schema with comprehensive procedure definitions.

This generated file provides good test coverage for procedure variants:

  • Various return types: User, User[], Role, Role[], Overview, Null, Void, Undefined, String, Json, Bytes
  • Parameter variations: required, optional (greet), array (findByIds)
  • No-param procedures for testing edge cases

The tuple type assertions (e.g., as [ id: { "name": "id"; "type": "Int"; } ]) ensure type-level param info is preserved.

tests/e2e/orm/schemas/procedures/models.ts (1)

1-13: Standard auto-generated models file.

The file correctly re-exports:

  • User type via $ModelResult
  • Overview type via $TypeDefResult
  • Role as both a const (values object) and type (union)

This pattern enables consumers to import typed model shapes and enum values from the generated schema.

packages/orm/src/client/plugin.ts (2)

39-43: Well-designed procedure interception hook.

The onProcedure hook follows the established pattern of onQuery and provides a clean interception point for procedure invocations.


64-95: Comprehensive hook context with clear documentation.

The OnProcedureHookContext type is well-documented and provides:

  • name: Procedure identifier
  • mutation: Distinguishes query vs mutation semantics
  • args: Immutable (readonly) input arguments
  • proceed: Continuation function allowing argument transformation
  • client: Access to the invoking client for additional operations

The readonly unknown[] type for args and proceed parameter enforces immutability at the type level, encouraging plugins to create new arrays rather than mutate.

Based on learnings, implementing plugin hooks at the ORM level for query interception is expected, and this aligns with that pattern.

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

128-213: Thorough RPC procedure test coverage.

This test block validates core procedure RPC behavior:

  • HTTP method enforcement: GET for queries (Lines 170-177), POST for mutations (Lines 188-194)
  • Error messaging for method violations (Lines 179-186, 196-202)
  • Falsy return serialization (false preserved, Line 207)
  • Undefined serialization with meta (Lines 209-212)

The helper handleProcRequest pattern keeps tests readable.


215-325: Excellent edge case coverage for procedure argument handling.

The test block covers important edge cases:

Scenario Lines Status Code
Multi-arg (>2) object mapping 267-273 200
All-optional params, no payload 276-278 200
Array-typed single param 281-287 200
Enum param 290-292 200
TypeDef param (object) 295-301 200
Wrong type input 304-306 422
Too many args 309-315 400
Unknown keys in args 318-324 400

This provides confidence in the argument mapping and validation logic.

packages/orm/test/procedures.test.ts (4)

15-35: Good coverage for procedure alias exposure.

Tests verify both $procs and $procedures accessors work identically, ensuring backward compatibility and alias consistency.


37-66: Error handling tests for configuration issues.

Properly tests two distinct error scenarios:

  1. Procedures defined in schema but no procedures option provided (Line 49)
  2. Procedures option provided but specific handler missing (Line 65)

These provide clear error messages for common configuration mistakes.


89-133: Hook execution order demonstrates LIFO plugin stack.

The test verifies:

  1. Plugins execute in reverse order (p2 before p1, Line 132)
  2. Argument mutations propagate through the chain (1+1=2 passed to handler, result = 2+2 = 4, Line 131)

This LIFO ordering (last plugin registered executes first) should be documented, as it affects how users compose plugins. Consider adding a brief comment in the plugin.ts documentation or here.

Please verify the intended plugin execution order (LIFO vs FIFO) is documented somewhere for users. The test shows [p1, p2] registration results in ['p2:add', 'p1:add'] execution order.


135-154: Transaction wrapping verification for mutation procedures.

The test elegantly uses c.isTransaction to verify:

  • Non-mutation procedures (qry) run outside transactions (Line 152)
  • Mutation procedures (mut) are automatically wrapped in transactions (Line 153)

This validates the transactional semantics mentioned in the PR context without requiring actual database state verification.

packages/language/src/validators/procedure-validator.ts (2)

36-44: LGTM!

The parameter type validation correctly checks for unresolved type references by verifying typeRef && !typeRef.ref. This properly handles the case where a type is referenced but the reference cannot be resolved.


46-54: LGTM!

Return type validation mirrors parameter validation logic, maintaining consistency. The error message properly includes the reference text for debugging.

packages/server/src/api/rest/index.ts (4)

346-349: LGTM!

Clean integration of procedure handling into the REST API path routing. The /$procs/ prefix is a sensible convention that clearly distinguishes procedure calls from model-based REST endpoints.


501-519: LGTM!

Proper enforcement of HTTP method semantics: mutations require POST, queries require GET. This aligns with REST conventions where mutations are non-idempotent operations.


619-653: LGTM!

Well-structured ORM error handling with proper HTTP status code mapping. The use of ts-pattern's match provides clear, exhaustive handling of error reasons.


577-577: Remove the suggestion for explicit validation of $procedures.

The $procedures property is a getter that always returns an object (never undefined), even when no procedures are configured. The optional chaining is already defensive, and procedure validation is handled by validateProcedureArgs() before this point.

packages/server/src/api/rpc/index.ts (2)

56-64: LGTM!

Clean integration point for procedure handling. The $procs model name convention clearly distinguishes procedure calls from model operations.


155-161: Good improvement to response serialization.

Always applying SuperJSON serialization ensures consistent handling of special types (Date, BigInt, etc.) and falsy values. The comment noting falsy value handling is helpful.

packages/orm/src/client/client-impl.ts (2)

254-278: LGTM! Well-structured plugin pipeline.

The plugin chain correctly:

  1. Wraps the original handler in a proceed function
  2. Iterates plugins and wraps each onProcedure hook
  3. Passes through args allowing plugins to modify them
  4. Maintains the chain by capturing previous proceed reference

This mirrors the CRUD operation plugin pattern, ensuring consistency.


280-286: LGTM! Sensible transaction semantics.

Auto-wrapping mutation procedures in transactions provides atomic behavior by default, which is the expected behavior for data-modifying operations. The check for !this.isTransaction correctly prevents nested transaction creation.

tests/e2e/orm/schemas/procedures/schema.zmodel (1)

1-35: Comprehensive test fixture for procedure types.

This schema provides excellent coverage for procedure validation and type generation:

  • Scalar returns (getRole, greet)
  • Array returns (listUsers, findByIds, getRoles)
  • Complex type returns (overview)
  • Special types (getNull, getVoid, getUndefined)
  • Array parameters (findByIds)
  • Optional parameters (greet)
  • Special data types (echoJson, echoBytes)

As per coding guidelines, remember to run zenstack generate after any modifications to this schema.

packages/clients/tanstack-query/test/schemas/procedures/schema-lite.ts (1)

38-56: LGTM! Good coverage of procedure variants.

The procedure definitions cover the key typing scenarios:

  • greet: Optional parameter, scalar return
  • greetMany: Optional parameter, array return (returnArray: true)
  • sum: Required parameters, mutation flag

This provides good coverage for validating TanStack Query hook type inference.

tests/e2e/orm/schemas/procedures/input.ts (1)

1-30: LGTM!

This is auto-generated code following the established pattern for ORM input types. The type aliases correctly parameterize the generic ORM types with the schema and model name.

packages/clients/tanstack-query/src/react.ts (4)

65-83: LGTM! Well-structured procedure type utilities.

The type definitions correctly extract procedure information from the schema and handle the various argument configurations (none, single, multiple) appropriately.


158-199: LGTM! Clean conditional hook group types.

The ProcedureHookGroup correctly differentiates between mutation and query procedures, and ProcedureHooks properly uses conditional typing to only expose $procs when procedures exist in the schema.


341-377: LGTM! Runtime procedure hook wiring is correctly implemented.

The implementation correctly:

  • Detects procedures from the schema
  • Differentiates between mutation and query procedures
  • Merges root options with hook-specific options
  • Follows the same pattern as model query hooks

391-394: Appropriate: Optimistic updates disabled for procedure queries.

Procedures correctly disable optimistic updates (optimisticUpdate: false) since they're arbitrary functions rather than entity CRUD operations, so there's no predictable data structure for optimistic state management.

@sanny-io
Copy link
Contributor

sanny-io commented Dec 30, 2025

Insane turnaround time on this @mwillbanks. Well done.

What do you all think of also having the ability to define procedures under models?

model User {
    ...
    mutation procedure signUp(info: SignUpInfo): User
}

I bring this up because I foresee that there will be a lot of unrelated functions grouped together both in the procedures' implementation, and in the intellisense when you're trying to call a procedure, and if this feature is not considered at this stage, it could be a breaking change.

For example, the above procedure really has no business being grouped together with the below procedure.

model Invoice {
    ...
    mutation procedure process(id: String): Void
}

In larger code bases especially, things will become very difficult to follow because procedures simply will not be organized.

With these "model-scoped procedures" (I don't know what else to call them lol), they could be invoked like db.$procs.user.signUp() or db.user.$procs.signUp. I'm thinking that this would be a strictly organizational feature, and that there would be no difference between how these are handled at runtime.

Model-scoped procedures could be disambiguated from "top-level" procedures by simply automatically prefixing the procedure's name with the model name and using a dot like user.signUp.

That leaves the question of how the procedures should then be defined. Something like this maybe?

const db = new ZenStackClient(schema, {
    ...,
    procedures: {
        // db.$procs.user.signUp or db.user.$procs.signUp
        user: {
            async signUp(client, args) {
                ...
            },
        },

        // db.$procs.topLevelProcedure
        $global: {
            async topLevelProcedure(client, args) {
                ...
            },
        },
    },
};)

@mwillbanks
Copy link
Contributor Author

Insane turnaround time on this @mwillbanks. Well done.

Thanks :) I have a very big need for this so need to get something going here quickly that I can start leveraging.

What do you all think of also having the ability to define procedures under models?

model User {
    ...
    mutation procedure signUp(info: SignUpInfo): User
}

...

In larger code bases especially, things will become very difficult to follow because procedures simply will not be organized.

With these "model-scoped procedures" (I don't know what else to call them lol), they could be invoked like db.$procs.user.signUp() or db.user.$procs.signUp. I'm thinking that this would be a strictly organizational feature, and that there would be no difference between how these are handled at runtime.

You bring up a very good point... I don't think it would be too difficult to add it but certainly could be something that could follow. I was thinking through that while I was working on this last night and I was thinking about some type of logical grouping. The problem I foresee here will be dealing with the policy plugin as now we're talking about nesting and that'll certainly have some various changes. This may be better to do in a future release, I like the idea of it but the implementation of it can make things far more challenging. What I think could be difficult here is that if you do this inside of a model, I believe the expected return should more than likely then be either the model or a value within a particular record.

Model-scoped procedures could be disambiguated from "top-level" procedures by simply automatically prefixing the procedure's name with the model name and using a dot like user.signUp.

That leaves the question of how the procedures should then be defined. Something like this maybe?

const db = new ZenStackClient(schema, {
    ...,
    procedures: {
        // db.$procs.user.signUp or db.user.$procs.signUp
        user: {
            async signUp(client, args) {
                ...
            },
        },

        // db.$procs.topLevelProcedure
        $global: {
            async topLevelProcedure(client, args) {
                ...
            },
        },
    },
};)

So this is where it gets really interesting... at this point I feel like it leads to instance based procs where you could say do something like: db.user.findFirst({ where: { id: 1 } }).$procs.doSomethingWithUser() basically when it's scoped inside of a model, I almost expect it to work on the instance of a model and it could expose some cool behaviors in the future so I think there is more to it than just global and scoped and maybe that has some syntax of stating instance...

Alternatively, it could be possible to allow more freehand organization of the top level to basically achieve the same thing:

const db = new ZenStackClient(schema, {
  ...,
  procedures: {
    async foo(client, args) {
      // ..
    },
    bar: {
      async baz(client, args) {
        // ...
      },
    },
  },
});

Which achieves a similar design as to what you mention but allows for categorization without purely tying it to the model and is safer in that respect to be able to organize it without other potential issues.

@mwillbanks

This comment was marked as resolved.

@ymc9
Copy link
Member

ymc9 commented Dec 31, 2025

Hi @mwillbanks , this is truly great work done at an amazing pace! I haven't thoroughly gone through all details yet, but it overall looks very solid.

Here are my thoughts about your RFC. In fact, I feel what you implemented is already very good as the first release of this feature.

Access Control

My original thought was that procedures don't involve a new kind of access control. Instead, if a proc calls into the ORM client (in most cases, it should), it's automatically governed by the underlying CRUD rules, provided the call is initiated with a policy-enabled ORM client. Conceptually, it's equivalent to a Postgres stored procedure + RLS.

The additional challenge with "execute" policy kind, if we were to introduce it, is that since procs are not bound to specific models, the policy rules can pretty much only access auth(), no fields, no relations. Such simple rules can be easily expressed outside of ZModel (e.g., as a configuration of QaaS API handlers). The main advantage of expressing policies in ZModel is that it allows you freely traverse the db relation graph.

Grouping

I do share @sanny-io 's concern that the TS API can become cluttered when there are many procs. However, I also feel the logical grouping may not necessarily align with model boundaries, since procs don't have a natural model context.

If we were to introduce grouping just for organization, maybe we could simply do it with naming?

mutate procedure user.signUp(email: String) { ... }

And at TS level (ORM and hooks), it's mapped to client.$procs.user.signUp. Downside is without a real grouping you can put shared annotations for the grouped procs. But if we don't have proc access control or visibility settings in ZModel, I can't yet see what other use there is to it. I've always hoped to keep the ZModel language as simple as possible.

QaaS Visibility

ZModel is pretty flexible for expressing many different aspects of an app, but I'm worried putting too many things into it will result in a bloated, hard-to-read model. My principle so far is that, hopefully, everything in it has an effect on every layer of the stack: ORM -> QaaS -> Hooks.

This makes me feel proc's visibility to QaaS should be configured at the QaaS layer (as it has no effect to the ORM). We need more control flexibility there anyway, but details should be for a separate RFC. Basically, something like the following?

API Handler:

const handler = new RPCApiHandler({
  ...,
  visibility: {
    models: {
      User: true, Post: { delete: false, deleteMany: false }, Comment: false
    },
    procs: {
      'user.signUp': false
    }
});

Hooks:

// client hooks creation use the same `visibility` settings to control its typing
const client = useClientQueries(schema, { ..., visibility: { ... } });

Proc Client API

This is not from the RFC but some change I'd like to propose. Instead of calling the db.$procs.signUp with a flat list of args, shall we group it into an object for future extensibility (and consistency with ORM client APIs like findMany etc.)? We may introduce a second argument across all ORM APIs for settings. For example a { debug: true } to dump diagnostics information for troubleshooting access control, etc.

await db.$procs.signUp({ args: { email } });

The accidental benefit is that we don't need to introduce Undefined into the language just for omitting optional arguments.


Looking forward to hearing your thoughts.

- Switch procedure calls to `db.$procs.name({ args: {...} })` (no positional args)
- Remove legacy `$procedures` alias entirely (client API + server routing/logging)
- Validate procedure envelope input (`args` object, required/unknown keys)
- Keep TanStack Query procedure hooks as `(args, options)` (with conditional args optionality)
- Update server/ORM/client tests for the envelope API
@mwillbanks mwillbanks marked this pull request as ready for review December 31, 2025 11:43
Copilot AI review requested due to automatic review settings December 31, 2025 11:43
@mwillbanks mwillbanks changed the title draft: custom procedures custom procedures Dec 31, 2025
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 introduces comprehensive support for custom procedures across the ZenStack platform, enabling developers to define type-safe, reusable server-side operations beyond standard CRUD. The implementation includes schema language extensions, ORM client integration, API handlers, and TanStack Query hooks.

Key Changes

  • Extended the schema language to support procedure declarations with parameters and return types (including arrays)
  • Implemented procedure invocation in the ORM client with validation, transaction support for mutations, and plugin hooks
  • Added REST and RPC API endpoints for procedures with SuperJSON serialization and proper HTTP method enforcement

Reviewed changes

Copilot reviewed 34 out of 36 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
packages/language/src/zmodel.langium Grammar updates to support procedure syntax with optional mutation keyword
packages/language/src/validators/procedure-validator.ts New validator for procedure declarations including reserved name checks
packages/schema/src/schema.ts Updated ProcedureDef type to support array parameters and return types
packages/orm/src/client/contract.ts Type definitions for procedure functions and envelopes
packages/orm/src/client/client-impl.ts Core procedure invocation logic with validation and transaction handling
packages/orm/src/client/plugin.ts New onProcedure hook for plugin integration
packages/orm/src/client/crud/validator/index.ts Input validation for procedure arguments
packages/server/src/api/common/procedures.ts Shared utilities for argument mapping and validation
packages/server/src/api/rest/index.ts REST API handler for procedure endpoints
packages/server/src/api/rpc/index.ts RPC API handler for procedure endpoints
packages/clients/tanstack-query/src/react.ts React hooks for procedures (query and mutation)
packages/clients/tanstack-query/src/vue.ts Vue hooks for procedures
packages/clients/tanstack-query/src/svelte/index.svelte.ts Svelte hooks for procedures
tests/e2e/orm/schemas/procedures/* Test schema and generated types
packages/server/test/api/*.test.ts Comprehensive test coverage for procedure APIs

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

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: 1

🧹 Nitpick comments (8)
packages/clients/tanstack-query/src/react.ts (2)

438-452: Minor: Inconsistent indentation in parameter list.

The endpointModel parameter has extra indentation compared to other parameters.

🔎 Proposed fix
 export function useInternalProcedureInfiniteQuery<TQueryFnData, TData>(
     schema: SchemaDef,
-        endpointModel: '$procs',
+    endpointModel: '$procs',
     procedure: string,
     args: unknown,

454-465: Minor: Inconsistent indentation in parameter list.

Same indentation issue as the infinite query hook.

🔎 Proposed fix
 export function useInternalProcedureSuspenseInfiniteQuery<TQueryFnData, TData>(
     schema: SchemaDef,
-        endpointModel: '$procs',
+    endpointModel: '$procs',
     procedure: string,
     args: unknown,
packages/clients/tanstack-query/src/svelte/index.svelte.ts (1)

276-282: Consider extracting duplicate merge helper.

The merge helper function is duplicated in both useClientQueries (lines 276-282) and useModelQueries (lines 384-390). Consider extracting it to a shared location to reduce duplication.

packages/clients/tanstack-query/src/vue.ts (2)

288-294: Same merge helper duplication as Svelte.

The merge helper is duplicated in useClientQueries (lines 288-294) and useModelQueries (lines 407-411). Consider extracting to a shared utility.


376-376: Reactive options not fully unwrapped in mutation hook.

Unlike useInternalMutation (line 598-602) which wraps the final options in a computed() to maintain reactivity, useInternalProcedureMutation only calls toValue(options) once at hook creation time. This means changes to options won't be reactive.

Consider wrapping in computed() for consistency:

🔎 Proposed fix
 export function useInternalProcedureMutation<TArgs, R = any>(
     _schema: SchemaDef,
     endpointModel: '$procs',
     procedure: string,
     options?: MaybeRefOrGetter<Omit<UnwrapRef<UseMutationOptions<R, DefaultError, TArgs>>, 'mutationFn'> & QueryContext>,
 ) {
-    const { endpoint, fetch } = useFetchOptions(toValue(options));
     const mutationFn = (data: any) => {
+        const { endpoint, fetch } = useFetchOptions(toValue(options));
         const reqUrl = makeUrl(endpoint, endpointModel, procedure);
         const fetchInit: RequestInit = {
             method: 'POST',
             headers: {
                 'content-type': 'application/json',
             },
             body: marshal(data),
         };
         return fetcher<R>(reqUrl, fetchInit, fetch) as Promise<R>;
     };
 
-    return useMutation({ ...(toValue(options) as any), mutationFn });
+    const finalOptions = computed(() => ({
+        ...(toValue(options) as any),
+        mutationFn,
+    }));
+    return useMutation(finalOptions);
 }
packages/orm/src/client/contract.ts (1)

300-309: Consider consolidating ProcedureFunc and ProcedureHandlerFunc.

Both types have identical signatures. If they serve the same purpose, consider keeping only one to reduce API surface complexity. If they differ semantically (e.g., one for client invocation, one for handler registration), consider adding JSDoc comments to clarify the distinction.

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

274-274: Consider handling missing $procs gracefully.

The optional chaining client.$procs?.[proc] will return undefined if $procs doesn't exist, which would then fail silently or throw a less informative error when called.

Consider adding an explicit check before invocation to provide a clearer error message if procedures are not configured on the client.

Proposed improvement
         try {
             log(this.options.log, 'debug', () => `handling "$procs.${proc}" request`);

-            const clientResult = await (client as any).$procs?.[proc](procInput);
+            const procFn = (client as any).$procs?.[proc];
+            if (!procFn) {
+                return this.makeBadInputErrorResponse(`procedure "${proc}" is not available on the client`);
+            }
+            const clientResult = await procFn(procInput);
packages/server/src/api/rest/index.ts (1)

577-577: Consider adding defensive check for missing $procs.

Similar to the RPC handler, the direct access client.$procs[proc] could fail if procedures aren't configured. Consider adding an explicit check.

Proposed improvement
-            const clientResult = await (client as any).$procs[proc](procInput);
+            const procFn = (client as any).$procs?.[proc];
+            if (!procFn) {
+                return this.makeProcBadInputErrorResponse(`procedure "${proc}" is not available on the client`);
+            }
+            const clientResult = await procFn(procInput);
📜 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 2b09acb and 2a9ca93.

📒 Files selected for processing (18)
  • packages/clients/tanstack-query/src/react.ts
  • packages/clients/tanstack-query/src/svelte/index.svelte.ts
  • packages/clients/tanstack-query/src/vue.ts
  • packages/clients/tanstack-query/test/react-typing-test.ts
  • packages/clients/tanstack-query/test/svelte-typing-test.ts
  • packages/clients/tanstack-query/test/vue-typing-test.ts
  • packages/orm/src/client/client-impl.ts
  • packages/orm/src/client/contract.ts
  • packages/orm/src/client/crud/validator/index.ts
  • packages/orm/src/client/options.ts
  • packages/orm/src/client/plugin.ts
  • packages/orm/test/procedures.test.ts
  • packages/server/src/api/common/procedures.ts
  • packages/server/src/api/rest/index.ts
  • packages/server/src/api/rpc/index.ts
  • packages/server/test/api/procedures.e2e.test.ts
  • packages/server/test/api/rest.test.ts
  • packages/server/test/api/rpc.test.ts
🚧 Files skipped from review as they are similar to previous changes (7)
  • packages/clients/tanstack-query/test/react-typing-test.ts
  • packages/server/test/api/procedures.e2e.test.ts
  • packages/server/test/api/rest.test.ts
  • packages/clients/tanstack-query/test/svelte-typing-test.ts
  • packages/orm/src/client/crud/validator/index.ts
  • packages/clients/tanstack-query/test/vue-typing-test.ts
  • packages/orm/src/client/client-impl.ts
🧰 Additional context used
🧠 Learnings (8)
📓 Common learnings
Learnt from: CR
Repo: zenstackhq/zenstack-v3 PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-26T01:55:04.540Z
Learning: Applies to packages/zenstackhq/orm/**/*.{ts,tsx} : Implement plugin hooks at ORM, Kysely, and entity mutation levels for query interception and customization
Learnt from: CR
Repo: zenstackhq/zenstack-v3 PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-26T01:55:04.540Z
Learning: Applies to packages/zenstackhq/orm/**/*.{ts,tsx} : Use Kysely as the query builder interface for low-level database queries, avoiding raw SQL when possible
📚 Learning: 2025-11-26T01:55:04.540Z
Learnt from: CR
Repo: zenstackhq/zenstack-v3 PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-26T01:55:04.540Z
Learning: Applies to packages/zenstackhq/orm/**/*.{ts,tsx} : Implement plugin hooks at ORM, Kysely, and entity mutation levels for query interception and customization

Applied to files:

  • packages/orm/src/client/plugin.ts
  • packages/orm/test/procedures.test.ts
  • packages/orm/src/client/options.ts
  • packages/clients/tanstack-query/src/vue.ts
  • packages/clients/tanstack-query/src/react.ts
  • packages/clients/tanstack-query/src/svelte/index.svelte.ts
📚 Learning: 2025-12-30T15:07:06.254Z
Learnt from: mwillbanks
Repo: zenstackhq/zenstack-v3 PR: 550
File: packages/orm/src/client/crud/operations/base.ts:158-159
Timestamp: 2025-12-30T15:07:06.254Z
Learning: Do not use ts-expect-error in production code within the zenstackhq/zenstack-v3 repo (e.g., packages/*). Use explicit type annotations, targeted type assertions, or refactor to resolve the type error. ts-expect-error may be acceptable only in test files for stubbing or temporary silencing. Ensure production code is type-safe and maintainable.

Applied to files:

  • packages/orm/src/client/plugin.ts
  • packages/orm/src/client/options.ts
  • packages/clients/tanstack-query/src/vue.ts
  • packages/server/src/api/common/procedures.ts
  • packages/clients/tanstack-query/src/react.ts
  • packages/orm/src/client/contract.ts
  • packages/clients/tanstack-query/src/svelte/index.svelte.ts
  • packages/server/src/api/rpc/index.ts
  • packages/server/src/api/rest/index.ts
📚 Learning: 2025-11-26T01:55:04.540Z
Learnt from: CR
Repo: zenstackhq/zenstack-v3 PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-26T01:55:04.540Z
Learning: Applies to packages/zenstackhq/orm/**/*.test.{ts,tsx} : ORM package tests should include comprehensive client API tests and policy tests

Applied to files:

  • packages/orm/test/procedures.test.ts
  • packages/orm/src/client/contract.ts
  • packages/server/test/api/rpc.test.ts
📚 Learning: 2025-11-26T01:55:04.540Z
Learnt from: CR
Repo: zenstackhq/zenstack-v3 PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-26T01:55:04.540Z
Learning: Applies to packages/zenstackhq/orm/**/*.{ts,tsx} : Use Kysely as the query builder interface for low-level database queries, avoiding raw SQL when possible

Applied to files:

  • packages/orm/test/procedures.test.ts
  • packages/orm/src/client/options.ts
  • packages/clients/tanstack-query/src/vue.ts
  • packages/clients/tanstack-query/src/react.ts
📚 Learning: 2025-11-26T01:55:04.540Z
Learnt from: CR
Repo: zenstackhq/zenstack-v3 PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-26T01:55:04.540Z
Learning: Applies to packages/zenstackhq/cli/**/*.test.{ts,tsx} : CLI package tests should focus on action-specific tests for each command

Applied to files:

  • packages/orm/test/procedures.test.ts
📚 Learning: 2025-11-26T01:55:04.540Z
Learnt from: CR
Repo: zenstackhq/zenstack-v3 PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-26T01:55:04.540Z
Learning: Applies to tests/e2e/**/*.{ts,tsx} : E2E tests should validate real-world schema compatibility with established projects

Applied to files:

  • packages/orm/test/procedures.test.ts
  • packages/server/test/api/rpc.test.ts
📚 Learning: 2025-11-26T01:55:04.540Z
Learnt from: CR
Repo: zenstackhq/zenstack-v3 PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-26T01:55:04.540Z
Learning: Applies to **/*.zmodel : ZModel schema files should define database structure and policies that compile to TypeScript via `zenstack generate`

Applied to files:

  • packages/orm/src/client/contract.ts
🧬 Code graph analysis (7)
packages/orm/src/client/plugin.ts (2)
packages/schema/src/schema.ts (1)
  • SchemaDef (11-19)
packages/orm/src/client/contract.ts (1)
  • ClientContract (65-199)
packages/orm/test/procedures.test.ts (4)
packages/orm/src/client/client-impl.ts (1)
  • ZenStackClient (46-52)
packages/orm/src/client/index.ts (1)
  • ZenStackClient (1-1)
packages/orm/src/dialects/sqlite.ts (1)
  • SqliteDialect (1-1)
packages/orm/src/client/plugin.ts (1)
  • definePlugin (58-60)
packages/orm/src/client/options.ts (1)
packages/orm/src/client/contract.ts (1)
  • ProcedureHandlerFunc (307-309)
packages/clients/tanstack-query/src/vue.ts (4)
packages/schema/src/schema.ts (1)
  • SchemaDef (11-19)
packages/orm/src/client/contract.ts (1)
  • ProcedureFunc (300-302)
packages/clients/tanstack-query/src/common/types.ts (2)
  • QueryContext (9-24)
  • ExtraQueryOptions (29-34)
packages/clients/client-helpers/src/fetch.ts (2)
  • marshal (86-93)
  • fetcher (15-37)
packages/server/src/api/common/procedures.ts (2)
packages/server/src/api/rest/index.ts (3)
  • schema (285-287)
  • unmarshalQ (602-604)
  • mapProcedureArgs (595-600)
packages/schema/src/schema.ts (2)
  • SchemaDef (11-19)
  • ProcedureDef (82-87)
packages/server/test/api/rpc.test.ts (2)
packages/testtools/src/client.ts (1)
  • createPolicyTestClient (258-269)
packages/server/src/api/rpc/index.ts (1)
  • RPCApiHandler (36-377)
packages/server/src/api/rpc/index.ts (4)
packages/server/src/api/common/procedures.ts (3)
  • getProcedureDef (8-14)
  • processSuperJsonRequestPayload (44-66)
  • validateProcedureArgs (143-155)
packages/orm/src/client/errors.ts (1)
  • ORMError (66-104)
packages/server/src/api/utils.ts (1)
  • log (9-27)
packages/common-helpers/src/safe-json-stringify.ts (1)
  • safeJSONStringify (4-12)
⏰ 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: Agent
  • GitHub Check: CodeQL analysis (javascript-typescript)
🔇 Additional comments (27)
packages/clients/tanstack-query/src/react.ts (5)

65-88: LGTM! Type utilities are well-designed.

The procedure type utilities correctly extract and normalize procedure arguments and return types from the schema. The NormalizeProcedurePayload type handles empty args, single arg, and multi-arg cases appropriately, and ProcedureHookFn correctly makes args optional when undefined extends Payload.


162-209: LGTM! Procedure hooks types are comprehensive.

The ProcedureHookGroup correctly differentiates between mutation and non-mutation procedures, providing appropriate hook signatures for each. The conditional extension of ClientHooks with ProcedureHooks ensures the $procs namespace only appears when procedures exist in the schema.


351-390: LGTM! Runtime procedure hook wiring is correct.

The buildProcedureHooks helper correctly iterates over schema procedures and wires each to the appropriate hook type based on the mutation flag. Options are properly merged with root options.


392-413: Procedure query hooks hardcode optimisticUpdate: false - verify this is intentional.

The procedure query hooks set optimisticUpdate: false in the query key options, unlike model queries which respect the options?.optimisticUpdate !== false pattern. This means procedure queries won't participate in optimistic update invalidation.

If this is intentional (procedures may have side effects that make optimistic updates unsafe), consider adding a brief comment explaining the design decision.


467-487: LGTM! Mutation now correctly sends data in request body.

The mutation hook properly constructs the request with the payload in the body using marshal(data) and sets the appropriate content-type header. This addresses the previous review concern about data being sent in URL query parameters.

packages/clients/tanstack-query/src/svelte/index.svelte.ts (3)

66-89: LGTM! Type utilities consistent with React implementation.

The Svelte procedure type utilities mirror the React implementation correctly, with the appropriate Svelte-specific Accessor wrapper for the ProcedureHookFn type.


347-367: LGTM! Svelte mutation hook correctly sends data in body.

The mutation implementation properly marshals data to the request body with appropriate headers, consistent with the React implementation.


153-186: Svelte procedure hooks intentionally omit suspense variants—this is expected.

@tanstack/svelte-query does not provide suspense hooks like useSuspenseQuery or useSuspenseInfiniteQuery. Suspense is a React-specific feature in TanStack Query; Svelte uses different query primitives (createQuery / store-based APIs) that don't integrate with React's Suspense API. The React implementation's inclusion of suspense variants is therefore not relevant to parity with the Svelte implementation.

packages/clients/tanstack-query/src/vue.ts (3)

65-88: LGTM! Type utilities consistent across frameworks.

The Vue procedure type utilities correctly use MaybeRefOrGetter for the reactive wrapper pattern, maintaining consistency with the React and Svelte implementations.


154-196: Vue procedure hooks also missing suspense variants - same consideration as Svelte.

The Vue ProcedureHookGroup only provides useQuery and useInfiniteQuery for non-mutation procedures, omitting suspense variants. Verify if @tanstack/vue-query supports suspense hooks and whether they should be included for API parity with React.


370-390: LGTM! Vue mutation hook correctly sends data in body.

The implementation properly marshals data to the request body with marshal(data) and sets the content-type header, consistent with React and Svelte implementations.

packages/orm/src/client/contract.ts (3)

217-224: LGTM - Extended type map covers procedure return types.

The additions to _TypeMap properly support all ZModel primitive types that procedures can return, including Void and Undefined for procedures with no meaningful return value.


227-240: LGTM - Clean enum and type resolution.

The EnumValue type correctly extracts enum member values, and the updated MapType properly chains resolution for models, typedefs, and enums.


254-298: Well-designed envelope typing with proper optionality handling.

The conditional types correctly distinguish between procedures with required vs optional parameters, making the args key required only when there are required parameters. The pattern allows calling db.$procs.noArgProc() or db.$procs.noArgProc({ args: {} }) interchangeably.

packages/orm/src/client/options.ts (1)

134-143: LGTM - Handler type alignment.

The switch to ProcedureHandlerFunc properly types procedure handlers that receive the client as the first parameter via PrependParameter.

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

128-213: Comprehensive procedure RPC tests.

Good coverage of:

  • HTTP method enforcement (GET for queries, POST for mutations)
  • Argument passing via query string and request body
  • Falsy/undefined return value serialization with SuperJSON metadata

The test for getUndefined correctly verifies that undefined is serialized to null with SuperJSON metadata present.


215-338: Thorough edge case coverage.

The tests properly validate:

  • Multi-parameter procedures with object mapping
  • All-optional parameters allowing omitted payloads
  • Array-typed parameters
  • Enum and typedef (object) parameters
  • Input type validation (422 status)
  • Unknown argument rejection (400 status)
  • Invalid args payload structure (400 status)
packages/orm/src/client/plugin.ts (1)

64-98: Well-designed procedure interception hook.

The OnProcedureHookContext provides all necessary context for plugins to:

  • Identify the procedure (name, mutation)
  • Access and mutate input before proceeding
  • Control execution flow via proceed
  • Access the client for additional operations

This mirrors the OnQueryHookContext pattern, maintaining API consistency.

packages/orm/test/procedures.test.ts (1)

139-158: Good transaction behavior verification.

The test correctly validates that:

  • Query procedures (qry) execute outside transactions (isTransaction === false)
  • Mutation procedures (mut) are automatically wrapped in transactions (isTransaction === true)

This aligns with the PR objective to provide default transactional wrapping for mutations.

packages/server/src/api/rpc/index.ts (2)

56-64: Clean routing for procedure requests.

The $procs path prefix is correctly detected and delegated to the specialized handler, keeping the CRUD and procedure flows separated.


180-292: Robust procedure request handling.

The implementation correctly:

  • Validates procedure existence via getProcedureDef
  • Enforces HTTP method constraints (POST for mutations, GET for queries)
  • Supports argument passing via both request body and query string
  • Processes SuperJSON payloads when metadata is present
  • Maps and validates arguments before invocation
  • Serializes responses with SuperJSON for type preservation
packages/server/src/api/common/procedures.ts (3)

68-141: Thorough argument mapping with helpful error messages.

The mapProcedureArgs function handles multiple scenarios well:

  • Zero-parameter procedures (lines 73-92)
  • All-optional parameter procedures (lines 95-97)
  • Missing/invalid payload structures
  • Unknown argument rejection (lines 124-128)
  • Required parameter validation (lines 131-138)

The error messages are clear and actionable for API consumers.


143-155: InputValidator integration respects client options.

Good defensive check for validateInput === false before running validation, allowing users to bypass validation when needed.


157-159: Verify that isOrmError is used externally before keeping this export.

The isOrmError type guard is exported but its usage across the codebase should be verified. If it's not imported or used in other files, consider removing it to reduce the public API surface. Run a codebase search for all references to isOrmError to confirm its necessity.

packages/server/src/api/rest/index.ts (3)

346-349: Clean procedure routing in REST handler.

The /$procs/ path prefix detection appropriately routes procedure requests to the specialized handler, maintaining separation from JSON:API resource routes.


488-593: Consistent procedure handling with REST conventions.

The processProcedureRequest method mirrors the RPC implementation while integrating with REST-specific:

  • Error response format (JSON:API style via makeError)
  • Logging patterns
  • Response structure

This maintains a consistent developer experience across both API styles.


619-653: Well-structured ORM error mapping for procedures.

The makeProcORMErrorResponse method properly maps ORM error reasons to appropriate HTTP status codes and includes relevant context (model, rejectReason, dbErrorCode) in the response body.

Good use of paramCase for consistent error reason formatting.

Copy link
Member

@ymc9 ymc9 left a comment

Choose a reason for hiding this comment

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

I've finished reviewing the changes to "packages/language", "packages/schema", "packages/sdk", and "packages/orm". Please find my comments attached. Thanks!

Will complete the review tomorrow.

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/language/src/zmodel.langium (1)

269-269: Verify whether Undefined type is needed.

A previous reviewer (ymc9) asked whether Undefined should be removed if it's not needed. The Void type makes sense for procedures that don't return a value, but Undefined seems redundant. Could you clarify the use case for Undefined as a distinct expression type?

packages/orm/src/client/client-impl.ts (1)

217-222: Consider using Object.create(null) for the accumulator.

While Object.keys() only returns own enumerable properties (not inherited ones), using Object.create(null) for the accumulator provides additional safety against prototype pollution when building the procedures map:

🔎 Suggested improvement
 get $procs() {
-    return Object.keys(this.$schema.procedures ?? {}).reduce((acc, name) => {
+    return Object.keys(this.$schema.procedures ?? {}).reduce((acc, name) => {
         acc[name] = (input?: unknown) => this.handleProc(name, input);
         return acc;
-    }, {} as any);
+    }, Object.create(null) as any);
 }
packages/server/src/api/rest/index.ts (1)

574-592: Consider explicit procedure existence check and avoid broad type assertion.

Two concerns:

  1. Type assertion: Using (client as any) at line 577 bypasses TypeScript's type safety. Consider using a more specific type assertion or extending the ClientContract type to include $procs if it's not already present.

  2. Defensive check: While optional chaining $procs?.[proc] handles undefined gracefully, the subsequent call could throw a generic error if the procedure doesn't exist at runtime. Consider an explicit check to provide a clearer error message.

Based on learnings, avoid broad type assertions in production code.

🔎 Suggested improvements
         try {
             log(this.log, 'debug', () => `handling "$procs.${proc}" request`);
 
-            const clientResult = await (client as any).$procs?.[proc](procInput);
+            const procs = (client as any).$procs;
+            const procFn = procs?.[proc];
+            if (!procFn || typeof procFn !== 'function') {
+                return this.makeProcBadInputErrorResponse(`unknown procedure: ${proc}`);
+            }
+
+            const clientResult = await procFn(procInput);
 
             const { json, meta } = SuperJSON.serialize(clientResult);

For the type assertion issue, verify if $procs should be part of the ClientContract type definition:

#!/bin/bash
# Search for ClientContract type definition to check if $procs is included
ast-grep --pattern $'type ClientContract<$$$> = $$$'

# Also search for where $procs is added to the client
rg -n --type=ts -C3 '\$procs'
packages/orm/test/procedures.test.ts (1)

87-137: LGTM: Plugin execution order test is correct.

The test correctly validates that:

  1. Plugins execute in reverse order of registration (line 136: ['p2:add', 'p1:add'] for registration [p1, p2])
  2. Plugins can mutate procedure inputs (p2 adds 1 to parameter a)
  3. Mutations propagate correctly through the chain (final result: (1+1) + 2 = 4)

This aligns with the middleware-style wrapping pattern where the last-registered plugin becomes the outermost wrapper and executes first.

Note: A previous review comment flagged that this execution order behavior should be documented in packages/orm/src/client/client-impl.ts (lines 256-269) for future maintainers.

🧹 Nitpick comments (3)
packages/server/src/api/rest/index.ts (1)

521-546: Refactor duplicated query parameter extraction.

The logic for extracting and unmarshaling the 'q' query parameter is duplicated between the POST (lines 527-534) and GET (lines 538-545) branches. Consider extracting this into a helper method to reduce duplication and improve maintainability.

🔎 Suggested refactor
+    private extractQueryArgs(query?: Record<string, string | string[]>): unknown {
+        return query?.['q']
+            ? this.unmarshalQ(query['q'] as string, query['meta'] as string | undefined)
+            : undefined;
+    }
+
     private async processProcedureRequest({
         // ... existing code ...
         let argsPayload: unknown;
         if (method === 'POST') {
             if (typeof requestBody !== 'undefined') {
                 argsPayload = requestBody;
             } else {
                 try {
-                    argsPayload = query?.['q']
-                        ? this.unmarshalQ(query['q'] as string, query['meta'] as string | undefined)
-                        : undefined;
+                    argsPayload = this.extractQueryArgs(query);
                 } catch (err) {
                     return this.makeProcBadInputErrorResponse(
                         err instanceof Error ? err.message : 'invalid "q" query parameter',
                     );
                 }
             }
         } else {
             try {
-                argsPayload = query?.['q']
-                    ? this.unmarshalQ(query['q'] as string, query['meta'] as string | undefined)
-                    : undefined;
+                argsPayload = this.extractQueryArgs(query);
             } catch (err) {
                 return this.makeProcBadInputErrorResponse(
                     err instanceof Error ? err.message : 'invalid "q" query parameter',
                 );
             }
         }
packages/orm/test/procedures.test.ts (2)

66-85: Add a success case for procedure argument validation.

The test correctly validates that invalid arguments are rejected (passing a string when an Int is expected). However, it only tests the error path. Add a companion assertion or test case that verifies valid arguments pass validation and execute successfully, e.g., await expect(client.$procs.echoInt({ args: { n: 1 } })).resolves.toBe(1).

🔎 Suggested addition
     });

     await expect(client.$procs.echoInt({ args: { n: '1' } })).rejects.toThrow(/invalid input/i);
+    await expect(client.$procs.echoInt({ args: { n: 1 } })).resolves.toBe(1);
 });

1-159: Consider additional test coverage for RFC requirements.

The current test suite provides solid coverage of core procedure functionality: exposure, error handling, validation, plugin hooks, and transactions. However, based on the RFC outlined in PR comments and the learning that "ORM package tests should include comprehensive client API tests and policy tests," consider adding tests for:

  1. Policy integration: Test procedure execution with policy-enabled clients and the "execute" policy scope mentioned in the RFC
  2. Grouped procedures: Validate nested organization patterns discussed in PR comments (e.g., db.$procs.user.signUp)
  3. Return type validation: Currently only input validation is tested; verify return values are validated against schema
  4. Complex argument types: Test procedures with nested objects, arrays, or enum parameters
  5. Error propagation: Verify that errors thrown in handlers are properly wrapped/reported

Note: Some of these may be covered in other test files (e.g., packages/server/test/api/procedures.e2e.test.ts), but ORM-level policy and validation tests would strengthen this suite.

Based on learnings, as per coding guidelines: ORM package tests should include comprehensive client API tests and policy tests.

📜 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 2a9ca93 and 80eb234.

⛔ Files ignored due to path filters (2)
  • packages/language/src/generated/ast.ts is excluded by !**/generated/**
  • packages/language/src/generated/grammar.ts is excluded by !**/generated/**
📒 Files selected for processing (12)
  • packages/language/src/validators/procedure-validator.ts
  • packages/language/src/zmodel.langium
  • packages/orm/src/client/client-impl.ts
  • packages/orm/src/client/contract.ts
  • packages/orm/src/client/crud/validator/index.ts
  • packages/orm/src/client/options.ts
  • packages/orm/src/client/plugin.ts
  • packages/orm/test/procedures.test.ts
  • packages/server/src/api/rest/index.ts
  • packages/server/test/api/procedures.e2e.test.ts
  • packages/server/test/api/rest.test.ts
  • packages/server/test/api/rpc.test.ts
🚧 Files skipped from review as they are similar to previous changes (5)
  • packages/orm/src/client/crud/validator/index.ts
  • packages/server/test/api/rpc.test.ts
  • packages/language/src/validators/procedure-validator.ts
  • packages/server/test/api/procedures.e2e.test.ts
  • packages/orm/src/client/options.ts
🧰 Additional context used
🧠 Learnings (8)
📓 Common learnings
Learnt from: CR
Repo: zenstackhq/zenstack-v3 PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-26T01:55:04.540Z
Learning: Applies to packages/zenstackhq/orm/**/*.{ts,tsx} : Implement plugin hooks at ORM, Kysely, and entity mutation levels for query interception and customization
Learnt from: CR
Repo: zenstackhq/zenstack-v3 PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-26T01:55:04.540Z
Learning: Applies to packages/zenstackhq/orm/**/*.{ts,tsx} : Use Kysely as the query builder interface for low-level database queries, avoiding raw SQL when possible
Learnt from: CR
Repo: zenstackhq/zenstack-v3 PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-26T01:55:04.540Z
Learning: Applies to packages/zenstackhq/orm/**/*.test.{ts,tsx} : ORM package tests should include comprehensive client API tests and policy tests
📚 Learning: 2025-11-26T01:55:04.540Z
Learnt from: CR
Repo: zenstackhq/zenstack-v3 PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-26T01:55:04.540Z
Learning: Applies to packages/zenstackhq/orm/**/*.{ts,tsx} : Implement plugin hooks at ORM, Kysely, and entity mutation levels for query interception and customization

Applied to files:

  • packages/orm/src/client/plugin.ts
  • packages/orm/test/procedures.test.ts
  • packages/orm/src/client/client-impl.ts
📚 Learning: 2025-12-30T15:07:06.254Z
Learnt from: mwillbanks
Repo: zenstackhq/zenstack-v3 PR: 550
File: packages/orm/src/client/crud/operations/base.ts:158-159
Timestamp: 2025-12-30T15:07:06.254Z
Learning: Do not use ts-expect-error in production code within the zenstackhq/zenstack-v3 repo (e.g., packages/*). Use explicit type annotations, targeted type assertions, or refactor to resolve the type error. ts-expect-error may be acceptable only in test files for stubbing or temporary silencing. Ensure production code is type-safe and maintainable.

Applied to files:

  • packages/orm/src/client/plugin.ts
  • packages/server/src/api/rest/index.ts
  • packages/orm/src/client/client-impl.ts
  • packages/orm/src/client/contract.ts
📚 Learning: 2025-11-26T01:55:04.540Z
Learnt from: CR
Repo: zenstackhq/zenstack-v3 PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-26T01:55:04.540Z
Learning: Applies to tests/e2e/**/*.{ts,tsx} : E2E tests should validate real-world schema compatibility with established projects

Applied to files:

  • packages/server/test/api/rest.test.ts
  • packages/orm/test/procedures.test.ts
📚 Learning: 2025-11-26T01:55:04.540Z
Learnt from: CR
Repo: zenstackhq/zenstack-v3 PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-26T01:55:04.540Z
Learning: Applies to packages/zenstackhq/orm/**/*.test.{ts,tsx} : ORM package tests should include comprehensive client API tests and policy tests

Applied to files:

  • packages/server/test/api/rest.test.ts
  • packages/orm/test/procedures.test.ts
📚 Learning: 2025-11-26T01:55:04.540Z
Learnt from: CR
Repo: zenstackhq/zenstack-v3 PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-26T01:55:04.540Z
Learning: Applies to tests/**/type*.{ts,tsx} : Ensure TypeScript inference and type coverage are validated through type coverage tests

Applied to files:

  • packages/server/test/api/rest.test.ts
📚 Learning: 2025-11-26T01:55:04.540Z
Learnt from: CR
Repo: zenstackhq/zenstack-v3 PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-26T01:55:04.540Z
Learning: Applies to packages/zenstackhq/orm/**/*.{ts,tsx} : Use Kysely as the query builder interface for low-level database queries, avoiding raw SQL when possible

Applied to files:

  • packages/orm/test/procedures.test.ts
  • packages/orm/src/client/contract.ts
📚 Learning: 2025-11-26T01:55:04.540Z
Learnt from: CR
Repo: zenstackhq/zenstack-v3 PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-26T01:55:04.540Z
Learning: Applies to packages/zenstackhq/cli/**/*.test.{ts,tsx} : CLI package tests should focus on action-specific tests for each command

Applied to files:

  • packages/orm/test/procedures.test.ts
🧬 Code graph analysis (4)
packages/orm/src/client/plugin.ts (2)
packages/schema/src/schema.ts (1)
  • SchemaDef (11-19)
packages/orm/src/client/contract.ts (1)
  • ClientContract (65-199)
packages/server/test/api/rest.test.ts (2)
packages/server/src/api/rest/index.ts (2)
  • schema (285-287)
  • RestApiHandler (126-2254)
packages/orm/src/client/contract.ts (2)
  • ClientContract (65-199)
  • Procedures (242-252)
packages/orm/test/procedures.test.ts (2)
packages/orm/src/client/client-impl.ts (1)
  • ZenStackClient (46-52)
packages/orm/src/client/plugin.ts (1)
  • definePlugin (58-60)
packages/server/src/api/rest/index.ts (7)
packages/orm/src/client/contract.ts (1)
  • ClientContract (65-199)
packages/server/src/types.ts (1)
  • Response (52-62)
packages/server/src/api/common/procedures.ts (3)
  • getProcedureDef (8-14)
  • processSuperJsonRequestPayload (44-66)
  • validateProcedureArgs (143-155)
packages/orm/src/client/errors.ts (1)
  • ORMError (66-104)
packages/server/src/api/rpc/index.ts (1)
  • log (43-45)
packages/server/src/api/utils.ts (1)
  • log (9-27)
packages/common-helpers/src/param-case.ts (1)
  • paramCase (5-22)
⏰ 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 (22.x, sqlite)
  • GitHub Check: build-test (22.x, postgresql)
🔇 Additional comments (19)
packages/orm/src/client/plugin.ts (2)

39-42: LGTM! The new onProcedure hook follows existing plugin patterns.

The hook integrates cleanly with the existing plugin architecture, mirroring the structure of onQuery and onKyselyQuery hooks.


64-96: Well-structured hook context type with clear documentation.

The OnProcedureHookContext type provides all necessary fields for procedure interception: procedure name, mutation flag, input envelope, proceed callback, and client reference. This aligns with the plugin hook patterns established for query interception. Based on learnings, this correctly implements plugin hooks at the ORM level for procedure interception.

packages/language/src/zmodel.langium (1)

229-229: Good change: mutation flag is now optional for procedures.

This allows both query procedures (procedure foo(): String) and mutation procedures (mutation procedure foo(): String), providing cleaner syntax for read-only procedures.

packages/orm/src/client/contract.ts (3)

242-252: Good design: $procs with envelope-based invocation.

The rename from $procedures to $procs and the envelope-based call signature ({ args: {...} }) aligns with the RFC discussion in the PR comments. This provides a consistent and extensible API surface.


254-298: Well-designed procedure parameter typing with envelope support.

The type utilities correctly handle:

  • Required vs optional parameter detection
  • Envelope optionality based on parameter requirements
  • Proper type mapping for procedure arguments

The conditional types ensure type safety while maintaining good DX.


309-318: MaybePromise return type addresses flexibility concern.

The ProcedureFunc and ProcedureHandlerFunc now return MaybePromise<...>, allowing handlers to be synchronous or asynchronous. This addresses the previous review feedback from ymc9.

packages/orm/src/client/client-impl.ts (2)

224-245: Good validation flow: schema check followed by input validation.

The implementation correctly:

  1. Validates procedures are configured on the client
  2. Verifies the procedure exists in the schema
  3. Validates handler is configured
  4. Runs input validation via InputValidator

This provides clear error messages at each failure point.


249-282: Well-structured plugin pipeline and transaction support.

The implementation correctly:

  • Chains onProcedure hooks in plugin order
  • Provides mutation metadata to plugins
  • Wraps mutation procedures in transactions when not already inside one
  • Extracts args from the envelope format

This aligns with the existing query plugin architecture. Based on learnings, this properly implements plugin hooks at the ORM level for procedure interception.

packages/server/test/api/rest.test.ts (4)

3167-3192: Comprehensive procedure schema for testing.

The test schema covers a good variety of procedure types:

  • Scalar types (Int, String, Decimal)
  • Optional parameters
  • Array parameters
  • Enum parameters
  • TypeDef/object parameters
  • Mutation procedures

This provides good coverage for the REST procedures feature.


3285-3332: Good test coverage for optional and required parameter handling.

Tests correctly validate:

  • SuperJSON serialization for complex types (Decimal)
  • Optional args allowing omission of envelope
  • Required args enforcing presence of arguments

The error messages are specific and helpful for debugging.


3382-3399: Type validation error test ensures proper input coercion.

This test verifies that passing incorrect types (string instead of Int) returns a 422 validation error, ensuring the input validation pipeline works correctly for procedures.


3418-3456: Good error scenario coverage for malformed payloads.

Tests verify:

  • Array as args value (should be object) → 400 error
  • Unknown argument keys → 400 error with descriptive message

This ensures the API provides clear feedback for invalid procedure calls.

packages/server/src/api/rest/index.ts (4)

346-349: Clean procedure routing logic.

The path-based routing for procedures is straightforward and delegates appropriately to the dedicated handler.


595-604: Good delegation to shared utilities.

The helper methods appropriately delegate to common procedure utilities, promoting code reuse across REST and RPC handlers.


606-653: Excellent error handling and response builders.

The error response builders provide comprehensive coverage:

  • Clear separation of input errors (400), generic errors (500), and ORM-specific errors
  • Pattern matching elegantly maps ORMErrorReason to appropriate HTTP status codes
  • Proper logging of error responses aids debugging
  • Structured error messages with metadata (reason, model, rejection details)

548-556: Concern already addressed in the implementation.

The processSuperJsonRequestPayload function correctly checks for meta?.serialization before attempting SuperJSON deserialization, not just the presence of the 'meta' property. When a payload contains a 'meta' property without a 'serialization' key, the function safely drops it (as intended) rather than misinterpreting it as SuperJSON format. False positives are prevented by the specific structure validation.

packages/orm/test/procedures.test.ts (3)

35-64: LGTM: Error handling tests cover key failure modes.

The tests properly validate two distinct error scenarios:

  1. Procedures not configured at all (lines 35-48)
  2. Specific handler missing from configuration (lines 50-64)

The error messages are appropriately descriptive and assertions use case-insensitive matching for robustness.


139-158: The test correctly validates that mutation procedures run in transactions by default while non-mutation procedures don't. The client.isTransaction property (lines 151-152) is a stable public getter that delegates to Kysely's transaction state and is documented through inline comments in the implementation (see client-impl.ts:276-277). The test properly verifies the documented transactional behavior.


16-33: Handler signatures must match ProcedureHandlerFunc type contract with context object.

Based on the type definitions in contract.ts, all procedure handlers must receive a context object ctx: ProcedureHandlerCtx<Schema, Params> containing at least { client: ClientContract<Schema> }. However, this test violates that contract:

  • Lines 16-33: Handler async () => 'ok' takes no parameters but should receive context object with client property to match ProcedureHandlerFunc signature.
  • Lines 139-158: Handlers destructure { client } from context but the invocations (client.$procs.qry(), client.$procs.mut()) don't pass an envelope. For consistency, either update handlers to not destructure context, or pass the envelope object in invocations.

Ensure all procedure handlers conform to the enforced type contract: (ctx: { client: ClientContract<Schema>, args?: ... }) => MaybePromise<Return>.

⛔ Skipped due to learnings
Learnt from: CR
Repo: zenstackhq/zenstack-v3 PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-26T01:55:04.540Z
Learning: Applies to packages/zenstackhq/orm/**/*.test.{ts,tsx} : ORM package tests should include comprehensive client API tests and policy tests
Learnt from: CR
Repo: zenstackhq/zenstack-v3 PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-26T01:55:04.540Z
Learning: Applies to tests/e2e/**/*.{ts,tsx} : E2E tests should validate real-world schema compatibility with established projects

Copy link
Member

@ymc9 ymc9 left a comment

Choose a reason for hiding this comment

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

Hi @mwillbanks , I've finished reviewing the remaining code. Please take a look at the new comments. Thanks!

@mwillbanks
Copy link
Contributor Author

@ymc9 i've updated the code based on your review and updated relevant tests.

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

🧹 Nitpick comments (7)
packages/server/test/api/rest.test.ts (1)

3285-3461: Excellent test coverage with one suggested addition.

The test cases comprehensively cover:

  • SuperJSON serialization/deserialization
  • Optional and required arguments
  • Array, enum, and custom type parameters
  • Error scenarios (invalid types, unknown arguments, missing args)
  • Both GET query and POST mutation procedures

Consider adding test cases for:

  1. Procedure invocation with policy enforcement (if execute policies are supported)
  2. Context propagation to procedure handlers
  3. Nested/complex typedef parameters

These would provide additional confidence in the procedure execution flow.

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

7-29: Simplify explicit undefined type annotations.

The explicit type annotations error: undefined as string | undefined on lines 9, 17, and 28 are redundant. TypeScript can infer the union type from the context.

Proposed simplification
 export async function processSuperJsonRequestPayload(payload: unknown) {
     if (!payload || typeof payload !== 'object' || Array.isArray(payload) || !('meta' in (payload as any))) {
-        return { result: payload, error: undefined as string | undefined };
+        return { result: payload, error: undefined };
     }

     const { meta, ...rest } = payload as any;
     if (meta?.serialization) {
         try {
             return {
                 result: SuperJSON.deserialize({ json: rest, meta: meta.serialization }),
-                error: undefined as string | undefined,
+                error: undefined,
             };
         } catch (err) {
             return {
                 result: undefined,
                 error: `failed to deserialize request payload: ${(err as Error).message}`,
             };
         }
     }

     // drop meta when no serialization info is present
-    return { result: rest, error: undefined as string | undefined };
+    return { result: rest, error: undefined };
 }

The union type will be correctly inferred by TypeScript from the different return paths.

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

157-157: Remove trailing whitespace.

Line 157 contains trailing whitespace.

             const clientResult = await (client as any)[model][op](processedArgs);
             let responseBody: any = { data: clientResult };
-            
+
             // superjson serialize response
packages/clients/tanstack-query/src/react.ts (2)

145-181: Consider whether all query variants are necessary for procedures.

Procedures expose four query hook variants (useQuery, useSuspenseQuery, useInfiniteQuery, useSuspenseInfiniteQuery) mirroring model hooks. However, procedures are custom operations with potentially different semantics than standard CRUD queries:

  • Suspense variants may not align with typical procedure usage patterns
  • Infinite queries might be useful for paginated custom queries (per past discussion), but suspense + infinite is a niche combination

Consider starting with just useQuery and useInfiniteQuery for non-mutation procedures, adding suspense variants only if user demand materializes. This reduces initial API surface and avoids committing to patterns that may not fit procedure semantics.

[scratchpad_end] -->


332-332: Unsafe type assertion when accessing procedures.

The code uses (schema as any).procedures which bypasses type safety. While there's a runtime check for undefined, this pattern could mask type errors.

🔎 Safer approach
-    const procedures = (schema as any).procedures as Record<string, { mutation?: boolean }> | undefined;
+    const procedures = 'procedures' in schema ? schema.procedures : undefined;

This leverages TypeScript's type narrowing via the in operator to safely access the optional property.

packages/server/src/api/rest/index.ts (2)

539-539: Optional chaining may be redundant after validation.

Line 539 uses optional chaining for client.$procs?.[proc], but the procedure has already been validated to exist in the schema (line 505-508). If client.$procs is guaranteed to exist when schema.procedures exists, the optional chaining could be replaced with a more assertive access pattern.

However, if there's a possibility of schema-client mismatch (e.g., outdated client), the defensive check is appropriate. Consider adding a runtime assertion if $procs is missing to surface configuration issues early:

🔎 More explicit error handling
         try {
             log(this.log, 'debug', () => `handling "$procs.${proc}" request`);
 
-            const clientResult = await (client as any).$procs?.[proc](procInput);
+            const procs = (client as any).$procs;
+            if (!procs) {
+                throw new Error('Client does not support procedures - schema/client version mismatch');
+            }
+            const clientResult = await procs[proc](procInput);

558-563: Consider inlining trivial delegation.

The mapProcedureArgs method simply delegates to mapProcedureArgsCommon without any additional logic. Unless you anticipate REST-specific argument mapping in the future, this could be inlined at the call site (line 531):

procInput = mapProcedureArgsCommon(procDef, processedArgsPayload);

This reduces indirection and makes the code flow clearer. If REST-specific mapping is planned, a comment explaining the future intent would be helpful.

📜 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 80eb234 and e8c726e.

📒 Files selected for processing (12)
  • packages/clients/tanstack-query/src/common/types.ts
  • packages/clients/tanstack-query/src/react.ts
  • packages/clients/tanstack-query/src/svelte/index.svelte.ts
  • packages/clients/tanstack-query/src/vue.ts
  • packages/orm/src/client/client-impl.ts
  • packages/orm/test/procedures.test.ts
  • packages/server/src/api/common/procedures.ts
  • packages/server/src/api/common/utils.ts
  • packages/server/src/api/rest/index.ts
  • packages/server/src/api/rpc/index.ts
  • packages/server/test/api/procedures.e2e.test.ts
  • packages/server/test/api/rest.test.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • packages/server/test/api/procedures.e2e.test.ts
  • packages/orm/src/client/client-impl.ts
  • packages/orm/test/procedures.test.ts
🧰 Additional context used
🧠 Learnings (7)
📓 Common learnings
Learnt from: CR
Repo: zenstackhq/zenstack-v3 PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-26T01:55:04.540Z
Learning: Applies to packages/zenstackhq/orm/**/*.{ts,tsx} : Implement plugin hooks at ORM, Kysely, and entity mutation levels for query interception and customization
📚 Learning: 2025-11-26T01:55:04.540Z
Learnt from: CR
Repo: zenstackhq/zenstack-v3 PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-26T01:55:04.540Z
Learning: Applies to packages/zenstackhq/orm/**/*.{ts,tsx} : Implement plugin hooks at ORM, Kysely, and entity mutation levels for query interception and customization

Applied to files:

  • packages/clients/tanstack-query/src/vue.ts
  • packages/clients/tanstack-query/src/common/types.ts
  • packages/clients/tanstack-query/src/react.ts
  • packages/clients/tanstack-query/src/svelte/index.svelte.ts
📚 Learning: 2025-11-26T01:55:04.540Z
Learnt from: CR
Repo: zenstackhq/zenstack-v3 PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-26T01:55:04.540Z
Learning: Applies to packages/zenstackhq/orm/**/*.{ts,tsx} : Use Kysely as the query builder interface for low-level database queries, avoiding raw SQL when possible

Applied to files:

  • packages/clients/tanstack-query/src/vue.ts
  • packages/clients/tanstack-query/src/common/types.ts
  • packages/clients/tanstack-query/src/react.ts
📚 Learning: 2025-12-30T15:07:06.254Z
Learnt from: mwillbanks
Repo: zenstackhq/zenstack-v3 PR: 550
File: packages/orm/src/client/crud/operations/base.ts:158-159
Timestamp: 2025-12-30T15:07:06.254Z
Learning: Do not use ts-expect-error in production code within the zenstackhq/zenstack-v3 repo (e.g., packages/*). Use explicit type annotations, targeted type assertions, or refactor to resolve the type error. ts-expect-error may be acceptable only in test files for stubbing or temporary silencing. Ensure production code is type-safe and maintainable.

Applied to files:

  • packages/clients/tanstack-query/src/vue.ts
  • packages/server/src/api/common/utils.ts
  • packages/clients/tanstack-query/src/common/types.ts
  • packages/clients/tanstack-query/src/react.ts
  • packages/server/src/api/common/procedures.ts
  • packages/clients/tanstack-query/src/svelte/index.svelte.ts
  • packages/server/src/api/rest/index.ts
  • packages/server/src/api/rpc/index.ts
📚 Learning: 2025-11-26T01:55:04.540Z
Learnt from: CR
Repo: zenstackhq/zenstack-v3 PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-26T01:55:04.540Z
Learning: Applies to tests/e2e/**/*.{ts,tsx} : E2E tests should validate real-world schema compatibility with established projects

Applied to files:

  • packages/server/test/api/rest.test.ts
📚 Learning: 2025-11-26T01:55:04.540Z
Learnt from: CR
Repo: zenstackhq/zenstack-v3 PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-26T01:55:04.540Z
Learning: Applies to packages/zenstackhq/orm/**/*.test.{ts,tsx} : ORM package tests should include comprehensive client API tests and policy tests

Applied to files:

  • packages/server/test/api/rest.test.ts
  • packages/clients/tanstack-query/src/common/types.ts
📚 Learning: 2025-11-26T01:55:04.540Z
Learnt from: CR
Repo: zenstackhq/zenstack-v3 PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-26T01:55:04.540Z
Learning: Applies to **/*.zmodel : ZModel schema files should define database structure and policies that compile to TypeScript via `zenstack generate`

Applied to files:

  • packages/clients/tanstack-query/src/common/types.ts
🧬 Code graph analysis (6)
packages/clients/tanstack-query/src/vue.ts (2)
packages/clients/tanstack-query/src/react.ts (1)
  • ProcedureHooks (183-190)
packages/clients/tanstack-query/src/common/types.ts (4)
  • ExtractProcedures (80-82)
  • ProcedureReturn (88-90)
  • ProcedurePayload (98-99)
  • QueryContext (9-24)
packages/clients/tanstack-query/src/common/types.ts (2)
packages/schema/src/schema.ts (1)
  • SchemaDef (11-19)
packages/orm/src/client/contract.ts (1)
  • ProcedureFunc (309-311)
packages/clients/tanstack-query/src/react.ts (3)
packages/schema/src/schema.ts (1)
  • SchemaDef (11-19)
packages/clients/tanstack-query/src/common/types.ts (3)
  • ExtractProcedures (80-82)
  • ProcedureReturn (88-90)
  • ProcedurePayload (98-99)
packages/clients/tanstack-query/src/common/query-key.ts (1)
  • QueryKey (6-15)
packages/server/src/api/common/procedures.ts (2)
packages/schema/src/schema.ts (2)
  • SchemaDef (11-19)
  • ProcedureDef (82-87)
packages/orm/src/client/index.ts (1)
  • ORMError (7-7)
packages/clients/tanstack-query/src/svelte/index.svelte.ts (3)
packages/clients/tanstack-query/src/react.ts (1)
  • ProcedureHooks (183-190)
packages/clients/tanstack-query/src/common/types.ts (3)
  • ExtractProcedures (80-82)
  • ProcedureReturn (88-90)
  • ProcedurePayload (98-99)
packages/clients/tanstack-query/src/common/query-key.ts (1)
  • QueryKey (6-15)
packages/server/src/api/rest/index.ts (4)
packages/server/src/types.ts (1)
  • Response (52-62)
packages/server/src/api/common/procedures.ts (1)
  • getProcedureDef (6-12)
packages/server/src/api/common/utils.ts (1)
  • processSuperJsonRequestPayload (7-29)
packages/server/src/api/utils.ts (1)
  • log (9-27)
⏰ 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 (22.x, postgresql)
  • GitHub Check: build-test (22.x, sqlite)
🔇 Additional comments (17)
packages/server/test/api/rest.test.ts (2)

3167-3193: LGTM! Well-structured test schema.

The test schema provides comprehensive coverage of procedure parameter types (Decimal, String, Int, arrays, enums, custom types) and both query and mutation procedures.


3194-3283: LGTM! Clear and type-safe procedure implementations.

The procedure implementations are well-typed using ProcCtx and ProcCtxOptionalArgs patterns, providing good examples for procedure usage.

packages/server/src/api/common/procedures.ts (3)

4-4: LGTM! Clear constant definition.

The PROCEDURE_ROUTE_PREFIXES constant uses proper const assertion for type safety.


6-12: LGTM! Safe procedure lookup.

The function correctly uses Object.prototype.hasOwnProperty.call() to avoid prototype pollution when checking for procedure existence.


89-91: LGTM! Clean type guard.

The isOrmError type guard correctly uses instanceof for runtime type checking.

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

34-56: LGTM! Clean query parameter unmarshaling.

The unmarshalQ function properly handles JSON parsing errors and optional SuperJSON deserialization with clear error messages.

packages/server/src/api/rpc/index.ts (6)

8-15: LGTM! Proper utility reuse.

The imports correctly leverage the new shared utilities from common/procedures and common/utils modules, promoting code reuse across REST and RPC handlers.


57-65: LGTM! Clean procedure routing.

The early return pattern for $procs routing is clear and doesn't disrupt the existing CRUD operation flow.


99-99: LGTM! Consistent utility usage.

Replacing direct unmarshalQ usage with the imported utility centralizes SuperJSON handling and adds serialization support to existing RPC operations.

Also applies to: 126-126


184-262: LGTM! Well-structured procedure request handling.

The handleProcedureRequest method implements a clean flow:

  1. Validates procedure existence
  2. Enforces HTTP method constraints per procedure type
  3. Processes SuperJSON payloads
  4. Maps and validates arguments
  5. Invokes procedures via client.$procs[proc]
  6. Serializes responses with proper metadata
  7. Handles errors with appropriate logging

The implementation follows the existing RPC handler patterns and integrates well with the new utilities.


264-269: LGTM! Clean delegation to shared utility.

The private mapProcedureArgs wrapper properly delegates to the shared utility, maintaining encapsulation while reusing common logic.


315-315: LGTM! Enhanced error details.

Adding rejectReason to the policy rejection error response provides valuable context for debugging policy-related failures.

packages/clients/tanstack-query/src/vue.ts (4)

55-63: LGTM! Proper type imports for procedures.

The new type imports (ExtractProcedures, ProcedurePayload, ProcedureReturn) enable strongly-typed procedure hooks aligned with the schema definitions.


67-69: LGTM! Ergonomic hook signature.

The ProcedureHookFn conditional type correctly makes the args parameter optional when the procedure has no required parameters, improving the developer experience.


133-177: LGTM! Well-structured procedure hook types.

The type definitions correctly:

  • Map mutation procedures to useMutation hooks
  • Map query procedures to useQuery and useInfiniteQuery hooks
  • Conditionally expose the $procs namespace only when procedures exist
  • Integrate with the ClientHooks type

The types mirror the React and Svelte implementations, maintaining consistency across client frameworks.


269-320: LGTM! Clean procedure runtime wiring.

The implementation correctly:

  1. Introduces a merge utility for combining root and per-call options with Vue reactivity
  2. Detects procedures in the schema
  3. Wires mutation procedures to useInternalMutation
  4. Wires query procedures to both useInternalQuery and useInternalInfiniteQuery
  5. Mounts all procedure hooks under the $procs namespace

The runtime wiring aligns with the type definitions and maintains consistency with the React and Svelte client implementations.

packages/clients/tanstack-query/src/common/types.ts (1)

80-99: LGTM! Well-structured procedure type utilities.

The type helpers provide a clean abstraction for extracting procedure signatures from the schema:

  • NormalizeProcedurePayload elegantly handles the common case of single-argument procedures by unwrapping the tuple, improving developer experience
  • Proper use of Awaited ensures async procedure return types are correctly resolved
  • Type constraints are well-specified and type-safe

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

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

314-314: Optional: clarify empty otherwise block.

The empty .otherwise() handler is functional but could be clearer with a comment explaining that no additional error fields are needed for other error reasons.

🔎 Optional improvement
-            .otherwise(() => { });
+            .otherwise(() => {
+                // No additional error fields for other error reasons
+            });
📜 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 e8c726e and 055952c.

📒 Files selected for processing (5)
  • packages/clients/tanstack-query/src/svelte/index.svelte.ts
  • packages/server/src/api/common/procedures.ts
  • packages/server/src/api/common/utils.ts
  • packages/server/src/api/rest/index.ts
  • packages/server/src/api/rpc/index.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/server/src/api/common/utils.ts
🧰 Additional context used
🧠 Learnings (3)
📓 Common learnings
Learnt from: CR
Repo: zenstackhq/zenstack-v3 PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-26T01:55:04.540Z
Learning: Applies to packages/zenstackhq/orm/**/*.{ts,tsx} : Implement plugin hooks at ORM, Kysely, and entity mutation levels for query interception and customization
📚 Learning: 2025-12-30T15:07:06.254Z
Learnt from: mwillbanks
Repo: zenstackhq/zenstack-v3 PR: 550
File: packages/orm/src/client/crud/operations/base.ts:158-159
Timestamp: 2025-12-30T15:07:06.254Z
Learning: Do not use ts-expect-error in production code within the zenstackhq/zenstack-v3 repo (e.g., packages/*). Use explicit type annotations, targeted type assertions, or refactor to resolve the type error. ts-expect-error may be acceptable only in test files for stubbing or temporary silencing. Ensure production code is type-safe and maintainable.

Applied to files:

  • packages/server/src/api/rpc/index.ts
  • packages/server/src/api/common/procedures.ts
  • packages/clients/tanstack-query/src/svelte/index.svelte.ts
  • packages/server/src/api/rest/index.ts
📚 Learning: 2025-11-26T01:55:04.540Z
Learnt from: CR
Repo: zenstackhq/zenstack-v3 PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-26T01:55:04.540Z
Learning: Applies to packages/zenstackhq/orm/**/*.{ts,tsx} : Implement plugin hooks at ORM, Kysely, and entity mutation levels for query interception and customization

Applied to files:

  • packages/clients/tanstack-query/src/svelte/index.svelte.ts
🧬 Code graph analysis (3)
packages/server/src/api/rpc/index.ts (2)
packages/server/src/api/common/utils.ts (2)
  • unmarshalQ (34-56)
  • processSuperJsonRequestPayload (7-29)
packages/server/src/api/common/procedures.ts (2)
  • getProcedureDef (6-12)
  • mapProcedureArgs (61-134)
packages/server/src/api/common/procedures.ts (2)
packages/schema/src/schema.ts (2)
  • SchemaDef (11-19)
  • ProcedureDef (82-87)
packages/orm/src/client/index.ts (1)
  • ORMError (7-7)
packages/clients/tanstack-query/src/svelte/index.svelte.ts (4)
packages/clients/tanstack-query/src/vue.ts (1)
  • ProcedureHooks (170-177)
packages/clients/tanstack-query/src/react.ts (1)
  • ProcedureHooks (183-190)
packages/clients/tanstack-query/src/common/types.ts (3)
  • ExtractProcedures (80-82)
  • ProcedureReturn (88-90)
  • ProcedurePayload (98-99)
packages/clients/tanstack-query/src/common/query-key.ts (1)
  • QueryKey (6-15)
⏰ 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 (22.x, postgresql)
  • GitHub Check: build-test (22.x, sqlite)
🔇 Additional comments (15)
packages/server/src/api/common/procedures.ts (3)

1-12: LGTM: Clean utility setup.

The imports, constant export, and getProcedureDef function are well-structured. The use of Object.prototype.hasOwnProperty.call is the correct defensive pattern for checking object properties.


14-134: Well-documented complex validation logic.

The extensive JSDoc documentation added in a previous commit successfully addresses the complexity of this function. The validation paths are now clear, and the rationale for rejecting null/falsey payloads is well-explained.


136-138: LGTM: Correct type guard implementation.

packages/clients/tanstack-query/src/svelte/index.svelte.ts (4)

57-70: LGTM: Well-designed conditional type for procedure hooks.

The ProcedureHookFn type correctly handles optional arguments when the payload is undefined, providing good DX for procedures with no required parameters.


98-104: Good refactor: eliminated code duplication.

The extraction of the merge helper eliminates the duplication that existed between useClientQueries and useModelQueries. Verified that it's used consistently at lines 322, 326, and 330.


138-175: LGTM: Type definitions are consistent across framework adapters.

The procedure hook types properly distinguish between mutations and queries, and the structure mirrors the React and Vue implementations mentioned in relevant code snippets, ensuring consistency.


265-302: LGTM: Procedure hooks generation is well-structured.

The runtime wiring correctly:

  • Iterates over schema procedures when present
  • Distinguishes mutations from queries
  • Uses the extracted merge helper consistently
  • Routes to appropriate internal hook implementations
packages/server/src/api/rpc/index.ts (4)

8-15: LGTM: Appropriate imports of shared utilities.

The imports of procedure utilities from the common modules promote code reuse between RPC and REST handlers.


57-65: LGTM: Clean routing for procedure requests.

The early detection and routing of $procs requests before regular model operations is a clean pattern, consistent with the REST handler implementation.


184-262: LGTM: Comprehensive procedure request handling.

The implementation properly:

  • Validates procedure existence and HTTP methods
  • Uses shared utilities for argument mapping
  • Employs optional chaining for defensive access to $procs
  • Handles SuperJSON serialization consistently
  • Distinguishes ORM errors from generic errors

308-308: Good addition: propagates policy rejection reason.

Including rejectReason in the error response provides valuable debugging context for policy rejections.

packages/server/src/api/rest/index.ts (4)

12-18: LGTM: Consistent imports with RPC handler.

Using the same shared utilities as the RPC handler promotes consistency and reduces duplication.


346-349: LGTM: Early routing with proper validation.

The early detection of procedure paths is clean. The downstream validation at line 501 properly handles missing procedure names.


488-556: LGTM: Well-structured procedure request handling.

The implementation is consistent with the RPC handler while adapting to REST-specific patterns:

  • Uses optional chaining defensively (line 539) - consistent with RPC
  • Calls toPlainObject before SuperJSON (line 540) - necessary for REST serialization
  • Rethrows ORMError (line 552) which is caught by top-level handler (line 476)

The optional chaining mentioned in a past review comment is appropriate here, as it prevents runtime errors if $procs is undefined.


558-569: LGTM: Clean error response helpers.

The procedure-specific error response helpers appropriately delegate to the existing makeError infrastructure and include proper logging.

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

🧹 Nitpick comments (3)
tests/e2e/orm/schemas/procedures/schema.zmodel (1)

24-29: Good test coverage across procedure variations.

The procedure definitions cover a comprehensive range of scenarios: single/array returns, custom types, optional/array parameters, and both query and mutation operations.

Optionally, consider adding @@allow('execute', ...) policies on some procedures in future test iterations to validate policy enforcement integration mentioned in the PR objectives.

tests/e2e/orm/client-api/procedures.test.ts (2)

188-200: Consider more specific error assertions for transaction rollback test.

The test verifies transactional rollback behavior but uses a generic .toThrow() assertion. For better test precision and debugging, consider asserting the specific error type and code.

🔎 Example of more specific error assertion
-        await expect(client.$procs.createMultiple({ args: { names: ['Alice', 'Alice'] } })).rejects.toThrow();
+        await expect(
+            client.$procs.createMultiple({ args: { names: ['Alice', 'Alice'] } })
+        ).rejects.toThrow(/unique constraint|Unique constraint/i);
         await expect(client.user.count()).resolves.toBe(0);

This makes test failures easier to diagnose and ensures you're catching the expected constraint violation rather than any arbitrary error.


7-201: Consider expanding test coverage for real-world scenarios.

The current tests cover core happy paths effectively. To align with the coding guideline that "E2E tests should validate real-world schema compatibility with established projects" and the RFC's emphasis on policy integration and QaaS visibility, consider adding tests for:

  • Procedure argument validation failures (invalid/missing required args)
  • Error handling and custom error propagation from procedures
  • Authorization context and policy enforcement (mentioned in PR objectives/RFC)
  • Procedures with complex nested transactions or error recovery
  • QaaS visibility and handler-specific context features (if applicable at this layer)

These additions would make the test suite more comprehensive and better validate the procedures feature against real-world usage patterns.

📜 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 055952c and fd8f9c6.

📒 Files selected for processing (3)
  • tests/e2e/orm/client-api/procedures.test.ts
  • tests/e2e/orm/schemas/procedures/schema.ts
  • tests/e2e/orm/schemas/procedures/schema.zmodel
🚧 Files skipped from review as they are similar to previous changes (1)
  • tests/e2e/orm/schemas/procedures/schema.ts
🧰 Additional context used
📓 Path-based instructions (2)
tests/e2e/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

E2E tests should validate real-world schema compatibility with established projects

Files:

  • tests/e2e/orm/client-api/procedures.test.ts
**/*.zmodel

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.zmodel: Always run zenstack generate after modifying ZModel schemas
ZModel schema files should define database structure and policies that compile to TypeScript via zenstack generate

Files:

  • tests/e2e/orm/schemas/procedures/schema.zmodel
🧠 Learnings (5)
📓 Common learnings
Learnt from: CR
Repo: zenstackhq/zenstack-v3 PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-26T01:55:04.540Z
Learning: Applies to packages/zenstackhq/orm/**/*.{ts,tsx} : Implement plugin hooks at ORM, Kysely, and entity mutation levels for query interception and customization
📚 Learning: 2025-11-26T01:55:04.540Z
Learnt from: CR
Repo: zenstackhq/zenstack-v3 PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-26T01:55:04.540Z
Learning: Applies to packages/zenstackhq/orm/**/*.test.{ts,tsx} : ORM package tests should include comprehensive client API tests and policy tests

Applied to files:

  • tests/e2e/orm/client-api/procedures.test.ts
📚 Learning: 2025-11-26T01:55:04.540Z
Learnt from: CR
Repo: zenstackhq/zenstack-v3 PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-26T01:55:04.540Z
Learning: Applies to tests/e2e/**/*.{ts,tsx} : E2E tests should validate real-world schema compatibility with established projects

Applied to files:

  • tests/e2e/orm/client-api/procedures.test.ts
📚 Learning: 2025-11-26T01:55:04.540Z
Learnt from: CR
Repo: zenstackhq/zenstack-v3 PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-26T01:55:04.540Z
Learning: Applies to packages/zenstackhq/orm/**/*.{ts,tsx} : Implement plugin hooks at ORM, Kysely, and entity mutation levels for query interception and customization

Applied to files:

  • tests/e2e/orm/client-api/procedures.test.ts
📚 Learning: 2025-11-26T01:55:04.540Z
Learnt from: CR
Repo: zenstackhq/zenstack-v3 PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-26T01:55:04.540Z
Learning: Applies to **/*.zmodel : ZModel schema files should define database structure and policies that compile to TypeScript via `zenstack generate`

Applied to files:

  • tests/e2e/orm/schemas/procedures/schema.zmodel
🧬 Code graph analysis (1)
tests/e2e/orm/client-api/procedures.test.ts (3)
packages/orm/src/client/contract.ts (1)
  • ClientContract (65-199)
packages/testtools/src/client.ts (1)
  • createTestClient (101-248)
packages/plugins/policy/src/plugin.ts (1)
  • name (11-13)
⏰ 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 (22.x, sqlite)
  • GitHub Check: build-test (22.x, postgresql)
🔇 Additional comments (5)
tests/e2e/orm/schemas/procedures/schema.zmodel (5)

1-4: LGTM!

The SQLite datasource configuration is appropriate for an e2e test schema.


6-9: LGTM!

The Role enum is well-defined and provides necessary values for testing role-based scenarios in procedures.


11-16: LGTM!

The Overview type effectively tests array field support (Int[], Role[]) and optional fields, which aligns well with the procedure feature's array handling capabilities.


18-22: LGTM!

The User model provides a clean test entity for procedure operations with appropriate constraints.


1-29: Reminder: Run zenstack generate after modifying this schema.

As per coding guidelines, ensure you run zenstack generate to compile this ZModel schema to TypeScript after any modifications.

Based on coding guidelines.

ymc9 and others added 5 commits January 3, 2026 18:47
- Simplified procedure's params definition from a tuple an object, since procs are now called with an envelop now

- Refactored procedure related typing to make them more consistent with other CURD types (that usually takes the schema as the first type parameter, and a name as the second)

- Moved detailed procedure's types to "crud-types" where other ORM client detailed types are defined

- Removed some type duplication from hooks side

- Updated the "orm" sample to demonstrate procedures
refactor: procedure params generation fix and type refactors
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: 1

🤖 Fix all issues with AI Agents
In @packages/orm/src/client/crud/validator/index.ts:
- Around line 76-157: validateProcedureInput currently iterates only known
params and doesn't reject unknown keys in argsPayload; replace the manual
per-param loop with a single Zod strict object validation: build a mapping from
procDef.params to Zod schemas by calling makeProcedureParamSchema for each param
name, create a z.strictObject schema (so unknown keys are rejected), run
safeParse against argsPayload, and on failure throw createInvalidInputError with
formatError; keep the existing early handling for zero-params and undefined
argsPayload but use the strict schema for all non-empty param sets to ensure
extraneous/misspelled args are rejected.
📜 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 bcd5444 and 9630d8f.

📒 Files selected for processing (19)
  • packages/cli/test/ts-schema-gen.test.ts
  • packages/clients/tanstack-query/src/common/types.ts
  • packages/clients/tanstack-query/src/react.ts
  • packages/clients/tanstack-query/src/svelte/index.svelte.ts
  • packages/clients/tanstack-query/src/vue.ts
  • packages/clients/tanstack-query/test/react-typing-test.ts
  • packages/clients/tanstack-query/test/schemas/procedures/schema-lite.ts
  • packages/orm/src/client/contract.ts
  • packages/orm/src/client/crud-types.ts
  • packages/orm/src/client/crud/validator/index.ts
  • packages/orm/src/client/options.ts
  • packages/orm/src/utils/type-utils.ts
  • packages/schema/src/schema.ts
  • packages/sdk/src/ts-schema-generator.ts
  • packages/server/src/api/common/procedures.ts
  • samples/orm/main.ts
  • samples/orm/zenstack/schema.ts
  • samples/orm/zenstack/schema.zmodel
  • tests/e2e/orm/schemas/procedures/schema.ts
🚧 Files skipped from review as they are similar to previous changes (6)
  • packages/clients/tanstack-query/src/common/types.ts
  • packages/clients/tanstack-query/src/vue.ts
  • packages/clients/tanstack-query/test/schemas/procedures/schema-lite.ts
  • packages/orm/src/client/options.ts
  • packages/schema/src/schema.ts
  • tests/e2e/orm/schemas/procedures/schema.ts
🧰 Additional context used
📓 Path-based instructions (1)
**/*.zmodel

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.zmodel: Always run zenstack generate after modifying ZModel schemas
ZModel schema files should define database structure and policies that compile to TypeScript via zenstack generate

Files:

  • samples/orm/zenstack/schema.zmodel
🧠 Learnings (7)
📓 Common learnings
Learnt from: CR
Repo: zenstackhq/zenstack-v3 PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-26T01:55:04.540Z
Learning: Applies to packages/zenstackhq/orm/**/*.{ts,tsx} : Implement plugin hooks at ORM, Kysely, and entity mutation levels for query interception and customization
📚 Learning: 2025-11-26T01:55:04.540Z
Learnt from: CR
Repo: zenstackhq/zenstack-v3 PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-26T01:55:04.540Z
Learning: Applies to **/*.zmodel : ZModel schema files should define database structure and policies that compile to TypeScript via `zenstack generate`

Applied to files:

  • samples/orm/zenstack/schema.ts
  • samples/orm/zenstack/schema.zmodel
  • packages/orm/src/client/contract.ts
  • packages/cli/test/ts-schema-gen.test.ts
  • packages/clients/tanstack-query/src/react.ts
📚 Learning: 2025-11-26T01:55:04.540Z
Learnt from: CR
Repo: zenstackhq/zenstack-v3 PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-26T01:55:04.540Z
Learning: Applies to packages/zenstackhq/orm/**/*.{ts,tsx} : Implement plugin hooks at ORM, Kysely, and entity mutation levels for query interception and customization

Applied to files:

  • samples/orm/zenstack/schema.ts
  • samples/orm/main.ts
  • packages/clients/tanstack-query/src/react.ts
  • packages/clients/tanstack-query/src/svelte/index.svelte.ts
  • packages/orm/src/client/crud-types.ts
📚 Learning: 2025-11-26T01:55:04.540Z
Learnt from: CR
Repo: zenstackhq/zenstack-v3 PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-26T01:55:04.540Z
Learning: Applies to packages/zenstackhq/orm/**/*.test.{ts,tsx} : ORM package tests should include comprehensive client API tests and policy tests

Applied to files:

  • packages/clients/tanstack-query/test/react-typing-test.ts
  • samples/orm/main.ts
  • packages/orm/src/client/crud-types.ts
📚 Learning: 2025-11-26T01:55:04.540Z
Learnt from: CR
Repo: zenstackhq/zenstack-v3 PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-26T01:55:04.540Z
Learning: Applies to tests/e2e/**/*.{ts,tsx} : E2E tests should validate real-world schema compatibility with established projects

Applied to files:

  • packages/clients/tanstack-query/test/react-typing-test.ts
  • packages/cli/test/ts-schema-gen.test.ts
📚 Learning: 2025-11-26T01:55:04.540Z
Learnt from: CR
Repo: zenstackhq/zenstack-v3 PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-26T01:55:04.540Z
Learning: Applies to packages/zenstackhq/orm/**/*.{ts,tsx} : Use Kysely as the query builder interface for low-level database queries, avoiding raw SQL when possible

Applied to files:

  • packages/clients/tanstack-query/test/react-typing-test.ts
  • packages/orm/src/client/contract.ts
  • packages/clients/tanstack-query/src/react.ts
  • packages/orm/src/client/crud/validator/index.ts
  • packages/clients/tanstack-query/src/svelte/index.svelte.ts
  • packages/orm/src/client/crud-types.ts
📚 Learning: 2025-12-30T15:07:06.254Z
Learnt from: mwillbanks
Repo: zenstackhq/zenstack-v3 PR: 550
File: packages/orm/src/client/crud/operations/base.ts:158-159
Timestamp: 2025-12-30T15:07:06.254Z
Learning: Do not use ts-expect-error in production code within the zenstackhq/zenstack-v3 repo (e.g., packages/*). Use explicit type annotations, targeted type assertions, or refactor to resolve the type error. ts-expect-error may be acceptable only in test files for stubbing or temporary silencing. Ensure production code is type-safe and maintainable.

Applied to files:

  • packages/orm/src/utils/type-utils.ts
  • packages/orm/src/client/contract.ts
  • packages/clients/tanstack-query/src/react.ts
  • packages/orm/src/client/crud/validator/index.ts
  • packages/server/src/api/common/procedures.ts
  • packages/clients/tanstack-query/src/svelte/index.svelte.ts
  • packages/sdk/src/ts-schema-generator.ts
  • packages/orm/src/client/crud-types.ts
🧬 Code graph analysis (6)
packages/clients/tanstack-query/test/react-typing-test.ts (3)
packages/clients/tanstack-query/src/react.ts (1)
  • useClientQueries (327-382)
packages/clients/tanstack-query/src/svelte/index.svelte.ts (1)
  • useClientQueries (274-316)
packages/clients/tanstack-query/src/vue.ts (1)
  • useClientQueries (278-328)
packages/orm/src/client/contract.ts (2)
packages/schema/src/schema.ts (2)
  • SchemaDef (11-19)
  • ProcedureDef (82-87)
packages/orm/src/client/crud-types.ts (2)
  • GetProcedureNames (1986-1988)
  • ProcedureFunc (2041-2045)
packages/cli/test/ts-schema-gen.test.ts (1)
packages/testtools/src/schema.ts (1)
  • generateTsSchema (40-74)
packages/clients/tanstack-query/src/react.ts (3)
packages/schema/src/schema.ts (1)
  • SchemaDef (11-19)
packages/orm/src/client/crud-types.ts (3)
  • GetProcedureNames (1986-1988)
  • ProcedureEnvelope (2021-2032)
  • GetProcedure (1996-2000)
packages/clients/tanstack-query/src/common/types.ts (1)
  • ProcedureReturn (80-82)
packages/orm/src/client/crud/validator/index.ts (4)
packages/schema/src/schema.ts (2)
  • ProcedureDef (82-87)
  • BuiltinType (89-99)
packages/orm/src/client/errors.ts (2)
  • createInvalidInputError (116-120)
  • createInternalError (147-151)
packages/orm/src/utils/zod-utils.ts (1)
  • formatError (7-9)
packages/orm/src/client/query-utils.ts (2)
  • isTypeDef (201-203)
  • isEnum (193-195)
packages/server/src/api/common/procedures.ts (2)
packages/schema/src/schema.ts (3)
  • SchemaDef (11-19)
  • ProcedureDef (82-87)
  • ProcedureParam (80-80)
packages/orm/src/client/index.ts (1)
  • ORMError (7-7)
⏰ 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 (22.x, sqlite)
  • GitHub Check: build-test (22.x, postgresql)
🔇 Additional comments (15)
packages/orm/src/client/crud/validator/index.ts (2)

12-12: LGTM: Import additions support procedure validation.

The new imports (ProcedureDef, isEnum, isTypeDef) are properly utilized in the validateProcedureInput and makeProcedureParamSchema methods.

Also applies to: 43-44


159-188: LGTM: Per-parameter schema builder is well-structured.

The helper correctly:

  • Uses isTypeDef and isEnum helpers (addressing past review feedback)
  • Handles TypeDefs, Enums, Models, and builtin scalar types
  • Applies array and optional modifiers appropriately
  • Throws an internal error for unsupported types

The permissive validation for model-typed parameters (line 168: z.record(z.string(), z.unknown())) is a reasonable trade-off that avoids deep object validation while still ensuring the value is an object.

packages/clients/tanstack-query/test/react-typing-test.ts (2)

3-6: LGTM! Procedures client setup follows established patterns.

The import and client initialization for procedures testing mirrors the existing pattern used for the basic schema, ensuring consistency in test structure.


117-137: Excellent comprehensive test coverage for procedures API.

The test cases thoroughly exercise the new $procs namespace across multiple scenarios:

  • Query procedures with optional arguments and type validation
  • Infinite query support for array-returning procedures
  • Mutation procedures with envelope-style invocation ({ args: { ... } })
  • Type safety enforcement via @ts-expect-error annotations
  • Both synchronous and async (thenable) execution patterns

The tests align well with the existing model-based test patterns and validate the strongly-typed per-procedure hook surface introduced by this PR.

packages/cli/test/ts-schema-gen.test.ts (1)

187-209: LGTM! Essential test coverage for array parameter and return type handling.

This test validates that the TypeScript schema generator correctly captures array semantics for procedures:

  • Array parameters are marked with array: true on the param definition
  • Array return types set returnArray: true while keeping returnType as the element type

This metadata is crucial for both client-side type generation and runtime input validation in the ORM layer. The test complements the broader procedure typing system introduced in this PR.

packages/orm/src/utils/type-utils.ts (1)

34-50: LGTM! TypeMap extension supports expanded procedure type coverage.

The changes enhance the type mapping system appropriately:

  1. Export addition - Making TypeMap exported enables reuse across the codebase (referenced in crud-types.ts and elsewhere)
  2. New type mappings:
    • Void - Essential for procedures with side effects that return no value
    • Null, Undefined - Support nullable types in procedure signatures
    • Object - Maps to Record<string, unknown> for generic object parameters
    • Any, Unsupported - Reasonable fallbacks for types that cannot be precisely mapped

These additions align with the expanded procedure type system and enable more comprehensive ZModel type coverage.

samples/orm/zenstack/schema.ts (1)

242-257: Generated code correctly reflects ZModel procedure definitions.

The generated procedures property accurately represents the procedure declarations from schema.zmodel:

  • signUp with properly typed parameters (including optional flags) and mutation: true
  • listPublicPosts with returnArray: true for array return type

As this is auto-generated code (per the file header warning), no manual changes should be made. The generation appears to be working correctly.

samples/orm/zenstack/schema.zmodel (1)

58-59: Procedure declarations are correct and properly generated.

The signUp and listPublicPosts procedures follow ZModel conventions with correct syntax:

  • signUp correctly defined as a mutation with required email and optional name/role parameters, returning User
  • listPublicPosts correctly returns an array of Post models

The generated schema.ts file reflects these definitions accurately with proper parameter configurations and type mappings.

samples/orm/main.ts (1)

19-30: Procedures wiring and sample usage look consistent end‑to‑end

procedures registration matches the new $procs contract, and the example calls via authDb.$procs.signUp / authDb.$procs.listPublicPosts correctly demonstrate mutation vs query semantics while flowing through policy-enabled clients.

Also applies to: 116-121

packages/server/src/api/common/procedures.ts (1)

4-12: Server‑side procedure utilities are coherent and defensively implemented

Route prefixes, schema lookup, and mapProcedureArgs’ validation logic (zero/all‑optional/required params, envelope handling, and strict object checks) align with the documented behavior and provide a solid contract for REST/RPC layers; isOrmError is a minimal, correct type guard.

Also applies to: 14-133, 135-137

packages/sdk/src/ts-schema-generator.ts (1)

1126-1158: Procedure schema emission now matches runtime ProcedureDef shape

Object‑keyed params with { name, type, optional?, array? } entries plus the new returnArray flag correctly mirror the updated ProcedureDef/ProcedureParam types and should make downstream procedure typing much simpler.

packages/orm/src/client/contract.ts (1)

12-12: Typed $procs surface integrates cleanly into ClientContract

ProcedureOperations<Schema> conditionally adds $procs keyed by GetProcedureNames<Schema> with ProcedureFunc signatures, so client instances gain strongly typed custom procedures only when the schema defines them, without impacting existing CRUD typings.

Also applies to: 29-30, 33-33, 195-217

packages/clients/tanstack-query/src/react.ts (1)

39-41: Procedure hooks for React reuse existing plumbing and preserve typing semantics

The new ProcedureHookFn / ProcedureHookGroup / ProcedureHooks types plus $procs integration in useClientQueries cleanly extend model hooks to procedures, correctly distinguish mutation vs query procedures, and enforce the envelope contract while reusing the existing internal query/mutation helpers and $procs endpoint routing.

Also applies to: 44-44, 61-65, 68-77, 148-151, 152-201, 327-382

packages/clients/tanstack-query/src/svelte/index.svelte.ts (1)

40-45: Svelte procedure hooks and shared merge helper are well‑structured

Accessor‑aware ProcedureHookFn / ProcedureHookGroup / ProcedureHooks, the extracted merge helper, and the $procs wiring in useClientQueries extend the model hook pattern to procedures cleanly while avoiding duplication and keeping options merging consistent.

Also applies to: 62-66, 69-78, 105-111, 145-148, 149-189, 274-316

packages/orm/src/client/crud-types.ts (1)

26-34: Procedure typing utilities provide a solid, composable foundation

GetProcedureNames / GetProcedureParams / GetProcedure, ProcedureEnvelope, and ProcedureFunc/ProcedureHandlerFunc together give strongly typed procedure inputs and outputs, while MapProcedureReturn/MapProcedureParam + MapType correctly resolve schema string types to model/typedef/enum/builtin shapes; this should make both client $procs and server handlers type‑safe without extra casting.

Also applies to: 35-50, 51-53, 1984-2067, 2085-2099

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

🧹 Nitpick comments (3)
packages/clients/tanstack-query/src/vue.ts (1)

314-320: Runtime exposes useInfiniteQuery for procedures but types exclude it.

The runtime wiring at lines 318-319 creates useInfiniteQuery for non-mutation procedures, but the ProcedureHookGroup type (lines 174-181) has this commented out. This inconsistency means the hook exists at runtime but TypeScript won't recognize it.

Consider removing the runtime wiring to match the type definition, keeping behavior consistent with the decision to defer infinite queries for procedures.

♻️ Proposed fix to align runtime with types
                 } else {
                     acc[name] = {
                         useQuery: (args?: any, hookOptions?: any) =>
                             useInternalQuery(schema, endpointModel, name, args, merge(options, hookOptions)),
-                        useInfiniteQuery: (args?: any, hookOptions?: any) =>
-                            useInternalInfiniteQuery(schema, endpointModel, name, args, merge(options, hookOptions)),
                     };
                 }
packages/clients/tanstack-query/src/react.ts (1)

364-374: Runtime wires infinite query hooks for procedures but types exclude them.

Lines 364-373 create useInfiniteQuery and useSuspenseInfiniteQuery hooks for procedures at runtime, but the corresponding types in ProcedureHookGroup (lines 178-192) are commented out. Users cannot access these hooks with type safety.

Remove this dead runtime code to match the type definitions and the decision to defer infinite queries for procedures.

♻️ Proposed fix to remove untyped runtime hooks
                 } else {
                     acc[name] = {
                         useQuery: (args?: any, hookOptions?: any) =>
                             useInternalQuery(schema, endpointModel, name, args, { ...options, ...hookOptions }),
                         useSuspenseQuery: (args?: any, hookOptions?: any) =>
                             useInternalSuspenseQuery(schema, endpointModel, name, args, {
                                 ...options,
                                 ...hookOptions,
                             }),
-                        useInfiniteQuery: (args?: any, hookOptions?: any) =>
-                            useInternalInfiniteQuery(schema, endpointModel, name, args, {
-                                ...options,
-                                ...hookOptions,
-                            }),
-                        useSuspenseInfiniteQuery: (args?: any, hookOptions?: any) =>
-                            useInternalSuspenseInfiniteQuery(schema, endpointModel, name, args, {
-                                ...options,
-                                ...hookOptions,
-                            }),
                     };
                 }
packages/clients/tanstack-query/src/svelte/index.svelte.ts (1)

304-308: Runtime exposes useInfiniteQuery for procedures but types exclude it.

Similar to React and Vue, line 306-307 creates useInfiniteQuery for non-mutation procedures at runtime, but the ProcedureHookGroup type (lines 172-179) has this commented out.

For consistency across all three frameworks and to match the decision to defer infinite queries, remove this runtime wiring.

♻️ Proposed fix
                 } else {
                     acc[name] = {
                         useQuery: (args?: any, hookOptions?: any) =>
                             useInternalQuery(schema, endpointModel, name, args, merge(options, hookOptions)),
-                        useInfiniteQuery: (args?: any, hookOptions?: any) =>
-                            useInternalInfiniteQuery(schema, endpointModel, name, args, merge(options, hookOptions)),
                     };
                 }
📜 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 9630d8f and db3c499.

📒 Files selected for processing (6)
  • packages/clients/tanstack-query/src/react.ts
  • packages/clients/tanstack-query/src/svelte/index.svelte.ts
  • packages/clients/tanstack-query/src/vue.ts
  • packages/clients/tanstack-query/test/react-typing-test.ts
  • packages/clients/tanstack-query/test/svelte-typing-test.ts
  • packages/clients/tanstack-query/test/vue-typing-test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/clients/tanstack-query/test/vue-typing-test.ts
🧰 Additional context used
🧠 Learnings (8)
📓 Common learnings
Learnt from: CR
Repo: zenstackhq/zenstack-v3 PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-26T01:55:04.540Z
Learning: Applies to packages/zenstackhq/orm/**/*.{ts,tsx} : Implement plugin hooks at ORM, Kysely, and entity mutation levels for query interception and customization
📚 Learning: 2025-11-26T01:55:04.540Z
Learnt from: CR
Repo: zenstackhq/zenstack-v3 PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-26T01:55:04.540Z
Learning: Applies to packages/zenstackhq/orm/**/*.test.{ts,tsx} : ORM package tests should include comprehensive client API tests and policy tests

Applied to files:

  • packages/clients/tanstack-query/test/react-typing-test.ts
  • packages/clients/tanstack-query/test/svelte-typing-test.ts
📚 Learning: 2025-11-26T01:55:04.540Z
Learnt from: CR
Repo: zenstackhq/zenstack-v3 PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-26T01:55:04.540Z
Learning: Applies to tests/e2e/**/*.{ts,tsx} : E2E tests should validate real-world schema compatibility with established projects

Applied to files:

  • packages/clients/tanstack-query/test/react-typing-test.ts
  • packages/clients/tanstack-query/test/svelte-typing-test.ts
📚 Learning: 2025-11-26T01:55:04.540Z
Learnt from: CR
Repo: zenstackhq/zenstack-v3 PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-26T01:55:04.540Z
Learning: Applies to packages/zenstackhq/orm/**/*.{ts,tsx} : Implement plugin hooks at ORM, Kysely, and entity mutation levels for query interception and customization

Applied to files:

  • packages/clients/tanstack-query/src/svelte/index.svelte.ts
  • packages/clients/tanstack-query/test/svelte-typing-test.ts
  • packages/clients/tanstack-query/src/vue.ts
  • packages/clients/tanstack-query/src/react.ts
📚 Learning: 2025-11-26T01:55:04.540Z
Learnt from: CR
Repo: zenstackhq/zenstack-v3 PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-26T01:55:04.540Z
Learning: Applies to packages/zenstackhq/orm/**/*.{ts,tsx} : Use Kysely as the query builder interface for low-level database queries, avoiding raw SQL when possible

Applied to files:

  • packages/clients/tanstack-query/src/svelte/index.svelte.ts
  • packages/clients/tanstack-query/test/svelte-typing-test.ts
  • packages/clients/tanstack-query/src/vue.ts
  • packages/clients/tanstack-query/src/react.ts
📚 Learning: 2025-12-30T15:07:06.254Z
Learnt from: mwillbanks
Repo: zenstackhq/zenstack-v3 PR: 550
File: packages/orm/src/client/crud/operations/base.ts:158-159
Timestamp: 2025-12-30T15:07:06.254Z
Learning: Do not use ts-expect-error in production code within the zenstackhq/zenstack-v3 repo (e.g., packages/*). Use explicit type annotations, targeted type assertions, or refactor to resolve the type error. ts-expect-error may be acceptable only in test files for stubbing or temporary silencing. Ensure production code is type-safe and maintainable.

Applied to files:

  • packages/clients/tanstack-query/src/svelte/index.svelte.ts
  • packages/clients/tanstack-query/src/vue.ts
  • packages/clients/tanstack-query/src/react.ts
📚 Learning: 2025-11-26T01:55:04.540Z
Learnt from: CR
Repo: zenstackhq/zenstack-v3 PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-26T01:55:04.540Z
Learning: Applies to tests/**/type*.{ts,tsx} : Ensure TypeScript inference and type coverage are validated through type coverage tests

Applied to files:

  • packages/clients/tanstack-query/test/svelte-typing-test.ts
📚 Learning: 2025-11-26T01:55:04.540Z
Learnt from: CR
Repo: zenstackhq/zenstack-v3 PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-26T01:55:04.540Z
Learning: Applies to **/*.zmodel : ZModel schema files should define database structure and policies that compile to TypeScript via `zenstack generate`

Applied to files:

  • packages/clients/tanstack-query/src/react.ts
🧬 Code graph analysis (4)
packages/clients/tanstack-query/test/react-typing-test.ts (5)
packages/clients/tanstack-query/src/react.ts (1)
  • useClientQueries (329-384)
packages/clients/tanstack-query/test/schemas/procedures/schema-lite.ts (1)
  • schema (60-60)
samples/orm/zenstack/schema.ts (1)
  • schema (260-260)
tests/e2e/orm/schemas/procedures/schema.ts (1)
  • schema (121-121)
packages/clients/tanstack-query/test/schemas/basic/schema-lite.ts (1)
  • schema (172-172)
packages/clients/tanstack-query/src/svelte/index.svelte.ts (3)
packages/orm/src/client/crud-types.ts (3)
  • GetProcedureNames (1986-1988)
  • ProcedureEnvelope (2021-2032)
  • GetProcedure (1996-2000)
packages/clients/tanstack-query/src/react.ts (2)
  • useInternalMutation (613-700)
  • useInternalQuery (502-523)
packages/clients/tanstack-query/src/common/types.ts (1)
  • ProcedureReturn (80-82)
packages/clients/tanstack-query/test/svelte-typing-test.ts (1)
packages/clients/tanstack-query/src/svelte/index.svelte.ts (1)
  • useClientQueries (276-318)
packages/clients/tanstack-query/src/vue.ts (4)
packages/schema/src/schema.ts (1)
  • SchemaDef (11-19)
packages/orm/src/client/crud-types.ts (3)
  • GetProcedureNames (1986-1988)
  • ProcedureEnvelope (2021-2032)
  • GetProcedure (1996-2000)
packages/clients/tanstack-query/src/common/types.ts (1)
  • ProcedureReturn (80-82)
packages/clients/tanstack-query/src/common/query-key.ts (1)
  • QueryKey (6-15)
⏰ 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 (22.x, sqlite)
  • GitHub Check: build-test (22.x, postgresql)
🔇 Additional comments (9)
packages/clients/tanstack-query/test/svelte-typing-test.ts (1)

123-147: Comprehensive procedure typing tests for Svelte client.

Good coverage of the new $procs API surface:

  • Query procedures with optional and required args
  • Mutation procedures with envelope-style invocation
  • Type constraint validation via @ts-expect-error comments
  • Proper use of Svelte's Accessor pattern with arrow functions

The commented-out infinite query tests align with the decision to defer that feature.

packages/clients/tanstack-query/test/react-typing-test.ts (1)

117-140: Solid typing tests for React procedure hooks.

Tests comprehensively validate the $procs API:

  • Query hooks with args, options, and queryKey access
  • Type errors for wrong arg shapes and missing required args
  • Mutation hooks with both mutate and mutateAsync
  • Proper detection of query vs mutation procedures via @ts-expect-error
packages/clients/tanstack-query/src/vue.ts (2)

68-77: Well-designed conditional type for procedure hook signatures.

The ProcedureHookFn type elegantly handles optional vs required args using conditional typing based on the ProcedureEnvelope. Vue's MaybeRefOrGetter wrapper is correctly applied for reactivity.


142-193: Clean procedure hook type definitions.

The ProcedureHookGroup and ProcedureHooks types properly:

  • Differentiate mutation vs query procedures via conditional mapping
  • Exclude optimisticUpdate from query options (not applicable to procedures)
  • Use conditional extension to only add $procs when schema has procedures
packages/clients/tanstack-query/src/react.ts (2)

68-77: Good conditional type for procedure hook function signatures.

The ProcedureHookFn type correctly uses { args: undefined } extends Input to determine if input is optional, providing appropriate overloads for procedures with and without required arguments.


148-204: Well-structured procedure hook types for React.

The type definitions properly:

  • Map procedure names to their appropriate hook shapes
  • Include useSuspenseQuery for React (correctly matching runtime)
  • Exclude optimisticUpdate from procedure query options
  • Conditionally add $procs only when schema defines procedures
packages/clients/tanstack-query/src/svelte/index.svelte.ts (3)

105-111: Good extraction of merge helper to module level.

The merge helper is now properly defined at module scope, eliminating the duplication that was previously flagged. The implementation correctly handles both function and non-function values for Svelte's reactive patterns.


69-78: Correct Accessor-wrapped procedure hook function type.

The ProcedureHookFn for Svelte properly wraps both args and options in Accessor<>, aligning with Svelte's reactive patterns. The conditional logic for optional vs required args is correctly implemented.


149-191: Clean procedure hook type definitions for Svelte.

The types correctly:

  • Use Svelte-specific CreateQueryResult and CreateMutationResult
  • Wrap options in Accessor<> for reactivity
  • Exclude optimisticUpdate from procedure query options
  • Conditionally expose $procs based on schema.procedures

@ymc9 ymc9 merged commit 2172614 into zenstackhq:dev Jan 8, 2026
5 of 6 checks passed
@claude claude bot mentioned this pull request Jan 8, 2026
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.

3 participants