Skip to content

Split fmodata count into count-only and counted list#210

Merged
eluce2 merged 4 commits intomainfrom
feature/split-fmodata-count-api
Apr 17, 2026
Merged

Split fmodata count into count-only and counted list#210
eluce2 merged 4 commits intomainfrom
feature/split-fmodata-count-api

Conversation

@eluce2
Copy link
Copy Markdown
Collaborator

@eluce2 eluce2 commented Apr 7, 2026

Summary

  • Add db.from(table).count() as a count-only flow against /$count.
  • Keep db.from(table).list().count() for one-request list plus total count, returning { records, count }.
  • Update query typing, URL building, response parsing, tests, docs, and README for the new split.
  • Add a minor changeset for @proofkit/fmodata.

Testing

  • Not run
  • Added/updated package tests for batch, mock, query strings, e2e, and TypeScript behavior.
  • Updated docs examples and method reference to match the new API.

Summary by CodeRabbit

  • New Features

    • Count API split: top-level count returns a numeric total; list-based count returns { records, count } to support paginated queries.
  • Documentation

    • Docs and examples updated to illustrate both count patterns and recommended usage.
  • Tests

    • Added unit, integration, e2e and type tests covering both count flows, batching, error paths, and compile-time constraints.
  • Chores

    • Added changeset recording the minor version bump.

- add count-only `db.from(table).count()`
- keep `list().count()` for `{ records, count }`
@changeset-bot
Copy link
Copy Markdown

changeset-bot bot commented Apr 7, 2026

🦋 Changeset detected

Latest commit: 98d2542

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 4 packages
Name Type
@proofkit/fmodata Minor
@proofkit/better-auth Patch
@proofkit/typegen Patch
@proofkit/cli Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@vercel
Copy link
Copy Markdown

vercel bot commented Apr 7, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
proofkit-docs Ready Ready Preview Apr 17, 2026 3:50am

Request Review

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new bot commented Apr 7, 2026

Open in StackBlitz

@proofkit/better-auth

pnpm add https://pkg.pr.new/proofsh/proofkit/@proofkit/better-auth@210

@proofkit/cli

pnpm add https://pkg.pr.new/proofsh/proofkit/@proofkit/cli@210

create-proofkit

pnpm add https://pkg.pr.new/proofsh/proofkit/create-proofkit@210

@proofkit/fmdapi

pnpm add https://pkg.pr.new/proofsh/proofkit/@proofkit/fmdapi@210

@proofkit/fmodata

pnpm add https://pkg.pr.new/proofsh/proofkit/@proofkit/fmodata@210

@proofkit/typegen

pnpm add https://pkg.pr.new/proofsh/proofkit/@proofkit/typegen@210

@proofkit/webviewer

pnpm add https://pkg.pr.new/proofsh/proofkit/@proofkit/webviewer@210

commit: 98d2542

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 7, 2026

📝 Walkthrough

Walkthrough

Split the fmodata count API: db.from(table).count() issues a count-only /$count request returning a number, while db.from(table).list().count() performs a list query returning { records, count }. Adds CountBuilder, URL/path logic, type plumbing, response parsing, docs, and tests.

Changes

