Skip to content

Add totalCount to all GraphQL Connections#1719

Draft
shrugs wants to merge 5 commits intomainfrom
feat/total-count
Draft

Add totalCount to all GraphQL Connections#1719
shrugs wants to merge 5 commits intomainfrom
feat/total-count

Conversation

@shrugs
Copy link
Collaborator

@shrugs shrugs commented Mar 5, 2026

  • adds lazy totalCount to all t.connection fields

Copilot AI review requested due to automatic review settings March 5, 2026 01:04
@vercel
Copy link
Contributor

vercel bot commented Mar 5, 2026

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

Project Deployment Actions Updated (UTC)
admin.ensnode.io Ready Ready Preview, Comment Mar 5, 2026 1:05am
ensnode.io Ready Ready Preview, Comment Mar 5, 2026 1:05am
ensrainbow.io Ready Ready Preview, Comment Mar 5, 2026 1:05am

@changeset-bot
Copy link

changeset-bot bot commented Mar 5, 2026

🦋 Changeset detected

Latest commit: b465a67

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

This PR includes changesets to release 18 packages
Name Type
ensapi Major
ensindexer Major
ensadmin Major
ensrainbow Major
fallback-ensapi Major
@ensnode/datasources Major
@ensnode/ensrainbow-sdk Major
@ensnode/ensnode-schema Major
@ensnode/ensnode-react Major
@ensnode/ensnode-sdk Major
@ensnode/ponder-sdk Major
@ensnode/ponder-subgraph Major
@ensnode/shared-configs Major
@docs/ensnode Major
@docs/ensrainbow Major
@docs/mintlify Major
@namehash/ens-referrals Major
@namehash/namehash-ui Major

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

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 5, 2026

📝 Walkthrough

Walkthrough

This PR refactors the GraphQL API's pagination infrastructure by introducing a global totalCount field on connections and replacing direct resolveCursorConnection calls with a lazyConnection pattern that defers both connection and count computation. It removes the Domain.subdomainCount field and ENSv1Domain.children field, deprecating them in favor of the paginated Domain.subdomains with its totalCount.

Changes

Cohort / File(s) Summary
Changesets
.changeset/eager-parrots-follow.md, .changeset/shiny-cameras-judge.md
Documents breaking API changes: removal of ENSv1Domain.children and Domain.subdomainCount, replaced by Domain.subdomains and Domain.subdomains.totalCount.
GraphQL Builder & Connection Utilities
apps/ensapi/src/graphql-api/builder.ts, apps/ensapi/src/graphql-api/lib/connection-helpers.ts, apps/ensapi/src/graphql-api/lib/lazy-connection.ts
Adds totalCount field to the SchemaBuilder connection type; introduces paginateBy() and orderPaginationBy() helper functions for cursor-based pagination; adds lazyConnection() utility to memoize and expose edges, pageInfo, and totalCount from a pending connection promise.
Connection Global Field
apps/ensapi/src/graphql-api/schema/connection.ts
Registers a global totalCount field (non-nullable Int) on all GraphQL connections, resolving from the parent connection's totalCount property.
Schema Resolvers - Domain & Registration
apps/ensapi/src/graphql-api/schema/domain.ts, apps/ensapi/src/graphql-api/schema/registration.ts, apps/ensapi/src/graphql-api/lib/find-domains/find-domains-resolver.ts
Removes DomainInterfaceRef.subdomainCount and ENSv1DomainRef.children fields; refactors registrations and renewals resolvers to use lazyConnection with separated count and connection queries; adds totalCount via db.$count() with pagination helpers.
Schema Resolvers - Accounts & Permissions
apps/ensapi/src/graphql-api/schema/account.ts, apps/ensapi/src/graphql-api/schema/permissions.ts
Migrates permissions, registryPermissions, resolverPermissions, and resource/user connection resolvers from inline resolveCursorConnection to lazyConnection pattern; removes unused context parameter from resolver signatures; applies pagination via paginateBy() and orderPaginationBy().
Schema Resolvers - Query, Registry, Resolver
apps/ensapi/src/graphql-api/schema/query.ts, apps/ensapi/src/graphql-api/schema/registry.ts, apps/ensapi/src/graphql-api/schema/resolver.ts
Updates v1Domains, v2Domains, resolvers, registrations query fields and Registry.parents, Resolver.records resolvers to use lazyConnection pattern; removes context parameter from resolver signatures; replaces cursor-based ID filtering with schema-based field references and pagination helpers.
Schema Integration & Cleanup
apps/ensapi/src/graphql-api/schema.ts, apps/ensindexer/src/lib/version-info.ts
Adds new schema module imports (connection, event, label, name-or-node, order-direction, renewal, etc.); removes unused imports in version-info.ts.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related issues

  • ENSv2 GraphQL API: Connection.totalCount #1669: Directly addresses the request to expose connection.totalCount and subdomain counts by implementing the global totalCount field on connections and replacing Domain.subdomainCount with Domain.subdomains.totalCount.

