Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/events-filters-oneof.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"ensapi": minor
---

**Omnigraph (breaking)**: filter args on `*.events` and `*.permissions` connections now use operator-based inputs. `EventsWhereInput`/`AccountEventsWhereInput`: `selector_in: [Hex]` → `selector: { eq | in }`, `timestamp_gte`/`timestamp_lte` → `timestamp: { gt?, gte?, lt?, lte? }`, `from`/`sender` → `{ eq | in }`. `DomainPermissionsWhereInput.user` → `{ eq | in }`. `Account.permissions(in: AccountIdInput)` → `Account.permissions(where: { contract: AccountIdInput })`. Set-membership `in` is capped at 10 items; timestamp ranges require ≥1 bound and reject `gt`+`gte` / `lt`+`lte` combinations and inverted bounds.
Comment thread
shrugs marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,7 @@ import { type BaseDomainSet, selectBase } from "./base-domain-set";
* @param names - Exact InterpretedNames to match against
*/
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
Comment thread
shrugs marked this conversation as resolved.
if (names.length === 0) {
return ensDb.select(selectBase(base)).from(base).where(sql`false`).as("baseDomains");
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,18 @@
import { type ResolveCursorConnectionArgs, resolveCursorConnection } from "@pothos/plugin-relay";
import { and, count, eq, getTableColumns, gte, inArray, lte, type SQL, sql } from "drizzle-orm";
import {
type AnyColumn,
and,
count,
eq,
getTableColumns,
gt,
gte,
inArray,
lt,
lte,
type SQL,
sql,
} from "drizzle-orm";
import type { Address, Hex } from "enssdk";

import { ensDb, ensIndexerSchema } from "@/lib/ensdb/singleton";
Expand All @@ -16,20 +29,54 @@ type EventJoinTable =
| typeof ensIndexerSchema.permissionsEvent
| typeof ensIndexerSchema.permissionsUserEvent;

/**
* @oneOf set-membership filter shape: exactly one of `eq` or `in` is set.
*/
type SetFilter<T> = {
eq?: T | null;
in?: T[] | null;
};

/**
* Range filter shape: at least one bound is set. `gt`/`gte` are mutually exclusive; `lt`/`lte` are
* mutually exclusive (enforced by the input type's validators).
*/
type RangeFilter<T> = {
gt?: T | null;
gte?: T | null;
lt?: T | null;
lte?: T | null;
};

/**
* Available filter options for find-events queries.
*/
interface EventsWhere {
/** Filter to events whose selector (event signature) matches any of the provided values. */
selector_in?: Hex[] | null;
/** Filter to events at or after this timestamp. */
timestamp_gte?: bigint | null;
/** Filter to events at or before this timestamp. */
timestamp_lte?: bigint | null;
/** Filter to events whose `tx.from` matches. Not HCA-aware. */
from?: Address | null;
/** Filter to events whose HCA-aware `sender` matches. */
sender?: Address | null;
selector?: SetFilter<Hex> | null;
timestamp?: RangeFilter<bigint> | null;
from?: SetFilter<Address> | null;
sender?: SetFilter<Address> | null;
}

function setFilterCondition<T>(column: AnyColumn, filter?: SetFilter<T> | null): SQL | undefined {
if (!filter) return undefined;
const values = filter.in ?? [filter.eq];
Comment thread
vercel[bot] marked this conversation as resolved.
// NOTE: avoid inArray([]) runtime error by short-circuit to `false`
if (values.length === 0) return sql`false`;
return inArray(column, values);
}
Comment thread
shrugs marked this conversation as resolved.

function rangeFilterCondition<T>(
column: AnyColumn,
filter?: RangeFilter<T> | null,
): SQL | undefined {
if (!filter) return undefined;
return and(
filter.gt != null ? gt(column, filter.gt) : undefined,
filter.gte != null ? gte(column, filter.gte) : undefined,
filter.lt != null ? lt(column, filter.lt) : undefined,
filter.lte != null ? lte(column, filter.lte) : undefined,
);
}

/**
Expand All @@ -39,19 +86,10 @@ function eventsWhereConditions(where?: EventsWhere | null): SQL | undefined {
if (!where) return undefined;

return and(
where.selector_in
? where.selector_in.length
? inArray(ensIndexerSchema.event.selector, where.selector_in)
: sql`false`
: undefined,
typeof where.timestamp_gte === "bigint"
? gte(ensIndexerSchema.event.timestamp, where.timestamp_gte)
: undefined,
typeof where.timestamp_lte === "bigint"
? lte(ensIndexerSchema.event.timestamp, where.timestamp_lte)
: undefined,
where.from ? eq(ensIndexerSchema.event.from, where.from) : undefined,
where.sender ? eq(ensIndexerSchema.event.sender, where.sender) : undefined,
setFilterCondition(ensIndexerSchema.event.selector, where.selector),
rangeFilterCondition(ensIndexerSchema.event.timestamp, where.timestamp),
setFilterCondition(ensIndexerSchema.event.from, where.from),
setFilterCondition(ensIndexerSchema.event.sender, where.sender),
);
}

Expand Down
48 changes: 34 additions & 14 deletions apps/ensapi/src/omnigraph-api/schema/account.integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -179,12 +179,12 @@ describe("Account.events filtering (AccountEventsWhereInput)", () => {
expect(allEvents.length).toBeGreaterThan(0);
});

it("filters by selector_in", async () => {
it("filters by selector eq", async () => {
const targetSelector = allEvents[0].topics[0];

const result = await request<AccountEventsResult>(AccountEventsFiltered, {
address: accounts.deployer.address,
where: { selector_in: [targetSelector] },
where: { selector: { eq: targetSelector } },
});
const events = flattenConnection(result.account.events);

Expand All @@ -194,32 +194,49 @@ describe("Account.events filtering (AccountEventsWhereInput)", () => {
}
});

it("filters by selector_in with unknown topic returns no results", async () => {
it("filters by selector in", async () => {
const targetSelector = allEvents[0].topics[0];

const result = await request<AccountEventsResult>(AccountEventsFiltered, {
address: accounts.deployer.address,
where: { selector: { in: [targetSelector] } },
});
const events = flattenConnection(result.account.events);

expect(events.length).toBeGreaterThan(0);
for (const event of events) {
expect(event.topics[0]).toBe(targetSelector);
}
});

it("filters by selector in with unknown topic returns no results", async () => {
const result = await request<AccountEventsResult>(AccountEventsFiltered, {
address: accounts.deployer.address,
where: {
selector_in: ["0x0000000000000000000000000000000000000000000000000000000000000001"],
selector: {
in: ["0x0000000000000000000000000000000000000000000000000000000000000001"],
},
},
});
const events = flattenConnection(result.account.events);
expect(events.length).toBe(0);
});

it("filters by empty selector_in returns no results", async () => {
it("filters by empty selector in returns no results", async () => {
const result = await request<AccountEventsResult>(AccountEventsFiltered, {
address: accounts.deployer.address,
where: { selector_in: [] },
where: { selector: { in: [] } },
});
const events = flattenConnection(result.account.events);
expect(events.length).toBe(0);
});

it("filters by timestamp_gte", async () => {
it("filters by timestamp gte", async () => {
const midTimestamp = allEvents[Math.floor(allEvents.length / 2)].timestamp;

const result = await request<AccountEventsResult>(AccountEventsFiltered, {
address: accounts.deployer.address,
where: { timestamp_gte: midTimestamp },
where: { timestamp: { gte: midTimestamp } },
});
const events = flattenConnection(result.account.events);

Expand All @@ -230,12 +247,12 @@ describe("Account.events filtering (AccountEventsWhereInput)", () => {
}
});

it("filters by timestamp_lte", async () => {
it("filters by timestamp lte", async () => {
const midTimestamp = allEvents[Math.floor(allEvents.length / 2)].timestamp;

const result = await request<AccountEventsResult>(AccountEventsFiltered, {
address: accounts.deployer.address,
where: { timestamp_lte: midTimestamp },
where: { timestamp: { lte: midTimestamp } },
});
const events = flattenConnection(result.account.events);

Expand All @@ -252,14 +269,14 @@ describe("Account.events filtering (AccountEventsWhereInput)", () => {

const result = await request<AccountEventsResult>(AccountEventsFiltered, {
address: accounts.deployer.address,
where: { timestamp_gte: minTs, timestamp_lte: maxTs },
where: { timestamp: { gte: minTs, lte: maxTs } },
first: 1000,
});
const events = flattenConnection(result.account.events);
expect(events.length).toBe(allEvents.length);
});

it("combines selector_in and timestamp_gte", async () => {
it("combines selector and timestamp", async () => {
// pick a seed event from the second half so its selector is guaranteed to
// appear at or after midTimestamp, avoiding flaky empty-result failures
const midIndex = Math.floor(allEvents.length / 2);
Expand All @@ -269,7 +286,10 @@ describe("Account.events filtering (AccountEventsWhereInput)", () => {

const result = await request<AccountEventsResult>(AccountEventsFiltered, {
address: accounts.deployer.address,
where: { selector_in: [targetSelector], timestamp_gte: midTimestamp },
where: {
selector: { eq: targetSelector },
timestamp: { gte: midTimestamp },
},
});
const events = flattenConnection(result.account.events);

Expand All @@ -286,7 +306,7 @@ describe("Account.events filtering (AccountEventsWhereInput)", () => {

const result = await request<AccountEventsResult>(AccountEventsFiltered, {
address: accounts.deployer.address,
where: { timestamp_gte: (maxTimestamp + 1n).toString() },
where: { timestamp: { gte: (maxTimestamp + 1n).toString() } },
});
const events = flattenConnection(result.account.events);
expect(events.length).toBe(0);
Expand Down
27 changes: 21 additions & 6 deletions apps/ensapi/src/omnigraph-api/schema/account.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ import { AccountIdInput } from "@/omnigraph-api/schema/account-id";
import { ID_PAGINATED_CONNECTION_ARGS } from "@/omnigraph-api/schema/constants";
import { DomainInterfaceRef } from "@/omnigraph-api/schema/domain";
import { AccountDomainsWhereInput, DomainsOrderInput } from "@/omnigraph-api/schema/domain-inputs";
import { AccountEventsWhereInput, EventRef } from "@/omnigraph-api/schema/event";
import { EventRef } from "@/omnigraph-api/schema/event";
import { AccountEventsWhereInput } from "@/omnigraph-api/schema/event-inputs";
import { PermissionsUserRef } from "@/omnigraph-api/schema/permissions";
import { RegistryPermissionsUserRef } from "@/omnigraph-api/schema/registry-permissions-user";
import { ResolverPermissionsUserRef } from "@/omnigraph-api/schema/resolver-permissions-user";
Expand Down Expand Up @@ -96,7 +97,10 @@ AccountRef.implement({
where: t.arg({ type: AccountEventsWhereInput }),
},
resolve: (parent, args) =>
resolveFindEvents({ ...args, where: { ...args.where, sender: parent.id } }),
resolveFindEvents({
...args,
where: { ...args.where, sender: { eq: parent.id } },
}),
}),

///////////////////////
Expand All @@ -107,17 +111,18 @@ AccountRef.implement({
"The Permissions granted to this Account, optionally filtered to Permissions in a specific contract.",
type: PermissionsUserRef,
args: {
in: t.arg({ type: AccountIdInput }),
where: t.arg({ type: AccountPermissionsWhereInput }),
Comment thread
shrugs marked this conversation as resolved.
},
resolve: (parent, args) => {
const contract = args.where?.contract;
const scope = and(
// this user's permissions
eq(ensIndexerSchema.permissionsUser.user, parent.id),
// optionally filtered by contract
args.in
contract
? and(
eq(ensIndexerSchema.permissionsUser.chainId, args.in.chainId),
eq(ensIndexerSchema.permissionsUser.address, args.in.address),
eq(ensIndexerSchema.permissionsUser.chainId, contract.chainId),
eq(ensIndexerSchema.permissionsUser.address, contract.address),
)
: undefined,
);
Expand Down Expand Up @@ -227,3 +232,13 @@ export const AccountByInput = builder.inputType("AccountByInput", {
address: t.field({ type: "Address" }),
}),
});

export const AccountPermissionsWhereInput = builder.inputType("AccountPermissionsWhereInput", {
description: "Filter for Account.permissions.",
fields: (t) => ({
contract: t.field({
type: AccountIdInput,
description: "If set, filters this Account's Permissions to those granted in this contract.",
}),
}),
});
31 changes: 29 additions & 2 deletions apps/ensapi/src/omnigraph-api/schema/domain-inputs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,37 @@ import { OrderDirection } from "@/omnigraph-api/schema/order-direction";
// Inputs
//////////////////////

/**
* Max number of addresses accepted by `DomainPermissionsUserFilter.in`.
*/
export const DOMAIN_PERMISSIONS_USER_FILTER_IN_MAX = 10;

/**
* @oneOf filter for Permissions users. Exactly one of `eq` or `in` must be provided.
*/
export const DomainPermissionsUserFilter = builder.inputType("DomainPermissionsUserFilter", {
description: "Filter Permissions by user address. Exactly one of `eq` or `in` must be provided.",
isOneOf: true,
fields: (t) => ({
eq: t.field({
type: "Address",
description: "Exact user address match.",
}),
in: t.field({
type: ["Address"],
description: `User address matches any value in the set. Max ${DOMAIN_PERMISSIONS_USER_FILTER_IN_MAX} items. An empty set matches nothing.`,
validate: { maxLength: DOMAIN_PERMISSIONS_USER_FILTER_IN_MAX },
}),
}),
});

export const DomainPermissionsWhereInput = builder.inputType("DomainPermissionsWhereInput", {
description: "Filter Permissions over this Domain by a specific User address.",
description: "Filter Permissions over this Domain by user.",
fields: (t) => ({
user: t.field({ type: "Address" }),
user: t.field({
type: DomainPermissionsUserFilter,
description: "Filter Permissions to those whose user matches the provided filter.",
}),
}),
});

Expand Down
Loading
Loading