Skip to content

feat(ensapi): operator-based filter inputs for events and permissions#2126

Open
shrugs wants to merge 3 commits into
feat/domain-name-queriesfrom
feat/eq-extension
Open

feat(ensapi): operator-based filter inputs for events and permissions#2126
shrugs wants to merge 3 commits into
feat/domain-name-queriesfrom
feat/eq-extension

Conversation

@shrugs
Copy link
Copy Markdown
Member

@shrugs shrugs commented May 15, 2026

Reviewer Focus

  • find-events-resolver.ts set/range helpers + empty in: [] → no matches (mirrors filter-by-name-in.ts).
  • EventsTimestampFilter zod refines (≥1 bound; gt/gte mutex; lt/lte mutex; inverted-range rejected) — not expressible via @oneOf.

Problem & Motivation

extends the @oneOf operator pattern from Domain.name to the rest of Omnigraph's filter args where multiple operators have a real use case today. tight scope; expand on demand.

What Changed

  1. new @oneOf { eq, in } filters (in: max 10): EventsSelectorFilter, EventsFromFilter, EventsSenderFilter, DomainPermissionsUserFilter.
  2. new flat range filter EventsTimestampFilter { gt?, gte?, lt?, lte? }.
  3. EventsWhereInput / AccountEventsWhereInput / DomainPermissionsWhereInput reshaped to use them.
  4. Account.permissions(in: AccountIdInput)Account.permissions(where: { contract }).
  5. event inputs extracted to event-inputs.ts.

Design

