feat(ensapi): refactor domain name queries onto materialized canonical fields#2125
Conversation
Replace `where: { name: String }` on Query.domains, Account.domains,
Registry.domains, and Domain.subdomains with a DomainsNameFilter @OneOf
input supporting `starts_with` (prefix autocomplete, unchanged behavior),
`eq` (exact match), and `in` (exact match against a set, max 100).
Exact-match implementation (filterByNameIn) currently uses an upward
recursive CTE that walks the canonical-edge agreement and verifies the
topmost matched ancestor lives in a configured root registry. To be
replaced with a single-column lookup on Domain.canonicalName once that
is materialized.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
2 Skipped Deployments
|
🦋 Changeset detectedLatest commit: 6fc3989 The changes in this PR will be included in the next version bump. This PR includes changesets to release 25 packages
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 |
|
Warning Rate limit exceeded
You’ve run out of usage credits. Purchase more in the billing tab. ⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (4)
📝 WalkthroughWalkthroughThis pull request refactors ENS domain name filtering to a GraphQL DomainsNameFilter ChangesDomain Filtering & Canonical Field Materialization
🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly Related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Pull request overview
Refactors ENSAPI domain-name queries onto materialized canonical columns. The previous upward recursive CTE for where: { name } and the dataloader-backed reverse traversal for DomainCanonical.path are replaced with single-column reads against canonicalName / canonicalDepth / canonicalPath. Adds a DomainsNameFilter @oneOf GraphQL input (starts_with / eq / in), a DEPTH ordering enum value, materializes the new canonicalPath and canonicalDepth columns on the canonical write path, and updates docs and example queries accordingly.
Changes:
- Introduces
DomainsNameFilter(@oneOf) replacingwhere: { name: String }onQuery.domains,Account.domains,Registry.domains,Domain.subdomains; addsDEPTHordering and routesstarts_withto default-DEPTH ordering. - Adds
canonicalPathandcanonicalDepthmaterialization (initial write + cascade CTE) on Domain; replacesgetCanonicalPath+ canonical-path dataloader with direct column reads inDomainCanonical.pathand addsDomainCanonical.depth. - Drops the label join in
domainsBase, swapssortableLabelforcanonicalName/canonicalDepth, and updates docs, examples, integration tests, and pagination tests.
Reviewed changes
Copilot reviewed 33 out of 36 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| pnpm-lock.yaml | Adds @pothos/plugin-zod and incidental vitest mocker entry. |
| apps/ensapi/package.json | Adds @pothos/plugin-zod dep. |
| apps/ensapi/src/omnigraph-api/builder.ts | Registers ZodPlugin for input validation. |
| apps/ensapi/src/omnigraph-api/context.ts | Removes canonicalPath dataloader and helper. |
| apps/ensapi/src/omnigraph-api/lib/get-canonical-path.ts | Deletes the recursive-CTE path traversal. |
| apps/ensapi/src/omnigraph-api/schema/domain-canonical.ts | Adds depth, reads path from materialized column. |
| apps/ensapi/src/omnigraph-api/schema/domain-inputs.ts | Adds DomainsNameFilter, DEPTH enum value, switches name field to oneOf input. |
| apps/ensapi/src/omnigraph-api/schema/{query,domain,registry,account}.ts | Threads defaultOrderBy from filterByName. |
| apps/ensapi/src/omnigraph-api/lib/find-domains/layers/filter-by-name.ts | Becomes thin dispatcher onto starts_with/in/eq. |
| apps/ensapi/src/omnigraph-api/lib/find-domains/layers/filter-by-name-starts-with.ts | New ILIKE prefix on canonicalName. |
| apps/ensapi/src/omnigraph-api/lib/find-domains/layers/filter-by-name-in.ts | New inArray(canonicalName) with empty-array short-circuit. |
| apps/ensapi/src/omnigraph-api/lib/find-domains/layers/{base-domain-set,with-ordering-metadata,index}.ts | Drops label join; exposes canonicalName/canonicalDepth. |
| apps/ensapi/src/omnigraph-api/lib/find-domains/{find-domains-resolver,find-domains-resolver-helpers,types}.ts | Adds DEPTH ordering / cursor cast / default-order-by plumbing. |
| apps/ensapi/src/omnigraph-api/schema/{query,domain}.integration.test.ts | New tests for eq/in/empty/@oneOf/depth. |
| apps/ensapi/src/test/integration/find-domains/{domain-pagination-queries,test-domain-pagination}.ts | Updates fragment + DEPTH permutations. |
| apps/ensindexer/src/lib/ensv2/canonicality-db-helpers.ts | Materializes canonicalPath and canonicalDepth (insert + cascade CTE). |
| packages/ensdb-sdk/src/ensindexer-abstract/ensv2.schema.ts | Adds canonicalPath, canonicalDepth columns + index. |
| packages/enssdk/src/omnigraph/generated/{schema.graphql,introspection.ts} | Regenerated for new filter and depth/order-by. |
| packages/ensnode-sdk/src/omnigraph-api/example-queries.ts | Updates example to new filter shape. |
| examples/enskit-react-example/src/SearchView.tsx | Migrates to name: { starts_with }. |
| docs/ensnode.io/.../{database-schemas,sdk,sql,ensdb}.mdx | Documents canonical-* columns and switches examples to canonical_name. |
| .changeset/domains-name-filter-oneof.md, .changeset/domains-orderby-depth.md | Changesets for breaking filter change and DEPTH addition. |
Files not reviewed (1)
- pnpm-lock.yaml: Language not supported
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
apps/ensindexer/src/lib/ensv2/canonicality-db-helpers.ts (1)
533-536:⚠️ Potential issue | 🟠 Major | ⚡ Quick winRefresh predicate misses stale
canonical_pathwhen parent identity changes without label-path change.On Line 535, updates are gated by
canonical_label_hash_pathdrift only. If a registry is reparented to a canonical parent with the same label-hash path/name but different Domain IDs,canonical_pathremains stale because this row won’t update.Proposed fix
WHERE d.id = dt.domain_id AND ( d.canonical IS DISTINCT FROM ${nextCanonical} - OR (${nextCanonical} AND d.canonical_label_hash_path IS DISTINCT FROM dt.new_path) + OR ( + ${nextCanonical} + AND ( + d.canonical_label_hash_path IS DISTINCT FROM dt.new_path + OR d.canonical_path IS DISTINCT FROM dt.new_path_ids + ) + ) )🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/ensindexer/src/lib/ensv2/canonicality-db-helpers.ts` around lines 533 - 536, The refresh predicate only checks d.canonical and d.canonical_label_hash_path against nextCanonical/dt.new_path, but misses when the computed canonical_path changes due to a parent identity change even if the label-hash (dt.new_path) stays the same; update the WHERE clause to also trigger when the stored canonical_path differs from the newly computed canonical path (e.g., add a condition comparing d.canonical_path IS DISTINCT FROM dt.new_canonical_path or the expression that builds the new canonical_path) so rows with identical label-hash but different parent identity get refreshed (referencing d.canonical_path, d.canonical_label_hash_path, dt.new_path, dt.new_canonical_path, and nextCanonical).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Outside diff comments:
In `@apps/ensindexer/src/lib/ensv2/canonicality-db-helpers.ts`:
- Around line 533-536: The refresh predicate only checks d.canonical and
d.canonical_label_hash_path against nextCanonical/dt.new_path, but misses when
the computed canonical_path changes due to a parent identity change even if the
label-hash (dt.new_path) stays the same; update the WHERE clause to also trigger
when the stored canonical_path differs from the newly computed canonical path
(e.g., add a condition comparing d.canonical_path IS DISTINCT FROM
dt.new_canonical_path or the expression that builds the new canonical_path) so
rows with identical label-hash but different parent identity get refreshed
(referencing d.canonical_path, d.canonical_label_hash_path, dt.new_path,
dt.new_canonical_path, and nextCanonical).
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: 64601978-789b-437f-8122-7f6a9f6e4146
⛔ Files ignored due to path filters (3)
packages/enssdk/src/omnigraph/generated/introspection.tsis excluded by!**/generated/**packages/enssdk/src/omnigraph/generated/schema.graphqlis excluded by!**/generated/**pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (33)
.changeset/domains-name-filter-oneof.md.changeset/domains-orderby-depth.mdapps/ensapi/package.jsonapps/ensapi/src/omnigraph-api/builder.tsapps/ensapi/src/omnigraph-api/context.tsapps/ensapi/src/omnigraph-api/lib/find-domains/find-domains-resolver-helpers.tsapps/ensapi/src/omnigraph-api/lib/find-domains/find-domains-resolver.tsapps/ensapi/src/omnigraph-api/lib/find-domains/layers/base-domain-set.tsapps/ensapi/src/omnigraph-api/lib/find-domains/layers/filter-by-name-in.tsapps/ensapi/src/omnigraph-api/lib/find-domains/layers/filter-by-name-starts-with.tsapps/ensapi/src/omnigraph-api/lib/find-domains/layers/filter-by-name.tsapps/ensapi/src/omnigraph-api/lib/find-domains/layers/index.tsapps/ensapi/src/omnigraph-api/lib/find-domains/layers/with-ordering-metadata.tsapps/ensapi/src/omnigraph-api/lib/find-domains/types.tsapps/ensapi/src/omnigraph-api/lib/get-canonical-path.tsapps/ensapi/src/omnigraph-api/schema/account.tsapps/ensapi/src/omnigraph-api/schema/domain-canonical.tsapps/ensapi/src/omnigraph-api/schema/domain-inputs.tsapps/ensapi/src/omnigraph-api/schema/domain.integration.test.tsapps/ensapi/src/omnigraph-api/schema/domain.tsapps/ensapi/src/omnigraph-api/schema/query.integration.test.tsapps/ensapi/src/omnigraph-api/schema/query.tsapps/ensapi/src/omnigraph-api/schema/registry.tsapps/ensapi/src/test/integration/find-domains/domain-pagination-queries.tsapps/ensapi/src/test/integration/find-domains/test-domain-pagination.tsapps/ensindexer/src/lib/ensv2/canonicality-db-helpers.tsdocs/ensnode.io/src/content/docs/docs/integrate/integration-options/ensdb.mdxdocs/ensnode.io/src/content/docs/docs/services/ensdb/concepts/database-schemas.mdxdocs/ensnode.io/src/content/docs/docs/services/ensdb/usage/sdk.mdxdocs/ensnode.io/src/content/docs/docs/services/ensdb/usage/sql.mdxexamples/enskit-react-example/src/SearchView.tsxpackages/ensdb-sdk/src/ensindexer-abstract/ensv2.schema.tspackages/ensnode-sdk/src/omnigraph-api/example-queries.ts
💤 Files with no reviewable changes (1)
- apps/ensapi/src/omnigraph-api/lib/get-canonical-path.ts
Filter-supplied default ordering now carries both `by` and `dir` so a filter
can fully drive ordering when the caller doesn't pass `order`. `name:
{ starts_with }` surfaces `{ by: DEPTH, dir: ASC }` for typeahead.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…erInput Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@apps/ensindexer/src/lib/ensv2/canonicality-db-helpers.ts`:
- Around line 138-140: When deriving a child's canonicalPath and canonicalDepth,
ensure the parent invariant holds: if parentDomain is non-null then
parentDomain.canonicalPath must be non-nullish; otherwise throw or return an
explicit error instead of defaulting to [] and producing a truncated path.
Update the code around the canonicalPath and canonicalDepth construction (the
const canonicalPath: DomainId[] = [...(parentDomain?.canonicalPath ?? []),
domainId]; and the similar logic at lines handling canonicalDepth) to assert or
throw when parentDomain && parentDomain.canonicalPath == null, so callers never
silently get a malformed child path/depth.
🪄 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: Organization UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: 1c255b2c-fb69-4cf8-93e3-2da74948412b
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (12)
.changeset/materialize-canonical-path.mdapps/ensapi/src/omnigraph-api/lib/find-domains/find-domains-resolver.tsapps/ensapi/src/omnigraph-api/lib/find-domains/layers/filter-by-name-starts-with.tsapps/ensapi/src/omnigraph-api/lib/find-domains/layers/filter-by-name.tsapps/ensapi/src/omnigraph-api/schema/account.tsapps/ensapi/src/omnigraph-api/schema/domain.tsapps/ensapi/src/omnigraph-api/schema/query.tsapps/ensapi/src/omnigraph-api/schema/registry.tsapps/ensindexer/src/lib/ensv2/canonicality-db-helpers.tsdocs/ensnode.io/src/content/docs/docs/services/ensdb/concepts/database-schemas.mdxpackages/enssdk/src/lib/interpreted-names-and-labels.test.tspackages/enssdk/src/lib/interpreted-names-and-labels.ts
💤 Files with no reviewable changes (2)
- packages/enssdk/src/lib/interpreted-names-and-labels.ts
- packages/enssdk/src/lib/interpreted-names-and-labels.test.ts
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
@greptile review |
lightwalker-eth
left a comment
There was a problem hiding this comment.
@shrugs Super! 🚀 Another big milestone in our roadmap!
| "ensapi": minor | ||
| --- | ||
|
|
||
| **Omnigraph (breaking)**: `where: { name }` on `Query.domains`, `Account.domains`, `Registry.domains`, and `Domain.subdomains` now takes a `DomainsNameFilter` `@oneOf` input with three modes: `starts_with` (prefix autocomplete, the previous behavior), `eq` (exact InterpretedName match — sugar for `in: [eq]`), and `in` (exact match against a set of up to 100 InterpretedNames). The old shape `where: { name: "examp" }` becomes `where: { name: { starts_with: "examp" } }`; for exact lookups use `where: { name: { eq: "vitalik.eth" } }` or `where: { name: { in: ["alice.eth", "bob.eth"] } }`. Combine with `version` to disambiguate across ENS protocol versions (e.g. `{ name: { eq: "eth" }, version: ENSv1 }` returns a single Domain). |
Reviewer Focus (Read This First)
filterByNameIn/filterByNameStartsWithlayers — these replace the upward recursive CTE infilterByNamewith single-column lookups on the materializedcanonicalNameDomainsNameFilter@oneOfshape onQuery.domains/Account.domains/Registry.domains/Domain.subdomainsDomainCanonical.pathnow reads from the materializedcanonicalPathcolumn instead of the dataloader-backed reverse CTE —canonicalPathloader andgetCanonicalPathare deleted.Problem & Motivation
where: { name: String }did partial-prefix matching via an upward recursive CTE that walked the canonical-edge agreement (registry.canonicalDomainId↔domain.subregistryId) once per query — expensive, and the only path to exact-match was prefix sugar that didn't actually constrain to exact.DomainCanonical.pathwalked the same reverse traversal per-Domain via a dataloader.Domain.canonicalName/canonicalLabelHashPath/canonicalNode; this PR adds the remainingcanonicalPath(head-first DomainId array) andcanonicalDepth, and replaces the runtime CTEs with column reads.What Changed (Concrete)
DomainsWhereInput.name(and the equivalents on Account/Registry/Subdomains) goes fromString→DomainsNameFilter(@oneOfofstarts_with|eq|in).filterByNamebecomes a thin dispatcher. NewfilterByNameStartsWith(ILIKE oncanonicalName) andfilterByNameIn(inArray(canonicalName, names), with empty-array short-circuit toWHERE false).DomainsOrderBygainsDEPTH(orders bycanonicalDepth).name: { starts_with }surfacesdefaultOrderBy: "DEPTH"so typeahead naturally surfaces shorter names first.DomainCanonical.depth: Int!added.DomainCanonical.pathnow reads from materializedcanonicalPath;getCanonicalPath+canonicalPathdataloader deleted. Path direction is now root→leaf inclusive (matchescanonicalLabelHashPath); previous comment said leaf→root.domainsBasedrops thelabeljoin —sortableLabelis replaced bycanonicalName/canonicalDepthpassed through from the materialized Domain columns.canonicality-db-helpers.ts: materializescanonicalPathandcanonicalDepthon the canonical write path and in the cascade CTE alongside the existingcanonicalLabelHashPath/canonicalName.WHERE type = ...toWHERE canonical_name = ...to model the intended query shape.@pothos/plugin-zodadded forDomainsNameFilter.inmaxLength: 100validation.Design & Planning
the materialized columns landed in feat: materialize Domain.canonical{Name,LabelHashPath,Node} #2101; this PR is the consumer-side cleanup. no separate design doc.
alternatives considered: backwards compatibility, but ignored for this breaking change
Planning artifacts: none beyond the changesets in this PR
Reviewed / approved by: none pre-merge
Self-Review
-n/a
Cross-Codebase Alignment
sortableLabel,canonicalPath,getCanonicalPath,domainsByLabelHashPath,parsePartialInterpretedName,FILTER_BY_NAME_MAX_DEPTH,where: { namewhere: { name: String }— not encountered in this branch; expected to be picked up via the breaking changeset.Downstream & Consumer Impact
where: { name: "x" }→where: { name: { starts_with: "x" } }onQuery.domains,Account.domains,Registry.domains,Domain.subdomains. exact-match consumers now useeq/in. enssdk generated schema + introspection regenerated. The example app (examples/enskit-react-example) is updated in-tree.DEPTHordering value is new;Domain.canonical.depthis new and required (non-null). Both throw the standardInvariant(...)if the column is null on a canonical Domain.Testing Evidence
new integration tests for
name: { eq },name: { in }, emptyin,eq + version, and@oneOfrejection inquery.integration.test.ts.domain.integration.test.tsupdated to assertcanonical.depthmatchesname.split('.').lengthacross DEVNET fixtures.pagination test (
test-domain-pagination.ts) updated for the new filter shape.Testing performed:
pnpm typecheck,pnpm lint,pnpm test --project ensapi, integration tests pass against the devnet fixture.Known gaps: no perf benchmark in this PR — the simplification removes a recursive CTE per query, but the EPS impact is qualitative.
Reviewers reason about manually: ILIKE pattern construction (the existing TODO about LIKE-escaping is preserved verbatim).
Scope Reductions
no perf measurement run; baseline is the post-canonicality snapshot, and the new path is strictly less work per query.
Follow-ups: migrate in-repo GraphQL consumers off
name: String; optionally measure the EPS / p99 delta onQuery.domainstypeahead.Risk Analysis
relies on
canonicalName/canonicalDepth/canonicalPathbeing correctly materialized on every canonical write path. cascade tests in feat: materialize Domain.canonical{Name,LabelHashPath,Node} #2101 cover the cascade case; the new columns ride the same code path.breaking GraphQL change — any out-of-tree consumer using
where: { name: "x" }will fail at parse time with a clear schema errorrollback: revert PR; materialized columns remain populated and harmless.
Risk areas: any consumer of the old
where: { name }shape, including downstream ENSAdmin / ensapp / SDK users.Mitigations: changeset flagged minor+breaking; new
DomainCanonical.depthis non-null and will throwInvariant(...)rather than silently returning null.Named owner if this causes problems: @shrugs
Pre-Review Checklist (Blocking)