diff --git a/.changeset/floppy-years-sneeze.md b/.changeset/floppy-years-sneeze.md new file mode 100644 index 000000000..b801c3df4 --- /dev/null +++ b/.changeset/floppy-years-sneeze.md @@ -0,0 +1,5 @@ +--- +"@ensnode/ensdb-sdk": minor +--- + +Renamed the `client` getter on `EnsDbReader` class to `ensDb`. diff --git a/.changeset/pink-steaks-think.md b/.changeset/pink-steaks-think.md new file mode 100644 index 000000000..fa3788994 --- /dev/null +++ b/.changeset/pink-steaks-think.md @@ -0,0 +1,5 @@ +--- +"ensapi": minor +--- + +Updated custom queries for ENSDb to implement data model from ENSDb SDK. diff --git a/.changeset/tasty-seals-shout.md b/.changeset/tasty-seals-shout.md new file mode 100644 index 000000000..83754708e --- /dev/null +++ b/.changeset/tasty-seals-shout.md @@ -0,0 +1,5 @@ +--- +"ensapi": minor +--- + +Decoupled ENSApi from ENSIndexer by updating the data source for `EnsIndexerPublicConfig` from ENSIndexer to ENSDb. diff --git a/.changeset/thin-flies-build.md b/.changeset/thin-flies-build.md new file mode 100644 index 000000000..f6e46e813 --- /dev/null +++ b/.changeset/thin-flies-build.md @@ -0,0 +1,5 @@ +--- +"ensapi": minor +--- + +Decoupled ENSApi from ENSIndexer by updating the data source for Indexing Status snapshots, from ENSIndexer's Indexing Status API route to ENSDb. diff --git a/apps/ensapi/.env.local.example b/apps/ensapi/.env.local.example index 2b31163e7..8d4832ea0 100644 --- a/apps/ensapi/.env.local.example +++ b/apps/ensapi/.env.local.example @@ -2,26 +2,26 @@ # Optional. If this is not set, the default value is 4334. # PORT=4334 -# ENSIndexer: The "primary" ENSIndexer service URL. -# Required. This URL is used to read ENSIndexer's Config and Indexing Status APIs. -ENSINDEXER_URL=http://localhost:42069 - # ENSDb: Database URL # Required. This is the connection string for the ENSDb database in which ENSIndexer is storing data. -# It should match the DATABASE_URL used by the connected ENSIndexer. +# It should match the DATABASE_URL used by your ENSIndexer. # It should be in the format of `postgresql://:@:/` # # See https://ensnode.io/ensindexer/usage/configuration for additional information. -# NOTE that ENSApi does NOT need to define DATABASE_SCHEMA, as it is inferred from the connected ENSIndexer's Config. DATABASE_URL=postgresql://dbuser:abcd1234@localhost:5432/my_database +# ENSDb: ENSIndexer Schema Name +# Required. This is a name of the database schema in DATABASE_URL where ENSApi should read indexed data stored by your ENSIndexer. +# It should match the DATABASE_SCHEMA used by your ENSIndexer. +ENSINDEXER_SCHEMA_NAME=ensindexer_0 + # ENSApi: RPC Configuration -# Required. ENSApi requires an HTTP RPC to the connected ENSIndexer's ENS Root Chain, which depends -# on ENSIndexer's NAMESPACE (ex: mainnet, sepolia, ens-test-env). This ENS Root Chain RPC -# is used to power the Resolution API, in situations where Protocol Acceleration is not possible. +# Required. ENSApi requires a HTTP RPC exclusively for the ENS Root Chain of the ENS namespace (ex: mainnet, sepolia, ens-test-env) that your ENSIndexer is configured to use. +# This ENS Root Chain RPC is used to power the Resolution API in situations where Protocol Acceleration is not possible and dynamic RPC calls are required to serve a resolution request. +# Note that ENS resolution only requires an RPC for the ENS Root Chain. All lookups of data off of the ENS Root Chain are achieved through CCIP-read. # -# When ENSApi starts up it connects to the indicated ENSINDEXER_URL verifies that the ENS Root Chain -# RPC for the specified namespace is defined. +# When ENSApi starts up it loads an ENSIndexer public config from ENSDb and verifies that +# the ENS Root Chain for the configured ENS namespace (ex: mainnet / sepolia / etc) has an RPC defined for ENSApi to use. # # NOTE: You must configure your own private RPC endpoints. Public RPC endpoints are rate limited and # will likely not provide acceptable performance (though this depends on how many non-acceleratable diff --git a/apps/ensapi/src/cache/indexing-status.cache.ts b/apps/ensapi/src/cache/indexing-status.cache.ts index 4a4364e11..627ed9c3a 100644 --- a/apps/ensapi/src/cache/indexing-status.cache.ts +++ b/apps/ensapi/src/cache/indexing-status.cache.ts @@ -1,48 +1,46 @@ -import config from "@/config"; - -import { - type CrossChainIndexingStatusSnapshot, - ENSNodeClient, - IndexingStatusResponseCodes, - SWRCache, -} from "@ensnode/ensnode-sdk"; +import { EnsNodeMetadataKeys } from "@ensnode/ensdb-sdk"; +import { type CrossChainIndexingStatusSnapshot, SWRCache } from "@ensnode/ensnode-sdk"; +import { ensDbClient } from "@/lib/ensdb/singleton"; import { makeLogger } from "@/lib/logger"; const logger = makeLogger("indexing-status.cache"); -const client = new ENSNodeClient({ url: config.ensIndexerUrl }); export const indexingStatusCache = new SWRCache({ fn: async (_cachedResult) => - client - .indexingStatus() // fetch a new indexing status snapshot - .then((response) => { - if (response.responseCode !== IndexingStatusResponseCodes.Ok) { - // An indexing status response was successfully fetched, but the response code contained within the response was not 'ok'. + ensDbClient + .getIndexingStatusSnapshot() // get the latest indexing status snapshot + .then((snapshot) => { + if (snapshot === undefined) { + // An indexing status snapshot has not been found in ENSDb yet. + // This might happen during application startup, i.e. when ENSDb + // has not yet been populated with the first snapshot. // Therefore, throw an error to trigger the subsequent `.catch` handler. - throw new Error("Received Indexing Status response with responseCode other than 'ok'."); + throw new Error("Indexing Status snapshot not found in ENSDb yet."); } - logger.info("Fetched Indexing Status to be cached"); - // The indexing status snapshot has been fetched and successfully validated for caching. // Therefore, return it so that this current invocation of `readCache` will: // - Replace the currently cached value (if any) with this new value. // - Return this non-null value. - return response.realtimeProjection.snapshot; + return snapshot; }) .catch((error) => { - // Either the indexing status snapshot fetch failed, or the indexing status response was not 'ok'. + // Either the indexing status snapshot fetch failed, or the indexing status snapshot was not found in ENSDb yet. // Therefore, throw an error so that this current invocation of `readCache` will: // - Reject the newly fetched response (if any) such that it won't be cached. // - Return the most recently cached value from prior invocations, or `null` if no prior invocation successfully cached a value. logger.error( error, - "Error occurred while fetching a new indexing status snapshot. The cached indexing status snapshot (if any) will not be updated.", + `Error occurred while loading Indexing Status snapshot record from ENSNode Metadata table in ENSDb. ` + + `Where clause applied: ("ensIndexerSchemaName" = "${ensDbClient.ensIndexerSchemaName}", "key" = "${EnsNodeMetadataKeys.EnsIndexerIndexingStatus}"). ` + + `The cached indexing status snapshot (if any) will not be updated.`, ); throw error; }), - ttl: 5, // 5 seconds - proactiveRevalidationInterval: 10, // 10 seconds + // We need to refresh the indexing status cache very frequently. + // ENSDb won't have issues handling this frequency of queries. + ttl: 1, // 1 second + proactiveRevalidationInterval: 1, // 1 second proactivelyInitialize: true, }); diff --git a/apps/ensapi/src/config/config.schema.test.ts b/apps/ensapi/src/config/config.schema.test.ts index 407effcb2..7c4ef0cce 100644 --- a/apps/ensapi/src/config/config.schema.test.ts +++ b/apps/ensapi/src/config/config.schema.test.ts @@ -2,13 +2,15 @@ import packageJson from "@/../package.json" with { type: "json" }; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import { - type ENSIndexerPublicConfig, - PluginName, - serializeENSIndexerPublicConfig, -} from "@ensnode/ensnode-sdk"; +import { type ENSIndexerPublicConfig, PluginName } from "@ensnode/ensnode-sdk"; import type { RpcConfig } from "@ensnode/ensnode-sdk/internal"; +vi.mock("@/lib/ensdb/singleton", () => ({ + ensDbClient: { + getEnsIndexerPublicConfig: vi.fn(async () => ENSINDEXER_PUBLIC_CONFIG), + }, +})); + import { buildConfigFromEnvironment, buildEnsApiPublicConfig } from "@/config/config.schema"; import { ENSApi_DEFAULT_PORT } from "@/config/defaults"; import type { EnsApiEnvironment } from "@/config/environment"; @@ -25,7 +27,6 @@ const VALID_RPC_URL = "https://eth-sepolia.g.alchemy.com/v2/1234"; const BASE_ENV = { DATABASE_URL: "postgresql://user:password@localhost:5432/mydb", - ENSINDEXER_URL: "http://localhost:42069", RPC_URL_1: VALID_RPC_URL, } satisfies EnsApiEnvironment; @@ -50,29 +51,16 @@ const ENSINDEXER_PUBLIC_CONFIG = { }, } satisfies ENSIndexerPublicConfig; -const mockFetch = vi.fn(); -vi.stubGlobal("fetch", mockFetch); - describe("buildConfigFromEnvironment", () => { - afterEach(() => { - mockFetch.mockReset(); - }); - it("returns a valid config object using environment variables", async () => { - mockFetch.mockResolvedValueOnce({ - ok: true, - json: () => Promise.resolve(serializeENSIndexerPublicConfig(ENSINDEXER_PUBLIC_CONFIG)), - }); - await expect(buildConfigFromEnvironment(BASE_ENV)).resolves.toStrictEqual({ port: ENSApi_DEFAULT_PORT, databaseUrl: BASE_ENV.DATABASE_URL, - ensIndexerUrl: new URL(BASE_ENV.ENSINDEXER_URL), theGraphApiKey: undefined, ensIndexerPublicConfig: ENSINDEXER_PUBLIC_CONFIG, namespace: ENSINDEXER_PUBLIC_CONFIG.namespace, - databaseSchemaName: ENSINDEXER_PUBLIC_CONFIG.databaseSchemaName, + ensIndexerSchemaName: ENSINDEXER_PUBLIC_CONFIG.databaseSchemaName, rpcConfigs: new Map([ [ 1, @@ -89,11 +77,6 @@ describe("buildConfigFromEnvironment", () => { it("parses CUSTOM_REFERRAL_PROGRAM_EDITIONS as a URL object", async () => { const customUrl = "https://example.com/editions.json"; - mockFetch.mockResolvedValueOnce({ - ok: true, - json: () => Promise.resolve(serializeENSIndexerPublicConfig(ENSINDEXER_PUBLIC_CONFIG)), - }); - const config = await buildConfigFromEnvironment({ ...BASE_ENV, CUSTOM_REFERRAL_PROGRAM_EDITIONS: customUrl, @@ -116,15 +99,9 @@ describe("buildConfigFromEnvironment", () => { const TEST_ENV: EnsApiEnvironment = { DATABASE_URL: BASE_ENV.DATABASE_URL, - ENSINDEXER_URL: BASE_ENV.ENSINDEXER_URL, }; it("logs error and exits when CUSTOM_REFERRAL_PROGRAM_EDITIONS is not a valid URL", async () => { - mockFetch.mockResolvedValueOnce({ - ok: true, - json: () => Promise.resolve(serializeENSIndexerPublicConfig(ENSINDEXER_PUBLIC_CONFIG)), - }); - await buildConfigFromEnvironment({ ...TEST_ENV, CUSTOM_REFERRAL_PROGRAM_EDITIONS: "not-a-url", @@ -137,11 +114,6 @@ describe("buildConfigFromEnvironment", () => { }); it("logs error message when QuickNode RPC config was partially configured (missing endpoint name)", async () => { - mockFetch.mockResolvedValueOnce({ - ok: true, - json: () => Promise.resolve(serializeENSIndexerPublicConfig(ENSINDEXER_PUBLIC_CONFIG)), - }); - await buildConfigFromEnvironment({ ...TEST_ENV, QUICKNODE_API_KEY: "my-api-key", @@ -157,11 +129,6 @@ describe("buildConfigFromEnvironment", () => { }); it("logs error message when QuickNode RPC config was partially configured (missing API key)", async () => { - mockFetch.mockResolvedValueOnce({ - ok: true, - json: () => Promise.resolve(serializeENSIndexerPublicConfig(ENSINDEXER_PUBLIC_CONFIG)), - }); - await buildConfigFromEnvironment({ ...TEST_ENV, QUICKNODE_ENDPOINT_NAME: "my-endpoint-name", @@ -183,10 +150,9 @@ describe("buildEnsApiPublicConfig", () => { const mockConfig = { port: ENSApi_DEFAULT_PORT, databaseUrl: BASE_ENV.DATABASE_URL, - ensIndexerUrl: new URL(BASE_ENV.ENSINDEXER_URL), ensIndexerPublicConfig: ENSINDEXER_PUBLIC_CONFIG, namespace: ENSINDEXER_PUBLIC_CONFIG.namespace, - databaseSchemaName: ENSINDEXER_PUBLIC_CONFIG.databaseSchemaName, + ensIndexerSchemaName: ENSINDEXER_PUBLIC_CONFIG.databaseSchemaName, rpcConfigs: new Map([ [ 1, @@ -215,10 +181,9 @@ describe("buildEnsApiPublicConfig", () => { const mockConfig = { port: ENSApi_DEFAULT_PORT, databaseUrl: BASE_ENV.DATABASE_URL, - ensIndexerUrl: new URL(BASE_ENV.ENSINDEXER_URL), ensIndexerPublicConfig: ENSINDEXER_PUBLIC_CONFIG, namespace: ENSINDEXER_PUBLIC_CONFIG.namespace, - databaseSchemaName: ENSINDEXER_PUBLIC_CONFIG.databaseSchemaName, + ensIndexerSchemaName: ENSINDEXER_PUBLIC_CONFIG.databaseSchemaName, rpcConfigs: new Map(), customReferralProgramEditionConfigSetUrl: undefined, }; @@ -245,14 +210,13 @@ describe("buildEnsApiPublicConfig", () => { const mockConfig = { port: ENSApi_DEFAULT_PORT, databaseUrl: BASE_ENV.DATABASE_URL, - ensIndexerUrl: new URL(BASE_ENV.ENSINDEXER_URL), ensIndexerPublicConfig: { ...ENSINDEXER_PUBLIC_CONFIG, plugins: ["subgraph"], isSubgraphCompatible: true, }, namespace: ENSINDEXER_PUBLIC_CONFIG.namespace, - databaseSchemaName: ENSINDEXER_PUBLIC_CONFIG.databaseSchemaName, + ensIndexerSchemaName: ENSINDEXER_PUBLIC_CONFIG.databaseSchemaName, rpcConfigs: new Map(), customReferralProgramEditionConfigSetUrl: undefined, theGraphApiKey: "secret-api-key", diff --git a/apps/ensapi/src/config/config.schema.ts b/apps/ensapi/src/config/config.schema.ts index ec402ba89..e608b74c0 100644 --- a/apps/ensapi/src/config/config.schema.ts +++ b/apps/ensapi/src/config/config.schema.ts @@ -1,16 +1,13 @@ import packageJson from "@/../package.json" with { type: "json" }; import pRetry from "p-retry"; -import { parse as parseConnectionString } from "pg-connection-string"; import { prettifyError, ZodError, z } from "zod/v4"; import type { EnsApiPublicConfig } from "@ensnode/ensnode-sdk"; import { buildRpcConfigsFromEnv, canFallbackToTheGraph, - DatabaseSchemaNameSchema, ENSNamespaceSchema, - EnsIndexerUrlSchema, invariant_rpcConfigsSpecifiedForRootChain, makeENSIndexerPublicConfigSchema, OptionalPortNumberSchema, @@ -19,29 +16,12 @@ import { } from "@ensnode/ensnode-sdk/internal"; import { ENSApi_DEFAULT_PORT } from "@/config/defaults"; +import { EnsDbConfigSchema } from "@/config/ensdb-config.schema"; import type { EnsApiEnvironment } from "@/config/environment"; import { invariant_ensIndexerPublicConfigVersionInfo } from "@/config/validations"; -import { fetchENSIndexerConfig } from "@/lib/fetch-ensindexer-config"; +import { ensDbClient } from "@/lib/ensdb/singleton"; import logger from "@/lib/logger"; -export const DatabaseUrlSchema = z.string().refine( - (url) => { - try { - if (!url.startsWith("postgresql://") && !url.startsWith("postgres://")) { - return false; - } - const config = parseConnectionString(url); - return !!(config.host && config.port && config.database); - } catch { - return false; - } - }, - { - error: - "Invalid PostgreSQL connection string. Expected format: postgresql://username:password@host:port/database", - }, -); - /** * Schema for validating custom referral program edition config set URL. */ @@ -63,15 +43,13 @@ const CustomReferralProgramEditionConfigSetUrlSchema = z const EnsApiConfigSchema = z .object({ port: OptionalPortNumberSchema.default(ENSApi_DEFAULT_PORT), - databaseUrl: DatabaseUrlSchema, - databaseSchemaName: DatabaseSchemaNameSchema, - ensIndexerUrl: EnsIndexerUrlSchema, theGraphApiKey: TheGraphApiKeySchema, namespace: ENSNamespaceSchema, rpcConfigs: RpcConfigsSchema, ensIndexerPublicConfig: makeENSIndexerPublicConfigSchema("ensIndexerPublicConfig"), customReferralProgramEditionConfigSetUrl: CustomReferralProgramEditionConfigSetUrlSchema, }) + .extend(EnsDbConfigSchema.shape) .check(invariant_rpcConfigsSpecifiedForRootChain) .check(invariant_ensIndexerPublicConfigVersionInfo); @@ -85,27 +63,38 @@ export type EnsApiConfig = z.infer; */ export async function buildConfigFromEnvironment(env: EnsApiEnvironment): Promise { try { - const ensIndexerUrl = EnsIndexerUrlSchema.parse(env.ENSINDEXER_URL); + // TODO: transfer the responsibility of fetching + // the ENSIndexer Public Config to a middleware layer, as per: + // https://github.com/namehash/ensnode/issues/1806 + const ensIndexerPublicConfig = await pRetry( + async () => { + const config = await ensDbClient.getEnsIndexerPublicConfig(); - const ensIndexerPublicConfig = await pRetry(() => fetchENSIndexerConfig(ensIndexerUrl), { - retries: 3, - onFailedAttempt: ({ error, attemptNumber, retriesLeft }) => { - logger.info( - `ENSIndexer Config fetch attempt ${attemptNumber} failed (${error.message}). ${retriesLeft} retries left.`, - ); + if (!config) { + throw new Error("ENSIndexer Public Config not yet available in ENSDb."); + } + + return config; }, - }); + { + retries: 13, // This allows for a total of over 1 hour of retries with the exponential backoff strategy + onFailedAttempt: ({ error, attemptNumber, retriesLeft }) => { + logger.info( + `ENSIndexer Public Config fetch attempt ${attemptNumber} failed (${error.message}). ${retriesLeft} retries left.`, + ); + }, + }, + ); const rpcConfigs = buildRpcConfigsFromEnv(env, ensIndexerPublicConfig.namespace); return EnsApiConfigSchema.parse({ port: env.PORT, databaseUrl: env.DATABASE_URL, - ensIndexerUrl: env.ENSINDEXER_URL, theGraphApiKey: env.THEGRAPH_API_KEY, ensIndexerPublicConfig, namespace: ensIndexerPublicConfig.namespace, - databaseSchemaName: ensIndexerPublicConfig.databaseSchemaName, + ensIndexerSchemaName: ensIndexerPublicConfig.databaseSchemaName, rpcConfigs, customReferralProgramEditionConfigSetUrl: env.CUSTOM_REFERRAL_PROGRAM_EDITIONS, }); diff --git a/apps/ensapi/src/config/config.singleton.test.ts b/apps/ensapi/src/config/config.singleton.test.ts new file mode 100644 index 000000000..ef4402851 --- /dev/null +++ b/apps/ensapi/src/config/config.singleton.test.ts @@ -0,0 +1,60 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +vi.mock("@/lib/logger", () => ({ + default: { + error: vi.fn(), + info: vi.fn(), + }, +})); + +const VALID_DB_URL = "postgresql://user:password@localhost:5432/mydb"; +const VALID_SCHEMA_NAME = "ensapi"; + +describe("ensdb singleton bootstrap", () => { + beforeEach(() => { + vi.resetModules(); + vi.stubEnv("DATABASE_URL", VALID_DB_URL); + vi.stubEnv("ENSINDEXER_SCHEMA_NAME", VALID_SCHEMA_NAME); + }); + + afterEach(() => { + vi.unstubAllEnvs(); + }); + + it("constructs EnsDbReader from real env wiring without errors", async () => { + const { EnsDbReader } = await import("@ensnode/ensdb-sdk"); + const { ensDbClient, ensDb, ensIndexerSchema } = await import("@/lib/ensdb/singleton"); + + expect(ensDbClient).toBeInstanceOf(EnsDbReader); + expect(ensDb).toBeDefined(); + expect(ensIndexerSchema).toBeDefined(); + }); + + it("exits when DATABASE_URL is missing", async () => { + const mockExit = vi.spyOn(process, "exit").mockImplementation((() => { + throw new Error("process.exit"); + }) as never); + const { default: logger } = await import("@/lib/logger"); + + vi.stubEnv("DATABASE_URL", ""); + await expect(import("@/lib/ensdb/singleton")).rejects.toThrow("process.exit"); + + expect(logger.error).toHaveBeenCalled(); + expect(mockExit).toHaveBeenCalledWith(1); + mockExit.mockRestore(); + }); + + it("exits when ENSINDEXER_SCHEMA_NAME is missing", async () => { + const mockExit = vi.spyOn(process, "exit").mockImplementation((() => { + throw new Error("process.exit"); + }) as never); + const { default: logger } = await import("@/lib/logger"); + + vi.stubEnv("ENSINDEXER_SCHEMA_NAME", ""); + await expect(import("@/lib/ensdb/singleton")).rejects.toThrow("process.exit"); + + expect(logger.error).toHaveBeenCalled(); + expect(mockExit).toHaveBeenCalledWith(1); + mockExit.mockRestore(); + }); +}); diff --git a/apps/ensapi/src/config/ensdb-config.schema.ts b/apps/ensapi/src/config/ensdb-config.schema.ts new file mode 100644 index 000000000..eabf2b0fc --- /dev/null +++ b/apps/ensapi/src/config/ensdb-config.schema.ts @@ -0,0 +1,60 @@ +import { parse as parseConnectionString } from "pg-connection-string"; +import { prettifyError, z } from "zod/v4"; + +import type { EnsApiEnvironment } from "@/config/environment"; +import logger from "@/lib/logger"; + +export const DatabaseUrlSchema = z.string().refine( + (url) => { + try { + if (!url.startsWith("postgresql://") && !url.startsWith("postgres://")) { + return false; + } + const config = parseConnectionString(url); + return !!(config.host && config.port && config.database); + } catch { + return false; + } + }, + { + error: + "Invalid PostgreSQL connection string. Expected format: postgresql://username:password@host:port/database", + }, +); + +const EnsIndexerSchemaNameSchema = z + .string({ + error: "ENSINDEXER_SCHEMA_NAME is required.", + }) + .trim() + .min(1, { + error: "ENSINDEXER_SCHEMA_NAME is required and cannot be an empty string.", + }); + +export const EnsDbConfigSchema = z.object({ + databaseUrl: DatabaseUrlSchema, + ensIndexerSchemaName: EnsIndexerSchemaNameSchema, +}); + +export type EnsDbConfig = z.infer; + +/** + * Build ENSDb config from environment variables. + * + * Exits the process if the configuration is invalid, logging the error details. + */ +export function buildEnsDbConfigFromEnvironment(env: EnsApiEnvironment): EnsDbConfig { + const ensDbConfig = EnsDbConfigSchema.safeParse({ + databaseUrl: env.DATABASE_URL, + ensIndexerSchemaName: env.ENSINDEXER_SCHEMA_NAME, + }); + + if (!ensDbConfig.success) { + logger.error( + `Failed to parse ENSDb configuration from environment: \n${prettifyError(ensDbConfig.error)}\n`, + ); + process.exit(1); + } + + return ensDbConfig.data; +} diff --git a/apps/ensapi/src/config/ensdb-config.ts b/apps/ensapi/src/config/ensdb-config.ts new file mode 100644 index 000000000..12863f8a8 --- /dev/null +++ b/apps/ensapi/src/config/ensdb-config.ts @@ -0,0 +1,3 @@ +import { buildEnsDbConfigFromEnvironment } from "./ensdb-config.schema"; + +export default buildEnsDbConfigFromEnvironment(process.env); diff --git a/apps/ensapi/src/config/environment.ts b/apps/ensapi/src/config/environment.ts index 119490fdf..305f671b4 100644 --- a/apps/ensapi/src/config/environment.ts +++ b/apps/ensapi/src/config/environment.ts @@ -1,6 +1,5 @@ import type { - DatabaseEnvironment, - EnsIndexerUrlEnvironment, + EnsApiDatabaseEnvironment, LogLevelEnvironment, PortEnvironment, ReferralProgramEditionsEnvironment, @@ -15,8 +14,7 @@ import type { * their state in `process.env`. This interface is intended to be the source type which then gets * mapped/parsed into a structured configuration object like `EnsApiConfig`. */ -export type EnsApiEnvironment = Omit & - EnsIndexerUrlEnvironment & +export type EnsApiEnvironment = EnsApiDatabaseEnvironment & RpcEnvironment & PortEnvironment & LogLevelEnvironment & diff --git a/apps/ensapi/src/graphql-api/lib/find-domains/canonical-registries-cte.ts b/apps/ensapi/src/graphql-api/lib/find-domains/canonical-registries-cte.ts index d6088a8a4..83e0f3cfe 100644 --- a/apps/ensapi/src/graphql-api/lib/find-domains/canonical-registries-cte.ts +++ b/apps/ensapi/src/graphql-api/lib/find-domains/canonical-registries-cte.ts @@ -2,10 +2,9 @@ import config from "@/config"; import { sql } from "drizzle-orm"; -import * as schema from "@ensnode/ensdb-sdk"; import { maybeGetENSv2RootRegistryId } from "@ensnode/ensnode-sdk"; -import { db } from "@/lib/db"; +import { ensDb, ensIndexerSchema } from "@/lib/ensdb/singleton"; /** * The maximum depth to traverse the ENSv2 namegraph in order to construct the set of Canonical @@ -34,13 +33,13 @@ const ENSV2_ROOT_REGISTRY_ID = maybeGetENSv2RootRegistryId(config.namespace); export const getCanonicalRegistriesCTE = () => { // if ENSv2 is not defined, return an empty set with identical structure to below if (!ENSV2_ROOT_REGISTRY_ID) { - return db + return ensDb .select({ id: sql`registry_id`.as("id") }) .from(sql`(SELECT NULL::text AS registry_id WHERE FALSE) AS canonical_registries_cte`) .as("canonical_registries"); } - return db + return ensDb .select({ // NOTE: using `id` here to avoid clobbering `registryId` in consuming queries, which would // result in '_ is ambiguous' error messages from postgres because drizzle isn't scoping the @@ -54,8 +53,8 @@ export const getCanonicalRegistriesCTE = () => { SELECT ${ENSV2_ROOT_REGISTRY_ID}::text AS registry_id, 0 AS depth UNION ALL SELECT rcd.registry_id, cr.depth + 1 - FROM ${schema.registryCanonicalDomain} rcd - JOIN ${schema.v2Domain} parent ON parent.id = rcd.domain_id AND parent.subregistry_id = rcd.registry_id + FROM ${ensIndexerSchema.registryCanonicalDomain} rcd + JOIN ${ensIndexerSchema.v2Domain} parent ON parent.id = rcd.domain_id AND parent.subregistry_id = rcd.registry_id JOIN canonical_registries cr ON cr.registry_id = parent.registry_id WHERE cr.depth < ${CANONICAL_REGISTRIES_MAX_DEPTH} ) diff --git a/apps/ensapi/src/graphql-api/lib/find-domains/find-domains-resolver-helpers.test.ts b/apps/ensapi/src/graphql-api/lib/find-domains/find-domains-resolver-helpers.test.ts index 3ea110145..875719f6e 100644 --- a/apps/ensapi/src/graphql-api/lib/find-domains/find-domains-resolver-helpers.test.ts +++ b/apps/ensapi/src/graphql-api/lib/find-domains/find-domains-resolver-helpers.test.ts @@ -1,7 +1,6 @@ import { describe, expect, it, vi } from "vitest"; vi.mock("@/config", () => ({ default: { namespace: "mainnet" } })); -vi.mock("@/lib/db", () => ({ db: {} })); vi.mock("@/graphql-api/lib/find-domains/find-domains-by-labelhash-path", () => ({})); import { isEffectiveDesc } from "./find-domains-resolver-helpers"; diff --git a/apps/ensapi/src/graphql-api/lib/find-domains/find-domains-resolver.ts b/apps/ensapi/src/graphql-api/lib/find-domains/find-domains-resolver.ts index 37ef33dbf..ae26b32bb 100644 --- a/apps/ensapi/src/graphql-api/lib/find-domains/find-domains-resolver.ts +++ b/apps/ensapi/src/graphql-api/lib/find-domains/find-domains-resolver.ts @@ -20,7 +20,7 @@ import { type DomainsOrderBy, } from "@/graphql-api/schema/domain"; import type { OrderDirection } from "@/graphql-api/schema/order-direction"; -import { db } from "@/lib/db"; +import { ensDb } from "@/lib/ensdb/singleton"; import { makeLogger } from "@/lib/logger"; import { DomainCursors } from "./domain-cursor"; @@ -97,7 +97,7 @@ export function resolveFindDomains( return lazyConnection({ totalCount: () => - db + ensDb .with(domains) .select({ count: count() }) .from(domains) @@ -126,7 +126,7 @@ export function resolveFindDomains( const afterCursor = after ? DomainCursors.decode(after) : undefined; // build query with pagination constraints - const query = db + const query = ensDb .with(domains) .select() .from(domains) diff --git a/apps/ensapi/src/graphql-api/lib/find-domains/layers/base-domain-set.ts b/apps/ensapi/src/graphql-api/lib/find-domains/layers/base-domain-set.ts index 847b11329..adf976cd1 100644 --- a/apps/ensapi/src/graphql-api/lib/find-domains/layers/base-domain-set.ts +++ b/apps/ensapi/src/graphql-api/lib/find-domains/layers/base-domain-set.ts @@ -2,10 +2,9 @@ import { and, eq, sql } from "drizzle-orm"; import { alias, unionAll } from "drizzle-orm/pg-core"; import type { Address } from "viem"; -import * as schema from "@ensnode/ensdb-sdk"; import type { DomainId } from "@ensnode/ensnode-sdk"; -import { db } from "@/lib/db"; +import { ensDb, ensIndexerSchema } from "@/lib/ensdb/singleton"; /** * The type of the base domain set subquery. @@ -27,45 +26,58 @@ export type BaseDomainSet = ReturnType; * All downstream filters (owner, parent, registry, name, canonical) operate on this shape. */ export function domainsBase() { - const v2ParentDomain = alias(schema.v2Domain, "v2ParentDomain"); + const v2ParentDomain = alias(ensIndexerSchema.v2Domain, "v2ParentDomain"); return unionAll( - db + ensDb .select({ - domainId: sql`${schema.v1Domain.id}`.as("domainId"), - ownerId: sql
`${schema.v1Domain.ownerId}`.as("ownerId"), + domainId: sql`${ensIndexerSchema.v1Domain.id}`.as("domainId"), + ownerId: sql
`${ensIndexerSchema.v1Domain.ownerId}`.as("ownerId"), registryId: sql`NULL::text`.as("registryId"), - parentId: sql`${schema.v1Domain.parentId}`.as("parentId"), - labelHash: sql`${schema.v1Domain.labelHash}`.as("labelHash"), - sortableLabel: sql`${schema.label.interpreted}`.as("sortableLabel"), + parentId: sql`${ensIndexerSchema.v1Domain.parentId}`.as("parentId"), + labelHash: sql`${ensIndexerSchema.v1Domain.labelHash}`.as("labelHash"), + sortableLabel: sql`${ensIndexerSchema.label.interpreted}`.as( + "sortableLabel", + ), }) - .from(schema.v1Domain) - .leftJoin(schema.label, eq(schema.label.labelHash, schema.v1Domain.labelHash)), - db + .from(ensIndexerSchema.v1Domain) + .leftJoin( + ensIndexerSchema.label, + eq(ensIndexerSchema.label.labelHash, ensIndexerSchema.v1Domain.labelHash), + ), + ensDb .select({ - domainId: sql`${schema.v2Domain.id}`.as("domainId"), - ownerId: sql
`${schema.v2Domain.ownerId}`.as("ownerId"), - registryId: sql`${schema.v2Domain.registryId}`.as("registryId"), + domainId: sql`${ensIndexerSchema.v2Domain.id}`.as("domainId"), + ownerId: sql
`${ensIndexerSchema.v2Domain.ownerId}`.as("ownerId"), + registryId: sql`${ensIndexerSchema.v2Domain.registryId}`.as("registryId"), parentId: sql`${v2ParentDomain.id}`.as("parentId"), - labelHash: sql`${schema.v2Domain.labelHash}`.as("labelHash"), - sortableLabel: sql`${schema.label.interpreted}`.as("sortableLabel"), + labelHash: sql`${ensIndexerSchema.v2Domain.labelHash}`.as("labelHash"), + sortableLabel: sql`${ensIndexerSchema.label.interpreted}`.as( + "sortableLabel", + ), }) - .from(schema.v2Domain) + .from(ensIndexerSchema.v2Domain) // derive v2 parentId via canonical registry traversal: // 1. find the canonical domain for this domain's registry .leftJoin( - schema.registryCanonicalDomain, - eq(schema.registryCanonicalDomain.registryId, schema.v2Domain.registryId), + ensIndexerSchema.registryCanonicalDomain, + eq( + ensIndexerSchema.registryCanonicalDomain.registryId, + ensIndexerSchema.v2Domain.registryId, + ), ) // 2. verify the reverse pointer: parent.id = rcd.domainId AND parent.subregistryId = child.registryId .leftJoin( v2ParentDomain, and( - eq(v2ParentDomain.id, schema.registryCanonicalDomain.domainId), - eq(v2ParentDomain.subregistryId, schema.v2Domain.registryId), + eq(v2ParentDomain.id, ensIndexerSchema.registryCanonicalDomain.domainId), + eq(v2ParentDomain.subregistryId, ensIndexerSchema.v2Domain.registryId), ), ) - .leftJoin(schema.label, eq(schema.label.labelHash, schema.v2Domain.labelHash)), + .leftJoin( + ensIndexerSchema.label, + eq(ensIndexerSchema.label.labelHash, ensIndexerSchema.v2Domain.labelHash), + ), ).as("baseDomains"); } diff --git a/apps/ensapi/src/graphql-api/lib/find-domains/layers/filter-by-canonical.ts b/apps/ensapi/src/graphql-api/lib/find-domains/layers/filter-by-canonical.ts index 0e2e855e8..bc5d63213 100644 --- a/apps/ensapi/src/graphql-api/lib/find-domains/layers/filter-by-canonical.ts +++ b/apps/ensapi/src/graphql-api/lib/find-domains/layers/filter-by-canonical.ts @@ -1,6 +1,6 @@ import { eq, isNotNull, isNull, or } from "drizzle-orm"; -import { db } from "@/lib/db"; +import { ensDb } from "@/lib/ensdb/singleton"; import { getCanonicalRegistriesCTE } from "../canonical-registries-cte"; import { type BaseDomainSet, selectBase } from "./base-domain-set"; @@ -17,7 +17,7 @@ import { type BaseDomainSet, selectBase } from "./base-domain-set"; export function filterByCanonical(base: BaseDomainSet) { const canonicalRegistries = getCanonicalRegistriesCTE(); - return db + return ensDb .select(selectBase(base)) .from(base) .leftJoin(canonicalRegistries, eq(canonicalRegistries.id, base.registryId)) diff --git a/apps/ensapi/src/graphql-api/lib/find-domains/layers/filter-by-name.ts b/apps/ensapi/src/graphql-api/lib/find-domains/layers/filter-by-name.ts index 6457422db..611a9b290 100644 --- a/apps/ensapi/src/graphql-api/lib/find-domains/layers/filter-by-name.ts +++ b/apps/ensapi/src/graphql-api/lib/find-domains/layers/filter-by-name.ts @@ -1,7 +1,6 @@ import { eq, like, Param, sql } from "drizzle-orm"; import { alias, unionAll } from "drizzle-orm/pg-core"; -import * as schema from "@ensnode/ensdb-sdk"; import type { ENSv1DomainId, ENSv2DomainId, LabelHashPath } from "@ensnode/ensnode-sdk"; import { type DomainId, @@ -9,7 +8,7 @@ import { parsePartialInterpretedName, } from "@ensnode/ensnode-sdk"; -import { db } from "@/lib/db"; +import { ensDb, ensIndexerSchema } from "@/lib/ensdb/singleton"; import { type BaseDomainSet, selectBase } from "./base-domain-set"; @@ -37,12 +36,12 @@ function v1DomainsByLabelHashPath(labelHashPath: LabelHashPath) { // If no concrete path, return all domains (leaf = head = self) // Postgres will optimize this simple subquery when joined if (labelHashPath.length === 0) { - return db + return ensDb .select({ - leafId: sql`${schema.v1Domain.id}`.as("leafId"), - headId: sql`${schema.v1Domain.id}`.as("headId"), + leafId: sql`${ensIndexerSchema.v1Domain.id}`.as("leafId"), + headId: sql`${ensIndexerSchema.v1Domain.id}`.as("headId"), }) - .from(schema.v1Domain) + .from(ensIndexerSchema.v1Domain) .as("v1_path"); } @@ -55,7 +54,7 @@ function v1DomainsByLabelHashPath(labelHashPath: LabelHashPath) { // 1. Starts with domains matching the leaf labelHash (deepest child) // 2. Recursively joins parents, verifying each ancestor's labelHash // 3. Returns both the leaf (for result/ownership) and head (for partial match) - return db + return ensDb .select({ // https://github.com/drizzle-team/drizzle-orm/issues/1242 leafId: sql`v1_path_check.leaf_id`.as("leafId"), @@ -69,7 +68,7 @@ function v1DomainsByLabelHashPath(labelHashPath: LabelHashPath) { d.id AS leaf_id, d.parent_id AS current_id, 1 AS depth - FROM ${schema.v1Domain} d + FROM ${ensIndexerSchema.v1Domain} d WHERE d.label_hash = (${rawLabelHashPathArray})[${pathLength}] UNION ALL @@ -80,7 +79,7 @@ function v1DomainsByLabelHashPath(labelHashPath: LabelHashPath) { pd.parent_id AS current_id, upward_check.depth + 1 FROM upward_check - JOIN ${schema.v1Domain} pd + JOIN ${ensIndexerSchema.v1Domain} pd ON pd.id = upward_check.current_id WHERE upward_check.depth < ${pathLength} AND pd.label_hash = (${rawLabelHashPathArray})[${pathLength} - upward_check.depth] @@ -112,12 +111,12 @@ function v2DomainsByLabelHashPath(labelHashPath: LabelHashPath) { // If no concrete path, return all domains (leaf = head = self) // Postgres will optimize this simple subquery when joined if (labelHashPath.length === 0) { - return db + return ensDb .select({ - leafId: sql`${schema.v2Domain.id}`.as("leafId"), - headId: sql`${schema.v2Domain.id}`.as("headId"), + leafId: sql`${ensIndexerSchema.v2Domain.id}`.as("leafId"), + headId: sql`${ensIndexerSchema.v2Domain.id}`.as("headId"), }) - .from(schema.v2Domain) + .from(ensIndexerSchema.v2Domain) .as("v2_path"); } @@ -130,7 +129,7 @@ function v2DomainsByLabelHashPath(labelHashPath: LabelHashPath) { // 1. Starts with domains matching the leaf labelHash (deepest child) // 2. Recursively joins parents via registryCanonicalDomain, verifying each ancestor's labelHash // 3. Returns both the leaf (for result/ownership) and head (for partial match) - return db + return ensDb .select({ // https://github.com/drizzle-team/drizzle-orm/issues/1242 leafId: sql`v2_path_check.leaf_id`.as("leafId"), @@ -147,10 +146,10 @@ function v2DomainsByLabelHashPath(labelHashPath: LabelHashPath) { d.id AS leaf_id, rcd.domain_id AS current_id, 1 AS depth - FROM ${schema.v2Domain} d - JOIN ${schema.registryCanonicalDomain} rcd + FROM ${ensIndexerSchema.v2Domain} d + JOIN ${ensIndexerSchema.registryCanonicalDomain} rcd ON rcd.registry_id = d.registry_id - JOIN ${schema.v2Domain} rcd_parent + JOIN ${ensIndexerSchema.v2Domain} rcd_parent ON rcd_parent.id = rcd.domain_id AND rcd_parent.subregistry_id = d.registry_id WHERE d.label_hash = (${rawLabelHashPathArray})[${pathLength}] @@ -163,11 +162,11 @@ function v2DomainsByLabelHashPath(labelHashPath: LabelHashPath) { rcd.domain_id AS current_id, upward_check.depth + 1 FROM upward_check - JOIN ${schema.v2Domain} pd + JOIN ${ensIndexerSchema.v2Domain} pd ON pd.id = upward_check.current_id - JOIN ${schema.registryCanonicalDomain} rcd + JOIN ${ensIndexerSchema.registryCanonicalDomain} rcd ON rcd.registry_id = pd.registry_id - JOIN ${schema.v2Domain} rcd_parent + JOIN ${ensIndexerSchema.v2Domain} rcd_parent ON rcd_parent.id = rcd.domain_id AND rcd_parent.subregistry_id = pd.registry_id WHERE upward_check.depth < ${pathLength} AND pd.label_hash = (${rawLabelHashPathArray})[${pathLength} - upward_check.depth] @@ -202,7 +201,7 @@ export function filterByName(base: BaseDomainSet, name?: string | null) { if (concrete.length === 0) { // No path traversal — sortableLabel is already the domain's own label from the base set - return db + return ensDb .select(selectBase(base)) .from(base) .where( @@ -220,13 +219,13 @@ export function filterByName(base: BaseDomainSet, name?: string | null) { // Union path results into a single set of {leafId, headId} const pathResults = unionAll( - db + ensDb .select({ leafId: sql`${v1Path.leafId}`.as("leafId"), headId: sql`${v1Path.headId}`.as("headId"), }) .from(v1Path), - db + ensDb .select({ leafId: sql`${v2Path.leafId}`.as("leafId"), headId: sql`${v2Path.headId}`.as("headId"), @@ -235,14 +234,14 @@ export function filterByName(base: BaseDomainSet, name?: string | null) { ).as("pathResults"); // Aliases for head domain lookup (to get headLabelHash for label join) - const v1HeadDomain = alias(schema.v1Domain, "v1HeadDomain"); - const v2HeadDomain = alias(schema.v2Domain, "v2HeadDomain"); - const headLabel = alias(schema.label, "headLabel"); + const v1HeadDomain = alias(ensIndexerSchema.v1Domain, "v1HeadDomain"); + const v2HeadDomain = alias(ensIndexerSchema.v2Domain, "v2HeadDomain"); + const headLabel = alias(ensIndexerSchema.label, "headLabel"); // Join base set with path results, look up head domain's label, override sortableLabel. // The inner join on pathResults scopes results to domains matching the concrete path. // LEFT JOINs on head domains: exactly one will match (v1 or v2). - return db + return ensDb .select({ ...selectBase(base), // Override sortableLabel with head domain's label for NAME ordering diff --git a/apps/ensapi/src/graphql-api/lib/find-domains/layers/filter-by-owner.ts b/apps/ensapi/src/graphql-api/lib/find-domains/layers/filter-by-owner.ts index 8acc829e9..f63ed4e31 100644 --- a/apps/ensapi/src/graphql-api/lib/find-domains/layers/filter-by-owner.ts +++ b/apps/ensapi/src/graphql-api/lib/find-domains/layers/filter-by-owner.ts @@ -1,7 +1,7 @@ import { eq } from "drizzle-orm"; import type { Address } from "viem"; -import { db } from "@/lib/db"; +import { ensDb } from "@/lib/ensdb/singleton"; import { type BaseDomainSet, selectBase } from "./base-domain-set"; @@ -9,7 +9,7 @@ import { type BaseDomainSet, selectBase } from "./base-domain-set"; * Filter a base domain set by owner address. */ export function filterByOwner(base: BaseDomainSet, owner: Address) { - return db // + return ensDb // .select(selectBase(base)) .from(base) .where(eq(base.ownerId, owner)) diff --git a/apps/ensapi/src/graphql-api/lib/find-domains/layers/filter-by-parent.ts b/apps/ensapi/src/graphql-api/lib/find-domains/layers/filter-by-parent.ts index 26a707f6d..ff8e70855 100644 --- a/apps/ensapi/src/graphql-api/lib/find-domains/layers/filter-by-parent.ts +++ b/apps/ensapi/src/graphql-api/lib/find-domains/layers/filter-by-parent.ts @@ -2,7 +2,7 @@ import { eq } from "drizzle-orm"; import type { DomainId } from "@ensnode/ensnode-sdk"; -import { db } from "@/lib/db"; +import { ensDb } from "@/lib/ensdb/singleton"; import { type BaseDomainSet, selectBase } from "./base-domain-set"; @@ -13,7 +13,7 @@ import { type BaseDomainSet, selectBase } from "./base-domain-set"; * parentId for both: v1 from the parentId column, v2 via canonical registry traversal. */ export function filterByParent(base: BaseDomainSet, parentId: DomainId) { - return db + return ensDb .select(selectBase(base)) .from(base) .where(eq(base.parentId, parentId)) diff --git a/apps/ensapi/src/graphql-api/lib/find-domains/layers/filter-by-registry.ts b/apps/ensapi/src/graphql-api/lib/find-domains/layers/filter-by-registry.ts index 95e82c11b..c28104771 100644 --- a/apps/ensapi/src/graphql-api/lib/find-domains/layers/filter-by-registry.ts +++ b/apps/ensapi/src/graphql-api/lib/find-domains/layers/filter-by-registry.ts @@ -2,7 +2,7 @@ import { eq } from "drizzle-orm"; import type { RegistryId } from "@ensnode/ensnode-sdk"; -import { db } from "@/lib/db"; +import { ensDb } from "@/lib/ensdb/singleton"; import { type BaseDomainSet, selectBase } from "./base-domain-set"; @@ -13,7 +13,7 @@ import { type BaseDomainSet, selectBase } from "./base-domain-set"; * in the given registry. */ export function filterByRegistry(base: BaseDomainSet, registryId: RegistryId) { - return db + return ensDb .select(selectBase(base)) .from(base) .where(eq(base.registryId, registryId)) diff --git a/apps/ensapi/src/graphql-api/lib/find-domains/layers/with-ordering-metadata.ts b/apps/ensapi/src/graphql-api/lib/find-domains/layers/with-ordering-metadata.ts index cad823b89..1669ff554 100644 --- a/apps/ensapi/src/graphql-api/lib/find-domains/layers/with-ordering-metadata.ts +++ b/apps/ensapi/src/graphql-api/lib/find-domains/layers/with-ordering-metadata.ts @@ -1,9 +1,8 @@ import { and, eq, sql } from "drizzle-orm"; -import * as schema from "@ensnode/ensdb-sdk"; import type { DomainId } from "@ensnode/ensnode-sdk"; -import { db } from "@/lib/db"; +import { ensDb, ensIndexerSchema } from "@/lib/ensdb/singleton"; import type { BaseDomainSet } from "./base-domain-set"; @@ -33,7 +32,7 @@ export type DomainsWithOrderingMetadataResult = { * @param base - A base domain set (output of any filter layer) */ export function withOrderingMetadata(base: BaseDomainSet) { - const domains = db + const domains = ensDb .select({ id: sql`${base.domainId}`.as("id"), @@ -41,25 +40,25 @@ export function withOrderingMetadata(base: BaseDomainSet) { sortableLabel: base.sortableLabel, // for REGISTRATION_TIMESTAMP ordering (materialized on registration) - registrationTimestamp: schema.registration.start, + registrationTimestamp: ensIndexerSchema.registration.start, // for REGISTRATION_EXPIRY ordering - registrationExpiry: schema.registration.expiry, + registrationExpiry: ensIndexerSchema.registration.expiry, }) .from(base) // join latestRegistrationIndex .leftJoin( - schema.latestRegistrationIndex, - eq(schema.latestRegistrationIndex.domainId, base.domainId), + ensIndexerSchema.latestRegistrationIndex, + eq(ensIndexerSchema.latestRegistrationIndex.domainId, base.domainId), ) // join (latest) Registration .leftJoin( - schema.registration, + ensIndexerSchema.registration, and( - eq(schema.registration.domainId, base.domainId), - eq(schema.registration.index, schema.latestRegistrationIndex.index), + eq(ensIndexerSchema.registration.domainId, base.domainId), + eq(ensIndexerSchema.registration.index, ensIndexerSchema.latestRegistrationIndex.index), ), ); - return db.$with("domains").as(domains); + return ensDb.$with("domains").as(domains); } diff --git a/apps/ensapi/src/graphql-api/lib/find-events/find-events-resolver.ts b/apps/ensapi/src/graphql-api/lib/find-events/find-events-resolver.ts index 0430a303a..09936f49a 100644 --- a/apps/ensapi/src/graphql-api/lib/find-events/find-events-resolver.ts +++ b/apps/ensapi/src/graphql-api/lib/find-events/find-events-resolver.ts @@ -2,20 +2,18 @@ import { type ResolveCursorConnectionArgs, resolveCursorConnection } from "@poth import { and, count, eq, getTableColumns, gte, inArray, lte, type SQL, sql } from "drizzle-orm"; import type { Address, Hex } from "viem"; -import * as schema from "@ensnode/ensdb-sdk"; - import { orderPaginationBy, paginateBy } from "@/graphql-api/lib/connection-helpers"; import { lazyConnection } from "@/graphql-api/lib/lazy-connection"; import { ID_PAGINATED_CONNECTION_ARGS } from "@/graphql-api/schema/constants"; -import { db } from "@/lib/db"; +import { ensDb, ensIndexerSchema } from "@/lib/ensdb/singleton"; /** * A join table that relates some entity to events via an `eventId` column. */ type EventJoinTable = - | typeof schema.domainEvent - | typeof schema.resolverEvent - | typeof schema.permissionsEvent; + | typeof ensIndexerSchema.domainEvent + | typeof ensIndexerSchema.resolverEvent + | typeof ensIndexerSchema.permissionsEvent; /** * Available filter options for find-events queries. @@ -40,16 +38,16 @@ function eventsWhereConditions(where?: EventsWhere | null): SQL | undefined { return and( where.selector_in ? where.selector_in.length - ? inArray(schema.event.selector, where.selector_in) + ? inArray(ensIndexerSchema.event.selector, where.selector_in) : sql`false` : undefined, typeof where.timestamp_gte === "bigint" - ? gte(schema.event.timestamp, where.timestamp_gte) + ? gte(ensIndexerSchema.event.timestamp, where.timestamp_gte) : undefined, typeof where.timestamp_lte === "bigint" - ? lte(schema.event.timestamp, where.timestamp_lte) + ? lte(ensIndexerSchema.event.timestamp, where.timestamp_lte) : undefined, - where.from ? eq(schema.event.from, where.from) : undefined, + where.from ? eq(ensIndexerSchema.event.from, where.from) : undefined, ); } @@ -86,9 +84,12 @@ export function resolveFindEvents( return lazyConnection({ totalCount: () => { // note: not possible to dynamically change the .select() columns so we make a new query - let query = db.select({ count: count() }).from(schema.event).$dynamic(); + let query = ensDb.select({ count: count() }).from(ensIndexerSchema.event).$dynamic(); if (through) { - query = query.innerJoin(through.table, eq(through.table.eventId, schema.event.id)); + query = query.innerJoin( + through.table, + eq(through.table.eventId, ensIndexerSchema.event.id), + ); } return query.where(conditions).then((rows) => rows[0].count); @@ -101,14 +102,20 @@ export function resolveFindEvents( }, ({ before, after, limit, inverted }: ResolveCursorConnectionArgs) => { // note: not possible to dynamically change the .select() columns so we make a new query - let query = db.select(getTableColumns(schema.event)).from(schema.event).$dynamic(); + let query = ensDb + .select(getTableColumns(ensIndexerSchema.event)) + .from(ensIndexerSchema.event) + .$dynamic(); if (through) { - query = query.innerJoin(through.table, eq(through.table.eventId, schema.event.id)); + query = query.innerJoin( + through.table, + eq(through.table.eventId, ensIndexerSchema.event.id), + ); } return query - .where(and(conditions, paginateBy(schema.event.id, before, after))) - .orderBy(orderPaginationBy(schema.event.id, inverted)) + .where(and(conditions, paginateBy(ensIndexerSchema.event.id, before, after))) + .orderBy(orderPaginationBy(ensIndexerSchema.event.id, inverted)) .limit(limit); }, ), diff --git a/apps/ensapi/src/graphql-api/lib/get-canonical-path.ts b/apps/ensapi/src/graphql-api/lib/get-canonical-path.ts index c42c170fc..a2fe1ec04 100644 --- a/apps/ensapi/src/graphql-api/lib/get-canonical-path.ts +++ b/apps/ensapi/src/graphql-api/lib/get-canonical-path.ts @@ -2,7 +2,6 @@ import config from "@/config"; import { sql } from "drizzle-orm"; -import * as schema from "@ensnode/ensdb-sdk"; import { type CanonicalPath, type DomainId, @@ -13,7 +12,7 @@ import { ROOT_NODE, } from "@ensnode/ensnode-sdk"; -import { db } from "@/lib/db"; +import { ensDb, ensIndexerSchema } from "@/lib/ensdb/singleton"; const MAX_DEPTH = 16; const ENSv2_ROOT_REGISTRY_ID = maybeGetENSv2RootRegistryId(config.namespace); @@ -24,7 +23,7 @@ const ENSv2_ROOT_REGISTRY_ID = maybeGetENSv2RootRegistryId(config.namespace); * i.e. reverse traversal of the nametree */ export async function getV1CanonicalPath(domainId: ENSv1DomainId): Promise { - const result = await db.execute(sql` + const result = await ensDb.execute(sql` WITH RECURSIVE upward AS ( -- Base case: start from the target domain SELECT @@ -32,7 +31,7 @@ export async function getV1CanonicalPath(domainId: ENSv1DomainId): Promise eq(t.id, domainId) }); + const domain = await ensDb.query.v1Domain.findFirst({ where: (t, { eq }) => eq(t.id, domainId) }); const exists = domain !== undefined; v1Logger.debug({ node, exists }); @@ -104,14 +103,14 @@ async function v2_getDomainIdByInterpretedName( // TODO: need to join latest registration and confirm that it's not expired, if expired should treat the domain as not existing - const result = await db.execute(sql` + const result = await ensDb.execute(sql` WITH RECURSIVE path AS ( SELECT r.id AS registry_id, NULL::text AS domain_id, NULL::text AS label_hash, 0 AS depth - FROM ${schema.registry} r + FROM ${ensIndexerSchema.registry} r WHERE r.id = ${rootRegistryId} UNION ALL @@ -122,7 +121,7 @@ async function v2_getDomainIdByInterpretedName( d.label_hash, path.depth + 1 FROM path - JOIN ${schema.v2Domain} d + JOIN ${ensIndexerSchema.v2Domain} d ON d.registry_id = path.registry_id WHERE d.label_hash = (${rawLabelHashPathArray})[path.depth + 1] AND path.depth + 1 <= array_length(${rawLabelHashPathArray}, 1) diff --git a/apps/ensapi/src/graphql-api/lib/get-domain-resolver.ts b/apps/ensapi/src/graphql-api/lib/get-domain-resolver.ts index 2913d22a7..1698ada89 100644 --- a/apps/ensapi/src/graphql-api/lib/get-domain-resolver.ts +++ b/apps/ensapi/src/graphql-api/lib/get-domain-resolver.ts @@ -1,9 +1,9 @@ import type { DomainId } from "@ensnode/ensnode-sdk"; -import { db } from "@/lib/db"; +import { ensDb } from "@/lib/ensdb/singleton"; export async function getDomainResolver(domainId: DomainId) { - const drr = await db.query.domainResolverRelation.findFirst({ + const drr = await ensDb.query.domainResolverRelation.findFirst({ where: (t, { eq }) => eq(t.domainId, domainId), with: { resolver: true }, }); diff --git a/apps/ensapi/src/graphql-api/lib/get-latest-registration.ts b/apps/ensapi/src/graphql-api/lib/get-latest-registration.ts index 6706ee575..f5c79d91b 100644 --- a/apps/ensapi/src/graphql-api/lib/get-latest-registration.ts +++ b/apps/ensapi/src/graphql-api/lib/get-latest-registration.ts @@ -1,12 +1,12 @@ import type { DomainId } from "@ensnode/ensnode-sdk"; -import { db } from "@/lib/db"; +import { ensDb } from "@/lib/ensdb/singleton"; /** * Gets the latest Registration entity for Domain `domainId`. */ export async function getLatestRegistration(domainId: DomainId) { - return await db.query.registration.findFirst({ + return await ensDb.query.registration.findFirst({ where: (t, { eq }) => eq(t.domainId, domainId), orderBy: (t, { desc }) => desc(t.index), }); diff --git a/apps/ensapi/src/graphql-api/schema/account.ts b/apps/ensapi/src/graphql-api/schema/account.ts index 738dc4d9f..cb1b3ed0d 100644 --- a/apps/ensapi/src/graphql-api/schema/account.ts +++ b/apps/ensapi/src/graphql-api/schema/account.ts @@ -2,8 +2,6 @@ import { type ResolveCursorConnectionArgs, resolveCursorConnection } from "@poth import { and, count, eq, getTableColumns } from "drizzle-orm"; import type { Address } from "viem"; -import * as schema from "@ensnode/ensdb-sdk"; - import { builder } from "@/graphql-api/builder"; import { orderPaginationBy, paginateBy } from "@/graphql-api/lib/connection-helpers"; import { resolveFindDomains } from "@/graphql-api/lib/find-domains/find-domains-resolver"; @@ -28,11 +26,11 @@ import { AccountEventsWhereInput, EventRef } from "@/graphql-api/schema/event"; import { PermissionsUserRef } from "@/graphql-api/schema/permissions"; import { RegistryPermissionsUserRef } from "@/graphql-api/schema/registry-permissions-user"; import { ResolverPermissionsUserRef } from "@/graphql-api/schema/resolver-permissions-user"; -import { db } from "@/lib/db"; +import { ensDb, ensIndexerSchema } from "@/lib/ensdb/singleton"; export const AccountRef = builder.loadableObjectRef("Account", { load: (ids: Address[]) => - db.query.account.findMany({ + ensDb.query.account.findMany({ where: (t, { inArray }) => inArray(t.id, ids), }), toKey: getModelId, @@ -114,27 +112,27 @@ AccountRef.implement({ resolve: (parent, args) => { const scope = and( // this user's permissions - eq(schema.permissionsUser.user, parent.id), + eq(ensIndexerSchema.permissionsUser.user, parent.id), // optionally filtered by contract args.in ? and( - eq(schema.permissionsUser.chainId, args.in.chainId), - eq(schema.permissionsUser.address, args.in.address), + eq(ensIndexerSchema.permissionsUser.chainId, args.in.chainId), + eq(ensIndexerSchema.permissionsUser.address, args.in.address), ) : undefined, ); return lazyConnection({ - totalCount: () => db.$count(schema.permissionsUser, scope), + totalCount: () => ensDb.$count(ensIndexerSchema.permissionsUser, scope), connection: () => resolveCursorConnection( { ...ID_PAGINATED_CONNECTION_ARGS, args }, ({ before, after, limit, inverted }: ResolveCursorConnectionArgs) => - db + ensDb .select() - .from(schema.permissionsUser) - .where(and(scope, paginateBy(schema.permissionsUser.id, before, after))) - .orderBy(orderPaginationBy(schema.permissionsUser.id, inverted)) + .from(ensIndexerSchema.permissionsUser) + .where(and(scope, paginateBy(ensIndexerSchema.permissionsUser.id, before, after))) + .orderBy(orderPaginationBy(ensIndexerSchema.permissionsUser.id, inverted)) .limit(limit), ), }); @@ -148,30 +146,30 @@ AccountRef.implement({ description: "The Permissions on Registries granted to this Account.", type: RegistryPermissionsUserRef, resolve: (parent, args) => { - const scope = eq(schema.permissionsUser.user, parent.id); + const scope = eq(ensIndexerSchema.permissionsUser.user, parent.id); const join = and( - eq(schema.permissionsUser.chainId, schema.registry.chainId), - eq(schema.permissionsUser.address, schema.registry.address), + eq(ensIndexerSchema.permissionsUser.chainId, ensIndexerSchema.registry.chainId), + eq(ensIndexerSchema.permissionsUser.address, ensIndexerSchema.registry.address), ); return lazyConnection({ totalCount: () => - db + ensDb .select({ count: count() }) - .from(schema.permissionsUser) - .innerJoin(schema.registry, join) + .from(ensIndexerSchema.permissionsUser) + .innerJoin(ensIndexerSchema.registry, join) .where(scope) .then((r) => r[0].count), connection: () => resolveCursorConnection( { ...ID_PAGINATED_CONNECTION_ARGS, args }, ({ before, after, limit, inverted }: ResolveCursorConnectionArgs) => - db - .select(getTableColumns(schema.permissionsUser)) - .from(schema.permissionsUser) - .innerJoin(schema.registry, join) - .where(and(scope, paginateBy(schema.permissionsUser.id, before, after))) - .orderBy(orderPaginationBy(schema.permissionsUser.id, inverted)) + ensDb + .select(getTableColumns(ensIndexerSchema.permissionsUser)) + .from(ensIndexerSchema.permissionsUser) + .innerJoin(ensIndexerSchema.registry, join) + .where(and(scope, paginateBy(ensIndexerSchema.permissionsUser.id, before, after))) + .orderBy(orderPaginationBy(ensIndexerSchema.permissionsUser.id, inverted)) .limit(limit), ), }); @@ -185,30 +183,30 @@ AccountRef.implement({ description: "The Permissions on Resolvers granted to this Account.", type: ResolverPermissionsUserRef, resolve: (parent, args) => { - const scope = eq(schema.permissionsUser.user, parent.id); + const scope = eq(ensIndexerSchema.permissionsUser.user, parent.id); const join = and( - eq(schema.permissionsUser.chainId, schema.resolver.chainId), - eq(schema.permissionsUser.address, schema.resolver.address), + eq(ensIndexerSchema.permissionsUser.chainId, ensIndexerSchema.resolver.chainId), + eq(ensIndexerSchema.permissionsUser.address, ensIndexerSchema.resolver.address), ); return lazyConnection({ totalCount: () => - db + ensDb .select({ count: count() }) - .from(schema.permissionsUser) - .innerJoin(schema.resolver, join) + .from(ensIndexerSchema.permissionsUser) + .innerJoin(ensIndexerSchema.resolver, join) .where(scope) .then((r) => r[0].count), connection: () => resolveCursorConnection( { ...ID_PAGINATED_CONNECTION_ARGS, args }, ({ before, after, limit, inverted }: ResolveCursorConnectionArgs) => - db - .select(getTableColumns(schema.permissionsUser)) - .from(schema.permissionsUser) - .innerJoin(schema.resolver, join) - .where(and(scope, paginateBy(schema.permissionsUser.id, before, after))) - .orderBy(orderPaginationBy(schema.permissionsUser.id, inverted)) + ensDb + .select(getTableColumns(ensIndexerSchema.permissionsUser)) + .from(ensIndexerSchema.permissionsUser) + .innerJoin(ensIndexerSchema.resolver, join) + .where(and(scope, paginateBy(ensIndexerSchema.permissionsUser.id, before, after))) + .orderBy(orderPaginationBy(ensIndexerSchema.permissionsUser.id, inverted)) .limit(limit), ), }); diff --git a/apps/ensapi/src/graphql-api/schema/domain.ts b/apps/ensapi/src/graphql-api/schema/domain.ts index 942b37b48..d218faabf 100644 --- a/apps/ensapi/src/graphql-api/schema/domain.ts +++ b/apps/ensapi/src/graphql-api/schema/domain.ts @@ -1,7 +1,6 @@ import { type ResolveCursorConnectionArgs, resolveCursorConnection } from "@pothos/plugin-relay"; import { and, count, eq, getTableColumns } from "drizzle-orm"; -import * as schema from "@ensnode/ensdb-sdk"; import { type DomainId, type ENSv1DomainId, @@ -37,7 +36,7 @@ import { PermissionsUserRef } from "@/graphql-api/schema/permissions"; import { RegistrationInterfaceRef } from "@/graphql-api/schema/registration"; import { RegistryRef } from "@/graphql-api/schema/registry"; import { ResolverRef } from "@/graphql-api/schema/resolver"; -import { db } from "@/lib/db"; +import { ensDb, ensIndexerSchema } from "@/lib/ensdb/singleton"; const isENSv1Domain = (domain: Domain): domain is ENSv1Domain => "parentId" in domain; @@ -47,7 +46,7 @@ const isENSv1Domain = (domain: Domain): domain is ENSv1Domain => "parentId" in d export const ENSv1DomainRef = builder.loadableObjectRef("ENSv1Domain", { load: (ids: ENSv1DomainId[]) => - db.query.v1Domain.findMany({ + ensDb.query.v1Domain.findMany({ where: (t, { inArray }) => inArray(t.id, ids), with: { label: true }, }), @@ -58,7 +57,7 @@ export const ENSv1DomainRef = builder.loadableObjectRef("ENSv1Domain", { export const ENSv2DomainRef = builder.loadableObjectRef("ENSv2Domain", { load: (ids: ENSv2DomainId[]) => - db.query.v2Domain.findMany({ + ensDb.query.v2Domain.findMany({ where: (t, { inArray }) => inArray(t.id, ids), with: { label: true }, }), @@ -70,11 +69,11 @@ export const ENSv2DomainRef = builder.loadableObjectRef("ENSv2Domain", { export const DomainInterfaceRef = builder.loadableInterfaceRef("Domain", { load: async (ids: DomainId[]): Promise<(ENSv1Domain | ENSv2Domain)[]> => { const [v1Domains, v2Domains] = await Promise.all([ - db.query.v1Domain.findMany({ + ensDb.query.v1Domain.findMany({ where: (t, { inArray }) => inArray(t.id, ids as any), // ignore downcast to ENSv1DomainId with: { label: true }, }), - db.query.v2Domain.findMany({ + ensDb.query.v2Domain.findMany({ where: (t, { inArray }) => inArray(t.id, ids as any), // ignore downcast to ENSv2DomainId with: { label: true }, }), @@ -211,19 +210,21 @@ DomainInterfaceRef.implement({ description: "All Registrations for a Domain, including the latest Registration.", type: RegistrationInterfaceRef, resolve: (parent, args) => { - const scope = eq(schema.registration.domainId, parent.id); + const scope = eq(ensIndexerSchema.registration.domainId, parent.id); return lazyConnection({ - totalCount: () => db.$count(schema.registration, scope), + totalCount: () => ensDb.$count(ensIndexerSchema.registration, scope), connection: () => resolveCursorConnection( { ...INDEX_PAGINATED_CONNECTION_ARGS, args }, ({ before, after, limit, inverted }: ResolveCursorConnectionArgs) => - db + ensDb .select() - .from(schema.registration) - .where(and(scope, paginateByInt(schema.registration.index, before, after))) - .orderBy(orderPaginationBy(schema.registration.index, inverted)) + .from(ensIndexerSchema.registration) + .where( + and(scope, paginateByInt(ensIndexerSchema.registration.index, before, after)), + ) + .orderBy(orderPaginationBy(ensIndexerSchema.registration.index, inverted)) .limit(limit), ), }); @@ -261,8 +262,8 @@ DomainInterfaceRef.implement({ resolve: (parent, args) => resolveFindEvents(args, { through: { - table: schema.domainEvent, - scope: eq(schema.domainEvent.domainId, parent.id), + table: ensIndexerSchema.domainEvent, + scope: eq(ensIndexerSchema.domainEvent.domainId, parent.id), }, }), }), @@ -361,36 +362,36 @@ ENSv2DomainRef.implement({ resolve: (parent, args) => { const scope = and( // filter by resource === tokenId - eq(schema.permissionsUser.resource, parent.tokenId), + eq(ensIndexerSchema.permissionsUser.resource, parent.tokenId), // optionally filter by user - args.where?.user ? eq(schema.permissionsUser.user, args.where.user) : undefined, + args.where?.user ? eq(ensIndexerSchema.permissionsUser.user, args.where.user) : undefined, ); // inner join against this Domain's registry to filter Permissions by those in said registry const join = and( - eq(schema.permissionsUser.chainId, schema.registry.chainId), - eq(schema.permissionsUser.address, schema.registry.address), - eq(schema.registry.id, parent.registryId), + eq(ensIndexerSchema.permissionsUser.chainId, ensIndexerSchema.registry.chainId), + eq(ensIndexerSchema.permissionsUser.address, ensIndexerSchema.registry.address), + eq(ensIndexerSchema.registry.id, parent.registryId), ); return lazyConnection({ totalCount: () => - db + ensDb .select({ count: count() }) - .from(schema.permissionsUser) - .innerJoin(schema.registry, join) + .from(ensIndexerSchema.permissionsUser) + .innerJoin(ensIndexerSchema.registry, join) .where(scope) .then((r) => r[0].count), connection: () => resolveCursorConnection( { ...ID_PAGINATED_CONNECTION_ARGS, args }, ({ before, after, limit, inverted }: ResolveCursorConnectionArgs) => - db - .select(getTableColumns(schema.permissionsUser)) - .from(schema.permissionsUser) - .innerJoin(schema.registry, join) - .where(and(scope, paginateBy(schema.permissionsUser.id, before, after))) - .orderBy(orderPaginationBy(schema.permissionsUser.id, inverted)) + ensDb + .select(getTableColumns(ensIndexerSchema.permissionsUser)) + .from(ensIndexerSchema.permissionsUser) + .innerJoin(ensIndexerSchema.registry, join) + .where(and(scope, paginateBy(ensIndexerSchema.permissionsUser.id, before, after))) + .orderBy(orderPaginationBy(ensIndexerSchema.permissionsUser.id, inverted)) .limit(limit), ), }); diff --git a/apps/ensapi/src/graphql-api/schema/event.ts b/apps/ensapi/src/graphql-api/schema/event.ts index 0fa83eff0..404f493ae 100644 --- a/apps/ensapi/src/graphql-api/schema/event.ts +++ b/apps/ensapi/src/graphql-api/schema/event.ts @@ -1,10 +1,10 @@ import { builder } from "@/graphql-api/builder"; import { getModelId } from "@/graphql-api/lib/get-model-id"; -import { db } from "@/lib/db"; +import { ensDb } from "@/lib/ensdb/singleton"; export const EventRef = builder.loadableObjectRef("Event", { load: (ids: string[]) => - db.query.event.findMany({ + ensDb.query.event.findMany({ where: (t, { inArray }) => inArray(t.id, ids), }), toKey: getModelId, diff --git a/apps/ensapi/src/graphql-api/schema/label.ts b/apps/ensapi/src/graphql-api/schema/label.ts index ec2a2b8af..004022b3a 100644 --- a/apps/ensapi/src/graphql-api/schema/label.ts +++ b/apps/ensapi/src/graphql-api/schema/label.ts @@ -1,8 +1,7 @@ -import type * as schema from "@ensnode/ensdb-sdk"; - import { builder } from "@/graphql-api/builder"; +import type { ensIndexerSchema } from "@/lib/ensdb/singleton"; -export const LabelRef = builder.objectRef("Label"); +export const LabelRef = builder.objectRef("Label"); LabelRef.implement({ description: "Represents a Label within ENS, providing its hash and interpreted representation.", fields: (t) => ({ diff --git a/apps/ensapi/src/graphql-api/schema/permissions.ts b/apps/ensapi/src/graphql-api/schema/permissions.ts index 74ea09421..1087c1aef 100644 --- a/apps/ensapi/src/graphql-api/schema/permissions.ts +++ b/apps/ensapi/src/graphql-api/schema/permissions.ts @@ -1,7 +1,6 @@ import { type ResolveCursorConnectionArgs, resolveCursorConnection } from "@pothos/plugin-relay"; import { and, eq } from "drizzle-orm"; -import * as schema from "@ensnode/ensdb-sdk"; import { makePermissionsId, makePermissionsResourceId, @@ -20,11 +19,11 @@ import { AccountRef } from "@/graphql-api/schema/account"; import { AccountIdRef } from "@/graphql-api/schema/account-id"; import { ID_PAGINATED_CONNECTION_ARGS } from "@/graphql-api/schema/constants"; import { EventRef, EventsWhereInput } from "@/graphql-api/schema/event"; -import { db } from "@/lib/db"; +import { ensDb, ensIndexerSchema } from "@/lib/ensdb/singleton"; export const PermissionsRef = builder.loadableObjectRef("Permissions", { load: (ids: PermissionsId[]) => - db.query.permissions.findMany({ where: (t, { inArray }) => inArray(t.id, ids) }), + ensDb.query.permissions.findMany({ where: (t, { inArray }) => inArray(t.id, ids) }), toKey: getModelId, cacheResolved: true, sort: true, @@ -32,7 +31,7 @@ export const PermissionsRef = builder.loadableObjectRef("Permissions", { export const PermissionsResourceRef = builder.loadableObjectRef("PermissionsResource", { load: (ids: PermissionsResourceId[]) => - db.query.permissionsResource.findMany({ where: (t, { inArray }) => inArray(t.id, ids) }), + ensDb.query.permissionsResource.findMany({ where: (t, { inArray }) => inArray(t.id, ids) }), toKey: getModelId, cacheResolved: true, sort: true, @@ -40,7 +39,7 @@ export const PermissionsResourceRef = builder.loadableObjectRef("PermissionsReso export const PermissionsUserRef = builder.loadableObjectRef("PermissionsUser", { load: (ids: PermissionsUserId[]) => - db.query.permissionsUser.findMany({ where: (t, { inArray }) => inArray(t.id, ids) }), + ensDb.query.permissionsUser.findMany({ where: (t, { inArray }) => inArray(t.id, ids) }), toKey: getModelId, cacheResolved: true, sort: true, @@ -101,21 +100,23 @@ PermissionsRef.implement({ type: PermissionsResourceRef, resolve: (parent, args) => { const scope = and( - eq(schema.permissionsResource.chainId, parent.chainId), - eq(schema.permissionsResource.address, parent.address), + eq(ensIndexerSchema.permissionsResource.chainId, parent.chainId), + eq(ensIndexerSchema.permissionsResource.address, parent.address), ); return lazyConnection({ - totalCount: () => db.$count(schema.permissionsResource, scope), + totalCount: () => ensDb.$count(ensIndexerSchema.permissionsResource, scope), connection: () => resolveCursorConnection( { ...ID_PAGINATED_CONNECTION_ARGS, args }, ({ before, after, limit, inverted }: ResolveCursorConnectionArgs) => - db + ensDb .select() - .from(schema.permissionsResource) - .where(and(scope, paginateBy(schema.permissionsResource.id, before, after))) - .orderBy(orderPaginationBy(schema.permissionsResource.id, inverted)) + .from(ensIndexerSchema.permissionsResource) + .where( + and(scope, paginateBy(ensIndexerSchema.permissionsResource.id, before, after)), + ) + .orderBy(orderPaginationBy(ensIndexerSchema.permissionsResource.id, inverted)) .limit(limit), ), }); @@ -134,8 +135,8 @@ PermissionsRef.implement({ resolve: (parent, args) => resolveFindEvents(args, { through: { - table: schema.permissionsEvent, - scope: eq(schema.permissionsEvent.permissionsId, parent.id), + table: ensIndexerSchema.permissionsEvent, + scope: eq(ensIndexerSchema.permissionsEvent.permissionsId, parent.id), }, }), }), @@ -196,22 +197,22 @@ PermissionsResourceRef.implement({ type: PermissionsUserRef, resolve: (parent, args) => { const scope = and( - eq(schema.permissionsUser.chainId, parent.chainId), - eq(schema.permissionsUser.address, parent.address), - eq(schema.permissionsUser.resource, parent.resource), + eq(ensIndexerSchema.permissionsUser.chainId, parent.chainId), + eq(ensIndexerSchema.permissionsUser.address, parent.address), + eq(ensIndexerSchema.permissionsUser.resource, parent.resource), ); return lazyConnection({ - totalCount: () => db.$count(schema.permissionsUser, scope), + totalCount: () => ensDb.$count(ensIndexerSchema.permissionsUser, scope), connection: () => resolveCursorConnection( { ...ID_PAGINATED_CONNECTION_ARGS, args }, ({ before, after, limit, inverted }: ResolveCursorConnectionArgs) => - db + ensDb .select() - .from(schema.permissionsUser) - .where(and(scope, paginateBy(schema.permissionsUser.id, before, after))) - .orderBy(orderPaginationBy(schema.permissionsUser.id, inverted)) + .from(ensIndexerSchema.permissionsUser) + .where(and(scope, paginateBy(ensIndexerSchema.permissionsUser.id, before, after))) + .orderBy(orderPaginationBy(ensIndexerSchema.permissionsUser.id, inverted)) .limit(limit), ), }); diff --git a/apps/ensapi/src/graphql-api/schema/query.ts b/apps/ensapi/src/graphql-api/schema/query.ts index 1918e74a3..331132b8a 100644 --- a/apps/ensapi/src/graphql-api/schema/query.ts +++ b/apps/ensapi/src/graphql-api/schema/query.ts @@ -2,7 +2,6 @@ import config from "@/config"; import { type ResolveCursorConnectionArgs, resolveCursorConnection } from "@pothos/plugin-relay"; -import * as schema from "@ensnode/ensdb-sdk"; import { makePermissionsId, makeRegistryId, @@ -36,7 +35,7 @@ import { PermissionsRef } from "@/graphql-api/schema/permissions"; import { RegistrationInterfaceRef } from "@/graphql-api/schema/registration"; import { RegistryIdInput, RegistryRef } from "@/graphql-api/schema/registry"; import { ResolverIdInput, ResolverRef } from "@/graphql-api/schema/resolver"; -import { db } from "@/lib/db"; +import { ensDb, ensIndexerSchema } from "@/lib/ensdb/singleton"; // don't want them to get familiar/accustomed to these methods until their necessity is certain const INCLUDE_DEV_METHODS = process.env.NODE_ENV !== "production"; @@ -52,14 +51,14 @@ builder.queryType({ type: ENSv1DomainRef, resolve: (parent, args) => lazyConnection({ - totalCount: () => db.$count(schema.v1Domain), + totalCount: () => ensDb.$count(ensIndexerSchema.v1Domain), connection: () => resolveCursorConnection( { ...ID_PAGINATED_CONNECTION_ARGS, args }, ({ before, after, limit, inverted }: ResolveCursorConnectionArgs) => - db.query.v1Domain.findMany({ - where: paginateBy(schema.v1Domain.id, before, after), - orderBy: orderPaginationBy(schema.v1Domain.id, inverted), + ensDb.query.v1Domain.findMany({ + where: paginateBy(ensIndexerSchema.v1Domain.id, before, after), + orderBy: orderPaginationBy(ensIndexerSchema.v1Domain.id, inverted), limit, with: { label: true }, }), @@ -75,14 +74,14 @@ builder.queryType({ type: ENSv2DomainRef, resolve: (parent, args) => lazyConnection({ - totalCount: () => db.$count(schema.v2Domain), + totalCount: () => ensDb.$count(ensIndexerSchema.v2Domain), connection: () => resolveCursorConnection( { ...ID_PAGINATED_CONNECTION_ARGS, args }, ({ before, after, limit, inverted }: ResolveCursorConnectionArgs) => - db.query.v2Domain.findMany({ - where: paginateBy(schema.v2Domain.id, before, after), - orderBy: orderPaginationBy(schema.v2Domain.id, inverted), + ensDb.query.v2Domain.findMany({ + where: paginateBy(ensIndexerSchema.v2Domain.id, before, after), + orderBy: orderPaginationBy(ensIndexerSchema.v2Domain.id, inverted), limit, with: { label: true }, }), @@ -98,16 +97,16 @@ builder.queryType({ type: ResolverRef, resolve: (parent, args) => lazyConnection({ - totalCount: () => db.$count(schema.resolver), + totalCount: () => ensDb.$count(ensIndexerSchema.resolver), connection: () => resolveCursorConnection( { ...ID_PAGINATED_CONNECTION_ARGS, args }, ({ before, after, limit, inverted }: ResolveCursorConnectionArgs) => - db + ensDb .select() - .from(schema.resolver) - .where(paginateBy(schema.resolver.id, before, after)) - .orderBy(orderPaginationBy(schema.resolver.id, inverted)) + .from(ensIndexerSchema.resolver) + .where(paginateBy(ensIndexerSchema.resolver.id, before, after)) + .orderBy(orderPaginationBy(ensIndexerSchema.resolver.id, inverted)) .limit(limit), ), }), @@ -121,16 +120,16 @@ builder.queryType({ type: RegistrationInterfaceRef, resolve: (parent, args) => lazyConnection({ - totalCount: () => db.$count(schema.registration), + totalCount: () => ensDb.$count(ensIndexerSchema.registration), connection: () => resolveCursorConnection( { ...ID_PAGINATED_CONNECTION_ARGS, args }, ({ before, after, limit, inverted }: ResolveCursorConnectionArgs) => - db + ensDb .select() - .from(schema.registration) - .where(paginateBy(schema.registration.id, before, after)) - .orderBy(orderPaginationBy(schema.registration.id, inverted)) + .from(ensIndexerSchema.registration) + .where(paginateBy(ensIndexerSchema.registration.id, before, after)) + .orderBy(orderPaginationBy(ensIndexerSchema.registration.id, inverted)) .limit(limit), ), }), diff --git a/apps/ensapi/src/graphql-api/schema/registration.ts b/apps/ensapi/src/graphql-api/schema/registration.ts index 1cf26b3a2..7d2faf2da 100644 --- a/apps/ensapi/src/graphql-api/schema/registration.ts +++ b/apps/ensapi/src/graphql-api/schema/registration.ts @@ -2,7 +2,6 @@ import { type ResolveCursorConnectionArgs, resolveCursorConnection } from "@poth import { and, eq } from "drizzle-orm"; import { hexToBigInt } from "viem"; -import * as schema from "@ensnode/ensdb-sdk"; import { type ENSv1DomainId, isRegistrationFullyExpired, @@ -20,11 +19,11 @@ import { INDEX_PAGINATED_CONNECTION_ARGS } from "@/graphql-api/schema/constants" import { DomainInterfaceRef } from "@/graphql-api/schema/domain"; import { EventRef } from "@/graphql-api/schema/event"; import { RenewalRef } from "@/graphql-api/schema/renewal"; -import { db } from "@/lib/db"; +import { ensDb, ensIndexerSchema } from "@/lib/ensdb/singleton"; export const RegistrationInterfaceRef = builder.loadableInterfaceRef("Registration", { load: (ids: RegistrationId[]) => - db.query.registration.findMany({ + ensDb.query.registration.findMany({ where: (t, { inArray }) => inArray(t.id, ids), }), toKey: getModelId, @@ -140,21 +139,21 @@ RegistrationInterfaceRef.implement({ type: RenewalRef, resolve: (parent, args) => { const scope = and( - eq(schema.renewal.domainId, parent.domainId), - eq(schema.renewal.registrationIndex, parent.index), + eq(ensIndexerSchema.renewal.domainId, parent.domainId), + eq(ensIndexerSchema.renewal.registrationIndex, parent.index), ); return lazyConnection({ - totalCount: () => db.$count(schema.renewal, scope), + totalCount: () => ensDb.$count(ensIndexerSchema.renewal, scope), connection: () => resolveCursorConnection( { ...INDEX_PAGINATED_CONNECTION_ARGS, args }, ({ before, after, limit, inverted }: ResolveCursorConnectionArgs) => - db + ensDb .select() - .from(schema.renewal) - .where(and(scope, paginateByInt(schema.renewal.index, before, after))) - .orderBy(orderPaginationBy(schema.renewal.index, inverted)) + .from(ensIndexerSchema.renewal) + .where(and(scope, paginateByInt(ensIndexerSchema.renewal.index, before, after))) + .orderBy(orderPaginationBy(ensIndexerSchema.renewal.index, inverted)) .limit(limit), ), }); diff --git a/apps/ensapi/src/graphql-api/schema/registry-permissions-user.ts b/apps/ensapi/src/graphql-api/schema/registry-permissions-user.ts index 9923b2651..34490d595 100644 --- a/apps/ensapi/src/graphql-api/schema/registry-permissions-user.ts +++ b/apps/ensapi/src/graphql-api/schema/registry-permissions-user.ts @@ -1,15 +1,17 @@ -import type * as schema from "@ensnode/ensdb-sdk"; import { makeRegistryId } from "@ensnode/ensnode-sdk"; import { builder } from "@/graphql-api/builder"; import { AccountRef } from "@/graphql-api/schema/account"; import { RegistryRef } from "@/graphql-api/schema/registry"; +import type { ensIndexerSchema } from "@/lib/ensdb/singleton"; /** * Represents a PermissionsUser whose contract is a Registry, providing a semantic `registry` field. */ export const RegistryPermissionsUserRef = - builder.objectRef("RegistryPermissionsUser"); + builder.objectRef( + "RegistryPermissionsUser", + ); RegistryPermissionsUserRef.implement({ fields: (t) => ({ diff --git a/apps/ensapi/src/graphql-api/schema/registry.ts b/apps/ensapi/src/graphql-api/schema/registry.ts index 6fffbe329..b57fe05da 100644 --- a/apps/ensapi/src/graphql-api/schema/registry.ts +++ b/apps/ensapi/src/graphql-api/schema/registry.ts @@ -1,7 +1,6 @@ import { type ResolveCursorConnectionArgs, resolveCursorConnection } from "@pothos/plugin-relay"; import { and, eq } from "drizzle-orm"; -import * as schema from "@ensnode/ensdb-sdk"; import { makePermissionsId, type RegistryId } from "@ensnode/ensnode-sdk"; import { builder } from "@/graphql-api/builder"; @@ -24,11 +23,11 @@ import { RegistryDomainsWhereInput, } from "@/graphql-api/schema/domain"; import { PermissionsRef } from "@/graphql-api/schema/permissions"; -import { db } from "@/lib/db"; +import { ensDb, ensIndexerSchema } from "@/lib/ensdb/singleton"; export const RegistryRef = builder.loadableObjectRef("Registry", { load: (ids: RegistryId[]) => - db.query.registry.findMany({ where: (t, { inArray }) => inArray(t.id, ids) }), + ensDb.query.registry.findMany({ where: (t, { inArray }) => inArray(t.id, ids) }), toKey: getModelId, cacheResolved: true, sort: true, @@ -56,17 +55,17 @@ RegistryRef.implement({ description: "The Domains for which this Registry is a Subregistry.", type: ENSv2DomainRef, resolve: (parent, args) => { - const scope = eq(schema.v2Domain.subregistryId, parent.id); + const scope = eq(ensIndexerSchema.v2Domain.subregistryId, parent.id); return lazyConnection({ - totalCount: () => db.$count(schema.v2Domain, scope), + totalCount: () => ensDb.$count(ensIndexerSchema.v2Domain, scope), connection: () => resolveCursorConnection( { ...ID_PAGINATED_CONNECTION_ARGS, args }, ({ before, after, limit, inverted }: ResolveCursorConnectionArgs) => - db.query.v2Domain.findMany({ - where: and(scope, paginateBy(schema.v2Domain.id, before, after)), - orderBy: orderPaginationBy(schema.v2Domain.id, inverted), + ensDb.query.v2Domain.findMany({ + where: and(scope, paginateBy(ensIndexerSchema.v2Domain.id, before, after)), + orderBy: orderPaginationBy(ensIndexerSchema.v2Domain.id, inverted), limit, with: { label: true }, }), diff --git a/apps/ensapi/src/graphql-api/schema/renewal.ts b/apps/ensapi/src/graphql-api/schema/renewal.ts index 77350b709..7ef01e9e5 100644 --- a/apps/ensapi/src/graphql-api/schema/renewal.ts +++ b/apps/ensapi/src/graphql-api/schema/renewal.ts @@ -3,11 +3,11 @@ import type { RenewalId } from "@ensnode/ensnode-sdk"; import { builder } from "@/graphql-api/builder"; import { getModelId } from "@/graphql-api/lib/get-model-id"; import { EventRef } from "@/graphql-api/schema/event"; -import { db } from "@/lib/db"; +import { ensDb } from "@/lib/ensdb/singleton"; export const RenewalRef = builder.loadableObjectRef("Renewal", { load: (ids: RenewalId[]) => - db.query.renewal.findMany({ + ensDb.query.renewal.findMany({ where: (t, { inArray }) => inArray(t.id, ids), }), toKey: getModelId, diff --git a/apps/ensapi/src/graphql-api/schema/resolver-permissions-user.ts b/apps/ensapi/src/graphql-api/schema/resolver-permissions-user.ts index af4e926f5..4b752164a 100644 --- a/apps/ensapi/src/graphql-api/schema/resolver-permissions-user.ts +++ b/apps/ensapi/src/graphql-api/schema/resolver-permissions-user.ts @@ -1,15 +1,17 @@ -import type * as schema from "@ensnode/ensdb-sdk"; import { makeResolverId } from "@ensnode/ensnode-sdk"; import { builder } from "@/graphql-api/builder"; import { AccountRef } from "@/graphql-api/schema/account"; import { ResolverRef } from "@/graphql-api/schema/resolver"; +import type { ensIndexerSchema } from "@/lib/ensdb/singleton"; /** * Represents a PermissionsUser whose contract is a Resolver, providing a semantic `resolver` field. */ export const ResolverPermissionsUserRef = - builder.objectRef("ResolverPermissionsUser"); + builder.objectRef( + "ResolverPermissionsUser", + ); ResolverPermissionsUserRef.implement({ fields: (t) => ({ diff --git a/apps/ensapi/src/graphql-api/schema/resolver-records.ts b/apps/ensapi/src/graphql-api/schema/resolver-records.ts index b6658ee8f..70b38bc3d 100644 --- a/apps/ensapi/src/graphql-api/schema/resolver-records.ts +++ b/apps/ensapi/src/graphql-api/schema/resolver-records.ts @@ -2,11 +2,11 @@ import { bigintToCoinType, type ResolverRecordsId } from "@ensnode/ensnode-sdk"; import { builder } from "@/graphql-api/builder"; import { getModelId } from "@/graphql-api/lib/get-model-id"; -import { db } from "@/lib/db"; +import { ensDb } from "@/lib/ensdb/singleton"; export const ResolverRecordsRef = builder.loadableObjectRef("ResolverRecords", { load: (ids: ResolverRecordsId[]) => - db.query.resolverRecords.findMany({ + ensDb.query.resolverRecords.findMany({ where: (t, { inArray }) => inArray(t.id, ids), with: { textRecords: true, addressRecords: true }, }), diff --git a/apps/ensapi/src/graphql-api/schema/resolver.ts b/apps/ensapi/src/graphql-api/schema/resolver.ts index 7ded6aa3f..f3119b100 100644 --- a/apps/ensapi/src/graphql-api/schema/resolver.ts +++ b/apps/ensapi/src/graphql-api/schema/resolver.ts @@ -4,7 +4,6 @@ import { type ResolveCursorConnectionArgs, resolveCursorConnection } from "@poth import { and, eq } from "drizzle-orm"; import { namehash } from "viem"; -import * as schema from "@ensnode/ensdb-sdk"; import { makePermissionsId, makeResolverRecordsId, @@ -26,7 +25,7 @@ import { EventRef, EventsWhereInput } from "@/graphql-api/schema/event"; import { NameOrNodeInput } from "@/graphql-api/schema/name-or-node"; import { PermissionsRef, type PermissionsUserResource } from "@/graphql-api/schema/permissions"; import { ResolverRecordsRef } from "@/graphql-api/schema/resolver-records"; -import { db } from "@/lib/db"; +import { ensDb, ensIndexerSchema } from "@/lib/ensdb/singleton"; /** * Note that this indexed Resolver entity represents not _all_ Resolver contracts that exist onchain, @@ -43,7 +42,7 @@ import { db } from "@/lib/db"; export const ResolverRef = builder.loadableObjectRef("Resolver", { load: (ids: ResolverId[]) => - db.query.resolver.findMany({ + ensDb.query.resolver.findMany({ where: (t, { inArray }) => inArray(t.id, ids), }), toKey: getModelId, @@ -87,19 +86,19 @@ ResolverRef.implement({ type: ResolverRecordsRef, resolve: (parent, args, context) => { const scope = and( - eq(schema.resolverRecords.chainId, parent.chainId), - eq(schema.resolverRecords.address, parent.address), + eq(ensIndexerSchema.resolverRecords.chainId, parent.chainId), + eq(ensIndexerSchema.resolverRecords.address, parent.address), ); return lazyConnection({ - totalCount: () => db.$count(schema.resolverRecords, scope), + totalCount: () => ensDb.$count(ensIndexerSchema.resolverRecords, scope), connection: () => resolveCursorConnection( { ...ID_PAGINATED_CONNECTION_ARGS, args }, ({ before, after, limit, inverted }: ResolveCursorConnectionArgs) => - db.query.resolverRecords.findMany({ - where: and(scope, paginateBy(schema.resolverRecords.id, before, after)), - orderBy: orderPaginationBy(schema.resolverRecords.id, inverted), + ensDb.query.resolverRecords.findMany({ + where: and(scope, paginateBy(ensIndexerSchema.resolverRecords.id, before, after)), + orderBy: orderPaginationBy(ensIndexerSchema.resolverRecords.id, inverted), limit, with: { textRecords: true, addressRecords: true }, }), @@ -130,7 +129,7 @@ ResolverRef.implement({ type: DedicatedResolverMetadataRef, nullable: true, resolve: async (parent, args, context) => - db.query.permissionsUser.findFirst({ + ensDb.query.permissionsUser.findFirst({ where: (t, { eq, and }) => and( eq(t.chainId, parent.chainId), @@ -171,8 +170,8 @@ ResolverRef.implement({ resolve: (parent, args) => resolveFindEvents(args, { through: { - table: schema.resolverEvent, - scope: eq(schema.resolverEvent.resolverId, parent.id), + table: ensIndexerSchema.resolverEvent, + scope: eq(ensIndexerSchema.resolverEvent.resolverId, parent.id), }, }), }), diff --git a/apps/ensapi/src/handlers/ensanalytics/ensanalytics-api-v1.test.ts b/apps/ensapi/src/handlers/ensanalytics/ensanalytics-api-v1.test.ts index 27b140055..633a1c43e 100644 --- a/apps/ensapi/src/handlers/ensanalytics/ensanalytics-api-v1.test.ts +++ b/apps/ensapi/src/handlers/ensanalytics/ensanalytics-api-v1.test.ts @@ -8,8 +8,7 @@ import * as editionSetMiddleware from "@/middleware/referral-program-edition-set vi.mock("@/config", () => ({ get default() { - const mockedConfig: Pick = { - ensIndexerUrl: new URL("https://ensnode.example.com"), + const mockedConfig: Pick = { namespace: ENSNamespaceIds.Mainnet, }; diff --git a/apps/ensapi/src/handlers/ensanalytics/ensanalytics-api.test.ts b/apps/ensapi/src/handlers/ensanalytics/ensanalytics-api.test.ts index 74582d1d7..4b8b44079 100644 --- a/apps/ensapi/src/handlers/ensanalytics/ensanalytics-api.test.ts +++ b/apps/ensapi/src/handlers/ensanalytics/ensanalytics-api.test.ts @@ -7,8 +7,7 @@ import * as middleware from "@/middleware/referrer-leaderboard.middleware"; vi.mock("@/config", () => ({ get default() { - const mockedConfig: Pick = { - ensIndexerUrl: new URL("https://ensnode.example.com"), + const mockedConfig: Pick = { namespace: ENSNamespaceIds.Mainnet, }; diff --git a/apps/ensapi/src/handlers/subgraph/subgraph-api.ts b/apps/ensapi/src/handlers/subgraph/subgraph-api.ts index d05e18cff..2da79e151 100644 --- a/apps/ensapi/src/handlers/subgraph/subgraph-api.ts +++ b/apps/ensapi/src/handlers/subgraph/subgraph-api.ts @@ -2,10 +2,10 @@ import config from "@/config"; import { createDocumentationMiddleware } from "ponder-enrich-gql-docs-middleware"; -import * as schema from "@ensnode/ensdb-sdk"; import { type Duration, hasSubgraphApiConfigSupport } from "@ensnode/ensnode-sdk"; import { subgraphGraphQLMiddleware } from "@ensnode/ponder-subgraph"; +import { ensIndexerSchema } from "@/lib/ensdb/singleton"; import { createApp } from "@/lib/hono-factory"; import { makeSubgraphApiDocumentation } from "@/lib/subgraph/api-documentation"; import { filterSchemaByPrefix } from "@/lib/subgraph/filter-schema-by-prefix"; @@ -18,7 +18,7 @@ import { thegraphFallbackMiddleware } from "@/middleware/thegraph-fallback.middl const MAX_REALTIME_DISTANCE_TO_RESOLVE: Duration = 10 * 60; // 10 minutes in seconds // generate a subgraph-specific subset of the schema -const subgraphSchema = filterSchemaByPrefix("subgraph_", schema); +const subgraphSchema = filterSchemaByPrefix("subgraph_", ensIndexerSchema); const app = createApp(); @@ -54,7 +54,7 @@ app.use(subgraphMetaMiddleware); app.use( subgraphGraphQLMiddleware({ databaseUrl: config.databaseUrl, - databaseSchema: config.databaseSchemaName, + databaseSchema: config.ensIndexerSchemaName, schema: subgraphSchema, // describes the polymorphic (interface) relationships in the schema polymorphicConfig: { diff --git a/apps/ensapi/src/lib/db.ts b/apps/ensapi/src/lib/db.ts deleted file mode 100644 index a5c04ee38..000000000 --- a/apps/ensapi/src/lib/db.ts +++ /dev/null @@ -1,11 +0,0 @@ -import config from "@/config"; - -import * as schema from "@ensnode/ensdb-sdk"; - -import { makeDrizzle } from "@/lib/handlers/drizzle"; - -export const db = makeDrizzle({ - databaseUrl: config.databaseUrl, - databaseSchema: config.databaseSchemaName, - schema, -}); diff --git a/apps/ensapi/src/lib/ensanalytics/referrer-leaderboard/database-v1.ts b/apps/ensapi/src/lib/ensanalytics/referrer-leaderboard/database-v1.ts index 29554f1f4..8ec23d912 100644 --- a/apps/ensapi/src/lib/ensanalytics/referrer-leaderboard/database-v1.ts +++ b/apps/ensapi/src/lib/ensanalytics/referrer-leaderboard/database-v1.ts @@ -7,10 +7,9 @@ import { import { and, asc, count, desc, eq, gte, isNotNull, lte, ne, sql, sum } from "drizzle-orm"; import { type Address, zeroAddress } from "viem"; -import * as schema from "@ensnode/ensdb-sdk"; import { deserializeDuration, formatAccountId, priceEth } from "@ensnode/ensnode-sdk"; -import { db } from "@/lib/db"; +import { ensDb, ensIndexerSchema } from "@/lib/ensdb/singleton"; import logger from "@/lib/logger"; /** @@ -38,35 +37,35 @@ export const getReferrerMetrics = async ( */ try { - const records = await db + const records = await ensDb .select({ - referrer: schema.registrarActions.decodedReferrer, + referrer: ensIndexerSchema.registrarActions.decodedReferrer, totalReferrals: count().as("total_referrals"), - totalIncrementalDuration: sum(schema.registrarActions.incrementalDuration).as( + totalIncrementalDuration: sum(ensIndexerSchema.registrarActions.incrementalDuration).as( "total_incremental_duration", ), // Note: Using raw SQL for COALESCE because Drizzle doesn't natively support it yet. // See: https://github.com/drizzle-team/drizzle-orm/issues/3708 totalRevenueContribution: - sql`COALESCE(SUM(${schema.registrarActions.total}), 0)`.as( + sql`COALESCE(SUM(${ensIndexerSchema.registrarActions.total}), 0)`.as( "total_revenue_contribution", ), }) - .from(schema.registrarActions) + .from(ensIndexerSchema.registrarActions) .where( and( // Filter by timestamp range - gte(schema.registrarActions.timestamp, BigInt(rules.startTime)), - lte(schema.registrarActions.timestamp, BigInt(rules.endTime)), + gte(ensIndexerSchema.registrarActions.timestamp, BigInt(rules.startTime)), + lte(ensIndexerSchema.registrarActions.timestamp, BigInt(rules.endTime)), // Filter by decodedReferrer not null - isNotNull(schema.registrarActions.decodedReferrer), + isNotNull(ensIndexerSchema.registrarActions.decodedReferrer), // Filter by decodedReferrer not zero address - ne(schema.registrarActions.decodedReferrer, zeroAddress), + ne(ensIndexerSchema.registrarActions.decodedReferrer, zeroAddress), // Filter by subregistryId matching the provided subregistryId - eq(schema.registrarActions.subregistryId, formatAccountId(rules.subregistryId)), + eq(ensIndexerSchema.registrarActions.subregistryId, formatAccountId(rules.subregistryId)), ), ) - .groupBy(schema.registrarActions.decodedReferrer) + .groupBy(ensIndexerSchema.registrarActions.decodedReferrer) .orderBy(desc(sql`total_incremental_duration`)); // Type assertion: The WHERE clause in the query above guarantees non-null values for: @@ -106,31 +105,31 @@ export const getReferrerMetrics = async ( */ export const getReferralEvents = async (rules: ReferralProgramRules): Promise => { try { - const records = await db + const records = await ensDb .select({ - id: schema.registrarActions.id, - referrer: schema.registrarActions.decodedReferrer, - timestamp: schema.registrarActions.timestamp, - incrementalDuration: schema.registrarActions.incrementalDuration, + id: ensIndexerSchema.registrarActions.id, + referrer: ensIndexerSchema.registrarActions.decodedReferrer, + timestamp: ensIndexerSchema.registrarActions.timestamp, + incrementalDuration: ensIndexerSchema.registrarActions.incrementalDuration, // Note: Using raw SQL for COALESCE because Drizzle doesn't natively support it yet. // See: https://github.com/drizzle-team/drizzle-orm/issues/3708 - total: sql`COALESCE(${schema.registrarActions.total}, 0)`.as("total"), + total: sql`COALESCE(${ensIndexerSchema.registrarActions.total}, 0)`.as("total"), }) - .from(schema.registrarActions) + .from(ensIndexerSchema.registrarActions) .where( and( // Filter by timestamp range - gte(schema.registrarActions.timestamp, BigInt(rules.startTime)), - lte(schema.registrarActions.timestamp, BigInt(rules.endTime)), + gte(ensIndexerSchema.registrarActions.timestamp, BigInt(rules.startTime)), + lte(ensIndexerSchema.registrarActions.timestamp, BigInt(rules.endTime)), // Filter by decodedReferrer not null - isNotNull(schema.registrarActions.decodedReferrer), + isNotNull(ensIndexerSchema.registrarActions.decodedReferrer), // Filter by decodedReferrer not zero address - ne(schema.registrarActions.decodedReferrer, zeroAddress), + ne(ensIndexerSchema.registrarActions.decodedReferrer, zeroAddress), // Filter by subregistryId matching the provided subregistryId - eq(schema.registrarActions.subregistryId, formatAccountId(rules.subregistryId)), + eq(ensIndexerSchema.registrarActions.subregistryId, formatAccountId(rules.subregistryId)), ), ) - .orderBy(asc(schema.registrarActions.id)); + .orderBy(asc(ensIndexerSchema.registrarActions.id)); // Type assertion: All fields in NonNullRecord are guaranteed non-null: // 1. `referrer` is guaranteed non-null by isNotNull WHERE filter diff --git a/apps/ensapi/src/lib/ensanalytics/referrer-leaderboard/database.ts b/apps/ensapi/src/lib/ensanalytics/referrer-leaderboard/database.ts index fef36f1a9..b8810d0e6 100644 --- a/apps/ensapi/src/lib/ensanalytics/referrer-leaderboard/database.ts +++ b/apps/ensapi/src/lib/ensanalytics/referrer-leaderboard/database.ts @@ -6,10 +6,9 @@ import { import { and, count, desc, eq, gte, isNotNull, lte, ne, sql, sum } from "drizzle-orm"; import { type Address, zeroAddress } from "viem"; -import * as schema from "@ensnode/ensdb-sdk"; import { deserializeDuration, formatAccountId } from "@ensnode/ensnode-sdk"; -import { db } from "@/lib/db"; +import { ensDb, ensIndexerSchema } from "@/lib/ensdb/singleton"; import logger from "@/lib/logger"; /** @@ -37,35 +36,35 @@ export const getReferrerMetrics = async ( */ try { - const records = await db + const records = await ensDb .select({ - referrer: schema.registrarActions.decodedReferrer, + referrer: ensIndexerSchema.registrarActions.decodedReferrer, totalReferrals: count().as("total_referrals"), - totalIncrementalDuration: sum(schema.registrarActions.incrementalDuration).as( + totalIncrementalDuration: sum(ensIndexerSchema.registrarActions.incrementalDuration).as( "total_incremental_duration", ), // Note: Using raw SQL for COALESCE because Drizzle doesn't natively support it yet. // See: https://github.com/drizzle-team/drizzle-orm/issues/3708 totalRevenueContribution: - sql`COALESCE(SUM(${schema.registrarActions.total}), 0)`.as( + sql`COALESCE(SUM(${ensIndexerSchema.registrarActions.total}), 0)`.as( "total_revenue_contribution", ), }) - .from(schema.registrarActions) + .from(ensIndexerSchema.registrarActions) .where( and( // Filter by timestamp range - gte(schema.registrarActions.timestamp, BigInt(rules.startTime)), - lte(schema.registrarActions.timestamp, BigInt(rules.endTime)), + gte(ensIndexerSchema.registrarActions.timestamp, BigInt(rules.startTime)), + lte(ensIndexerSchema.registrarActions.timestamp, BigInt(rules.endTime)), // Filter by decodedReferrer not null - isNotNull(schema.registrarActions.decodedReferrer), + isNotNull(ensIndexerSchema.registrarActions.decodedReferrer), // Filter by decodedReferrer not zero address - ne(schema.registrarActions.decodedReferrer, zeroAddress), + ne(ensIndexerSchema.registrarActions.decodedReferrer, zeroAddress), // Filter by subregistryId matching the provided subregistryId - eq(schema.registrarActions.subregistryId, formatAccountId(rules.subregistryId)), + eq(ensIndexerSchema.registrarActions.subregistryId, formatAccountId(rules.subregistryId)), ), ) - .groupBy(schema.registrarActions.decodedReferrer) + .groupBy(ensIndexerSchema.registrarActions.decodedReferrer) .orderBy(desc(sql`total_incremental_duration`)); // Type assertion: The WHERE clause in the query above guarantees non-null values for: diff --git a/apps/ensapi/src/lib/ensdb/singleton.ts b/apps/ensapi/src/lib/ensdb/singleton.ts new file mode 100644 index 000000000..73746c820 --- /dev/null +++ b/apps/ensapi/src/lib/ensdb/singleton.ts @@ -0,0 +1,22 @@ +import { EnsDbReader } from "@ensnode/ensdb-sdk"; + +import ensDbConfig from "@/config/ensdb-config"; + +const { databaseUrl: ensDbUrl, ensIndexerSchemaName } = ensDbConfig; + +/** + * Singleton instance of ENSDbReader for the ENSApi application. + */ +export const ensDbClient = new EnsDbReader(ensDbUrl, ensIndexerSchemaName); + +/** + * Convenience alias for {@link ensDbClient.ensDb} to be used for building + * custom ENSDb queries throughout the ENSApi codebase. + */ +export const ensDb = ensDbClient.ensDb; + +/** + * Convenience alias for {@link ensDbClient.ensIndexerSchema} to be used for building + * custom ENSDb queries throughout the ENSApi codebase. + */ +export const ensIndexerSchema = ensDbClient.ensIndexerSchema; diff --git a/apps/ensapi/src/lib/fetch-ensindexer-config.ts b/apps/ensapi/src/lib/fetch-ensindexer-config.ts deleted file mode 100644 index 0cd11beb8..000000000 --- a/apps/ensapi/src/lib/fetch-ensindexer-config.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { - deserializeENSIndexerPublicConfig, - deserializeErrorResponse, - type SerializedENSIndexerPublicConfig, -} from "@ensnode/ensnode-sdk"; - -export async function fetchENSIndexerConfig(url: URL) { - const response = await fetch(new URL(`/api/config`, url)); - const responseData = await response.json(); - - if (!response.ok) { - const errorResponse = deserializeErrorResponse(responseData); - throw new Error(`Fetching ENSNode Config Failed: ${errorResponse.message}`); - } - - return deserializeENSIndexerPublicConfig(responseData as SerializedENSIndexerPublicConfig); -} diff --git a/apps/ensapi/src/lib/name-tokens/find-name-tokens-for-domain.ts b/apps/ensapi/src/lib/name-tokens/find-name-tokens-for-domain.ts index 3062c2549..cca14cec1 100644 --- a/apps/ensapi/src/lib/name-tokens/find-name-tokens-for-domain.ts +++ b/apps/ensapi/src/lib/name-tokens/find-name-tokens-for-domain.ts @@ -2,7 +2,6 @@ import config from "@/config"; import { eq } from "drizzle-orm/sql"; -import * as schema from "@ensnode/ensdb-sdk"; import { type AccountId, bigIntToNumber, @@ -17,12 +16,12 @@ import { type UnixTimestamp, } from "@ensnode/ensnode-sdk"; -import { db } from "@/lib/db"; +import { ensDb, ensIndexerSchema } from "@/lib/ensdb/singleton"; interface FindRegisteredNameTokensForDomainRecord { - domains: typeof schema.subgraph_domain.$inferSelect; - nameTokens: typeof schema.nameTokens.$inferSelect; - registrationLifecycles: typeof schema.registrationLifecycles.$inferSelect; + domains: typeof ensIndexerSchema.subgraph_domain.$inferSelect; + nameTokens: typeof ensIndexerSchema.nameTokens.$inferSelect; + registrationLifecycles: typeof ensIndexerSchema.registrationLifecycles.$inferSelect; } /** @@ -32,21 +31,24 @@ interface FindRegisteredNameTokensForDomainRecord { async function _findRegisteredNameTokensForDomain( domainId: Node, ): Promise { - const query = db + const query = ensDb .select({ - nameTokens: schema.nameTokens, - registrationLifecycles: schema.registrationLifecycles, - domains: schema.subgraph_domain, + nameTokens: ensIndexerSchema.nameTokens, + registrationLifecycles: ensIndexerSchema.registrationLifecycles, + domains: ensIndexerSchema.subgraph_domain, }) - .from(schema.nameTokens) + .from(ensIndexerSchema.nameTokens) // join Registration Lifecycles associated with Name Tokens .innerJoin( - schema.registrationLifecycles, - eq(schema.nameTokens.domainId, schema.registrationLifecycles.node), + ensIndexerSchema.registrationLifecycles, + eq(ensIndexerSchema.nameTokens.domainId, ensIndexerSchema.registrationLifecycles.node), ) // join Domains associated with Name Tokens - .innerJoin(schema.subgraph_domain, eq(schema.nameTokens.domainId, schema.subgraph_domain.id)) - .where(eq(schema.nameTokens.domainId, domainId)); + .innerJoin( + ensIndexerSchema.subgraph_domain, + eq(ensIndexerSchema.nameTokens.domainId, ensIndexerSchema.subgraph_domain.id), + ) + .where(eq(ensIndexerSchema.nameTokens.domainId, domainId)); const records = await query; diff --git a/apps/ensapi/src/lib/protocol-acceleration/find-resolver.ts b/apps/ensapi/src/lib/protocol-acceleration/find-resolver.ts index b07233950..53cf2452d 100644 --- a/apps/ensapi/src/lib/protocol-acceleration/find-resolver.ts +++ b/apps/ensapi/src/lib/protocol-acceleration/find-resolver.ts @@ -25,7 +25,7 @@ import { type NormalizedName, } from "@ensnode/ensnode-sdk"; -import { db } from "@/lib/db"; +import { ensDb } from "@/lib/ensdb/singleton"; import { withActiveSpanAsync, withSpanAsync } from "@/lib/instrumentation/auto-span"; type FindResolverResult = @@ -212,7 +212,7 @@ async function findResolverWithIndex( // doesn't exist in its own storage, it directs the lookup to RegistryOld. We must encode // this logic here, so that the active resolver of unmigrated nodes can be correctly identified. // https://github.com/ensdomains/ens-contracts/blob/be53b9c25be5b2c7326f524bbd34a3939374ab1f/contracts/registry/ENSRegistryWithFallback.sol#L19 - const records = await db.query.domainResolverRelation.findMany({ + const records = await ensDb.query.domainResolverRelation.findMany({ where: (t, { inArray, and, or, eq }) => and( or( diff --git a/apps/ensapi/src/lib/protocol-acceleration/get-primary-name-from-index.ts b/apps/ensapi/src/lib/protocol-acceleration/get-primary-name-from-index.ts index 5eb0e6796..55d3f5b78 100644 --- a/apps/ensapi/src/lib/protocol-acceleration/get-primary-name-from-index.ts +++ b/apps/ensapi/src/lib/protocol-acceleration/get-primary-name-from-index.ts @@ -8,7 +8,7 @@ import { type Name, } from "@ensnode/ensnode-sdk"; -import { db } from "@/lib/db"; +import { ensDb } from "@/lib/ensdb/singleton"; import { withSpanAsync } from "@/lib/instrumentation/auto-span"; const tracer = trace.getTracer("get-primary-name"); @@ -27,7 +27,7 @@ export async function getENSIP19ReverseNameRecordFromIndex( "reverseNameRecord.findMany", { address, coinType: coinTypeReverseLabel(coinType) }, () => - db.query.reverseNameRecord.findMany({ + ensDb.query.reverseNameRecord.findMany({ where: (t, { and, inArray, eq }) => and( // address = address diff --git a/apps/ensapi/src/lib/protocol-acceleration/get-records-from-index.ts b/apps/ensapi/src/lib/protocol-acceleration/get-records-from-index.ts index 46fcb5579..4b1efbbdf 100644 --- a/apps/ensapi/src/lib/protocol-acceleration/get-records-from-index.ts +++ b/apps/ensapi/src/lib/protocol-acceleration/get-records-from-index.ts @@ -8,7 +8,7 @@ import { } from "@ensnode/ensnode-sdk"; import { staticResolverImplementsAddressRecordDefaulting } from "@ensnode/ensnode-sdk/internal"; -import { db } from "@/lib/db"; +import { ensDb } from "@/lib/ensdb/singleton"; import type { IndexedResolverRecords } from "@/lib/resolution/make-records-response"; const DEFAULT_EVM_COIN_TYPE_BIGINT = BigInt(DEFAULT_EVM_COIN_TYPE); @@ -22,7 +22,7 @@ export async function getRecordsFromIndex { - const records = (await db.query.resolverRecords.findFirst({ + const records = (await ensDb.query.resolverRecords.findFirst({ where: (t, { and, eq }) => and( // filter by specific resolver diff --git a/apps/ensapi/src/lib/registrar-actions/find-registrar-actions.ts b/apps/ensapi/src/lib/registrar-actions/find-registrar-actions.ts index 24f8cd647..faee5c0ec 100644 --- a/apps/ensapi/src/lib/registrar-actions/find-registrar-actions.ts +++ b/apps/ensapi/src/lib/registrar-actions/find-registrar-actions.ts @@ -1,6 +1,5 @@ import { and, count, desc, eq, gte, isNotNull, lte, not, type SQL } from "drizzle-orm/sql"; -import * as schema from "@ensnode/ensdb-sdk"; import { type BlockRef, bigIntToNumber, @@ -21,7 +20,7 @@ import { ZERO_ENCODED_REFERRER, } from "@ensnode/ensnode-sdk"; -import { db } from "@/lib/db"; +import { ensDb, ensIndexerSchema } from "@/lib/ensdb/singleton"; /** * Build SQL for order clause from provided order param. @@ -29,7 +28,7 @@ import { db } from "@/lib/db"; function buildOrderByClause(order: RegistrarActionsOrder): SQL { switch (order) { case RegistrarActionsOrders.LatestRegistrarActions: - return desc(schema.registrarActions.id); + return desc(ensIndexerSchema.registrarActions.id); } } @@ -42,13 +41,13 @@ function buildWhereClause(filters: RegistrarActionsFilter[] | undefined): SQL[] switch (filter.filterType) { case RegistrarActionsFilterTypes.BySubregistryNode: // apply subregistry node equality filter - return eq(schema.subregistries.node, filter.value); + return eq(ensIndexerSchema.subregistries.node, filter.value); case RegistrarActionsFilterTypes.WithEncodedReferral: { // apply referral encoded referrer inclusion filter const filterSql = and( - isNotNull(schema.registrarActions.encodedReferrer), - not(eq(schema.registrarActions.encodedReferrer, ZERO_ENCODED_REFERRER)), + isNotNull(ensIndexerSchema.registrarActions.encodedReferrer), + not(eq(ensIndexerSchema.registrarActions.encodedReferrer, ZERO_ENCODED_REFERRER)), ); // Invariant: filterSql is `SQL` object - should never occur @@ -63,15 +62,15 @@ function buildWhereClause(filters: RegistrarActionsFilter[] | undefined): SQL[] case RegistrarActionsFilterTypes.ByDecodedReferrer: // apply decoded referrer equality filter - return eq(schema.registrarActions.decodedReferrer, filter.value); + return eq(ensIndexerSchema.registrarActions.decodedReferrer, filter.value); case RegistrarActionsFilterTypes.BeginTimestamp: // apply begin timestamp filter (inclusive) - return gte(schema.registrarActions.timestamp, BigInt(filter.value)); + return gte(ensIndexerSchema.registrarActions.timestamp, BigInt(filter.value)); case RegistrarActionsFilterTypes.EndTimestamp: // apply end timestamp filter (inclusive) - return lte(schema.registrarActions.timestamp, BigInt(filter.value)); + return lte(ensIndexerSchema.registrarActions.timestamp, BigInt(filter.value)); default: // Invariant: Unknown filter type — should never occur @@ -98,25 +97,28 @@ interface FindRegistrarActionsOptions { export async function _countRegistrarActions( filters: RegistrarActionsFilter[] | undefined, ): Promise { - const countQuery = db + const countQuery = ensDb .select({ count: count(), }) - .from(schema.registrarActions) + .from(ensIndexerSchema.registrarActions) // join Registration Lifecycles associated with Registrar Actions .innerJoin( - schema.registrationLifecycles, - eq(schema.registrarActions.node, schema.registrationLifecycles.node), + ensIndexerSchema.registrationLifecycles, + eq(ensIndexerSchema.registrarActions.node, ensIndexerSchema.registrationLifecycles.node), ) // join Domains associated with Registration Lifecycles .innerJoin( - schema.subgraph_domain, - eq(schema.registrationLifecycles.node, schema.subgraph_domain.id), + ensIndexerSchema.subgraph_domain, + eq(ensIndexerSchema.registrationLifecycles.node, ensIndexerSchema.subgraph_domain.id), ) // join Subregistries associated with Registration Lifecycles .innerJoin( - schema.subregistries, - eq(schema.registrationLifecycles.subregistryId, schema.subregistries.subregistryId), + ensIndexerSchema.subregistries, + eq( + ensIndexerSchema.registrationLifecycles.subregistryId, + ensIndexerSchema.subregistries.subregistryId, + ), ) .where(and(...buildWhereClause(filters))); @@ -129,28 +131,31 @@ export async function _countRegistrarActions( * build a list of {@link NamedRegistrarAction} objects. */ export async function _findRegistrarActions(options: FindRegistrarActionsOptions) { - const query = db + const query = ensDb .select({ - registrarActions: schema.registrarActions, - registrationLifecycles: schema.registrationLifecycles, - subregistries: schema.subregistries, - domain: schema.subgraph_domain, + registrarActions: ensIndexerSchema.registrarActions, + registrationLifecycles: ensIndexerSchema.registrationLifecycles, + subregistries: ensIndexerSchema.subregistries, + domain: ensIndexerSchema.subgraph_domain, }) - .from(schema.registrarActions) + .from(ensIndexerSchema.registrarActions) // join Registration Lifecycles associated with Registrar Actions .innerJoin( - schema.registrationLifecycles, - eq(schema.registrarActions.node, schema.registrationLifecycles.node), + ensIndexerSchema.registrationLifecycles, + eq(ensIndexerSchema.registrarActions.node, ensIndexerSchema.registrationLifecycles.node), ) // join Domains associated with Registration Lifecycles .innerJoin( - schema.subgraph_domain, - eq(schema.registrationLifecycles.node, schema.subgraph_domain.id), + ensIndexerSchema.subgraph_domain, + eq(ensIndexerSchema.registrationLifecycles.node, ensIndexerSchema.subgraph_domain.id), ) // join Subregistries associated with Registration Lifecycles .innerJoin( - schema.subregistries, - eq(schema.registrationLifecycles.subregistryId, schema.subregistries.subregistryId), + ensIndexerSchema.subregistries, + eq( + ensIndexerSchema.registrationLifecycles.subregistryId, + ensIndexerSchema.subregistries.subregistryId, + ), ) .where(and(...buildWhereClause(options.filters))) .orderBy(buildOrderByClause(options.orderBy)) diff --git a/apps/ensindexer/src/config/environment.ts b/apps/ensindexer/src/config/environment.ts index ca2a46ffa..33092d21f 100644 --- a/apps/ensindexer/src/config/environment.ts +++ b/apps/ensindexer/src/config/environment.ts @@ -1,5 +1,5 @@ import type { - DatabaseEnvironment, + EnsIndexerDatabaseEnvironment, EnsIndexerUrlEnvironment, RpcEnvironment, } from "@ensnode/ensnode-sdk/internal"; @@ -11,7 +11,7 @@ import type { * their state in `process.env`. This interface is intended to be the source type which then gets * mapped/parsed into a structured configuration object like `ENSIndexerConfig`. */ -export type ENSIndexerEnvironment = DatabaseEnvironment & +export type ENSIndexerEnvironment = EnsIndexerDatabaseEnvironment & EnsIndexerUrlEnvironment & RpcEnvironment & { NAMESPACE?: string; diff --git a/apps/ensindexer/src/lib/ensdb/singleton.ts b/apps/ensindexer/src/lib/ensdb/singleton.ts index e667024d9..1250e4f41 100644 --- a/apps/ensindexer/src/lib/ensdb/singleton.ts +++ b/apps/ensindexer/src/lib/ensdb/singleton.ts @@ -2,9 +2,9 @@ import config from "@/config"; import { EnsDbWriter } from "@ensnode/ensdb-sdk"; -const { databaseUrl: ensDbConnectionString, databaseSchemaName: ensIndexerSchemaName } = config; +const { databaseUrl: ensDbUrl, databaseSchemaName: ensIndexerSchemaName } = config; /** * Singleton instance of ENSDbWriter for the ENSIndexer application. */ -export const ensDbClient = new EnsDbWriter(ensDbConnectionString, ensIndexerSchemaName); +export const ensDbClient = new EnsDbWriter(ensDbUrl, ensIndexerSchemaName); diff --git a/docker-compose.yml b/docker-compose.yml index f697e6332..7239f8066 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -7,6 +7,7 @@ services: environment: # Override environment variables to point to docker instances DATABASE_URL: postgresql://postgres:password@postgres:5432/postgres + DATABASE_SCHEMA: ensindexer_0 ENSRAINBOW_URL: http://ensrainbow:3223 ENSINDEXER_URL: http://ensindexer:42069 env_file: @@ -35,7 +36,7 @@ services: environment: # Override environment variables to point to docker instances DATABASE_URL: postgresql://postgres:password@postgres:5432/postgres - ENSINDEXER_URL: http://ensindexer:42069 + ENSINDEXER_SCHEMA_NAME: ensindexer_0 env_file: # NOTE: must define apps/ensapi/.env.local (see apps/ensapi/.env.local.example) # Copy .env.local.example to .env.local and configure all required values @@ -49,8 +50,6 @@ services: start_period: 1m start_interval: 1s depends_on: - ensindexer: - condition: service_healthy postgres: condition: service_started diff --git a/packages/ensdb-sdk/src/client/ensdb-reader.ts b/packages/ensdb-sdk/src/client/ensdb-reader.ts index 0eb3b1822..ac6b0b72e 100644 --- a/packages/ensdb-sdk/src/client/ensdb-reader.ts +++ b/packages/ensdb-sdk/src/client/ensdb-reader.ts @@ -22,6 +22,13 @@ import type { SerializedEnsNodeMetadataEnsIndexerPublicConfig, } from "./serialize/ensnode-metadata"; +/** + * Re-export the ENSDb Drizzle Client type for external use in building + * custom ENSDb queries with proper typing of the "concrete" ENSIndexer Schema + * from the ENSDbReader instance. + */ +export type { EnsDbDrizzleClient } from "../lib/drizzle"; + /** * ENSDb Reader * @@ -57,24 +64,24 @@ export class EnsDbReader< * This also identifies which ENSNode metadata records to read from the ENSNode Schema * as the ENSNode Schema is multi-tenant across ENSIndexer instances / ENSIndexer Schemas in an ENSDb. */ - protected ensIndexerSchemaName: string; + protected _ensIndexerSchemaName: string; protected _ensNodeSchema: EnsNodeSchema; /** - * @param ensDbConnectionString The connection string for Drizzle to connect to the ENSDb instance. + * @param ensDbUrl The connection string for Drizzle to connect to the ENSDb instance. * @param ensIndexerSchemaName The name of the ENSIndexer schema to read from in ENSDb, used to identify which ENSNode metadata records to read. */ - constructor(ensDbConnectionString: string, ensIndexerSchemaName: string) { + constructor(ensDbUrl: string, ensIndexerSchemaName: string) { const { concreteEnsIndexerSchema, ensNodeSchema } = buildIndividualEnsDbSchemas(ensIndexerSchemaName); - const ensDbDrizzleClient = buildEnsDbDrizzleClient( - ensDbConnectionString, + const ensDbDrizzleClient = buildEnsDbDrizzleClient( + ensDbUrl, concreteEnsIndexerSchema, ); this.drizzleClient = ensDbDrizzleClient; this._concreteEnsIndexerSchema = concreteEnsIndexerSchema; - this.ensIndexerSchemaName = ensIndexerSchemaName; + this._ensIndexerSchemaName = ensIndexerSchemaName; this._ensNodeSchema = ensNodeSchema; } @@ -83,7 +90,7 @@ export class EnsDbReader< * * Useful while working on complex queries for ENSDb. */ - get client(): EnsDbDrizzleClient { + get ensDb(): EnsDbDrizzleClient { return this.drizzleClient; } @@ -95,13 +102,20 @@ export class EnsDbReader< * * Note: using `ensIndexerSchema` name for this getter to make it read better * in the context of query building. For example: - * `this.client.select().from(this.ensIndexerSchema.event)` vs. - * `this.client.select().from(this.concreteEnsIndexerSchema.event)`. + * `this.ensDb.select().from(this.ensIndexerSchema.event)` vs. + * `this.ensDb.select().from(this.concreteEnsIndexerSchema.event)`. */ get ensIndexerSchema(): ConcreteEnsIndexerSchema { return this._concreteEnsIndexerSchema; } + /** + * Getter for the ENSIndexer Schema Name used by this ENSDbReader instance. + */ + get ensIndexerSchemaName(): string { + return this._ensIndexerSchemaName; + } + /** * Getter for the ENSNode Schema definition used in the Drizzle client * for ENSDb instance. @@ -172,7 +186,7 @@ export class EnsDbReader< private async getEnsNodeMetadata( metadata: Pick, ): Promise { - const result = await this.drizzleClient + const result = await this.ensDb .select() .from(this.ensNodeSchema.metadata) .where( diff --git a/packages/ensdb-sdk/src/client/ensdb-writer.ts b/packages/ensdb-sdk/src/client/ensdb-writer.ts index 725e9a619..2ebbd9e08 100644 --- a/packages/ensdb-sdk/src/client/ensdb-writer.ts +++ b/packages/ensdb-sdk/src/client/ensdb-writer.ts @@ -79,7 +79,7 @@ export class EnsDbWriter extends EnsDbReader { * @throws when upsert operation failed. */ private async upsertEnsNodeMetadata(metadata: SerializedEnsNodeMetadata): Promise { - await this.drizzleClient + await this.ensDb .insert(this.ensNodeSchema.metadata) .values({ ensIndexerSchemaName: this.ensIndexerSchemaName, diff --git a/packages/ensdb-sdk/src/index.ts b/packages/ensdb-sdk/src/index.ts index 0443c6f82..5ec76921e 100644 --- a/packages/ensdb-sdk/src/index.ts +++ b/packages/ensdb-sdk/src/index.ts @@ -1,2 +1 @@ export * from "./client"; -export * from "./ensindexer-abstract"; diff --git a/packages/ensnode-sdk/src/shared/config/environments.ts b/packages/ensnode-sdk/src/shared/config/environments.ts index 6f0f9a7bd..f4931608f 100644 --- a/packages/ensnode-sdk/src/shared/config/environments.ts +++ b/packages/ensnode-sdk/src/shared/config/environments.ts @@ -1,7 +1,15 @@ /** - * Environment variables for database configuration. + * Environment variables for ENSApi database configuration. */ -export interface DatabaseEnvironment { +export interface EnsApiDatabaseEnvironment { + DATABASE_URL?: string; + ENSINDEXER_SCHEMA_NAME?: string; +} + +/** + * Environment variables for ENSIndexer database configuration. + */ +export interface EnsIndexerDatabaseEnvironment { DATABASE_URL?: string; DATABASE_SCHEMA?: string; } diff --git a/packages/integration-test-env/package.json b/packages/integration-test-env/package.json index 069bd14e2..20638bb75 100644 --- a/packages/integration-test-env/package.json +++ b/packages/integration-test-env/package.json @@ -9,6 +9,7 @@ }, "dependencies": { "@ensnode/datasources": "workspace:*", + "@ensnode/ensdb-sdk": "workspace:*", "@ensnode/ensnode-sdk": "workspace:*", "@testcontainers/postgresql": "^11.12.0", "execa": "^9.6.1", diff --git a/packages/integration-test-env/src/orchestrator.ts b/packages/integration-test-env/src/orchestrator.ts index 9e8de4c17..e46ac7f3b 100644 --- a/packages/integration-test-env/src/orchestrator.ts +++ b/packages/integration-test-env/src/orchestrator.ts @@ -54,7 +54,6 @@ const ENSINDEXER_PORT = 42069; const ENSAPI_PORT = 4334; // Shared config -const ENSINDEXER_URL = `http://localhost:${ENSINDEXER_PORT}`; const ENSRAINBOW_URL = `http://localhost:${ENSRAINBOW_PORT}`; // Track resources for cleanup @@ -185,10 +184,15 @@ function spawnService( return subprocess; } -async function pollIndexingStatus(timeoutMs: number): Promise { - const client = new (await import("@ensnode/ensnode-sdk")).EnsIndexerClient({ - url: new URL(ENSINDEXER_URL), - }); +async function pollIndexingStatus( + ensDbUrl: string, + ensIndexerSchemaName: string, + timeoutMs: number, +): Promise { + const client = new (await import("@ensnode/ensdb-sdk")).EnsDbReader( + ensDbUrl, + ensIndexerSchemaName, + ); const start = Date.now(); log("Polling indexing status..."); @@ -196,10 +200,9 @@ async function pollIndexingStatus(timeoutMs: number): Promise { while (Date.now() - start < timeoutMs) { checkAborted(); try { - const status = await client.indexingStatus(); - if (status.responseCode === "ok") { - const omnichainStatus = - status.realtimeProjection.snapshot.omnichainSnapshot.omnichainStatus; + const snapshot = await client.getIndexingStatusSnapshot(); + if (snapshot !== undefined) { + const omnichainStatus = snapshot.omnichainSnapshot.omnichainStatus; log(`Omnichain status: ${omnichainStatus}`); if ( omnichainStatus === OmnichainIndexingStatusIds.Following || @@ -308,6 +311,9 @@ async function main() { await waitForHealth(`http://localhost:${ENSRAINBOW_PORT}/health`, 30_000, "ENSRainbow"); // Phase 3: Start ENSIndexer + const ENSINDEXER_URL = `http://localhost:${ENSINDEXER_PORT}`; + const ENSINDEXER_SCHEMA_NAME = "public"; + log("Starting ENSIndexer..."); spawnService( "pnpm", @@ -316,7 +322,7 @@ async function main() { { NAMESPACE: ENSNamespaceIds.EnsTestEnv, DATABASE_URL, - DATABASE_SCHEMA: "public", + DATABASE_SCHEMA: ENSINDEXER_SCHEMA_NAME, PLUGINS: "ensv2,protocol-acceleration", ENSRAINBOW_URL, ENSINDEXER_URL, @@ -328,7 +334,7 @@ async function main() { await waitForHealth(`http://localhost:${ENSINDEXER_PORT}/health`, 60_000, "ENSIndexer"); // Phase 4: Wait for indexing to complete - await pollIndexingStatus(30_000); + await pollIndexingStatus(DATABASE_URL, ENSINDEXER_SCHEMA_NAME, 30_000); // Phase 5: Start ENSApi log("Starting ENSApi..."); @@ -337,8 +343,8 @@ async function main() { ["start"], ENSAPI_DIR, { - ENSINDEXER_URL, DATABASE_URL, + ENSINDEXER_SCHEMA_NAME, }, "ensapi", ); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 803d80184..f6877a02e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -962,6 +962,9 @@ importers: '@ensnode/datasources': specifier: workspace:* version: link:../datasources + '@ensnode/ensdb-sdk': + specifier: workspace:* + version: link:../ensdb-sdk '@ensnode/ensnode-sdk': specifier: workspace:* version: link:../ensnode-sdk diff --git a/terraform/modules/ensindexer/main.tf b/terraform/modules/ensindexer/main.tf index 1c5d81204..847645f96 100644 --- a/terraform/modules/ensindexer/main.tf +++ b/terraform/modules/ensindexer/main.tf @@ -61,7 +61,7 @@ resource "render_web_service" "ensapi" { } env_vars = merge(local.common_variables, { - "ENSINDEXER_URL" = { value = "http://ensindexer-${var.ensnode_indexer_type}:10000" } + "ENSINDEXER_SCHEMA_NAME" = { value = var.database_schema }, }) # See https://render.com/docs/custom-domains