Cohort / File(s) Summary
Changeset & Docs
/.changeset/split-fmodata-count-api.md, apps/docs/content/docs/fmodata/methods.mdx, apps/docs/content/docs/fmodata/queries.mdx, packages/fmodata/README.md
Documented API split and updated examples to show top-level count() vs list-based list().count() usage.
New Count Builder
packages/fmodata/src/client/count-builder.ts
Added CountBuilder for standalone /$count requests: filter handling, query-string/path building, execution, numeric parsing, and response validation.
EntitySet & URL Building
packages/fmodata/src/client/entity-set.ts, packages/fmodata/src/client/query/url-builder.ts
Added EntitySet.count() factory; QueryUrlBuilder.buildPath() gains isCount to render /$count suffix instead of ?$count=true.
Query Builder & Types
packages/fmodata/src/client/query/query-builder.ts, packages/fmodata/src/client/query/types.ts, packages/fmodata/src/client/query/index.ts
Introduced IncludeCount generic and CountedListResult plumbing; preserved fluent chaining types; prevented .single()/.maybeSingle() when count-enabled; re-exported CountedListResult.
Response Processing & State
packages/fmodata/src/client/builders/read-builder-state.ts, packages/fmodata/src/client/builders/response-processor.ts
Added includeCountMode state; processQueryResponse accepts includeCount, validates/parses @odata.count, and returns { records, count } when enabled, with structure-error handling.
Types & Public Exports
packages/fmodata/src/types.ts, packages/fmodata/src/index.ts
Added CountedListResult<T> and related deep annotation/special-column types; exported type CountBuilder and CountedListResult; small ExecutableBuilder.execute(options?) typing change.
Internal Plumbing
packages/fmodata/src/client/batch-builder.ts, packages/fmodata/src/client/record-builder.ts
Updated internal type helpers to infer from processResponse signatures and adjusted generics to thread the new boolean includeCount state.
Tests
packages/fmodata/tests/*.test.ts, packages/fmodata/tests/e2e/*.test.ts, packages/fmodata/tests/*
Added/updated tests for top-level /​$count vs list-based count, batch with mixed count types, response-structure errors, query-string generation, E2E assertions, and TypeScript ergonomics/type-constraint tests.
Environment Config
.codex/environments/environment.toml
Updated setup script to pnpm i and removed redundant [[actions]] entry.

Sequence Diagram

sequenceDiagram
    participant Client
    participant EntitySet
    participant CountBuilder
    participant QueryBuilder
    participant ResponseProcessor
    participant FMODataServer

    rect rgba(100,200,150,0.5)
    Note over Client,FMODataServer: Count-Only Flow: db.from(table).count()
    Client->>EntitySet: count()
    EntitySet->>CountBuilder: instantiate
    Client->>CountBuilder: where(...)? / execute()
    CountBuilder->>FMODataServer: GET /table/$count[?$filter=...]
    FMODataServer-->>CountBuilder: "5" (plain text)
    CountBuilder->>ResponseProcessor: processResponse(response)
    ResponseProcessor-->>CountBuilder: { data: 5, error: undefined }
    CountBuilder-->>Client: Result<number>
    end

    rect rgba(150,150,200,0.5)
    Note over Client,FMODataServer: List-With-Count Flow: db.from(table).list().count()
    Client->>EntitySet: list()
    EntitySet->>QueryBuilder: instantiate
    Client->>QueryBuilder: top(...)/skip(...)/count()/execute()
    QueryBuilder->>FMODataServer: GET /table?$top=...&$skip=...&$count=true[&$filter=...]
    FMODataServer-->>QueryBuilder: { "@odata.count": 150, "value": [ ... ] }
    QueryBuilder->>ResponseProcessor: processQueryResponse(response, includeCount=true)
    ResponseProcessor-->>QueryBuilder: { data: { records: [...], count: 150 }, error: undefined }
    QueryBuilder-->>Client: Result<CountedListResult>
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 40.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely summarizes the main change: splitting the fmodata count API into two distinct flows (count-only via db.from(table).count() and counted list via db.from(table).list().count()).

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/split-fmodata-count-api

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

Copy link
Copy Markdown

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

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/fmodata/src/client/builders/response-processor.ts`:
- Around line 280-309: The counted-response branch currently ignores
processedResponse.error and can return a successful CountedListResult with
records undefined; update the includeCount handling in the function that
processes processODataResponse results to first check if processedResponse.error
exists and, if so, return { data: undefined, error: processedResponse.error }
before building the { records, count } result (preserve existing singleMode and
`@odata.count` validation and ResponseStructureError behavior). Ensure you
reference processedResponse, includeCount, singleMode, and the CountedListResult
wrapper when applying the guard so original validation/structure errors
propagate.

In `@packages/fmodata/src/client/count-builder.ts`:
- Around line 101-105: The navigated count URLs are built without the "/$count"
suffix because QueryUrlBuilder.build() returns navigation paths before applying
the isCount branch; update the code paths that construct count requests (methods
execute, getRequestConfig, and getQueryString usage) to call the path-building
logic that respects isCount or to use buildPath() for navigations so the isCount
flag results in a "/$count" segment; specifically, ensure
QueryUrlBuilder.build(queryString, { isCount: true, useEntityIds: ...,
navigation: this.navigationConfig }) produces a path with "/$count" by moving
the isCount handling ahead of the navigation-return branch or by delegating
navigation cases to buildPath() which already appends "/$count".

In `@packages/fmodata/src/types.ts`:
- Around line 11-12: The interface ExecutableBuilder<_T> is erasing its generic
by returning Result<unknown>; update the signatures to use the generic type
parameter (e.g., change execute(options?: ExecuteOptions):
Promise<Result<unknown>> to return Promise<Result<T>>) and likewise update
processResponse (and any other methods on ExecutableBuilder) to use T instead of
unknown so implementations like UpdateBuilder/QueryBuilder/InsertBuilder
correctly preserve their concrete type parameter through the API boundary.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: f62c5b5a-6052-4fab-9d4f-a2fc4b86ba76

📥 Commits

Reviewing files that changed from the base of the PR and between 175b0bd and ac7c9f4.

📒 Files selected for processing (21)
  • .changeset/split-fmodata-count-api.md
  • apps/docs/content/docs/fmodata/methods.mdx
  • apps/docs/content/docs/fmodata/queries.mdx
  • packages/fmodata/README.md
  • packages/fmodata/src/client/batch-builder.ts
  • packages/fmodata/src/client/builders/read-builder-state.ts
  • packages/fmodata/src/client/builders/response-processor.ts
  • packages/fmodata/src/client/count-builder.ts
  • packages/fmodata/src/client/entity-set.ts
  • packages/fmodata/src/client/query/index.ts
  • packages/fmodata/src/client/query/query-builder.ts
  • packages/fmodata/src/client/query/types.ts
  • packages/fmodata/src/client/query/url-builder.ts
  • packages/fmodata/src/client/record-builder.ts
  • packages/fmodata/src/index.ts
  • packages/fmodata/src/types.ts
  • packages/fmodata/tests/batch.test.ts
  • packages/fmodata/tests/e2e/e2e.test.ts
  • packages/fmodata/tests/mock.test.ts
  • packages/fmodata/tests/query-strings.test.ts
  • packages/fmodata/tests/typescript.test.ts

Comment thread packages/fmodata/src/client/builders/response-processor.ts
Comment thread packages/fmodata/src/client/count-builder.ts
Comment thread packages/fmodata/src/types.ts Outdated
- preserve query errors before counted result shaping
- keep typed execute/processResponse returns
- fix navigated /$count URL generation
// biome-ignore lint/suspicious/noExplicitAny: Accepts any FMTable configuration
Occ extends FMTable<any, any>,
Selected,
> = Selected extends Record<string, Column<any, any, any>> // biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any Column configuration
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The biome-ignore comment for noExplicitAny should be moved to the line above (line 71) instead of being at the end of line 72, so it properly suppresses the lint warning for the 'any' types in 'Column<any, any, any>'

Spotted by Graphite (based on CI logs)

Fix in Graphite


Is this helpful? React 👍 or 👎 to let us know.

Copy link
Copy Markdown

@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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
packages/fmodata/src/client/query/query-builder.ts (2)

1015-1030: ⚠️ Potential issue | 🟡 Minor

Return the counted-list shape from the 204 fallback.

Once includeCountMode is on, processResponse() is typed to return { records, count }, but this branch still returns a bare array. Any caller that round-trips a counted list through toRequest()/processResponse() will get a runtime shape mismatch here.

🐛 Proposed fix
     if (response.status === 204) {
       // Return empty list for list queries, null for single queries
       if (this.readState.singleMode !== false) {
         if (this.readState.singleMode === "maybe") {
           // biome-ignore lint/suspicious/noExplicitAny: Type assertion for generic return type
           return { data: null as any, error: undefined };
         }
         return {
           data: undefined,
           error: new RecordCountMismatchError("one", 0),
         };
       }
+      if (this.readState.includeCountMode) {
+        return {
+          data: { records: [], count: 0 } as any,
+          error: undefined,
+        };
+      }
       // biome-ignore lint/suspicious/noExplicitAny: Type assertion for generic return type
       return { data: [] as any, error: undefined };
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/fmodata/src/client/query/query-builder.ts` around lines 1015 - 1030,
The 204-response branch in processResponse() returns a bare array for list
queries but must return the counted-list shape when includeCountMode is enabled;
update the list-return branch (when this.readState.singleMode is false) to check
this.readState.includeCountMode and return { records: [] , count: 0 } (typed to
the generic) instead of [] so callers that expect { records, count } get the
correct shape; keep existing handling for singleMode/“maybe” and the
RecordCountMismatchError for the single-item case.

689-728: ⚠️ Potential issue | 🟠 Major

Pin IncludeCount to false in expand() callback return type.

The callback return type permits any for the sixth generic parameter (IncludeCount), allowing builder.count() to type-check inside expand callbacks. However, expanded relations are modeled as plain nested records without a { records, count } wrapper, making this state unsupported end-to-end. This creates a silent type-safety gap where valid TypeScript code produces incorrect runtime behavior.

Enforce IncludeCount: false in both the callback return type and the factory-created QueryBuilder to prevent count mode in nested expands unless full end-to-end support is added.

♻️ Proposed fix
   callback?: (
     // biome-ignore lint/complexity/noBannedTypes: Empty object type represents no expands in initial builder
     builder: QueryBuilder<TargetTable, keyof InferSchemaOutputFromFMTable<TargetTable>, false, false, {}, false>,
     // biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any QueryBuilder configuration
-    ) => QueryBuilder<TargetTable, TSelected, any, any, TNestedExpands, any, any>,
+    ) => QueryBuilder<
+      TargetTable,
+      TSelected,
+      any,
+      any,
+      TNestedExpands,
+      false,
+      DatabaseIncludeSpecialColumns,
+      any
+    >,
   ): QueryBuilder<
@@
       callback as ((builder: TargetBuilder) => TargetBuilder) | undefined,
       () =>
         // biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any QueryBuilder configuration
-        new QueryBuilder<TargetTable, any, any, any, any, any, DatabaseIncludeSpecialColumns, undefined>({
+        new QueryBuilder<TargetTable, any, any, any, any, false, DatabaseIncludeSpecialColumns, undefined>({
           occurrence: targetTable,
           layer: this.layer,
         }),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/fmodata/src/client/query/query-builder.ts` around lines 689 - 728,
The expand callback currently allows the sixth generic (IncludeCount) to be any,
enabling builder.count() inside expand; update the callback signature passed to
expand() and the QueryBuilder constructed in the processExpand call so the sixth
generic is explicitly false (i.e., change the callback type from
QueryBuilder<TargetTable, TSelected, any, any, TNestedExpands, any, any> to
QueryBuilder<TargetTable, TSelected, any, any, TNestedExpands, false, any> and
construct new QueryBuilder<TargetTable, any, any, any, any, false,
DatabaseIncludeSpecialColumns, undefined>), ensuring IncludeCount is pinned to
false in both the callback return type and the factory-created QueryBuilder used
in expandBuilder.processExpand.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@packages/fmodata/src/client/query/query-builder.ts`:
- Around line 1015-1030: The 204-response branch in processResponse() returns a
bare array for list queries but must return the counted-list shape when
includeCountMode is enabled; update the list-return branch (when
this.readState.singleMode is false) to check this.readState.includeCountMode and
return { records: [] , count: 0 } (typed to the generic) instead of [] so
callers that expect { records, count } get the correct shape; keep existing
handling for singleMode/“maybe” and the RecordCountMismatchError for the
single-item case.
- Around line 689-728: The expand callback currently allows the sixth generic
(IncludeCount) to be any, enabling builder.count() inside expand; update the
callback signature passed to expand() and the QueryBuilder constructed in the
processExpand call so the sixth generic is explicitly false (i.e., change the
callback type from QueryBuilder<TargetTable, TSelected, any, any,
TNestedExpands, any, any> to QueryBuilder<TargetTable, TSelected, any, any,
TNestedExpands, false, any> and construct new QueryBuilder<TargetTable, any,
any, any, any, false, DatabaseIncludeSpecialColumns, undefined>), ensuring
IncludeCount is pinned to false in both the callback return type and the
factory-created QueryBuilder used in expandBuilder.processExpand.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 59a45ace-c721-4d3d-8114-d6a9b5d3bbd1

📥 Commits

Reviewing files that changed from the base of the PR and between e78762a and 0959f8d.

📒 Files selected for processing (1)
  • packages/fmodata/src/client/query/query-builder.ts

- simplify codex environment config
- run `pnpm i` during setup
Copy link
Copy Markdown

@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.

🧹 Nitpick comments (1)
.codex/environments/environment.toml (1)

6-6: Use deterministic dependency install in setup script.

On Line 6, replace pnpm i with pnpm install --frozen-lockfile. The repository has lockfiles (pnpm-lock.yaml at root and in packages/), so frozen installs ensure reproducible environment setup.

Suggested change
 [setup]
-script = "pnpm i"
+script = "pnpm install --frozen-lockfile"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.codex/environments/environment.toml at line 6, The setup script currently
uses the non-deterministic command "pnpm i" in the environment.toml `script`
entry; change that value to use frozen installs by replacing "pnpm i" with "pnpm
install --frozen-lockfile" so installs respect the repository lockfiles and
produce reproducible environments.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In @.codex/environments/environment.toml:
- Line 6: The setup script currently uses the non-deterministic command "pnpm i"
in the environment.toml `script` entry; change that value to use frozen installs
by replacing "pnpm i" with "pnpm install --frozen-lockfile" so installs respect
the repository lockfiles and produce reproducible environments.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 4b3ec953-c9f7-4c3f-9dbd-db5858a05e9d

📥 Commits

Reviewing files that changed from the base of the PR and between 0959f8d and 98d2542.

📒 Files selected for processing (1)
  • .codex/environments/environment.toml

@eluce2 eluce2 merged commit e571d47 into main Apr 17, 2026
14 checks passed
@eluce2 eluce2 deleted the feature/split-fmodata-count-api branch April 17, 2026 04:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant