diff --git a/.changeset/events-filters-oneof.md b/.changeset/events-filters-oneof.md new file mode 100644 index 0000000000..c48d1908d9 --- /dev/null +++ b/.changeset/events-filters-oneof.md @@ -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. diff --git a/apps/ensapi/src/omnigraph-api/lib/find-domains/layers/filter-by-name-in.ts b/apps/ensapi/src/omnigraph-api/lib/find-domains/layers/filter-by-name-in.ts index cb2440dab3..5f5a648872 100644 --- a/apps/ensapi/src/omnigraph-api/lib/find-domains/layers/filter-by-name-in.ts +++ b/apps/ensapi/src/omnigraph-api/lib/find-domains/layers/filter-by-name-in.ts @@ -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 if (names.length === 0) { return ensDb.select(selectBase(base)).from(base).where(sql`false`).as("baseDomains"); } diff --git a/apps/ensapi/src/omnigraph-api/lib/find-events/find-events-resolver.ts b/apps/ensapi/src/omnigraph-api/lib/find-events/find-events-resolver.ts index f14bb692a1..fe98624d1a 100644 --- a/apps/ensapi/src/omnigraph-api/lib/find-events/find-events-resolver.ts +++ b/apps/ensapi/src/omnigraph-api/lib/find-events/find-events-resolver.ts @@ -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"; @@ -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 = { + 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 = { + 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 | null; + timestamp?: RangeFilter | null; + from?: SetFilter
| null; + sender?: SetFilter
| null; +} + +function setFilterCondition(column: AnyColumn, filter?: SetFilter | 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); +} + +function rangeFilterCondition( + column: AnyColumn, + filter?: RangeFilter | 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, + ); } /** @@ -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), ); } diff --git a/apps/ensapi/src/omnigraph-api/schema/account.integration.test.ts b/apps/ensapi/src/omnigraph-api/schema/account.integration.test.ts index a5ac70b5e0..f9ca9581a8 100644 --- a/apps/ensapi/src/omnigraph-api/schema/account.integration.test.ts +++ b/apps/ensapi/src/omnigraph-api/schema/account.integration.test.ts @@ -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(AccountEventsFiltered, { address: accounts.deployer.address, - where: { selector_in: [targetSelector] }, + where: { selector: { eq: targetSelector } }, }); const events = flattenConnection(result.account.events); @@ -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(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(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(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(AccountEventsFiltered, { address: accounts.deployer.address, - where: { timestamp_gte: midTimestamp }, + where: { timestamp: { gte: midTimestamp } }, }); const events = flattenConnection(result.account.events); @@ -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(AccountEventsFiltered, { address: accounts.deployer.address, - where: { timestamp_lte: midTimestamp }, + where: { timestamp: { lte: midTimestamp } }, }); const events = flattenConnection(result.account.events); @@ -252,14 +269,14 @@ describe("Account.events filtering (AccountEventsWhereInput)", () => { const result = await request(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); @@ -269,7 +286,10 @@ describe("Account.events filtering (AccountEventsWhereInput)", () => { const result = await request(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); @@ -286,7 +306,7 @@ describe("Account.events filtering (AccountEventsWhereInput)", () => { const result = await request(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); diff --git a/apps/ensapi/src/omnigraph-api/schema/account.ts b/apps/ensapi/src/omnigraph-api/schema/account.ts index 9016c2c40f..7953e60928 100644 --- a/apps/ensapi/src/omnigraph-api/schema/account.ts +++ b/apps/ensapi/src/omnigraph-api/schema/account.ts @@ -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"; @@ -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 } }, + }), }), /////////////////////// @@ -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 }), }, 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, ); @@ -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.", + }), + }), +}); diff --git a/apps/ensapi/src/omnigraph-api/schema/domain-inputs.ts b/apps/ensapi/src/omnigraph-api/schema/domain-inputs.ts index e8fee80d71..ba334b9048 100644 --- a/apps/ensapi/src/omnigraph-api/schema/domain-inputs.ts +++ b/apps/ensapi/src/omnigraph-api/schema/domain-inputs.ts @@ -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.", + }), }), }); diff --git a/apps/ensapi/src/omnigraph-api/schema/domain.integration.test.ts b/apps/ensapi/src/omnigraph-api/schema/domain.integration.test.ts index 89c548c62a..4ab0cdd64f 100644 --- a/apps/ensapi/src/omnigraph-api/schema/domain.integration.test.ts +++ b/apps/ensapi/src/omnigraph-api/schema/domain.integration.test.ts @@ -283,12 +283,12 @@ describe("Domain.events filtering (EventsWhereInput)", () => { 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(DomainEventsFiltered, { name: NAME_WITH_EVENTS, - where: { selector_in: [targetSelector] }, + where: { selector: { eq: targetSelector } }, }); const events = flattenConnection(result.domain.events); @@ -298,32 +298,49 @@ describe("Domain.events filtering (EventsWhereInput)", () => { } }); - 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(DomainEventsFiltered, { + name: NAME_WITH_EVENTS, + where: { selector: { in: [targetSelector] } }, + }); + const events = flattenConnection(result.domain.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(DomainEventsFiltered, { name: NAME_WITH_EVENTS, where: { - selector_in: ["0x0000000000000000000000000000000000000000000000000000000000000001"], + selector: { + in: ["0x0000000000000000000000000000000000000000000000000000000000000001"], + }, }, }); const events = flattenConnection(result.domain.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(DomainEventsFiltered, { name: NAME_WITH_EVENTS, - where: { selector_in: [] }, + where: { selector: { in: [] } }, }); const events = flattenConnection(result.domain.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(DomainEventsFiltered, { name: NAME_WITH_EVENTS, - where: { timestamp_gte: midTimestamp }, + where: { timestamp: { gte: midTimestamp } }, }); const events = flattenConnection(result.domain.events); @@ -334,12 +351,12 @@ describe("Domain.events filtering (EventsWhereInput)", () => { } }); - 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(DomainEventsFiltered, { name: NAME_WITH_EVENTS, - where: { timestamp_lte: midTimestamp }, + where: { timestamp: { lte: midTimestamp } }, }); const events = flattenConnection(result.domain.events); @@ -356,19 +373,19 @@ describe("Domain.events filtering (EventsWhereInput)", () => { const result = await request(DomainEventsFiltered, { name: NAME_WITH_EVENTS, - where: { timestamp_gte: minTs, timestamp_lte: maxTs }, + where: { timestamp: { gte: minTs, lte: maxTs } }, first: 1000, }); const events = flattenConnection(result.domain.events); expect(events.length).toBe(allEvents.length); }); - it("filters by from address", async () => { + it("filters by from eq", async () => { const targetFrom = allEvents[0].from; const result = await request(DomainEventsFiltered, { name: NAME_WITH_EVENTS, - where: { from: targetFrom }, + where: { from: { eq: targetFrom } }, }); const events = flattenConnection(result.domain.events); @@ -378,7 +395,7 @@ describe("Domain.events filtering (EventsWhereInput)", () => { } }); - 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); @@ -388,7 +405,10 @@ describe("Domain.events filtering (EventsWhereInput)", () => { const result = await request(DomainEventsFiltered, { name: NAME_WITH_EVENTS, - where: { selector_in: [targetSelector], timestamp_gte: midTimestamp }, + where: { + selector: { eq: targetSelector }, + timestamp: { gte: midTimestamp }, + }, }); const events = flattenConnection(result.domain.events); @@ -405,9 +425,62 @@ describe("Domain.events filtering (EventsWhereInput)", () => { const result = await request(DomainEventsFiltered, { name: NAME_WITH_EVENTS, - where: { timestamp_gte: (maxTimestamp + 1n).toString() }, + where: { timestamp: { gte: (maxTimestamp + 1n).toString() } }, }); const events = flattenConnection(result.domain.events); expect(events.length).toBe(0); }); + + it("rejects an empty timestamp filter (no bounds)", async () => { + await expect( + request(DomainEventsFiltered, { + name: NAME_WITH_EVENTS, + where: { timestamp: {} }, + }), + ).rejects.toThrow(); + }); + + it("rejects timestamp with both gt and gte", async () => { + const t = allEvents[0].timestamp; + await expect( + request(DomainEventsFiltered, { + name: NAME_WITH_EVENTS, + where: { timestamp: { gt: t, gte: t } }, + }), + ).rejects.toThrow(); + }); + + it("rejects timestamp with both lt and lte", async () => { + const t = allEvents[0].timestamp; + await expect( + request(DomainEventsFiltered, { + name: NAME_WITH_EVENTS, + where: { timestamp: { lt: t, lte: t } }, + }), + ).rejects.toThrow(); + }); + + it("rejects inverted timestamp range", async () => { + const lo = BigInt(allEvents[0].timestamp); + const hi = BigInt(allEvents[allEvents.length - 1].timestamp); + if (lo === hi) return; // dataset must have a range for this test + await expect( + request(DomainEventsFiltered, { + name: NAME_WITH_EVENTS, + where: { timestamp: { gte: hi.toString(), lte: lo.toString() } }, + }), + ).rejects.toThrow(); + }); + + it("accepts equal lower and upper bounds (pin-point timestamp)", async () => { + const t = allEvents[0].timestamp; + const result = await request(DomainEventsFiltered, { + name: NAME_WITH_EVENTS, + where: { timestamp: { gte: t, lte: t } }, + }); + const events = flattenConnection(result.domain.events); + for (const event of events) { + expect(event.timestamp).toBe(t); + } + }); }); diff --git a/apps/ensapi/src/omnigraph-api/schema/domain.ts b/apps/ensapi/src/omnigraph-api/schema/domain.ts index 52f2c73387..e757a905af 100644 --- a/apps/ensapi/src/omnigraph-api/schema/domain.ts +++ b/apps/ensapi/src/omnigraph-api/schema/domain.ts @@ -1,6 +1,6 @@ import { trace } from "@opentelemetry/api"; import { type ResolveCursorConnectionArgs, resolveCursorConnection } from "@pothos/plugin-relay"; -import { and, count, eq, getTableColumns } from "drizzle-orm"; +import { and, count, eq, getTableColumns, inArray, sql } from "drizzle-orm"; import type { DomainId } from "enssdk"; import type { RequiredAndNotNull, RequiredAndNull } from "@ensnode/ensnode-sdk"; @@ -38,7 +38,8 @@ import { DomainsOrderInput, SubdomainsWhereInput, } from "@/omnigraph-api/schema/domain-inputs"; -import { EventRef, EventsWhereInput } from "@/omnigraph-api/schema/event"; +import { EventRef } from "@/omnigraph-api/schema/event"; +import { EventsWhereInput } from "@/omnigraph-api/schema/event-inputs"; import { LabelRef } from "@/omnigraph-api/schema/label"; import { PermissionsUserRef } from "@/omnigraph-api/schema/permissions"; import { RegistrationInterfaceRef } from "@/omnigraph-api/schema/registration"; @@ -319,11 +320,23 @@ ENSv2DomainRef.implement({ where: t.arg({ type: DomainPermissionsWhereInput }), }, resolve: (parent, args) => { + const userScope = (() => { + const user = args.where?.user; + if (!user) return undefined; + + const userIn = user.in ?? [user.eq]; + + // NOTE: avoid inArray([]) runtime error by short-circuit to an explicit empty result + if (userIn.length === 0) return sql`false`; + + return inArray(ensIndexerSchema.permissionsUser.user, userIn); + })(); + const scope = and( // filter by resource === tokenId eq(ensIndexerSchema.permissionsUser.resource, parent.tokenId), // optionally filter by user - args.where?.user ? eq(ensIndexerSchema.permissionsUser.user, args.where.user) : undefined, + userScope, ); // inner join against this Domain's registry to filter Permissions by those in said registry diff --git a/apps/ensapi/src/omnigraph-api/schema/event-inputs.ts b/apps/ensapi/src/omnigraph-api/schema/event-inputs.ts new file mode 100644 index 0000000000..da7956e3c7 --- /dev/null +++ b/apps/ensapi/src/omnigraph-api/schema/event-inputs.ts @@ -0,0 +1,184 @@ +import { builder } from "@/omnigraph-api/builder"; + +////////// +// Inputs +////////// + +/** + * Max number of selectors accepted by `EventsSelectorFilter.in`. + */ +export const EVENTS_SELECTOR_FILTER_IN_MAX = 10; + +/** + * Max number of addresses accepted by `EventsFromFilter.in`. + */ +export const EVENTS_FROM_FILTER_IN_MAX = 10; + +/** + * Max number of addresses accepted by `EventsSenderFilter.in`. + */ +export const EVENTS_SENDER_FILTER_IN_MAX = 10; + +/** + * @oneOf filter for Event selectors. Exactly one of `eq` or `in` must be provided. + */ +export const EventsSelectorFilter = builder.inputType("EventsSelectorFilter", { + description: + "Filter Events by selector (event signature). Exactly one of `eq` or `in` must be provided.", + isOneOf: true, + fields: (t) => ({ + eq: t.field({ + type: "Hex", + description: "Exact selector match.", + }), + in: t.field({ + type: ["Hex"], + description: `Selector matches any value in the set. Max ${EVENTS_SELECTOR_FILTER_IN_MAX} items. An empty set matches nothing.`, + validate: { maxLength: EVENTS_SELECTOR_FILTER_IN_MAX }, + }), + }), +}); + +/** + * @oneOf filter for Event `tx.from`. Exactly one of `eq` or `in` must be provided. + */ +export const EventsFromFilter = builder.inputType("EventsFromFilter", { + description: + "Filter Events by `tx.from`. Not HCA-aware — use `sender` to filter by the HCA-aware actor. Exactly one of `eq` or `in` must be provided.", + isOneOf: true, + fields: (t) => ({ + eq: t.field({ + type: "Address", + description: "Exact `tx.from` match.", + }), + in: t.field({ + type: ["Address"], + description: `\`tx.from\` matches any address in the set. Max ${EVENTS_FROM_FILTER_IN_MAX} items. An empty set matches nothing.`, + validate: { maxLength: EVENTS_FROM_FILTER_IN_MAX }, + }), + }), +}); + +/** + * @oneOf filter for Event HCA-aware `sender`. Exactly one of `eq` or `in` must be provided. + */ +export const EventsSenderFilter = builder.inputType("EventsSenderFilter", { + description: + "Filter Events by HCA-aware `sender` (the HCA account address if used, otherwise Transaction.from). Exactly one of `eq` or `in` must be provided.", + isOneOf: true, + fields: (t) => ({ + eq: t.field({ + type: "Address", + description: "Exact `sender` match.", + }), + in: t.field({ + type: ["Address"], + description: `\`sender\` matches any address in the set. Max ${EVENTS_SENDER_FILTER_IN_MAX} items. An empty set matches nothing.`, + validate: { maxLength: EVENTS_SENDER_FILTER_IN_MAX }, + }), + }), +}); + +/** + * Range filter for Event timestamps. At least one bound must be provided. Bounds may combine + * (e.g. `{ gte, lte }` for a closed range), but `gt`/`gte` are mutually exclusive, as are + * `lt`/`lte`. If both a lower and upper bound are provided, the lower must be less than or equal + * to the upper. + */ +export const EventsTimestampFilter = builder.inputType("EventsTimestampFilter", { + description: + "Filter Events by timestamp range. At least one bound must be provided. `gt`/`gte` are mutually exclusive; `lt`/`lte` are mutually exclusive.", + fields: (t) => ({ + gt: t.field({ + type: "BigInt", + description: "Filter to events strictly after this UnixTimestamp.", + }), + gte: t.field({ + type: "BigInt", + description: "Filter to events at or after this UnixTimestamp.", + }), + lt: t.field({ + type: "BigInt", + description: "Filter to events strictly before this UnixTimestamp.", + }), + lte: t.field({ + type: "BigInt", + description: "Filter to events at or before this UnixTimestamp.", + }), + }), + validate: { + refine: [ + [ + (data) => [data.gt, data.gte, data.lt, data.lte].some((v) => v != null), + { message: "At least one bound (gt, gte, lt, lte) must be provided." }, + ], + [ + (data) => !(data.gt != null && data.gte != null), + { message: "`gt` and `gte` are mutually exclusive." }, + ], + [ + (data) => !(data.lt != null && data.lte != null), + { message: "`lt` and `lte` are mutually exclusive." }, + ], + [ + (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." }, + ], + ], + }, +}); + +/** + * Shared filter for events connections. Used by Domain.events, Resolver.events, Permissions.events, + * and Account.events (which excludes `sender` since it's implied). + */ +export const EventsWhereInput = builder.inputType("EventsWhereInput", { + description: "Filter conditions for an events connection.", + fields: (t) => ({ + selector: t.field({ + type: EventsSelectorFilter, + description: "Filter to events whose selector (event signature) matches the provided filter.", + }), + timestamp: t.field({ + type: EventsTimestampFilter, + description: "Filter to events whose UnixTimestamp falls within the provided range.", + }), + from: t.field({ + type: EventsFromFilter, + description: + "Filter to events whose `tx.from` matches the provided filter. Not HCA-aware — use `sender` to filter by the HCA-aware actor.", + }), + sender: t.field({ + type: EventsSenderFilter, + description: + "Filter to events whose HCA-aware `sender` matches the provided filter (the HCA account address if used, otherwise Transaction.from).", + }), + }), +}); + +/** + * Like EventsWhereInput but without `sender` (used where `sender` is implied, e.g. Account.events). + */ +export const AccountEventsWhereInput = builder.inputType("AccountEventsWhereInput", { + description: "Filter conditions for Account.events (where `sender` is implied by the Account).", + fields: (t) => ({ + selector: t.field({ + type: EventsSelectorFilter, + description: "Filter to events whose selector (event signature) matches the provided filter.", + }), + timestamp: t.field({ + type: EventsTimestampFilter, + description: "Filter to events whose UnixTimestamp falls within the provided range.", + }), + from: t.field({ + type: EventsFromFilter, + description: + "Filter to events whose `tx.from` matches the provided filter. Not HCA-aware — the Account's HCA-aware filter is applied via `sender = Account.id`.", + }), + }), +}); diff --git a/apps/ensapi/src/omnigraph-api/schema/event.ts b/apps/ensapi/src/omnigraph-api/schema/event.ts index 7e20923bf6..ac44855929 100644 --- a/apps/ensapi/src/omnigraph-api/schema/event.ts +++ b/apps/ensapi/src/omnigraph-api/schema/event.ts @@ -164,67 +164,3 @@ EventRef.implement({ }), }), }); - -////////// -// Inputs -////////// - -/** - * Shared filter for events connections. Used by Domain.events, Resolver.events, Permissions.events, - * and Account.events (which excludes `sender` since it's implied). - */ -export const EventsWhereInput = builder.inputType("EventsWhereInput", { - description: "Filter conditions for an events connection.", - fields: (t) => ({ - selector_in: t.field({ - type: ["Hex"], - description: - "Filter to events whose selector (event signature) is one of the provided values.", - }), - timestamp_gte: t.field({ - type: "BigInt", - description: "Filter to events at or after this UnixTimestamp.", - }), - timestamp_lte: t.field({ - type: "BigInt", - description: "Filter to events at or before this UnixTimestamp.", - }), - from: t.field({ - type: "Address", - description: - "Filter to events whose `tx.from` matches. Not HCA-aware — use `sender` to filter by the HCA account address.", - }), - sender: t.field({ - type: "Address", - description: - "Filter to events whose `sender` matches: the HCA account address if used, otherwise Transaction.from.", - }), - }), -}); - -/** - * Like EventsWhereInput but without `sender` (used where `sender` is implied, e.g. Account.events). - */ -export const AccountEventsWhereInput = builder.inputType("AccountEventsWhereInput", { - description: "Filter conditions for Account.events (where `sender` is implied by the Account).", - fields: (t) => ({ - selector_in: t.field({ - type: ["Hex"], - description: - "Filter to events whose selector (event signature) is one of the provided values.", - }), - timestamp_gte: t.field({ - type: "BigInt", - description: "Filter to events at or after this UnixTimestamp.", - }), - timestamp_lte: t.field({ - type: "BigInt", - description: "Filter to events at or before this UnixTimestamp.", - }), - from: t.field({ - type: "Address", - description: - "Filter to events whose `tx.from` matches. Not HCA-aware — the Account's HCA-aware filter is applied via `sender = Account.id`.", - }), - }), -}); diff --git a/apps/ensapi/src/omnigraph-api/schema/permissions.integration.test.ts b/apps/ensapi/src/omnigraph-api/schema/permissions.integration.test.ts index 5ab7edc20b..32803d1bf2 100644 --- a/apps/ensapi/src/omnigraph-api/schema/permissions.integration.test.ts +++ b/apps/ensapi/src/omnigraph-api/schema/permissions.integration.test.ts @@ -183,12 +183,12 @@ describe("Domain.permissions", () => { } }); - it("filters permissions by user address", async () => { + it("filters permissions by user address (eq)", async () => { const DomainPermissionsFiltered = gql` query DomainPermissionsFiltered($name: InterpretedName!, $user: Address!) { domain(by: { name: $name }) { ... on ENSv2Domain { - permissions(where: { user: $user }) { edges { node { id resource user { address } roles } } } + permissions(where: { user: { eq: $user } }) { edges { node { id resource user { address } roles } } } } } } @@ -207,6 +207,31 @@ describe("Domain.permissions", () => { expect(user.user.address).toBe(targetUser); } }); + + it("filters permissions by user address (in)", async () => { + const DomainPermissionsFiltered = gql` + query DomainPermissionsFiltered($name: InterpretedName!, $users: [Address!]!) { + domain(by: { name: $name }) { + ... on ENSv2Domain { + permissions(where: { user: { in: $users } }) { edges { node { id resource user { address } roles } } } + } + } + } + `; + + const targetUser = allUsers[0].user.address; + + const filtered = await request(DomainPermissionsFiltered, { + name: "test.eth", + users: [targetUser], + }); + const filteredUsers = flattenConnection(filtered.domain.permissions); + + expect(filteredUsers.length).toBeGreaterThan(0); + for (const user of filteredUsers) { + expect(user.user.address).toBe(targetUser); + } + }); }); describe("Account.permissions and Account.registryPermissions", () => { @@ -380,10 +405,10 @@ describe("Permissions.events filtering (EventsWhereInput)", () => { expect(allEvents.length).toBeGreaterThan(0); }); - it("filters by selector_in", async () => { + it("filters by selector eq", async () => { const result = await request(PermissionsEventsFiltered, { contract: V2_ETH_REGISTRY, - where: { selector_in: [EAC_ROLES_CHANGED_SELECTOR] }, + where: { selector: { eq: EAC_ROLES_CHANGED_SELECTOR } }, }); const events = flattenConnection(result.permissions.events); @@ -393,32 +418,34 @@ describe("Permissions.events filtering (EventsWhereInput)", () => { } }); - it("filters by selector_in with unknown topic returns no results", async () => { + it("filters by selector in with unknown topic returns no results", async () => { const result = await request(PermissionsEventsFiltered, { contract: V2_ETH_REGISTRY, where: { - selector_in: ["0x0000000000000000000000000000000000000000000000000000000000000001"], + selector: { + in: ["0x0000000000000000000000000000000000000000000000000000000000000001"], + }, }, }); const events = flattenConnection(result.permissions.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(PermissionsEventsFiltered, { contract: V2_ETH_REGISTRY, - where: { selector_in: [] }, + where: { selector: { in: [] } }, }); const events = flattenConnection(result.permissions.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(PermissionsEventsFiltered, { contract: V2_ETH_REGISTRY, - where: { timestamp_gte: midTimestamp }, + where: { timestamp: { gte: midTimestamp } }, }); const events = flattenConnection(result.permissions.events); @@ -429,12 +456,12 @@ describe("Permissions.events filtering (EventsWhereInput)", () => { } }); - 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(PermissionsEventsFiltered, { contract: V2_ETH_REGISTRY, - where: { timestamp_lte: midTimestamp }, + where: { timestamp: { lte: midTimestamp } }, }); const events = flattenConnection(result.permissions.events); @@ -451,7 +478,7 @@ describe("Permissions.events filtering (EventsWhereInput)", () => { const result = await request(PermissionsEventsFiltered, { contract: V2_ETH_REGISTRY, - where: { timestamp_gte: minTs, timestamp_lte: maxTs }, + where: { timestamp: { gte: minTs, lte: maxTs } }, first: 1000, }); const events = flattenConnection(result.permissions.events); @@ -460,12 +487,12 @@ describe("Permissions.events filtering (EventsWhereInput)", () => { expect(events.length).toBe(allEvents.length); }); - it("filters by from address", async () => { + it("filters by from eq", async () => { const targetFrom = allEvents[0].from; const result = await request(PermissionsEventsFiltered, { contract: V2_ETH_REGISTRY, - where: { from: targetFrom }, + where: { from: { eq: targetFrom } }, }); const events = flattenConnection(result.permissions.events); @@ -475,12 +502,15 @@ describe("Permissions.events filtering (EventsWhereInput)", () => { } }); - it("combines selector_in and timestamp_gte", async () => { + it("combines selector and timestamp", async () => { const midTimestamp = allEvents[Math.floor(allEvents.length / 2)].timestamp; const result = await request(PermissionsEventsFiltered, { contract: V2_ETH_REGISTRY, - where: { selector_in: [EAC_ROLES_CHANGED_SELECTOR], timestamp_gte: midTimestamp }, + where: { + selector: { eq: EAC_ROLES_CHANGED_SELECTOR }, + timestamp: { gte: midTimestamp }, + }, }); const events = flattenConnection(result.permissions.events); @@ -497,7 +527,7 @@ describe("Permissions.events filtering (EventsWhereInput)", () => { const result = await request(PermissionsEventsFiltered, { contract: V2_ETH_REGISTRY, - where: { timestamp_gte: (maxTimestamp + 1n).toString() }, + where: { timestamp: { gte: (maxTimestamp + 1n).toString() } }, }); const events = flattenConnection(result.permissions.events); expect(events.length).toBe(0); @@ -564,10 +594,10 @@ describe("PermissionsUser.events", () => { } }); - it("filters by selector_in", async () => { + it("filters by selector eq", async () => { const result = await request(PermissionsUserEvents, { contract: V2_ETH_REGISTRY, - where: { selector_in: [EAC_ROLES_CHANGED_SELECTOR] }, + where: { selector: { eq: EAC_ROLES_CHANGED_SELECTOR } }, }); const filteredUsers = flattenConnection(result.permissions.root.users); @@ -580,10 +610,10 @@ describe("PermissionsUser.events", () => { } }); - it("filters by empty selector_in returns no results", async () => { + it("filters by empty selector in returns no results", async () => { const result = await request(PermissionsUserEvents, { contract: V2_ETH_REGISTRY, - where: { selector_in: [] }, + where: { selector: { in: [] } }, }); const filteredUsers = flattenConnection(result.permissions.root.users); diff --git a/apps/ensapi/src/omnigraph-api/schema/permissions.ts b/apps/ensapi/src/omnigraph-api/schema/permissions.ts index de6a5bd903..1f6fb16d7d 100644 --- a/apps/ensapi/src/omnigraph-api/schema/permissions.ts +++ b/apps/ensapi/src/omnigraph-api/schema/permissions.ts @@ -18,7 +18,8 @@ import { lazyConnection } from "@/omnigraph-api/lib/lazy-connection"; import { AccountRef } from "@/omnigraph-api/schema/account"; import { AccountIdInput, AccountIdRef } from "@/omnigraph-api/schema/account-id"; import { ID_PAGINATED_CONNECTION_ARGS } from "@/omnigraph-api/schema/constants"; -import { EventRef, EventsWhereInput } from "@/omnigraph-api/schema/event"; +import { EventRef } from "@/omnigraph-api/schema/event"; +import { EventsWhereInput } from "@/omnigraph-api/schema/event-inputs"; export const PermissionsRef = builder.loadableObjectRef("Permissions", { load: (ids: PermissionsId[]) => diff --git a/apps/ensapi/src/omnigraph-api/schema/resolver.ts b/apps/ensapi/src/omnigraph-api/schema/resolver.ts index e181a86b9f..07e8d65bea 100644 --- a/apps/ensapi/src/omnigraph-api/schema/resolver.ts +++ b/apps/ensapi/src/omnigraph-api/schema/resolver.ts @@ -19,7 +19,8 @@ import { getModelId } from "@/omnigraph-api/lib/get-model-id"; import { lazyConnection } from "@/omnigraph-api/lib/lazy-connection"; import { AccountIdInput, AccountIdRef } from "@/omnigraph-api/schema/account-id"; import { ID_PAGINATED_CONNECTION_ARGS } from "@/omnigraph-api/schema/constants"; -import { EventRef, EventsWhereInput } from "@/omnigraph-api/schema/event"; +import { EventRef } from "@/omnigraph-api/schema/event"; +import { EventsWhereInput } from "@/omnigraph-api/schema/event-inputs"; import { NameOrNodeInput } from "@/omnigraph-api/schema/name-or-node"; import { PermissionsRef } from "@/omnigraph-api/schema/permissions"; import { RegistryInterfaceRef } from "@/omnigraph-api/schema/registry"; diff --git a/packages/enssdk/src/omnigraph/generated/introspection.ts b/packages/enssdk/src/omnigraph/generated/introspection.ts index c314b73369..eb772a6f17 100644 --- a/packages/enssdk/src/omnigraph/generated/introspection.ts +++ b/packages/enssdk/src/omnigraph/generated/introspection.ts @@ -184,17 +184,17 @@ const introspection = { } }, { - "name": "in", + "name": "last", "type": { - "kind": "INPUT_OBJECT", - "name": "AccountIdInput" + "kind": "SCALAR", + "name": "Int" } }, { - "name": "last", + "name": "where", "type": { - "kind": "SCALAR", - "name": "Int" + "kind": "INPUT_OBJECT", + "name": "AccountPermissionsWhereInput" } } ], @@ -496,35 +496,22 @@ const introspection = { { "name": "from", "type": { - "kind": "SCALAR", - "name": "Address" - } - }, - { - "name": "selector_in", - "type": { - "kind": "LIST", - "ofType": { - "kind": "NON_NULL", - "ofType": { - "kind": "SCALAR", - "name": "Hex" - } - } + "kind": "INPUT_OBJECT", + "name": "EventsFromFilter" } }, { - "name": "timestamp_gte", + "name": "selector", "type": { - "kind": "SCALAR", - "name": "BigInt" + "kind": "INPUT_OBJECT", + "name": "EventsSelectorFilter" } }, { - "name": "timestamp_lte", + "name": "timestamp", "type": { - "kind": "SCALAR", - "name": "BigInt" + "kind": "INPUT_OBJECT", + "name": "EventsTimestampFilter" } } ], @@ -668,6 +655,20 @@ const introspection = { ], "interfaces": [] }, + { + "kind": "INPUT_OBJECT", + "name": "AccountPermissionsWhereInput", + "inputFields": [ + { + "name": "contract", + "type": { + "kind": "INPUT_OBJECT", + "name": "AccountIdInput" + } + } + ], + "isOneOf": false + }, { "kind": "OBJECT", "name": "AccountRegistryPermissionsConnection", @@ -1455,14 +1456,41 @@ const introspection = { }, { "kind": "INPUT_OBJECT", - "name": "DomainPermissionsWhereInput", + "name": "DomainPermissionsUserFilter", "inputFields": [ { - "name": "user", + "name": "eq", "type": { "kind": "SCALAR", "name": "Address" } + }, + { + "name": "in", + "type": { + "kind": "LIST", + "ofType": { + "kind": "NON_NULL", + "ofType": { + "kind": "SCALAR", + "name": "Address" + } + } + } + } + ], + "isOneOf": true + }, + { + "kind": "INPUT_OBJECT", + "name": "DomainPermissionsWhereInput", + "inputFields": [ + { + "name": "user", + "type": { + "kind": "INPUT_OBJECT", + "name": "DomainPermissionsUserFilter" + } } ], "isOneOf": false @@ -3327,48 +3355,151 @@ const introspection = { }, { "kind": "INPUT_OBJECT", - "name": "EventsWhereInput", + "name": "EventsFromFilter", "inputFields": [ { - "name": "from", + "name": "eq", "type": { "kind": "SCALAR", "name": "Address" } }, { - "name": "selector_in", + "name": "in", "type": { "kind": "LIST", "ofType": { "kind": "NON_NULL", "ofType": { "kind": "SCALAR", - "name": "Hex" + "name": "Address" } } } + } + ], + "isOneOf": true + }, + { + "kind": "INPUT_OBJECT", + "name": "EventsSelectorFilter", + "inputFields": [ + { + "name": "eq", + "type": { + "kind": "SCALAR", + "name": "Hex" + } }, { - "name": "sender", + "name": "in", + "type": { + "kind": "LIST", + "ofType": { + "kind": "NON_NULL", + "ofType": { + "kind": "SCALAR", + "name": "Hex" + } + } + } + } + ], + "isOneOf": true + }, + { + "kind": "INPUT_OBJECT", + "name": "EventsSenderFilter", + "inputFields": [ + { + "name": "eq", "type": { "kind": "SCALAR", "name": "Address" } }, { - "name": "timestamp_gte", + "name": "in", + "type": { + "kind": "LIST", + "ofType": { + "kind": "NON_NULL", + "ofType": { + "kind": "SCALAR", + "name": "Address" + } + } + } + } + ], + "isOneOf": true + }, + { + "kind": "INPUT_OBJECT", + "name": "EventsTimestampFilter", + "inputFields": [ + { + "name": "gt", "type": { "kind": "SCALAR", "name": "BigInt" } }, { - "name": "timestamp_lte", + "name": "gte", "type": { "kind": "SCALAR", "name": "BigInt" } + }, + { + "name": "lt", + "type": { + "kind": "SCALAR", + "name": "BigInt" + } + }, + { + "name": "lte", + "type": { + "kind": "SCALAR", + "name": "BigInt" + } + } + ], + "isOneOf": false + }, + { + "kind": "INPUT_OBJECT", + "name": "EventsWhereInput", + "inputFields": [ + { + "name": "from", + "type": { + "kind": "INPUT_OBJECT", + "name": "EventsFromFilter" + } + }, + { + "name": "selector", + "type": { + "kind": "INPUT_OBJECT", + "name": "EventsSelectorFilter" + } + }, + { + "name": "sender", + "type": { + "kind": "INPUT_OBJECT", + "name": "EventsSenderFilter" + } + }, + { + "name": "timestamp", + "type": { + "kind": "INPUT_OBJECT", + "name": "EventsTimestampFilter" + } } ], "isOneOf": false diff --git a/packages/enssdk/src/omnigraph/generated/schema.graphql b/packages/enssdk/src/omnigraph/generated/schema.graphql index 9cd29d8055..26b450d1dd 100644 --- a/packages/enssdk/src/omnigraph/generated/schema.graphql +++ b/packages/enssdk/src/omnigraph/generated/schema.graphql @@ -17,7 +17,7 @@ type Account { """ The Permissions granted to this Account, optionally filtered to Permissions in a specific contract. """ - permissions(after: String, before: String, first: Int, in: AccountIdInput, last: Int): AccountPermissionsConnection + permissions(after: String, before: String, first: Int, last: Int, where: AccountPermissionsWhereInput): AccountPermissionsConnection """The Permissions on Registries granted to this Account.""" registryPermissions(after: String, before: String, first: Int, last: Int): AccountRegistryPermissionsConnection @@ -75,20 +75,17 @@ Filter conditions for Account.events (where `sender` is implied by the Account). """ input AccountEventsWhereInput { """ - Filter to events whose `tx.from` matches. Not HCA-aware — the Account's HCA-aware filter is applied via `sender = Account.id`. + Filter to events whose `tx.from` matches the provided filter. Not HCA-aware — the Account's HCA-aware filter is applied via `sender = Account.id`. """ - from: Address + from: EventsFromFilter """ - Filter to events whose selector (event signature) is one of the provided values. + Filter to events whose selector (event signature) matches the provided filter. """ - selector_in: [Hex!] + selector: EventsSelectorFilter - """Filter to events at or after this UnixTimestamp.""" - timestamp_gte: BigInt - - """Filter to events at or before this UnixTimestamp.""" - timestamp_lte: BigInt + """Filter to events whose UnixTimestamp falls within the provided range.""" + timestamp: EventsTimestampFilter } """A CAIP-10 Account ID including chainId and address.""" @@ -114,6 +111,14 @@ type AccountPermissionsConnectionEdge { node: PermissionsUser! } +"""Filter for Account.permissions.""" +input AccountPermissionsWhereInput { + """ + If set, filters this Account's Permissions to those granted in this contract. + """ + contract: AccountIdInput +} + type AccountRegistryPermissionsConnection { edges: [AccountRegistryPermissionsConnectionEdge!]! pageInfo: PageInfo! @@ -304,9 +309,23 @@ input DomainIdInput @oneOf { name: InterpretedName } -"""Filter Permissions over this Domain by a specific User address.""" +""" +Filter Permissions by user address. Exactly one of `eq` or `in` must be provided. +""" +input DomainPermissionsUserFilter @oneOf { + """Exact user address match.""" + eq: Address + + """ + User address matches any value in the set. Max 10 items. An empty set matches nothing. + """ + in: [Address!] +} + +"""Filter Permissions over this Domain by user.""" input DomainPermissionsWhereInput { - user: Address + """Filter Permissions to those whose user matches the provided filter.""" + user: DomainPermissionsUserFilter } type DomainRegistrationsConnection { @@ -732,28 +751,81 @@ type Event { transactionIndex: Int! } -"""Filter conditions for an events connection.""" -input EventsWhereInput { +""" +Filter Events by `tx.from`. Not HCA-aware — use `sender` to filter by the HCA-aware actor. Exactly one of `eq` or `in` must be provided. +""" +input EventsFromFilter @oneOf { + """Exact `tx.from` match.""" + eq: Address + """ - Filter to events whose `tx.from` matches. Not HCA-aware — use `sender` to filter by the HCA account address. + `tx.from` matches any address in the set. Max 10 items. An empty set matches nothing. """ - from: Address + in: [Address!] +} + +""" +Filter Events by selector (event signature). Exactly one of `eq` or `in` must be provided. +""" +input EventsSelectorFilter @oneOf { + """Exact selector match.""" + eq: Hex """ - Filter to events whose selector (event signature) is one of the provided values. + Selector matches any value in the set. Max 10 items. An empty set matches nothing. """ - selector_in: [Hex!] + in: [Hex!] +} + +""" +Filter Events by HCA-aware `sender` (the HCA account address if used, otherwise Transaction.from). Exactly one of `eq` or `in` must be provided. +""" +input EventsSenderFilter @oneOf { + """Exact `sender` match.""" + eq: Address """ - Filter to events whose `sender` matches: the HCA account address if used, otherwise Transaction.from. + `sender` matches any address in the set. Max 10 items. An empty set matches nothing. """ - sender: Address + in: [Address!] +} + +""" +Filter Events by timestamp range. At least one bound must be provided. `gt`/`gte` are mutually exclusive; `lt`/`lte` are mutually exclusive. +""" +input EventsTimestampFilter { + """Filter to events strictly after this UnixTimestamp.""" + gt: BigInt """Filter to events at or after this UnixTimestamp.""" - timestamp_gte: BigInt + gte: BigInt + + """Filter to events strictly before this UnixTimestamp.""" + lt: BigInt """Filter to events at or before this UnixTimestamp.""" - timestamp_lte: BigInt + lte: BigInt +} + +"""Filter conditions for an events connection.""" +input EventsWhereInput { + """ + Filter to events whose `tx.from` matches the provided filter. Not HCA-aware — use `sender` to filter by the HCA-aware actor. + """ + from: EventsFromFilter + + """ + Filter to events whose selector (event signature) matches the provided filter. + """ + selector: EventsSelectorFilter + + """ + Filter to events whose HCA-aware `sender` matches the provided filter (the HCA account address if used, otherwise Transaction.from). + """ + sender: EventsSenderFilter + + """Filter to events whose UnixTimestamp falls within the provided range.""" + timestamp: EventsTimestampFilter } """Hex represents viem#Hex."""