Possibly related PRs

Poem

🐰 A rabbit hops through lazy fields,
Where connections bloom and totalCount yields,
No more children, no more counts to tally,
Just subdomains in a paginated valley! ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The PR description is incomplete; it provides only a brief one-liner summary but lacks required sections: Why, Testing, and Notes for Reviewer from the template. Expand the description to include: Why this change exists (with GitHub issue links if relevant), how it was tested, and any non-obvious implementation notes for reviewers.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Add totalCount to all GraphQL Connections' accurately summarizes the main change: adding a totalCount field to GraphQL connection types throughout the codebase.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

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

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/total-count

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.

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

Adds a totalCount field to Relay-style GraphQL connections in ensapi by introducing a lazy connection wrapper that only executes count queries when requested, and refactors many connection resolvers to share pagination/count helpers. This also removes Domain.subdomainCount and ENSv1Domain.children in favor of using Domain.subdomains.totalCount / Domain.subdomains.

Changes:

  • Add global totalCount to all Pothos Relay connections and introduce lazyConnection to lazily compute it.
  • Refactor connection resolvers across schema modules to use shared paginateBy / orderPaginationBy helpers.
  • Remove Domain.subdomainCount and ENSv1Domain.children (documented via changesets).

Reviewed changes

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

Show a summary per file
File Description
apps/ensindexer/src/lib/version-info.ts Removes unused imports (cleanup unrelated to GraphQL connections).
apps/ensapi/src/graphql-api/builder.ts Extends Pothos builder connection typing to require totalCount.
apps/ensapi/src/graphql-api/lib/lazy-connection.ts Adds lazy/memoized connection wrapper for edges/pageInfo + lazy totalCount.
apps/ensapi/src/graphql-api/lib/connection-helpers.ts Adds shared cursor pagination helpers for Drizzle queries.
apps/ensapi/src/graphql-api/lib/find-domains/find-domains-resolver.ts Adds lazy totalCount to domains connections returned by resolveFindDomains.
apps/ensapi/src/graphql-api/schema/connection.ts Registers global Relay connection field totalCount.
apps/ensapi/src/graphql-api/schema.ts Ensures schema side-effect imports include new connection/type modules.
apps/ensapi/src/graphql-api/schema/query.ts Refactors dev/test connections to use lazy totalCount + shared pagination helpers.
apps/ensapi/src/graphql-api/schema/domain.ts Refactors Domain.registrations; removes subdomainCount and ENSv1Domain.children.
apps/ensapi/src/graphql-api/schema/account.ts Refactors account-related connections to use lazy totalCount and shared pagination helpers.
apps/ensapi/src/graphql-api/schema/permissions.ts Refactors permissions connections to use lazy totalCount and shared pagination helpers.
apps/ensapi/src/graphql-api/schema/registry.ts Refactors Registry.parents connection to include lazy totalCount.
apps/ensapi/src/graphql-api/schema/resolver.ts Refactors Resolver.records connection to include lazy totalCount.
apps/ensapi/src/graphql-api/schema/registration.ts Refactors Registration.renewals connection to include lazy totalCount.
.changeset/shiny-cameras-judge.md Changeset documenting breaking removal of Domain.subdomainCount.
.changeset/eager-parrots-follow.md Changeset documenting breaking removal of ENSv1Domain.children.

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

You can also share your feedback on Copilot code review. Take the survey.

@@ -0,0 +1,5 @@
---
"ensapi": minor
Copy link

Copilot AI Mar 5, 2026

Choose a reason for hiding this comment

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

