Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
50 changes: 26 additions & 24 deletions apps/ensindexer/src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { cors } from "hono/cors";
import { sdk } from "@/api/lib/tracing/instrumentation";
import config from "@/config";
import { makeApiDocumentationMiddleware } from "@/lib/api-documentation";
import { filterSchemaExtensions } from "@/lib/filter-schema-extensions";
import { filterSchemaByPrefix } from "@/lib/filter-schema-by-prefix";
import { fixContentLengthMiddleware } from "@/lib/fix-content-length-middleware";
import {
fetchEnsRainbowVersion,
Expand All @@ -25,8 +25,6 @@ import {

import ensNodeApi from "@/api/handlers/ensnode-api";

const schemaWithoutExtensions = filterSchemaExtensions(schema);

const app = new Hono();

// set the X-ENSNode-Version header to the current version
Expand Down Expand Up @@ -87,35 +85,39 @@ app.use(
subgraphGraphQL({
db,
graphqlSchema: buildSubgraphGraphQLSchema({
schema: schemaWithoutExtensions,
schema: filterSchemaByPrefix("subgraph_", schema),
// provide the schema with ponder's internal metadata to power _meta
metadataProvider: makePonderMetadataProvider({ db, publicClients }),
// describes the polymorphic (interface) relationships in the schema
polymorphicConfig: {
types: {
DomainEvent: [
schema.transfer,
schema.newOwner,
schema.newResolver,
schema.newTTL,
schema.wrappedTransfer,
schema.nameWrapped,
schema.nameUnwrapped,
schema.fusesSet,
schema.expiryExtended,
schema.subgraph_transfer,
schema.subgraph_newOwner,
schema.subgraph_newResolver,
schema.subgraph_newTTL,
schema.subgraph_wrappedTransfer,
schema.subgraph_nameWrapped,
schema.subgraph_nameUnwrapped,
schema.subgraph_fusesSet,
schema.subgraph_expiryExtended,
],
RegistrationEvent: [
schema.subgraph_nameRegistered,
schema.subgraph_nameRenewed,
schema.subgraph_nameTransferred,
],
RegistrationEvent: [schema.nameRegistered, schema.nameRenewed, schema.nameTransferred],
ResolverEvent: [
schema.addrChanged,
schema.multicoinAddrChanged,
schema.nameChanged,
schema.abiChanged,
schema.pubkeyChanged,
schema.textChanged,
schema.contenthashChanged,
schema.interfaceChanged,
schema.authorisationChanged,
schema.versionChanged,
schema.subgraph_addrChanged,
schema.subgraph_multicoinAddrChanged,
schema.subgraph_nameChanged,
schema.subgraph_abiChanged,
schema.subgraph_pubkeyChanged,
schema.subgraph_textChanged,
schema.subgraph_contenthashChanged,
schema.subgraph_interfaceChanged,
schema.subgraph_authorisationChanged,
schema.subgraph_versionChanged,
],
},
fields: {
Expand Down
49 changes: 49 additions & 0 deletions apps/ensindexer/src/lib/filter-schema-by-prefix.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/**
* A mapped type that filters a schema object by a prefix and removes the prefix from the keys.
*
* This type transformation takes all keys from SCHEMA that start with PREFIX, removes the prefix
* from those keys, and preserves the original value types. Keys that don't match the prefix are excluded.
*
* @template PREFIX - The string prefix to filter by
* @template SCHEMA - The input schema object type
*/
type FilteredSchema<PREFIX extends string, SCHEMA extends Record<string, unknown>> = {
[K in keyof SCHEMA as K extends `${PREFIX}${infer Rest}` ? Rest : never]: SCHEMA[K];
};

/**
* Filters and transforms a schema object by selecting only properties with keys that start with a given prefix,
* then strips the prefix from the keys while preserving the original value types.
*
* This function is particularly useful for extracting subsets of Drizzle/Ponder schemas where tables or
* relationships follow a naming convention with prefixes (e.g., "plugin_table", "namespace_relation").
*
* @param prefix - The string prefix to filter by (case-sensitive). Only keys starting with this prefix will be included
* @param schema - The input schema object containing tables, relationships, or other named entities
* @returns A new object with filtered entries where:
* - Keys: Original keys with the prefix removed (e.g., "plugin_users" → "users")
* - Values: Preserved exactly as they were in the original schema with their original types
*
* @example
* ```typescript
* const schema = {
* plugin_users: userTable,
* plugin_posts: postTable,
* other_table: otherTable,
* } as const;
*
* const filtered = filterSchemaByPrefix("plugin_", schema);
* // Result: { users: userTable, posts: postTable }
* // Types are preserved: typeof filtered.users === typeof schema.plugin_users
* ```
*/
export function filterSchemaByPrefix<PREFIX extends string, SCHEMA extends Record<string, unknown>>(
prefix: PREFIX,
schema: SCHEMA,
): FilteredSchema<PREFIX, SCHEMA> {
return Object.fromEntries(
Object.entries(schema)
.filter(([name]) => name.startsWith(prefix))
.map(([name, value]) => [name.slice(prefix.length), value]),
) as FilteredSchema<PREFIX, SCHEMA>;
}
12 changes: 0 additions & 12 deletions apps/ensindexer/src/lib/filter-schema-extensions.ts

This file was deleted.

37 changes: 37 additions & 0 deletions apps/ensindexer/src/lib/prefix-schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/**
* Adds a prefix to all keys in a schema object while preserving the original value types.
*
* This function is useful for namespacing schema objects to avoid conflicts when merging
* multiple schemas together, particularly in Drizzle/Ponder schema systems.
*
* @param prefix - The string prefix to add to all keys
* @param schema - The input schema object containing tables, relationships, or other named entities
* @returns A new object with all keys prefixed and original values preserved
*
* @example
* ```typescript
* const schema = {
* users: userTable,
* posts: postTable,
* } as const;
*
* const prefixed = prefixSchema("subgraph_", schema);
* // Result: { subgraph_users: userTable, subgraph_posts: postTable }
* // Types are preserved: typeof prefixed.subgraph_users === typeof schema.users
* ```
*/

type PrefixedSchema<PREFIX extends string, SCHEMA extends Record<string, unknown>> = {
[K in keyof SCHEMA as `${PREFIX}${K & string}`]: SCHEMA[K];
};

export function prefixSchema<PREFIX extends string, SCHEMA extends Record<string, unknown>>(
prefix: PREFIX,
schema: SCHEMA,
): PrefixedSchema<PREFIX, SCHEMA> {
const result = Object.fromEntries(
Object.entries(schema).map(([key, value]) => [`${prefix}${key}`, value]),
);

return result as PrefixedSchema<PREFIX, SCHEMA>;
}
20 changes: 13 additions & 7 deletions apps/ensindexer/src/lib/subgraph/db-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,34 +5,40 @@ import type { Address } from "viem";
import { makeEventId } from "@/lib/subgraph/ids";

export async function upsertAccount(context: Context, address: Address) {
return context.db.insert(schema.account).values({ id: address }).onConflictDoNothing();
return context.db.insert(schema.subgraph_account).values({ id: address }).onConflictDoNothing();
}

export async function upsertDomain(context: Context, values: typeof schema.domain.$inferInsert) {
export async function upsertDomain(
context: Context,
values: typeof schema.subgraph_domain.$inferInsert,
) {
// remove id primary key for update values
const { id, ...otherValues } = values;

return context.db.insert(schema.domain).values(values).onConflictDoUpdate(otherValues);
return context.db.insert(schema.subgraph_domain).values(values).onConflictDoUpdate(otherValues);
}

export async function upsertResolver(
context: Context,
values: typeof schema.resolver.$inferInsert,
values: typeof schema.subgraph_resolver.$inferInsert,
) {
// remove id primary key for update values
const { id, ...otherValues } = values;

return context.db.insert(schema.resolver).values(values).onConflictDoUpdate(otherValues);
return context.db.insert(schema.subgraph_resolver).values(values).onConflictDoUpdate(otherValues);
}

export async function upsertRegistration(
context: Context,
values: typeof schema.registration.$inferInsert,
values: typeof schema.subgraph_registration.$inferInsert,
) {
// remove id primary key for update values
const { id, ...otherValues } = values;

return context.db.insert(schema.registration).values(values).onConflictDoUpdate(otherValues);
return context.db
.insert(schema.subgraph_registration)
.values(values)
.onConflictDoUpdate(otherValues);
}

// simplifies generating the shared event column values from the ponder Event object
Expand Down
8 changes: 4 additions & 4 deletions apps/ensindexer/src/lib/subgraph/subgraph-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export async function setupRootNode({ context }: { context: Context }) {

// create the ENS root domain (if not exists)
await context.db
.insert(schema.domain)
.insert(schema.subgraph_domain)
.values({
id: ROOT_NODE,
ownerId: zeroAddress,
Expand All @@ -46,7 +46,7 @@ export async function setupRootNode({ context }: { context: Context }) {

// a domain is 'empty' if it has no resolver, no owner, and no subdomains
// via https://github.com/ensdomains/ens-subgraph/blob/c844791/src/ensRegistry.ts#L65
function isDomainEmpty(domain: typeof schema.domain.$inferSelect) {
function isDomainEmpty(domain: typeof schema.subgraph_domain.$inferSelect) {
return (
domain.resolverId === null &&
isAddressEqual(domain.ownerId, zeroAddress) &&
Expand All @@ -60,13 +60,13 @@ export async function recursivelyRemoveEmptyDomainFromParentSubdomainCount(
context: Context,
node: Node,
) {
const domain = await context.db.find(schema.domain, { id: node });
const domain = await context.db.find(schema.subgraph_domain, { id: node });
if (!domain) throw new Error(`Domain not found: ${node}`);

if (isDomainEmpty(domain) && domain.parentId !== null) {
// decrement parent's subdomain count
await context.db
.update(schema.domain, { id: domain.parentId })
.update(schema.subgraph_domain, { id: domain.parentId })
.set((row) => ({ subdomainCount: row.subdomainCount - 1 }));

// recurse to parent
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import {
// these handlers should ignore 'RegistryOld' events for a given domain if it has been migrated to the
// (new) Registry, which is tracked in the `Domain.isMigrated` field
async function shouldIgnoreRegistryOldEvents(context: Context, node: Node) {
const domain = await context.db.find(schema.domain, { id: node });
const domain = await context.db.find(schema.subgraph_domain, { id: node });
return domain?.isMigrated ?? false;
}

Expand Down
Loading