set-membership = @oneOf { eq, in }; range = flat (bounds combine, can't be @oneOf). per-field-named inputs (no shared generics). empty in: [] matches nothing rather than 400 — matches DomainsNameFilter precedent + search-page consumer DX.

Self-Review

  • reverted a mid-iteration minLength: 1 on in: that would have forced consumers to special-case empty UI state.
  • streamlined Domain.permissions userScope to mirror setFilterCondition's in ?? [eq] pattern.
  • extracted event inputs to event-inputs.ts for symmetry with domain-inputs.ts.

Cross-Codebase Alignment

grepped selector_in, timestamp_gte/_lte, EventsWhereInput, AccountEventsWhereInput, DomainPermissionsWhereInput, permissions(in:. no live references in apps/ensadmin, packages/ensnode-sdk, packages/ensnode-react, docs/ensnode.io, examples/. subgraph API intentionally untouched (compat).

Downstream Impact

breaking Omnigraph schema. SDL regenerated. no in-repo consumers affected. changeset added (minor, ensapi only).

Testing

typecheck clean, lint clean, 98 unit + 295 integration pass.

gap: timestamp cross-field refines not asserted by integration tests (zod-layer behavior).

Scope Reductions

no eq/in for timestamp, no negation, no version-filter extension, no shared generic filter types. open on demand.

Risk

pre-1.0 API. blast radius: ensapi GraphQL clients. rollback = revert.
named owner: @shrugs.

Pre-Review Checklist

  • I reviewed every line of this diff and understand it end-to-end
  • I'm prepared to defend this PR line-by-line in review
  • I'm comfortable being the on-call owner for this change
  • Relevant changesets are included (or explicitly not required)

Copilot AI review requested due to automatic review settings May 15, 2026 22:07
@shrugs shrugs requested a review from a team as a code owner May 15, 2026 22:07
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented May 15, 2026

🦋 Changeset detected

Latest commit: 35a1978

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

This PR includes changesets to release 25 packages
Name Type
ensapi Major
ensindexer Major
ensadmin Major
ensrainbow Major
fallback-ensapi Major
enssdk Major
enscli Major
enskit Major
ensskills Major
@ensnode/datasources Major
@ensnode/ensrainbow-sdk Major
@ensnode/ensdb-sdk Major
@ensnode/ensnode-react Major
@ensnode/ensnode-sdk Major
@ensnode/integration-test-env Major
@ensnode/ponder-sdk Major
@ensnode/ponder-subgraph Major
@ensnode/shared-configs Major
@docs/ensnode Major
@docs/ensrainbow Major
@namehash/ens-referrals Major
@namehash/namehash-ui Major
@ensnode/ensindexer-perf-testing Major
@ensnode/enskit-react-example Patch
@ensnode/enssdk-example 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
Contributor

vercel Bot commented May 15, 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 May 15, 2026 10:13pm
enskit-react-example.ensnode.io Ready Ready Preview, Comment May 15, 2026 10:13pm
ensnode.io Ready Ready Preview, Comment May 15, 2026 10:13pm
ensrainbow.io Ready Ready Preview, Comment May 15, 2026 10:13pm

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 15, 2026

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: f9888324-61ed-49be-b258-bdcf9a133e24

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/eq-extension

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
Copy Markdown
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 updates the Omnigraph GraphQL API to use operator-based filter input objects for events and permissions, aligning multiple fields with the existing @oneOf filter approach and adding a timestamp range filter with zod refinements. It rewires resolver SQL where-building to support { eq | in } set-membership filters and { gt/gte/lt/lte } timestamp ranges, regenerates the SDL, and updates integration tests accordingly.

Changes:

  • Introduces per-field @oneOf set filters (eq/in) for event selector/from/sender and domain-permissions user filtering, plus a refined timestamp range filter.
  • Refactors find-events-resolver to use shared setFilterCondition / rangeFilterCondition helpers (including empty-in: [] short-circuit semantics).
  • Updates GraphQL schema wiring (imports, new inputs) and integration tests; adds a Changeset entry.

Reviewed changes

Copilot reviewed 13 out of 15 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
packages/enssdk/src/omnigraph/generated/schema.graphql Regenerated SDL reflecting new operator-based filter input shapes and argument rename(s).
apps/ensapi/src/omnigraph-api/schema/resolver.ts Switches EventsWhereInput import to the new event-inputs module.
apps/ensapi/src/omnigraph-api/schema/permissions.ts Switches EventsWhereInput import to the new event-inputs module.
apps/ensapi/src/omnigraph-api/schema/permissions.integration.test.ts Updates event-filter queries to new shapes; adds Domain.permissions user in test.
apps/ensapi/src/omnigraph-api/schema/event.ts Removes event-related input definitions (leaving EventRef).
apps/ensapi/src/omnigraph-api/schema/event-inputs.ts Adds new event filter inputs and zod refinements; defines EventsWhereInput / AccountEventsWhereInput.
apps/ensapi/src/omnigraph-api/schema/domain.ts Updates Domain.permissions resolver to support `{ eq
apps/ensapi/src/omnigraph-api/schema/domain.integration.test.ts Updates event-filter queries to the new where shapes.
apps/ensapi/src/omnigraph-api/schema/domain-inputs.ts Adds DomainPermissionsUserFilter and updates DomainPermissionsWhereInput.user to use it.
apps/ensapi/src/omnigraph-api/schema/account.ts Updates Account.events sender filter shape; renames Account.permissions arg to where and adds AccountPermissionsWhereInput.
apps/ensapi/src/omnigraph-api/schema/account.integration.test.ts Updates Account.events filter tests to new operator-based input shapes.
apps/ensapi/src/omnigraph-api/lib/find-events/find-events-resolver.ts Refactors event WHERE building to generic helpers; implements empty-in short-circuit and range filters.
apps/ensapi/src/omnigraph-api/lib/find-domains/layers/filter-by-name-in.ts Minor comment adjustment; preserves empty-inArray([]) short-circuit semantics.
.changeset/events-filters-oneof.md Declares a minor release note describing the breaking Omnigraph filter shape changes.

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

Comment on lines +127 to +129
return lower < upper;
},
{ message: "Lower bound must be less than upper bound." },
Comment on lines +127 to +129
return lower < upper;
},
{ message: "Lower bound must be less than upper bound." },
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.

Suggested change
return lower < upper;
},
{ message: "Lower bound must be less than upper bound." },
return lower <= upper;
},
{ message: "Lower bound must be less than or equal to upper bound." },

EventsTimestampFilter validation incorrectly rejects equal bounds (e.g., gte: 100, lte: 100) which should match events at exactly that timestamp

Fix on Vercel

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 15, 2026

Greptile Summary

This PR extends the operator-based @oneOf filter pattern to events and permissions connections, replacing flat scalar fields with structured eq/in set-membership filters and a new EventsTimestampFilter range type across EventsWhereInput, AccountEventsWhereInput, and DomainPermissionsWhereInput. Event input definitions are extracted to event-inputs.ts for symmetry with domain-inputs.ts, and Account.permissions is reshaped from a bare in: AccountIdInput arg to where: { contract }.

  • New @oneOf set-membership inputs (EventsSelectorFilter, EventsFromFilter, EventsSenderFilter, DomainPermissionsUserFilter) cap in at 10 items and short-circuit empty arrays to false rather than generating a Postgres syntax error.
  • EventsTimestampFilter is a flat range input with Zod cross-field refines enforcing at least one bound, gt/gte exclusivity, lt/lte exclusivity, and a range-inversion check — though the inversion check uses strict < and incorrectly rejects equal-bound queries like { gte: T, lte: T }.
  • resolveFindEvents is refactored with setFilterCondition and rangeFilterCondition helpers that centralise the inArray-empty short-circuit and null-guard patterns."

Confidence Score: 3/5

Safe to merge after fixing the timestamp equal-bound validation; the rest of the filter logic and schema reshaping is correct.

The EventsTimestampFilter inverted-range Zod refine uses strict < instead of <=, so any client requesting events at a single precise timestamp with { gte: T, lte: T } will receive a validation error rather than results. This is a silent mismatch between the documented behaviour and the actual validator. All other filter helpers and schema changes look correct and well-tested.

apps/ensapi/src/omnigraph-api/schema/event-inputs.ts — the Zod inversion refine at the bottom of EventsTimestampFilter.

Important Files Changed

Filename Overview
apps/ensapi/src/omnigraph-api/schema/event-inputs.ts New file defining EventsSelectorFilter, EventsFromFilter, EventsSenderFilter, EventsTimestampFilter, EventsWhereInput, and AccountEventsWhereInput; the inverted-range Zod refine uses strict < instead of <=, incorrectly rejecting equal-bound timestamp queries like { gte: T, lte: T }.
apps/ensapi/src/omnigraph-api/lib/find-events/find-events-resolver.ts Refactored to use new SetFilter/RangeFilter helpers; setFilterCondition's filter.in ?? [filter.eq] fallback could pass [undefined] to inArray if a programmatic caller omits both fields, though @OneOf prevents this via the GraphQL layer.
apps/ensapi/src/omnigraph-api/schema/account.ts Account.permissions reshaped from in: AccountIdInput to where: { contract }, and Account.events correctly injects sender: { eq: parent.id } into the new SetFilter shape.
apps/ensapi/src/omnigraph-api/schema/domain.ts ENSv2Domain.permissions now uses DomainPermissionsUserFilter with the same in ?? [eq] inArray-safe pattern; logic matches setFilterCondition correctly.
apps/ensapi/src/omnigraph-api/schema/permissions.integration.test.ts Thorough integration tests for new filter shapes; equal-bound timestamp case is not tested, which would surface the strict-< validation bug.
apps/ensapi/src/omnigraph-api/schema/account.integration.test.ts Updated integration tests cover the new AccountEventsWhereInput filter shapes; equal-bound timestamp not tested here either.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    GQL["GraphQL Query"] --> WHERE["EventsWhereInput / AccountEventsWhereInput / DomainPermissionsWhereInput"]
    WHERE --> SEL["EventsSelectorFilter @oneOf { eq, in }"]
    WHERE --> TS["EventsTimestampFilter { gt?, gte?, lt?, lte? } + Zod refines"]
    WHERE --> FROM["EventsFromFilter @oneOf { eq, in }"]
    WHERE --> SEND["EventsSenderFilter @oneOf { eq, in }"]
    SEL --> SFC["setFilterCondition() in ?? [eq] inArray / [] false"]
    FROM --> SFC
    SEND --> SFC
    TS --> RFC["rangeFilterCondition() and(gt?, gte?, lt?, lte?)"]
    SFC --> AND["and(...conditions)"]
    RFC --> AND
    AND --> DRIZZLE["Drizzle ORM query"]
    DRIZZLE --> PG[(PostgreSQL)]
Loading

Reviews (1): Last reviewed commit: "fix: schema" | Re-trigger Greptile

Comment on lines +122 to +130
[
(data) => {
const lower = data.gt ?? data.gte;
const upper = data.lt ?? data.lte;
if (lower == null || upper == null) return true;
return lower < upper;
},
{ message: "Lower bound must be less than upper bound." },
],
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.

P1 The inverted-range check uses strict <, so supplying equal lower and upper bounds — e.g. { gte: T, lte: T } to pin-point a specific timestamp — will fail validation with "Lower bound must be less than upper bound." Equal bounds are a valid, semantically meaningful input (match events at exactly timestamp T) and should be allowed. The comparison should be <=.

Suggested change
[
(data) => {
const lower = data.gt ?? data.gte;
const upper = data.lt ?? data.lte;
if (lower == null || upper == null) return true;
return lower < upper;
},
{ message: "Lower bound must be less than upper bound." },
],
[
(data) => {
const lower = data.gt ?? data.gte;
const upper = data.lt ?? data.lte;
if (lower == null || upper == null) return true;
return lower <= upper;
},
{ message: "Lower bound must be less than or equal to upper bound." },
],

export function filterByNameIn(base: BaseDomainSet, names: InterpretedName[]) {
// Drizzle footgun: `inArray(col, [])` generates `col in ()`, a Postgres syntax error.
// Short-circuit to an explicit empty result.
// NOTE: avoid inArray([]) runtime error by short-circuit to an explicit empty result
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.

P2 The original comment was more informative — it explained the Drizzle footgun specifically: inArray(col, []) generates col in (), which is a Postgres syntax error, not just a generic "runtime error". The precise diagnosis helps future maintainers understand why the short-circuit is necessary.

Suggested change
// NOTE: avoid inArray([]) runtime error by short-circuit to an explicit empty result
// NOTE: Drizzle footgun — `inArray(col, [])` generates `col in ()`, a Postgres syntax error.
// Short-circuit to an explicit empty result instead.

Comment on lines +61 to +67
function setFilterCondition<T>(column: AnyColumn, filter?: SetFilter<T> | null): SQL | undefined {
if (!filter) return undefined;
const values = filter.in ?? [filter.eq];
// NOTE: avoid inArray([]) runtime error by short-circuit to `false`
if (values.length === 0) return sql`false`;
return inArray(column, values);
}
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.

P2 setFilterCondition passes [undefined] to inArray when neither eq nor in is set

filter.in ?? [filter.eq] produces [undefined] when both fields are absent (e.g. a programmatic caller passes {} without @oneOf enforcement). Drizzle's inArray with [undefined] does not short-circuit via the values.length === 0 guard (length is 1) and will generate SQL col IN (null) or a type coercion error. The @oneOf GraphQL directive prevents this path for schema consumers, but the internal EventsWhere interface accepts the same type and is composed directly in account.ts via { ...args.where, sender: { eq: parent.id } }. Adding a filter.eq != null guard or a stricter internal type would close the gap.

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