These changesets mark the release as a minor, but the note explicitly calls out a BREAKING API change (removing ENSv1Domain.children). Under SemVer for a 1.x package, a breaking public API change should be a major bump (or the change should be made non-breaking).

Suggested change
"ensapi": minor
"ensapi": major

Copilot uses AI. Check for mistakes.
t.field({
type: "Int",
nullable: false,
resolve: (parent) => parent.totalCount,
Copy link

Copilot AI Mar 5, 2026

Choose a reason for hiding this comment

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

totalCount is a new global field added to every Relay connection type, but there doesn’t appear to be any integration/unit test asserting its presence and correctness. Since this changes connection behavior across the schema, it would be good to add at least one integration test that queries a connection with totalCount (and ideally verifies it matches the unpaginated filtered count).

Suggested change
resolve: (parent) => parent.totalCount,
resolve: (parent) => parent.totalCount ?? 0,

Copilot uses AI. Check for mistakes.
.select()
.from(schema.registration)
.where(and(scope, paginateBy(schema.registration.id, before, after)))
.orderBy(orderPaginationBy(schema.registration.index, inverted))
Copy link

Copilot AI Mar 5, 2026

Choose a reason for hiding this comment

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

Domain.registrations is paginating by registration.id (via paginateBy(schema.registration.id, ...)) but ordering by registration.index. Relay cursor pagination requires the cursor filter and orderBy to be based on the same ordering keys; otherwise before/after can return inconsistent pages and skip/duplicate rows. Consider either ordering by id consistently, or switching the cursor to encode the (domainId, index) ordering (and apply pagination constraints on index, with a deterministic tie-breaker).

Suggested change
.orderBy(orderPaginationBy(schema.registration.index, inverted))
.orderBy(orderPaginationBy(schema.registration.id, inverted))

Copilot uses AI. Check for mistakes.
@@ -0,0 +1,5 @@
---
"ensapi": minor
Copy link

Copilot AI Mar 5, 2026

Choose a reason for hiding this comment

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

These changesets mark the release as a minor, but the note explicitly calls out a BREAKING API change (removing Domain.subdomainCount). Under SemVer for a 1.x package, a breaking public API change should be a major bump (or the change should be made non-breaking).

Suggested change
"ensapi": minor
"ensapi": major

Copilot uses AI. Check for mistakes.
"ensapi": minor
---

ENSNode GraphQL API: BREAKING: Removed `Domain.subdomainCount` in favor of `Domain.subdomains.totalCount`.
Copy link

Copilot AI Mar 5, 2026

Choose a reason for hiding this comment

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

PR description says this only "adds lazy totalCount to all t.connection fields", but this changeset indicates a breaking removal of an existing field (Domain.subdomainCount). The PR description should mention these removals so reviewers/consumers don’t miss the breaking surface-area change.

Copilot uses AI. Check for mistakes.
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

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

⚠️ Outside diff range comments (1)
apps/ensapi/src/graphql-api/schema/account.ts (1)

135-208: 🧹 Nitpick | 🔵 Trivial

Extract shared joined-permissions connection logic to avoid drift.

registryPermissions and resolverPermissions now duplicate the same lazy-count + paginated-join flow with only table/join/projection differences. A small shared builder would reduce maintenance risk.

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

In `@apps/ensapi/src/graphql-api/schema/account.ts` around lines 135 - 208,
Extract the duplicated lazyConnection + paginated-join flow into a shared helper
(e.g., buildJoinedPermissionsConnection or makePermissionsConnection) that takes
the varying pieces: the joined table/schema (registry vs resolver), the join
condition, and the projection alias (properties to select), and returns the
object used by t.connection (with totalCount and connection functions wired to
schema.permissionsUser, schema.* join, paginateBy(schema.permissionsUser.id,
...), orderPaginationBy(schema.permissionsUser.id, ...) and
resolveCursorConnection). Replace the current inline implementations in
resolverPermissions and the registryPermissions field with calls to this helper,
keeping existing scope = eq(schema.permissionsUser.user, parent.id) and using
the same symbols (lazyConnection, resolveCursorConnection, paginateBy,
orderPaginationBy) so behavior and projections (id: r.permissionsUser.id, ...r)
remain identical.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.changeset/eager-parrots-follow.md:
- Line 5: Update the changeset entry to mention the second breaking GraphQL
field removal by adding a line stating that Domain.subdomainCount was removed
and replaced by Domain.subdomains.totalCount; reference the existing
ENSv1Domain.children removal line and append or insert a sentence explicitly
naming Domain.subdomainCount -> Domain.subdomains.totalCount so the release
notes list both client-facing removals (ENSv1Domain.children and
Domain.subdomainCount).

In @.changeset/shiny-cameras-judge.md:
- Line 5: Update the changeset text to document all breaking GraphQL removals:
include both the removal of Domain.subdomainCount (noting it was replaced by
Domain.subdomains.totalCount) and the removal of ENSv1Domain.children so the
release notes list both breaking surface changes; edit the sentence in
.changeset/shiny-cameras-judge.md to mention both symbols (Domain.subdomainCount
and ENSv1Domain.children) and, where applicable, indicate the replacement or
alternative API (Domain.subdomains.totalCount) for clarity.

In `@apps/ensapi/src/graphql-api/schema/domain.ts`:
- Around line 219-220: The pagination is unstable because Domain.registrations
uses paginateBy(schema.registration.id, before, after) but orders by
orderPaginationBy(schema.registration.index, inverted); change one to match the
other so both pagination predicate and ordering key use the same field (either
both use schema.registration.id or both use schema.registration.index). Locate
the registrations resolver where paginateBy(...) and orderPaginationBy(...) are
applied and make them consistent (e.g., replace
paginateBy(schema.registration.id, ...) with
paginateBy(schema.registration.index, ...) or vice versa), ensuring the same
`inverted` ordering logic remains compatible with the chosen key.

---

Outside diff comments:
In `@apps/ensapi/src/graphql-api/schema/account.ts`:
- Around line 135-208: Extract the duplicated lazyConnection + paginated-join
flow into a shared helper (e.g., buildJoinedPermissionsConnection or
makePermissionsConnection) that takes the varying pieces: the joined
table/schema (registry vs resolver), the join condition, and the projection
alias (properties to select), and returns the object used by t.connection (with
totalCount and connection functions wired to schema.permissionsUser, schema.*
join, paginateBy(schema.permissionsUser.id, ...),
orderPaginationBy(schema.permissionsUser.id, ...) and resolveCursorConnection).
Replace the current inline implementations in resolverPermissions and the
registryPermissions field with calls to this helper, keeping existing scope =
eq(schema.permissionsUser.user, parent.id) and using the same symbols
(lazyConnection, resolveCursorConnection, paginateBy, orderPaginationBy) so
behavior and projections (id: r.permissionsUser.id, ...r) remain identical.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: e251a39d-09fb-4ec5-8e72-66dfabbd902a

📥 Commits

Reviewing files that changed from the base of the PR and between 9657006 and b465a67.

📒 Files selected for processing (16)
  • .changeset/eager-parrots-follow.md
  • .changeset/shiny-cameras-judge.md
  • apps/ensapi/src/graphql-api/builder.ts
  • apps/ensapi/src/graphql-api/lib/connection-helpers.ts
  • apps/ensapi/src/graphql-api/lib/find-domains/find-domains-resolver.ts
  • apps/ensapi/src/graphql-api/lib/lazy-connection.ts
  • apps/ensapi/src/graphql-api/schema.ts
  • apps/ensapi/src/graphql-api/schema/account.ts
  • apps/ensapi/src/graphql-api/schema/connection.ts
  • apps/ensapi/src/graphql-api/schema/domain.ts
  • apps/ensapi/src/graphql-api/schema/permissions.ts
  • apps/ensapi/src/graphql-api/schema/query.ts
  • apps/ensapi/src/graphql-api/schema/registration.ts
  • apps/ensapi/src/graphql-api/schema/registry.ts
  • apps/ensapi/src/graphql-api/schema/resolver.ts
  • apps/ensindexer/src/lib/version-info.ts
💤 Files with no reviewable changes (1)
  • apps/ensindexer/src/lib/version-info.ts

"ensapi": minor
---

ENSNode GraphQL API: BREAKING: Removed `ENSv1Domain.children` in favor of `Domain.subdomains`.
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Document the second breaking GraphQL field removal in this changeset.

This entry lists ENSv1Domain.children removal, but the PR also introduces a breaking removal of Domain.subdomainCount (replaced by Domain.subdomains.totalCount). Please include it here so release notes fully reflect client-facing breakage.

Suggested update
 ENSNode GraphQL API: BREAKING: Removed `ENSv1Domain.children` in favor of `Domain.subdomains`.
+ENSNode GraphQL API: BREAKING: Removed `Domain.subdomainCount` in favor of `Domain.subdomains.totalCount`.
📝 Committable suggestion

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

Suggested change
ENSNode GraphQL API: BREAKING: Removed `ENSv1Domain.children` in favor of `Domain.subdomains`.
ENSNode GraphQL API: BREAKING: Removed `ENSv1Domain.children` in favor of `Domain.subdomains`.
ENSNode GraphQL API: BREAKING: Removed `Domain.subdomainCount` in favor of `Domain.subdomains.totalCount`.
🧰 Tools
🪛 markdownlint-cli2 (0.21.0)

[warning] 5-5: First line in a file should be a top-level heading

(MD041, first-line-heading, first-line-h1)

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

In @.changeset/eager-parrots-follow.md at line 5, Update the changeset entry to
mention the second breaking GraphQL field removal by adding a line stating that
Domain.subdomainCount was removed and replaced by Domain.subdomains.totalCount;
reference the existing ENSv1Domain.children removal line and append or insert a
sentence explicitly naming Domain.subdomainCount -> Domain.subdomains.totalCount
so the release notes list both client-facing removals (ENSv1Domain.children and
Domain.subdomainCount).

"ensapi": minor
---

ENSNode GraphQL API: BREAKING: Removed `Domain.subdomainCount` in favor of `Domain.subdomains.totalCount`.
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Document all breaking GraphQL removals in this changeset.

This note covers Domain.subdomainCount, but the PR also removes ENSv1Domain.children. Please include both so release notes fully represent breaking surface changes.

📝 Proposed changeset update
 ENSNode GraphQL API: BREAKING: Removed `Domain.subdomainCount` in favor of `Domain.subdomains.totalCount`.
+ENSNode GraphQL API: BREAKING: Removed `ENSv1Domain.children` in favor of `Domain.subdomains`.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.changeset/shiny-cameras-judge.md at line 5, Update the changeset text to
document all breaking GraphQL removals: include both the removal of
Domain.subdomainCount (noting it was replaced by Domain.subdomains.totalCount)
and the removal of ENSv1Domain.children so the release notes list both breaking
surface changes; edit the sentence in .changeset/shiny-cameras-judge.md to
mention both symbols (Domain.subdomainCount and ENSv1Domain.children) and, where
applicable, indicate the replacement or alternative API
(Domain.subdomains.totalCount) for clarity.

Comment on lines +219 to +220
.where(and(scope, paginateBy(schema.registration.id, before, after)))
.orderBy(orderPaginationBy(schema.registration.index, inverted))
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Align pagination predicate and ordering key for Domain.registrations.

Line [219] pages by registration.id while Line [220] orders by registration.index. That can create unstable cursors across pages if those sort orders diverge.

Proposed minimal fix (use one consistent key)
-                  .orderBy(orderPaginationBy(schema.registration.index, inverted))
+                  .orderBy(orderPaginationBy(schema.registration.id, inverted))
📝 Committable suggestion

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

Suggested change
.where(and(scope, paginateBy(schema.registration.id, before, after)))
.orderBy(orderPaginationBy(schema.registration.index, inverted))
.where(and(scope, paginateBy(schema.registration.id, before, after)))
.orderBy(orderPaginationBy(schema.registration.id, inverted))
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/ensapi/src/graphql-api/schema/domain.ts` around lines 219 - 220, The
pagination is unstable because Domain.registrations uses
paginateBy(schema.registration.id, before, after) but orders by
orderPaginationBy(schema.registration.index, inverted); change one to match the
other so both pagination predicate and ordering key use the same field (either
both use schema.registration.id or both use schema.registration.index). Locate
the registrations resolver where paginateBy(...) and orderPaginationBy(...) are
applied and make them consistent (e.g., replace
paginateBy(schema.registration.id, ...) with
paginateBy(schema.registration.index, ...) or vice versa), ensuring the same
`inverted` ordering logic remains compatible with the chosen key.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants