diff --git a/.changeset/brown-spies-press.md b/.changeset/brown-spies-press.md new file mode 100644 index 0000000000..b32c7ab772 --- /dev/null +++ b/.changeset/brown-spies-press.md @@ -0,0 +1,5 @@ +--- +"@ensnode/ensdb-sdk": minor +--- + +Added `destroy()` method to `EnsDbReader` class that allows cleaning up database connection resources when the connection is no longer needed. diff --git a/apps/ensapi/src/cache/indexing-status.cache.ts b/apps/ensapi/src/cache/indexing-status.cache.ts index 51453f1534..4777fdaf8e 100644 --- a/apps/ensapi/src/cache/indexing-status.cache.ts +++ b/apps/ensapi/src/cache/indexing-status.cache.ts @@ -5,7 +5,6 @@ import { SWRCache, } from "@ensnode/ensnode-sdk"; -import { ensDbClient } from "@/lib/ensdb/singleton"; import { lazyProxy } from "@/lib/lazy"; import { makeLogger } from "@/lib/logger"; @@ -30,6 +29,12 @@ export const indexingStatusCache = lazyProxy( () => new SWRCache({ fn: async function loadIndexingStatusSnapshot() { + // Async import `di` here to avoid circular dependency between this cache module and the DI container module. + // NOTE: It will not be required soon, as we plan to create a factory function for this cache + // that accepts the necessary dependencies as parameters, instead of importing from the DI container. + const di = await import("@/di").then((mod) => mod.default); + const { ensDbClient } = di.context; + try { const indexingMetadataContext = await ensDbClient.getIndexingMetadataContext(); diff --git a/apps/ensapi/src/cache/stack-info.cache.ts b/apps/ensapi/src/cache/stack-info.cache.ts index 253be6e3cb..cc1940c27f 100644 --- a/apps/ensapi/src/cache/stack-info.cache.ts +++ b/apps/ensapi/src/cache/stack-info.cache.ts @@ -10,7 +10,6 @@ import { } from "@ensnode/ensnode-sdk"; import { buildEnsApiPublicConfig } from "@/config/config.schema"; -import { ensDbClient } from "@/lib/ensdb/singleton"; import { lazyProxy } from "@/lib/lazy"; import logger from "@/lib/logger"; @@ -41,6 +40,12 @@ export const stackInfoCache = lazyProxy( () => new SWRCache({ fn: async function loadEnsNodeStackInfo() { + // Async import `di` here to avoid circular dependency between this cache module and the DI container module. + // NOTE: It will not be required soon, as we plan to create a factory function for this cache + // that accepts the necessary dependencies as parameters, instead of importing from the DI container. + const di = await import("@/di").then((mod) => mod.default); + const { ensDbClient } = di.context; + try { const indexingMetadataContext = await ensDbClient.getIndexingMetadataContext(); @@ -54,11 +59,6 @@ export const stackInfoCache = lazyProxy( throw new Error("Indexing Metadata Context was uninitialized in ENSDb."); } - // Async import `di` here to avoid circular dependency between this cache module and the DI container module. - // NOTE: It will not be required soon, as we plan to create a factory function for this cache - // that accepts the necessary dependencies as parameters, instead of importing from the DI container. - const di = await import("@/di").then((mod) => mod.default); - const ensIndexerStackInfo = indexingMetadataContext.stackInfo; const ensNodeStackInfo = buildEnsNodeStackInfo( buildEnsApiPublicConfig(di.context.ensApiConfig, ensIndexerStackInfo.ensIndexer), diff --git a/apps/ensapi/src/config/config.schema.test.ts b/apps/ensapi/src/config/config.schema.test.ts index a56083d87b..7535b6758c 100644 --- a/apps/ensapi/src/config/config.schema.test.ts +++ b/apps/ensapi/src/config/config.schema.test.ts @@ -1,20 +1,5 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import { ensApiVersionInfo } from "@/lib/version-info"; - -vi.mock("@/lib/ensdb/singleton", () => ({ - ensDbClient: { - getIndexingMetadataContext: vi.fn(async () => indexingMetadataContextInitialized), - }, -})); - -vi.mock("@/config/ensdb-config", () => ({ - default: { - ensDbUrl: "postgresql://user:password@localhost:5432/mydb", - ensIndexerSchemaName: "ensindexer_0", - }, -})); - import { ENSNamespaceIds } from "@ensnode/ensnode-sdk"; import { @@ -25,6 +10,7 @@ import { import { BASE_ENV, indexingMetadataContextInitialized } from "@/config/config.schema.mock"; import { ENSApi_DEFAULT_PORT } from "@/config/defaults"; import logger from "@/lib/logger"; +import { ensApiVersionInfo } from "@/lib/version-info"; vi.mock("@/lib/logger", () => ({ default: { diff --git a/apps/ensapi/src/config/config.singleton.test.ts b/apps/ensapi/src/config/config.singleton.test.ts index 3e05ce71eb..4e72408524 100644 --- a/apps/ensapi/src/config/config.singleton.test.ts +++ b/apps/ensapi/src/config/config.singleton.test.ts @@ -1,31 +1,100 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import di from "@/di"; + vi.mock("@/lib/logger", () => ({ default: { error: vi.fn(), info: vi.fn(), }, + makeLogger: vi.fn(() => ({ + error: vi.fn(), + info: vi.fn(), + })), +})); + +vi.mock("@ensnode/ensdb-sdk", async (importOriginal) => { + class MockEnsDbReader { + ensDb = { + $client: { + end: vi.fn().mockResolvedValue(undefined), + }, + }; + ensIndexerSchema = {}; + ensIndexerSchemaName = "ensindexer_test"; + async isHealthy() { + return true; + } + async destroy() {} + } + + const mod = await importOriginal(); + return { + ...mod, + EnsDbReader: MockEnsDbReader, + }; +}); + +vi.mock("@/cache/indexing-status.cache", () => ({ + indexingStatusCache: { + read: vi.fn().mockResolvedValue({}), + destroy: vi.fn(), + }, +})); + +vi.mock("@/cache/stack-info.cache", () => ({ + stackInfoCache: { + read: vi.fn().mockResolvedValue({}), + destroy: vi.fn(), + peek: vi.fn().mockReturnValue({ + ensIndexer: { namespace: "mainnet" }, + }), + }, +})); + +vi.mock("@/cache/referral-program-edition-set.cache", () => ({ + referralProgramEditionConfigSetCache: { + read: vi.fn().mockResolvedValue({}), + destroy: vi.fn(), + }, })); +vi.mock("viem", async (importOriginal) => { + const mod = await importOriginal(); + return { + ...mod, + createPublicClient: vi.fn().mockReturnValue({ + getBlockNumber: vi.fn().mockResolvedValue(1n), + }), + }; +}); + const VALID_ENSDB_URL = "postgresql://user:password@localhost:5432/mydb"; const VALID_ENSINDEXER_SCHEMA_NAME = "ensindexer_test"; describe("ensdb singleton bootstrap", () => { beforeEach(() => { - vi.resetModules(); vi.stubEnv("ENSDB_URL", VALID_ENSDB_URL); vi.stubEnv("ENSINDEXER_SCHEMA_NAME", VALID_ENSINDEXER_SCHEMA_NAME); + vi.stubEnv("RPC_URL_1", "https://rpc.example.com"); }); - afterEach(() => { + afterEach(async () => { + // Restore env before destroying to prevent validation errors during cleanup vi.unstubAllEnvs(); + // Destroy might fail if init failed, but we want to clean up regardless + try { + await di.destroy(); + } catch { + // If destroy fails due to process.exit mock or other issues, force reset + // @ts-expect-error - accessing private member for test cleanup + di._context = undefined; + } }); it("constructs EnsDbReader from real env wiring without errors", async () => { - const { ensDbClient, ensDb, ensIndexerSchema } = await import("@/lib/ensdb/singleton"); - - // ensDbClient is a lazyProxy — construction is deferred until first property access. - // Accessing a property triggers EnsDbReader construction; verify it succeeds. + await di.init(); + const { ensDbClient, ensDb, ensIndexerSchema } = di.context; expect(ensDbClient.ensIndexerSchemaName).toBe(VALID_ENSINDEXER_SCHEMA_NAME); expect(ensDb).toBeDefined(); expect(ensIndexerSchema).toBeDefined(); @@ -38,14 +107,14 @@ describe("ensdb singleton bootstrap", () => { const { default: logger } = await import("@/lib/logger"); vi.stubEnv("ENSDB_URL", ""); - // ensDbClient is a lazyProxy — import succeeds but first property access triggers construction, - // which calls buildEnsDbConfigFromEnvironment and exits on invalid config. - const { ensDbClient } = await import("@/lib/ensdb/singleton"); - expect(() => ensDbClient.ensDb).toThrow("process.exit"); - - expect(logger.error).toHaveBeenCalled(); - expect(mockExit).toHaveBeenCalledWith(1); - mockExit.mockRestore(); + + try { + await expect(di.init()).rejects.toThrow("process.exit"); + expect(logger.error).toHaveBeenCalled(); + expect(mockExit).toHaveBeenCalledWith(1); + } finally { + mockExit.mockRestore(); + } }); it("exits when ENSINDEXER_SCHEMA_NAME is missing", async () => { @@ -55,13 +124,13 @@ describe("ensdb singleton bootstrap", () => { const { default: logger } = await import("@/lib/logger"); vi.stubEnv("ENSINDEXER_SCHEMA_NAME", ""); - // ensDbClient is a lazyProxy — import succeeds but first property access triggers construction, - // which calls buildEnsDbConfigFromEnvironment and exits on invalid config. - const { ensDbClient } = await import("@/lib/ensdb/singleton"); - expect(() => ensDbClient.ensDb).toThrow("process.exit"); - - expect(logger.error).toHaveBeenCalled(); - expect(mockExit).toHaveBeenCalledWith(1); - mockExit.mockRestore(); + + try { + await expect(di.init()).rejects.toThrow("process.exit"); + expect(logger.error).toHaveBeenCalled(); + expect(mockExit).toHaveBeenCalledWith(1); + } finally { + mockExit.mockRestore(); + } }); }); diff --git a/apps/ensapi/src/config/ensdb-config.ts b/apps/ensapi/src/config/ensdb-config.ts index 60b11a0547..b66a0d4632 100644 --- a/apps/ensapi/src/config/ensdb-config.ts +++ b/apps/ensapi/src/config/ensdb-config.ts @@ -1,7 +1,7 @@ import { type EnsDbConfig, validateEnsDbConfig } from "@ensnode/ensdb-sdk"; import type { Unvalidated } from "@ensnode/ensnode-sdk"; +import type { EnsDbEnvironment } from "@ensnode/ensnode-sdk/internal"; -import { lazyProxy } from "@/lib/lazy"; import logger from "@/lib/logger"; /** @@ -9,7 +9,7 @@ import logger from "@/lib/logger"; * * Exits the process if the configuration is invalid, logging the error details. */ -export function buildEnsDbConfigFromEnvironment(env: NodeJS.ProcessEnv): EnsDbConfig { +export function buildEnsDbConfigFromEnvironment(env: EnsDbEnvironment): EnsDbConfig { const unvalidatedConfig = { ensDbUrl: env.ENSDB_URL, ensIndexerSchemaName: env.ENSINDEXER_SCHEMA_NAME, @@ -23,9 +23,3 @@ export function buildEnsDbConfigFromEnvironment(env: NodeJS.ProcessEnv): EnsDbCo process.exit(1); } } - -// lazyProxy defers construction until first use so that this module can be -// imported without env vars being present (e.g. during OpenAPI generation). -const ensDbConfig = lazyProxy(() => buildEnsDbConfigFromEnvironment(process.env)); - -export default ensDbConfig; diff --git a/apps/ensapi/src/di.ts b/apps/ensapi/src/di.ts index 8293f8fb33..549af95009 100644 --- a/apps/ensapi/src/di.ts +++ b/apps/ensapi/src/di.ts @@ -2,7 +2,7 @@ import type { ChainId } from "enssdk"; import { createPublicClient, fallback, http, type PublicClient } from "viem"; import { type ENSNamespaceId, getENSRootChainId } from "@ensnode/datasources"; -import type { EnsDbConfig, EnsDbReader } from "@ensnode/ensdb-sdk"; +import { type EnsDbConfig, EnsDbReader } from "@ensnode/ensdb-sdk"; import type { EnsNodeStackInfo } from "@ensnode/ensnode-sdk"; import type { RpcConfig } from "@ensnode/ensnode-sdk/internal"; @@ -15,9 +15,8 @@ import type { EnsNodeStackInfoCache } from "@/cache/stack-info.cache"; import { stackInfoCache } from "@/cache/stack-info.cache"; import type { EnsApiConfig } from "@/config/config.schema"; import { buildConfigFromEnvironment, buildRootChainRpcConfig } from "@/config/config.schema"; -import ensDbConfig from "@/config/ensdb-config"; +import { buildEnsDbConfigFromEnvironment } from "@/config/ensdb-config"; import type { EnsApiEnvironment } from "@/config/environment"; -import { ensDbClient } from "@/lib/ensdb/singleton"; import { makeLogger } from "@/lib/logger"; const logger = makeLogger("di"); @@ -106,13 +105,18 @@ export function buildEnsApiDiContext(ensApiEnvironment: EnsApiEnvironment): EnsA get ensDbConfig(): EnsDbConfig { if (instances.ensDbConfig === undefined) { - instances.ensDbConfig = ensDbConfig; + instances.ensDbConfig = buildEnsDbConfigFromEnvironment(ensApiEnvironment); } return instances.ensDbConfig; }, get ensDbClient(): EnsDbReader { - return ensDbClient; + if (instances.ensDbClient === undefined) { + const { ensDbUrl, ensIndexerSchemaName } = context.ensDbConfig; + instances.ensDbClient = new EnsDbReader(ensDbUrl, ensIndexerSchemaName); + } + + return instances.ensDbClient; }, get ensDb(): EnsDbReader["ensDb"] { @@ -240,9 +244,10 @@ class EnsApiDiContainer { /** * Initializes the DI container by loading the context and initializing - * necessary resources. + * necessary resources that need to be evaluated eagerly, such as + * ENSDb client, caches, RPC client for the ENS Root Chain, etc. */ - init(): void { + async init(): Promise { if (this._context) { throw new Error( "DI context has already been initialized. If you want to re-initialize, call `di.destroy()` first to clean up resources.", @@ -252,19 +257,67 @@ class EnsApiDiContainer { // Load the DI context this.loadContext(); - logger.info("Initializing caches"); - void Promise.all([ - this.context.indexingStatusCache.read(), - this.context.stackInfoCache.read(), - this.context.referralProgramEditionConfigSetCache.read(), - ]).then(() => logger.info("Caches initialized")); + try { + // Initialize the ENSDb client and verify connectivity to the database. + logger.info("Initializing ENSDb client and verifying connectivity to ENSDb"); + const isEnsDbHealthy = await this.context.ensDbClient.isHealthy(); + + if (!isEnsDbHealthy) { + throw new Error("ENSDb health check failed"); + } + + logger.info( + { ensIndexerSchemaName: this.context.ensDbConfig.ensIndexerSchemaName }, + "Successfully connected to ENSDb", + ); + } catch (error) { + const errorMessage = error instanceof Error ? error.message : "Unknown error"; + throw new Error( + `DI container initialization failed: could not connect to ENSDb due to ${errorMessage}`, + ); + } + + try { + // Initialize caches + logger.info("Initializing caches"); + const [indexingStatus, stackInfo, referralProgramEditionConfigSet] = await Promise.all([ + this.context.indexingStatusCache.read(), + this.context.stackInfoCache.read(), + this.context.referralProgramEditionConfigSetCache.read(), + ]); + logger.info( + { indexingStatus, stackInfo, referralProgramEditionConfigSet }, + "Caches initialized", + ); + } catch (error) { + const errorMessage = error instanceof Error ? error.message : "Unknown error"; + throw new Error( + `DI container initialization failed: cache initialization error due to ${errorMessage}`, + ); + } + + // Initialize the RPC client for the ENS Root Chain by making a simple call to + // verify connectivity. + try { + logger.info("Initializing RPC client for the ENS Root Chain"); + await this.context.rootChainPublicClient.getBlockNumber(); + logger.info( + { rootChainId: this.context.rootChainId }, + "Successfully connected to the ENS Root Chain RPC", + ); + } catch (error) { + const errorMessage = error instanceof Error ? error.message : "Unknown error"; + throw new Error( + `DI container initialization failed: could not connect to ENS Root Chain RPC due to ${errorMessage}`, + ); + } } /** * Destroys any resources held by the DI container, such as caches, to * allow for clean shutdown or re-initialization. */ - destroy(): void { + async destroy(): Promise { if (!this._context) { logger.warn( "DI context is not loaded, so there are no resources to destroy. If you are trying to reload the context, call `di.init()` to load the context and initialize necessary resources.", @@ -279,6 +332,10 @@ class EnsApiDiContainer { this.context.referralProgramEditionConfigSetCache.destroy(); logger.info("Caches destroyed"); + // Destroy the ENSDb client to close the connection pool to ENSDb + await this.context.ensDbClient.destroy(); + logger.info("ENSDb client destroyed"); + this._context = undefined; } diff --git a/apps/ensapi/src/handlers/ensapi-probes/ensapi-probes-api.ts b/apps/ensapi/src/handlers/ensapi-probes/ensapi-probes-api.ts index e3c655e735..38b58133cb 100644 --- a/apps/ensapi/src/handlers/ensapi-probes/ensapi-probes-api.ts +++ b/apps/ensapi/src/handlers/ensapi-probes/ensapi-probes-api.ts @@ -1,8 +1,8 @@ +import di from "@/di"; import { healthCheckRoute, readinessCheckRoute, } from "@/handlers/ensapi-probes/ensapi-probes-api.routes"; -import { ensDbClient } from "@/lib/ensdb/singleton"; import { createApp } from "@/lib/hono-factory"; import logger from "@/lib/logger"; @@ -10,6 +10,7 @@ const app = createApp(); app.openapi(healthCheckRoute, async (c) => { try { + const { ensDbClient } = di.context; const isEnsDbHealthy = await ensDbClient.isHealthy(); if (!isEnsDbHealthy) { @@ -25,6 +26,7 @@ app.openapi(healthCheckRoute, async (c) => { app.openapi(readinessCheckRoute, async (c) => { try { + const { ensDbClient } = di.context; const isEnsDbReady = await ensDbClient.isReady(); if (!isEnsDbReady) { diff --git a/apps/ensapi/src/index.ts b/apps/ensapi/src/index.ts index d8e660ed8c..2decd9e754 100644 --- a/apps/ensapi/src/index.ts +++ b/apps/ensapi/src/index.ts @@ -11,8 +11,12 @@ import app from "./app"; // start ENSNode API OpenTelemetry SDK sdk.start(); -// initialize DI container and its resources -di.init(); +// initialize DI container and its resources in a non-blocking way to +// allow HTTP server to start immediately and serve requests +void di.init().catch((error) => { + logger.error(error, "Error initializing DI container"); + process.exit(1); +}); // start hono server const server = serve( @@ -61,7 +65,7 @@ const gracefulShutdown = async () => { } // Destroy DI container resources - di.destroy(); + await di.destroy(); process.exit(0); } catch (error) { diff --git a/apps/ensapi/src/lib/ensanalytics/referrer-leaderboard/database.ts b/apps/ensapi/src/lib/ensanalytics/referrer-leaderboard/database.ts index 7227936af5..7a74013245 100644 --- a/apps/ensapi/src/lib/ensanalytics/referrer-leaderboard/database.ts +++ b/apps/ensapi/src/lib/ensanalytics/referrer-leaderboard/database.ts @@ -16,7 +16,7 @@ import { zeroAddress } from "viem"; import { deserializeDuration, priceEth, RegistrarActionTypes } from "@ensnode/ensnode-sdk"; -import { ensDb, ensIndexerSchema } from "@/lib/ensdb/singleton"; +import di from "@/di"; import logger from "@/lib/logger"; /** @@ -44,6 +44,7 @@ export const getReferrerMetrics = async ( */ try { + const { ensDb, ensIndexerSchema } = di.context; const records = await ensDb .select({ referrer: ensIndexerSchema.registrarActions.decodedReferrer, @@ -115,6 +116,7 @@ export const getReferrerMetrics = async ( */ export const getReferralEvents = async (rules: ReferralProgramRules): Promise => { try { + const { ensDb, ensIndexerSchema } = di.context; const records = await ensDb .select({ id: ensIndexerSchema.registrarActions.id, diff --git a/apps/ensapi/src/lib/ensdb/singleton.ts b/apps/ensapi/src/lib/ensdb/singleton.ts deleted file mode 100644 index e718911e24..0000000000 --- a/apps/ensapi/src/lib/ensdb/singleton.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { EnsDbReader } from "@ensnode/ensdb-sdk"; - -import { buildEnsDbConfigFromEnvironment } from "@/config/ensdb-config"; -import { lazyProxy } from "@/lib/lazy"; - -// lazyProxy defers construction until first use so that this module can be -// imported without env vars being present (e.g. during OpenAPI generation). - -/** - * Singleton instance of ENSDbReader for the ENSApi application. - */ -export const ensDbClient = lazyProxy(() => { - const { ensDbUrl, ensIndexerSchemaName } = buildEnsDbConfigFromEnvironment(process.env); - return 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 = lazyProxy(() => ensDbClient.ensDb); - -/** - * Convenience alias for {@link ensDbClient.ensIndexerSchema} to be used for building - * custom ENSDb queries throughout the ENSApi codebase. - */ -export const ensIndexerSchema = lazyProxy( - () => ensDbClient.ensIndexerSchema, -); 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 e56981302a..ec432e8055 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 @@ -12,12 +12,11 @@ import { } from "@ensnode/ensnode-sdk"; import di from "@/di"; -import { ensDb, ensIndexerSchema } from "@/lib/ensdb/singleton"; interface FindRegisteredNameTokensForDomainRecord { - domains: typeof ensIndexerSchema.subgraph_domain.$inferSelect; - nameTokens: typeof ensIndexerSchema.nameTokens.$inferSelect; - registrationLifecycles: typeof ensIndexerSchema.registrationLifecycles.$inferSelect; + domains: typeof di.context.ensIndexerSchema.subgraph_domain.$inferSelect; + nameTokens: typeof di.context.ensIndexerSchema.nameTokens.$inferSelect; + registrationLifecycles: typeof di.context.ensIndexerSchema.registrationLifecycles.$inferSelect; } /** @@ -27,6 +26,7 @@ interface FindRegisteredNameTokensForDomainRecord { async function _findRegisteredNameTokensForDomain( domainId: Node, ): Promise { + const { ensDb, ensIndexerSchema } = di.context; const query = ensDb .select({ nameTokens: ensIndexerSchema.nameTokens, diff --git a/apps/ensapi/src/lib/protocol-acceleration/find-resolver.ts b/apps/ensapi/src/lib/protocol-acceleration/find-resolver.ts index d009afa039..ad9cee0283 100644 --- a/apps/ensapi/src/lib/protocol-acceleration/find-resolver.ts +++ b/apps/ensapi/src/lib/protocol-acceleration/find-resolver.ts @@ -17,7 +17,6 @@ import { DatasourceNames, getDatasource } from "@ensnode/datasources"; import { accountIdEqual, isENSv1Registry } from "@ensnode/ensnode-sdk"; import di from "@/di"; -import { ensDb } from "@/lib/ensdb/singleton"; import { withActiveSpanAsync, withSpanAsync } from "@/lib/instrumentation/auto-span"; type FindResolverResult = @@ -198,6 +197,7 @@ async function findResolverWithIndex( async () => { // NOTE: because DRRs are now canonicalized against the managedName's Registry, we no longer // need to also join ENSv1RegistryOld DRRs if the registry is the ENSv1Registry + const { ensDb } = di.context; const records = await ensDb.query.domainResolverRelation.findMany({ where: (t, { inArray, and, eq }) => and( 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 830b5d125e..ef2313a690 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 @@ -7,7 +7,7 @@ import { type NormalizedAddress, } from "enssdk"; -import { ensDb } from "@/lib/ensdb/singleton"; +import di from "@/di"; import { withSpanAsync } from "@/lib/instrumentation/auto-span"; const tracer = trace.getTracer("get-primary-name"); @@ -25,8 +25,9 @@ export async function getENSIP19ReverseNameRecordFromIndex( tracer, "reverseNameRecord.findMany", { address, coinType: coinTypeReverseLabel(coinType) }, - () => - ensDb.query.reverseNameRecord.findMany({ + () => { + const { ensDb } = di.context; + return ensDb.query.reverseNameRecord.findMany({ where: (t, { and, inArray, eq }) => and( // address = address @@ -35,7 +36,8 @@ export async function getENSIP19ReverseNameRecordFromIndex( inArray(t.coinType, [_coinType, DEFAULT_EVM_COIN_TYPE_BIGINT]), ), columns: { coinType: true, value: true }, - }), + }); + }, ); const coinTypeName = records.find((pn) => pn.coinType === _coinType)?.value ?? null; 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 4c68cd4493..f752cba80c 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 @@ -4,7 +4,6 @@ import type { ResolverRecordsSelection } from "@ensnode/ensnode-sdk"; import { staticResolverImplementsAddressRecordDefaulting } from "@ensnode/ensnode-sdk/internal"; import di from "@/di"; -import { ensDb } from "@/lib/ensdb/singleton"; const DEFAULT_EVM_COIN_TYPE_BIGINT = BigInt(DEFAULT_EVM_COIN_TYPE); @@ -17,6 +16,7 @@ export async function getRecordsFromIndex and( 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 73d8b56890..d72bea3439 100644 --- a/apps/ensapi/src/lib/registrar-actions/find-registrar-actions.ts +++ b/apps/ensapi/src/lib/registrar-actions/find-registrar-actions.ts @@ -21,7 +21,7 @@ import { ZERO_ENCODED_REFERRER, } from "@ensnode/ensnode-sdk"; -import { ensDb, ensIndexerSchema } from "@/lib/ensdb/singleton"; +import di from "@/di"; import { withSpanAsync } from "@/lib/instrumentation/auto-span"; const tracer = trace.getTracer("registrar-actions"); @@ -30,6 +30,7 @@ const tracer = trace.getTracer("registrar-actions"); * Build SQL for order clause from provided order param. */ function buildOrderByClause(order: RegistrarActionsOrder): SQL { + const { ensIndexerSchema } = di.context; switch (order) { case RegistrarActionsOrders.LatestRegistrarActions: return desc(ensIndexerSchema.registrarActions.id); @@ -40,6 +41,7 @@ function buildOrderByClause(order: RegistrarActionsOrder): SQL { * Build SQL for where clause from provided filter param. */ function buildWhereClause(filters: RegistrarActionsFilter[] | undefined): SQL[] { + const { ensIndexerSchema } = di.context; const binaryOperators: SQL[] = filters?.map((filter) => { switch (filter.filterType) { @@ -101,6 +103,7 @@ interface FindRegistrarActionsOptions { export async function _countRegistrarActions( filters: RegistrarActionsFilter[] | undefined, ): Promise { + const { ensDb, ensIndexerSchema } = di.context; return withSpanAsync( tracer, "registrarActions.count", @@ -132,6 +135,7 @@ export async function _countRegistrarActions( * build a list of {@link NamedRegistrarAction} objects. */ export async function _findRegistrarActions(options: FindRegistrarActionsOptions) { + const { ensDb, ensIndexerSchema } = di.context; return withSpanAsync( tracer, "registrarActions.find", diff --git a/apps/ensapi/src/omnigraph-api/context.ts b/apps/ensapi/src/omnigraph-api/context.ts index f6ac40df64..51b65489e6 100644 --- a/apps/ensapi/src/omnigraph-api/context.ts +++ b/apps/ensapi/src/omnigraph-api/context.ts @@ -3,10 +3,11 @@ import { getUnixTime } from "date-fns"; import { inArray } from "drizzle-orm"; import type { DomainId, RegistryId } from "enssdk"; -import { ensDb, ensIndexerSchema } from "@/lib/ensdb/singleton"; +import di from "@/di"; const createRegistryParentDomainLoader = () => new DataLoader(async (registryIds) => { + const { ensDb, ensIndexerSchema } = di.context; const rows = await ensDb .select({ id: ensIndexerSchema.registry.id, diff --git a/apps/ensapi/src/omnigraph-api/lib/find-domains/find-domains-resolver-helpers.ts b/apps/ensapi/src/omnigraph-api/lib/find-domains/find-domains-resolver-helpers.ts index d0d927788c..513950f549 100644 --- a/apps/ensapi/src/omnigraph-api/lib/find-domains/find-domains-resolver-helpers.ts +++ b/apps/ensapi/src/omnigraph-api/lib/find-domains/find-domains-resolver-helpers.ts @@ -1,6 +1,6 @@ import { asc, desc, type SQL, sql } from "drizzle-orm"; -import { ensIndexerSchema } from "@/lib/ensdb/singleton"; +import di from "@/di"; import type { DomainCursor } from "@/omnigraph-api/lib/find-domains/domain-cursor"; import type { DomainsOrderBy } from "@/omnigraph-api/schema/domain-inputs"; import type { OrderDirection } from "@/omnigraph-api/schema/order-direction"; @@ -51,6 +51,7 @@ export function truncateNameForCursor(name: string | null): string | null { * config wired up). */ function getOrderColumn(orderBy: typeof DomainsOrderBy.$inferType): SQL { + const { ensIndexerSchema } = di.context; switch (orderBy) { case "NAME": return sql`left(${ensIndexerSchema.domain.canonicalName}, ${sql.raw(String(CANONICAL_NAME_SORT_PREFIX))})`; @@ -100,6 +101,7 @@ export function cursorFilter( // "after" with ASC and "before" with DESC both step forward in cursor order (greater-than). const useGreaterThan = (direction === "after") !== (queryOrderDir === "DESC"); const op = sql.raw(useGreaterThan ? ">" : "<"); + const { ensIndexerSchema } = di.context; const idCmp = sql`${ensIndexerSchema.domain.id} ${op} ${cursor.id}`; // NULL cursor values need explicit handling because Postgres tuple comparison with NULL yields @@ -154,6 +156,7 @@ export function orderFindDomains( ? sql`${orderColumn} DESC NULLS LAST` : sql`${orderColumn} ASC NULLS LAST`; + const { ensIndexerSchema } = di.context; // Always include id as tiebreaker for stable ordering const tiebreaker = effectiveDesc ? desc(ensIndexerSchema.domain.id) diff --git a/apps/ensapi/src/omnigraph-api/lib/find-domains/find-domains-resolver.ts b/apps/ensapi/src/omnigraph-api/lib/find-domains/find-domains-resolver.ts index 663c366f69..a433bb46fb 100644 --- a/apps/ensapi/src/omnigraph-api/lib/find-domains/find-domains-resolver.ts +++ b/apps/ensapi/src/omnigraph-api/lib/find-domains/find-domains-resolver.ts @@ -3,7 +3,7 @@ import { type ResolveCursorConnectionArgs, resolveCursorConnection } from "@poth import { and, count, eq, ilike, inArray, type SQL, sql } from "drizzle-orm"; import type { NormalizedAddress, RegistryId } from "enssdk"; -import { ensDb, ensIndexerSchema } from "@/lib/ensdb/singleton"; +import di from "@/di"; import { withActiveSpanAsync } from "@/lib/instrumentation/auto-span"; import { makeLogger } from "@/lib/logger"; import type { context as createContext } from "@/omnigraph-api/context"; @@ -53,7 +53,7 @@ export interface DomainsWhere { const VERSION_TO_DOMAIN_TYPE: Record< typeof ENSProtocolVersion.$inferType, - (typeof ensIndexerSchema.domainType.enumValues)[number] + (typeof di.context.ensIndexerSchema.domainType.enumValues)[number] > = { ENSv1: "ENSv1Domain", ENSv2: "ENSv2Domain", @@ -63,6 +63,7 @@ const VERSION_TO_DOMAIN_TYPE: Record< * Build the SQL condition for `where.name`. */ function nameCondition(filter: typeof DomainsNameFilter.$inferInput): SQL { + const { ensIndexerSchema } = di.context; if (filter.starts_with) { return ilike(ensIndexerSchema.domain.canonicalName, `${filter.starts_with}%`); } @@ -122,6 +123,8 @@ export function resolveFindDomains( const needsRegistrationJoin = orderBy === "REGISTRATION_TIMESTAMP" || orderBy === "REGISTRATION_EXPIRY"; + const { ensIndexerSchema } = di.context; + const filterConditions = and( // by ownerId where?.ownerId ? eq(ensIndexerSchema.domain.ownerId, where.ownerId) : undefined, @@ -142,6 +145,7 @@ export function resolveFindDomains( return lazyConnection({ totalCount: () => withActiveSpanAsync(tracer, "find-domains.totalCount", {}, async () => { + const { ensDb } = di.context; const rows = await ensDb .select({ count: count() }) .from(ensIndexerSchema.domain) @@ -184,6 +188,7 @@ export function resolveFindDomains( } })(); + const { ensDb } = di.context; let query = ensDb .select({ id: ensIndexerSchema.domain.id, diff --git a/apps/ensapi/src/omnigraph-api/lib/find-events/find-events-resolver.ts b/apps/ensapi/src/omnigraph-api/lib/find-events/find-events-resolver.ts index fe98624d1a..d218bf5a77 100644 --- a/apps/ensapi/src/omnigraph-api/lib/find-events/find-events-resolver.ts +++ b/apps/ensapi/src/omnigraph-api/lib/find-events/find-events-resolver.ts @@ -15,7 +15,7 @@ import { } from "drizzle-orm"; import type { Address, Hex } from "enssdk"; -import { ensDb, ensIndexerSchema } from "@/lib/ensdb/singleton"; +import di from "@/di"; import { orderPaginationBy, paginateBy } from "@/omnigraph-api/lib/connection-helpers"; import { lazyConnection } from "@/omnigraph-api/lib/lazy-connection"; import { ID_PAGINATED_CONNECTION_ARGS } from "@/omnigraph-api/schema/constants"; @@ -24,10 +24,10 @@ import { ID_PAGINATED_CONNECTION_ARGS } from "@/omnigraph-api/schema/constants"; * A join table that relates some entity to events via an `eventId` column. */ type EventJoinTable = - | typeof ensIndexerSchema.domainEvent - | typeof ensIndexerSchema.resolverEvent - | typeof ensIndexerSchema.permissionsEvent - | typeof ensIndexerSchema.permissionsUserEvent; + | typeof di.context.ensIndexerSchema.domainEvent + | typeof di.context.ensIndexerSchema.resolverEvent + | typeof di.context.ensIndexerSchema.permissionsEvent + | typeof di.context.ensIndexerSchema.permissionsUserEvent; /** * @oneOf set-membership filter shape: exactly one of `eq` or `in` is set. @@ -85,6 +85,8 @@ function rangeFilterCondition( function eventsWhereConditions(where?: EventsWhere | null): SQL | undefined { if (!where) return undefined; + const { ensIndexerSchema } = di.context; + return and( setFilterCondition(ensIndexerSchema.event.selector, where.selector), rangeFilterCondition(ensIndexerSchema.event.timestamp, where.timestamp), @@ -125,6 +127,7 @@ export function resolveFindEvents( return lazyConnection({ totalCount: () => { + const { ensDb, ensIndexerSchema } = di.context; // note: not possible to dynamically change the .select() columns so we make a new query let query = ensDb.select({ count: count() }).from(ensIndexerSchema.event).$dynamic(); if (through) { @@ -143,6 +146,7 @@ export function resolveFindEvents( args, }, ({ before, after, limit, inverted }: ResolveCursorConnectionArgs) => { + const { ensDb, ensIndexerSchema } = di.context; // note: not possible to dynamically change the .select() columns so we make a new query let query = ensDb .select(getTableColumns(ensIndexerSchema.event)) diff --git a/apps/ensapi/src/omnigraph-api/lib/get-domain-by-interpreted-name.ts b/apps/ensapi/src/omnigraph-api/lib/get-domain-by-interpreted-name.ts index 745c1ec0cd..aad86518bf 100644 --- a/apps/ensapi/src/omnigraph-api/lib/get-domain-by-interpreted-name.ts +++ b/apps/ensapi/src/omnigraph-api/lib/get-domain-by-interpreted-name.ts @@ -23,7 +23,6 @@ import { import { isBridgedResolver } from "@ensnode/ensnode-sdk/internal"; import di from "@/di"; -import { ensDb, ensIndexerSchema } from "@/lib/ensdb/singleton"; import { withActiveSpanAsync, withSpanAsync } from "@/lib/instrumentation/auto-span"; import { MAX_SUPPORTED_NAME_DEPTH } from "@/omnigraph-api/lib/constants"; @@ -176,6 +175,8 @@ async function forwardWalkDisjointNamegraph(registryId: RegistryId, path: LabelH // NOTE: using new Param as per https://github.com/drizzle-team/drizzle-orm/issues/1289#issuecomment-2688581070 const rawLabelHashPathArray = sql`${new Param(path)}::text[]`; + const { ensDb, ensIndexerSchema } = di.context; + const result = await withSpanAsync(tracer, "forward-walk", { registryId, path }, () => ensDb.execute(sql` WITH RECURSIVE path AS ( diff --git a/apps/ensapi/src/omnigraph-api/lib/get-domain-resolver.ts b/apps/ensapi/src/omnigraph-api/lib/get-domain-resolver.ts index 4652b95756..346c229cde 100644 --- a/apps/ensapi/src/omnigraph-api/lib/get-domain-resolver.ts +++ b/apps/ensapi/src/omnigraph-api/lib/get-domain-resolver.ts @@ -1,8 +1,9 @@ import type { DomainId } from "enssdk"; -import { ensDb } from "@/lib/ensdb/singleton"; +import di from "@/di"; export async function getDomainResolver(domainId: DomainId) { + const { ensDb } = di.context; const drr = await ensDb.query.domainResolverRelation.findFirst({ where: (t, { eq }) => eq(t.domainId, domainId), with: { resolver: true }, diff --git a/apps/ensapi/src/omnigraph-api/lib/get-latest-registration.ts b/apps/ensapi/src/omnigraph-api/lib/get-latest-registration.ts index c5ea387cd5..797e91ff87 100644 --- a/apps/ensapi/src/omnigraph-api/lib/get-latest-registration.ts +++ b/apps/ensapi/src/omnigraph-api/lib/get-latest-registration.ts @@ -1,11 +1,12 @@ import type { DomainId } from "enssdk"; -import { ensDb } from "@/lib/ensdb/singleton"; +import di from "@/di"; /** * Gets the latest Registration entity for Domain `domainId`. */ export async function getLatestRegistration(domainId: DomainId) { + const { ensDb } = di.context; return await ensDb.query.registration.findFirst({ where: (t, { eq }) => eq(t.domainId, domainId), orderBy: (t, { desc }) => desc(t.registrationIndex), diff --git a/apps/ensapi/src/omnigraph-api/schema/account.ts b/apps/ensapi/src/omnigraph-api/schema/account.ts index f61c9eade1..1783d89bb2 100644 --- a/apps/ensapi/src/omnigraph-api/schema/account.ts +++ b/apps/ensapi/src/omnigraph-api/schema/account.ts @@ -2,7 +2,7 @@ import { type ResolveCursorConnectionArgs, resolveCursorConnection } from "@poth import { and, count, eq, getTableColumns } from "drizzle-orm"; import type { Address } from "enssdk"; -import { ensDb, ensIndexerSchema } from "@/lib/ensdb/singleton"; +import di from "@/di"; import { builder } from "@/omnigraph-api/builder"; import { orderPaginationBy, paginateBy } from "@/omnigraph-api/lib/connection-helpers"; import { resolveFindDomains } from "@/omnigraph-api/lib/find-domains/find-domains-resolver"; @@ -20,10 +20,12 @@ import { RegistryPermissionsUserRef } from "@/omnigraph-api/schema/registry-perm import { ResolverPermissionsUserRef } from "@/omnigraph-api/schema/resolver-permissions-user"; export const AccountRef = builder.loadableObjectRef("Account", { - load: (ids: Address[]) => - ensDb.query.account.findMany({ + load: (ids: Address[]) => { + const { ensDb } = di.context; + return ensDb.query.account.findMany({ where: (t, { inArray }) => inArray(t.id, ids), - }), + }); + }, toKey: getModelId, cacheResolved: true, sort: true, @@ -104,6 +106,7 @@ AccountRef.implement({ }, resolve: (parent, args) => { const contract = args.where?.contract; + const { ensDb, ensIndexerSchema } = di.context; const scope = and( // this user's permissions eq(ensIndexerSchema.permissionsUser.user, parent.id), @@ -140,6 +143,7 @@ AccountRef.implement({ description: "The Permissions on Registries granted to this Account.", type: RegistryPermissionsUserRef, resolve: (parent, args) => { + const { ensDb, ensIndexerSchema } = di.context; const scope = eq(ensIndexerSchema.permissionsUser.user, parent.id); const join = and( eq(ensIndexerSchema.permissionsUser.chainId, ensIndexerSchema.registry.chainId), @@ -177,6 +181,7 @@ AccountRef.implement({ description: "The Permissions on Resolvers granted to this Account.", type: ResolverPermissionsUserRef, resolve: (parent, args) => { + const { ensDb, ensIndexerSchema } = di.context; const scope = eq(ensIndexerSchema.permissionsUser.user, parent.id); const join = and( eq(ensIndexerSchema.permissionsUser.chainId, ensIndexerSchema.resolver.chainId), diff --git a/apps/ensapi/src/omnigraph-api/schema/domain.ts b/apps/ensapi/src/omnigraph-api/schema/domain.ts index 1645b66319..1ed25f7a20 100644 --- a/apps/ensapi/src/omnigraph-api/schema/domain.ts +++ b/apps/ensapi/src/omnigraph-api/schema/domain.ts @@ -5,7 +5,7 @@ import type { DomainId } from "enssdk"; import type { RequiredAndNotNull, RequiredAndNull } from "@ensnode/ensnode-sdk"; -import { ensDb, ensIndexerSchema } from "@/lib/ensdb/singleton"; +import di from "@/di"; import { withSpanAsync } from "@/lib/instrumentation/auto-span"; import { builder } from "@/omnigraph-api/builder"; import { @@ -48,12 +48,13 @@ const tracer = trace.getTracer("schema/Domain"); export const DomainInterfaceRef = builder.loadableInterfaceRef("Domain", { load: (ids: DomainId[]) => - withSpanAsync(tracer, "Domain.load", { count: ids.length }, () => - ensDb.query.domain.findMany({ + withSpanAsync(tracer, "Domain.load", { count: ids.length }, () => { + const { ensDb } = di.context; + return ensDb.query.domain.findMany({ where: (t, { inArray }) => inArray(t.id, ids), with: { label: true }, - }), - ), + }); + }), toKey: getModelId, cacheResolved: true, sort: true, @@ -183,6 +184,7 @@ DomainInterfaceRef.implement({ description: "All Registrations for a Domain, including the latest Registration.", type: RegistrationInterfaceRef, resolve: (parent, args) => { + const { ensDb, ensIndexerSchema } = di.context; const scope = eq(ensIndexerSchema.registration.domainId, parent.id); return lazyConnection({ @@ -244,13 +246,15 @@ DomainInterfaceRef.implement({ args: { where: t.arg({ type: EventsWhereInput }), }, - resolve: (parent, args) => - resolveFindEvents(args, { + resolve: (parent, args) => { + const { ensIndexerSchema } = di.context; + return resolveFindEvents(args, { through: { table: ensIndexerSchema.domainEvent, scope: eq(ensIndexerSchema.domainEvent.domainId, parent.id), }, - }), + }); + }, }), }), }); @@ -315,6 +319,7 @@ ENSv2DomainRef.implement({ where: t.arg({ type: DomainPermissionsWhereInput }), }, resolve: (parent, args) => { + const { ensDb, ensIndexerSchema } = di.context; const userScope = (() => { const user = args.where?.user; if (!user) return undefined; diff --git a/apps/ensapi/src/omnigraph-api/schema/event.ts b/apps/ensapi/src/omnigraph-api/schema/event.ts index ac44855929..02a73d8f16 100644 --- a/apps/ensapi/src/omnigraph-api/schema/event.ts +++ b/apps/ensapi/src/omnigraph-api/schema/event.ts @@ -1,10 +1,10 @@ -import { ensDb } from "@/lib/ensdb/singleton"; +import di from "@/di"; import { builder } from "@/omnigraph-api/builder"; import { getModelId } from "@/omnigraph-api/lib/get-model-id"; export const EventRef = builder.loadableObjectRef("Event", { load: (ids: string[]) => - ensDb.query.event.findMany({ + di.context.ensDb.query.event.findMany({ where: (t, { inArray }) => inArray(t.id, ids), }), toKey: getModelId, diff --git a/apps/ensapi/src/omnigraph-api/schema/label.ts b/apps/ensapi/src/omnigraph-api/schema/label.ts index 9b6dc42ad4..4ffe1f4034 100644 --- a/apps/ensapi/src/omnigraph-api/schema/label.ts +++ b/apps/ensapi/src/omnigraph-api/schema/label.ts @@ -1,9 +1,10 @@ import { beautifyInterpretedLabel } from "enssdk"; -import type { ensIndexerSchema } from "@/lib/ensdb/singleton"; +import type di from "@/di"; import { builder } from "@/omnigraph-api/builder"; -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/omnigraph-api/schema/permissions.ts b/apps/ensapi/src/omnigraph-api/schema/permissions.ts index 1f6fb16d7d..ebb2247cfa 100644 --- a/apps/ensapi/src/omnigraph-api/schema/permissions.ts +++ b/apps/ensapi/src/omnigraph-api/schema/permissions.ts @@ -9,7 +9,7 @@ import { ROOT_RESOURCE, } from "enssdk"; -import { ensDb, ensIndexerSchema } from "@/lib/ensdb/singleton"; +import di from "@/di"; import { builder } from "@/omnigraph-api/builder"; import { orderPaginationBy, paginateBy } from "@/omnigraph-api/lib/connection-helpers"; import { resolveFindEvents } from "@/omnigraph-api/lib/find-events/find-events-resolver"; @@ -23,7 +23,7 @@ import { EventsWhereInput } from "@/omnigraph-api/schema/event-inputs"; export const PermissionsRef = builder.loadableObjectRef("Permissions", { load: (ids: PermissionsId[]) => - ensDb.query.permissions.findMany({ where: (t, { inArray }) => inArray(t.id, ids) }), + di.context.ensDb.query.permissions.findMany({ where: (t, { inArray }) => inArray(t.id, ids) }), toKey: getModelId, cacheResolved: true, sort: true, @@ -31,7 +31,9 @@ export const PermissionsRef = builder.loadableObjectRef("Permissions", { export const PermissionsResourceRef = builder.loadableObjectRef("PermissionsResource", { load: (ids: PermissionsResourceId[]) => - ensDb.query.permissionsResource.findMany({ where: (t, { inArray }) => inArray(t.id, ids) }), + di.context.ensDb.query.permissionsResource.findMany({ + where: (t, { inArray }) => inArray(t.id, ids), + }), toKey: getModelId, cacheResolved: true, sort: true, @@ -39,7 +41,9 @@ export const PermissionsResourceRef = builder.loadableObjectRef("PermissionsReso export const PermissionsUserRef = builder.loadableObjectRef("PermissionsUser", { load: (ids: PermissionsUserId[]) => - ensDb.query.permissionsUser.findMany({ where: (t, { inArray }) => inArray(t.id, ids) }), + di.context.ensDb.query.permissionsUser.findMany({ + where: (t, { inArray }) => inArray(t.id, ids), + }), toKey: getModelId, cacheResolved: true, sort: true, @@ -99,6 +103,7 @@ PermissionsRef.implement({ description: "All PermissionResources managed by this contract.", type: PermissionsResourceRef, resolve: (parent, args) => { + const { ensDb, ensIndexerSchema } = di.context; const scope = and( eq(ensIndexerSchema.permissionsResource.chainId, parent.chainId), eq(ensIndexerSchema.permissionsResource.address, parent.address), @@ -132,13 +137,15 @@ PermissionsRef.implement({ args: { where: t.arg({ type: EventsWhereInput }), }, - resolve: (parent, args) => - resolveFindEvents(args, { + resolve: (parent, args) => { + const { ensIndexerSchema } = di.context; + return resolveFindEvents(args, { through: { table: ensIndexerSchema.permissionsEvent, scope: eq(ensIndexerSchema.permissionsEvent.permissionsId, parent.id), }, - }), + }); + }, }), }), }); @@ -196,6 +203,7 @@ PermissionsResourceRef.implement({ description: "The PermissionUsers who have Roles within this Resource.", type: PermissionsUserRef, resolve: (parent, args) => { + const { ensDb, ensIndexerSchema } = di.context; const scope = and( eq(ensIndexerSchema.permissionsUser.chainId, parent.chainId), eq(ensIndexerSchema.permissionsUser.address, parent.address), @@ -287,13 +295,15 @@ PermissionsUserRef.implement({ args: { where: t.arg({ type: EventsWhereInput }), }, - resolve: (parent, args) => - resolveFindEvents(args, { + resolve: (parent, args) => { + const { ensIndexerSchema } = di.context; + return resolveFindEvents(args, { through: { table: ensIndexerSchema.permissionsUserEvent, scope: eq(ensIndexerSchema.permissionsUserEvent.permissionsUserId, parent.id), }, - }), + }); + }, }), }), }); diff --git a/apps/ensapi/src/omnigraph-api/schema/query.ts b/apps/ensapi/src/omnigraph-api/schema/query.ts index 56464f2914..324d1d9dd2 100644 --- a/apps/ensapi/src/omnigraph-api/schema/query.ts +++ b/apps/ensapi/src/omnigraph-api/schema/query.ts @@ -4,7 +4,6 @@ import { makeConcreteRegistryId, makePermissionsId, makeResolverId } from "enssd import { getRootRegistryId } from "@ensnode/ensnode-sdk"; import di from "@/di"; -import { ensDb, ensIndexerSchema } from "@/lib/ensdb/singleton"; import { builder } from "@/omnigraph-api/builder"; import { orderPaginationBy, paginateBy } from "@/omnigraph-api/lib/connection-helpers"; import { resolveFindDomains } from "@/omnigraph-api/lib/find-domains/find-domains-resolver"; @@ -33,8 +32,9 @@ builder.queryType({ allDomains: t.connection({ description: "n/a, dev method", type: DomainInterfaceRef, - resolve: (parent, args) => - lazyConnection({ + resolve: (parent, args) => { + const { ensDb, ensIndexerSchema } = di.context; + return lazyConnection({ totalCount: () => ensDb.$count(ensIndexerSchema.domain), connection: () => resolveCursorConnection( @@ -47,7 +47,8 @@ builder.queryType({ with: { label: true }, }), ), - }), + }); + }, }), ///////////////////////////// @@ -56,8 +57,9 @@ builder.queryType({ resolvers: t.connection({ description: "n/a, dev method", type: ResolverRef, - resolve: (parent, args) => - lazyConnection({ + resolve: (parent, args) => { + const { ensDb, ensIndexerSchema } = di.context; + return lazyConnection({ totalCount: () => ensDb.$count(ensIndexerSchema.resolver), connection: () => resolveCursorConnection( @@ -70,7 +72,8 @@ builder.queryType({ .orderBy(orderPaginationBy(ensIndexerSchema.resolver.id, inverted)) .limit(limit), ), - }), + }); + }, }), ///////////////////////////////// @@ -79,8 +82,9 @@ builder.queryType({ registrations: t.connection({ description: "n/a, dev method", type: RegistrationInterfaceRef, - resolve: (parent, args) => - lazyConnection({ + resolve: (parent, args) => { + const { ensDb, ensIndexerSchema } = di.context; + return lazyConnection({ totalCount: () => ensDb.$count(ensIndexerSchema.registration), connection: () => resolveCursorConnection( @@ -93,7 +97,8 @@ builder.queryType({ .orderBy(orderPaginationBy(ensIndexerSchema.registration.id, inverted)) .limit(limit), ), - }), + }); + }, }), }), diff --git a/apps/ensapi/src/omnigraph-api/schema/registration.ts b/apps/ensapi/src/omnigraph-api/schema/registration.ts index 588e1c4a63..617a0b998e 100644 --- a/apps/ensapi/src/omnigraph-api/schema/registration.ts +++ b/apps/ensapi/src/omnigraph-api/schema/registration.ts @@ -9,7 +9,7 @@ import { type RequiredAndNotNull, } from "@ensnode/ensnode-sdk"; -import { ensDb, ensIndexerSchema } from "@/lib/ensdb/singleton"; +import di from "@/di"; import { builder } from "@/omnigraph-api/builder"; import { orderPaginationBy, paginateByInt } from "@/omnigraph-api/lib/connection-helpers"; import { cursors } from "@/omnigraph-api/lib/cursors"; @@ -26,10 +26,12 @@ import { EventRef } from "@/omnigraph-api/schema/event"; import { RenewalRef } from "@/omnigraph-api/schema/renewal"; export const RegistrationInterfaceRef = builder.loadableInterfaceRef("Registration", { - load: (ids: RegistrationId[]) => - ensDb.query.registration.findMany({ + load: (ids: RegistrationId[]) => { + const { ensDb } = di.context; + return ensDb.query.registration.findMany({ where: (t, { inArray }) => inArray(t.id, ids), - }), + }); + }, toKey: getModelId, cacheResolved: true, sort: true, @@ -166,6 +168,7 @@ RegistrationInterfaceRef.implement({ "Renewals that have occurred within this Registration's lifespan to extend its expiration.", type: RenewalRef, resolve: (parent, args) => { + const { ensDb, ensIndexerSchema } = di.context; const scope = and( eq(ensIndexerSchema.renewal.domainId, parent.domainId), eq(ensIndexerSchema.renewal.registrationIndex, parent.registrationIndex), diff --git a/apps/ensapi/src/omnigraph-api/schema/registry-permissions-user.ts b/apps/ensapi/src/omnigraph-api/schema/registry-permissions-user.ts index b5d7f0bc69..161db939e6 100644 --- a/apps/ensapi/src/omnigraph-api/schema/registry-permissions-user.ts +++ b/apps/ensapi/src/omnigraph-api/schema/registry-permissions-user.ts @@ -1,6 +1,6 @@ import { makeENSv2RegistryId } from "enssdk"; -import type { ensIndexerSchema } from "@/lib/ensdb/singleton"; +import type di from "@/di"; import { builder } from "@/omnigraph-api/builder"; import { AccountRef } from "@/omnigraph-api/schema/account"; import { RegistryInterfaceRef } from "@/omnigraph-api/schema/registry"; @@ -9,7 +9,7 @@ import { RegistryInterfaceRef } from "@/omnigraph-api/schema/registry"; * Represents a PermissionsUser whose contract is a Registry, providing a semantic `registry` field. */ export const RegistryPermissionsUserRef = - builder.objectRef( + builder.objectRef( "RegistryPermissionsUser", ); diff --git a/apps/ensapi/src/omnigraph-api/schema/registry.ts b/apps/ensapi/src/omnigraph-api/schema/registry.ts index ce73aaad05..d38d641e46 100644 --- a/apps/ensapi/src/omnigraph-api/schema/registry.ts +++ b/apps/ensapi/src/omnigraph-api/schema/registry.ts @@ -4,7 +4,7 @@ import { makePermissionsId, type RegistryId } from "enssdk"; import type { RequiredAndNotNull, RequiredAndNull } from "@ensnode/ensnode-sdk"; -import { ensDb, ensIndexerSchema } from "@/lib/ensdb/singleton"; +import di from "@/di"; import { builder } from "@/omnigraph-api/builder"; import { orderPaginationBy, paginateBy } from "@/omnigraph-api/lib/connection-helpers"; import { resolveFindDomains } from "@/omnigraph-api/lib/find-domains/find-domains-resolver"; @@ -21,8 +21,10 @@ import { PermissionsRef } from "@/omnigraph-api/schema/permissions"; /////////////////////////////////// export const RegistryInterfaceRef = builder.loadableInterfaceRef("Registry", { - load: (ids: RegistryId[]) => - ensDb.query.registry.findMany({ where: (t, { inArray }) => inArray(t.id, ids) }), + load: (ids: RegistryId[]) => { + const { ensDb } = di.context; + return ensDb.query.registry.findMany({ where: (t, { inArray }) => inArray(t.id, ids) }); + }, toKey: getModelId, cacheResolved: true, sort: true, @@ -95,6 +97,7 @@ RegistryInterfaceRef.implement({ description: "The Domains for which this Registry is a Subregistry.", type: DomainInterfaceRef, resolve: (parent, args) => { + const { ensDb, ensIndexerSchema } = di.context; const scope = eq(ensIndexerSchema.domain.subregistryId, parent.id); return lazyConnection({ diff --git a/apps/ensapi/src/omnigraph-api/schema/renewal.ts b/apps/ensapi/src/omnigraph-api/schema/renewal.ts index eea55888a7..cae200fafd 100644 --- a/apps/ensapi/src/omnigraph-api/schema/renewal.ts +++ b/apps/ensapi/src/omnigraph-api/schema/renewal.ts @@ -1,13 +1,13 @@ import type { RenewalId } from "enssdk"; -import { ensDb } from "@/lib/ensdb/singleton"; +import di from "@/di"; import { builder } from "@/omnigraph-api/builder"; import { getModelId } from "@/omnigraph-api/lib/get-model-id"; import { EventRef } from "@/omnigraph-api/schema/event"; export const RenewalRef = builder.loadableObjectRef("Renewal", { load: (ids: RenewalId[]) => - ensDb.query.renewal.findMany({ + di.context.ensDb.query.renewal.findMany({ where: (t, { inArray }) => inArray(t.id, ids), }), toKey: getModelId, diff --git a/apps/ensapi/src/omnigraph-api/schema/resolver-permissions-user.ts b/apps/ensapi/src/omnigraph-api/schema/resolver-permissions-user.ts index 3221d7ec5a..6db0f6b87d 100644 --- a/apps/ensapi/src/omnigraph-api/schema/resolver-permissions-user.ts +++ b/apps/ensapi/src/omnigraph-api/schema/resolver-permissions-user.ts @@ -1,6 +1,6 @@ import { makeResolverId } from "enssdk"; -import type { ensIndexerSchema } from "@/lib/ensdb/singleton"; +import type di from "@/di"; import { builder } from "@/omnigraph-api/builder"; import { AccountRef } from "@/omnigraph-api/schema/account"; import { ResolverRef } from "@/omnigraph-api/schema/resolver"; @@ -9,7 +9,7 @@ import { ResolverRef } from "@/omnigraph-api/schema/resolver"; * Represents a PermissionsUser whose contract is a Resolver, providing a semantic `resolver` field. */ export const ResolverPermissionsUserRef = - builder.objectRef( + builder.objectRef( "ResolverPermissionsUser", ); diff --git a/apps/ensapi/src/omnigraph-api/schema/resolver-records.ts b/apps/ensapi/src/omnigraph-api/schema/resolver-records.ts index bd86b519b3..4b9fc04db1 100644 --- a/apps/ensapi/src/omnigraph-api/schema/resolver-records.ts +++ b/apps/ensapi/src/omnigraph-api/schema/resolver-records.ts @@ -1,12 +1,12 @@ import { bigintToCoinType, type ResolverRecordsId } from "enssdk"; -import { ensDb } from "@/lib/ensdb/singleton"; +import di from "@/di"; import { builder } from "@/omnigraph-api/builder"; import { getModelId } from "@/omnigraph-api/lib/get-model-id"; export const ResolverRecordsRef = builder.loadableObjectRef("ResolverRecords", { load: (ids: ResolverRecordsId[]) => - ensDb.query.resolverRecords.findMany({ + di.context.ensDb.query.resolverRecords.findMany({ where: (t, { inArray }) => inArray(t.id, ids), with: { textRecords: true, addressRecords: true }, }), diff --git a/apps/ensapi/src/omnigraph-api/schema/resolver.ts b/apps/ensapi/src/omnigraph-api/schema/resolver.ts index f1db948a7d..6166608cfa 100644 --- a/apps/ensapi/src/omnigraph-api/schema/resolver.ts +++ b/apps/ensapi/src/omnigraph-api/schema/resolver.ts @@ -10,7 +10,6 @@ import { import { isBridgedResolver } from "@ensnode/ensnode-sdk/internal"; import di from "@/di"; -import { ensDb, ensIndexerSchema } from "@/lib/ensdb/singleton"; import { builder } from "@/omnigraph-api/builder"; import { orderPaginationBy, paginateBy } from "@/omnigraph-api/lib/connection-helpers"; import { resolveFindEvents } from "@/omnigraph-api/lib/find-events/find-events-resolver"; @@ -40,7 +39,7 @@ import { ResolverRecordsRef } from "@/omnigraph-api/schema/resolver-records"; export const ResolverRef = builder.loadableObjectRef("Resolver", { load: (ids: ResolverId[]) => - ensDb.query.resolver.findMany({ + di.context.ensDb.query.resolver.findMany({ where: (t, { inArray }) => inArray(t.id, ids), }), toKey: getModelId, @@ -83,6 +82,7 @@ ResolverRef.implement({ description: "ResolverRecords issued by this Resolver.", type: ResolverRecordsRef, resolve: (parent, args, context) => { + const { ensDb, ensIndexerSchema } = di.context; const scope = and( eq(ensIndexerSchema.resolverRecords.chainId, parent.chainId), eq(ensIndexerSchema.resolverRecords.address, parent.address), @@ -151,13 +151,15 @@ ResolverRef.implement({ args: { where: t.arg({ type: EventsWhereInput }), }, - resolve: (parent, args) => - resolveFindEvents(args, { + resolve: (parent, args) => { + const { ensIndexerSchema } = di.context; + return resolveFindEvents(args, { through: { table: ensIndexerSchema.resolverEvent, scope: eq(ensIndexerSchema.resolverEvent.resolverId, parent.id), }, - }), + }); + }, }), }), }); diff --git a/packages/ensdb-sdk/src/client/ensdb-reader.test.ts b/packages/ensdb-sdk/src/client/ensdb-reader.test.ts index 3a5d977995..48c47306fc 100644 --- a/packages/ensdb-sdk/src/client/ensdb-reader.test.ts +++ b/packages/ensdb-sdk/src/client/ensdb-reader.test.ts @@ -16,7 +16,12 @@ const executeMock = vi.fn(); const whereMock = vi.fn(async () => [] as Array<{ value: unknown }>); const fromMock = vi.fn(() => ({ where: whereMock })); const selectMock = vi.fn(() => ({ from: fromMock })); -const drizzleClientMock = { select: selectMock, execute: executeMock } as any; +const endMock = vi.fn().mockResolvedValue(undefined); +const drizzleClientMock = { + select: selectMock, + execute: executeMock, + $client: { end: endMock }, +} as any; vi.mock("drizzle-orm/node-postgres", () => ({ drizzle: vi.fn(() => drizzleClientMock), @@ -35,6 +40,7 @@ describe("EnsDbReader", () => { fromMock.mockClear(); selectMock.mockClear(); executeMock.mockClear(); + endMock.mockClear(); }); describe("getters", () => { @@ -206,4 +212,20 @@ describe("EnsDbReader", () => { ); }); }); + + describe("destroy", () => { + it("calls $client.end() to close the connection pool", async () => { + const ensDbReader = createEnsDbReader(); + + await ensDbReader.destroy(); + + expect(endMock).toHaveBeenCalledTimes(1); + }); + + it("propagates errors from $client.end()", async () => { + endMock.mockRejectedValueOnce(new Error("Connection already closed")); + + await expect(createEnsDbReader().destroy()).rejects.toThrow("Connection already closed"); + }); + }); }); diff --git a/packages/ensdb-sdk/src/client/ensdb-reader.ts b/packages/ensdb-sdk/src/client/ensdb-reader.ts index c613095d95..8645ee2551 100644 --- a/packages/ensdb-sdk/src/client/ensdb-reader.ts +++ b/packages/ensdb-sdk/src/client/ensdb-reader.ts @@ -189,6 +189,16 @@ export class EnsDbReader< return deserializeIndexingMetadataContext(record); } + /** + * Destroy the ENSDbReader instance by closing the ENSDb connection pool. + */ + async destroy(): Promise { + // @ts-expect-error - Drizzle Client does not have `end` method in its type definition, + // but it does exist in the actual implementation and is necessary to properly close + // the connection pool to the ENSDb instance when destroying the ENSDbReader instance. + await this.ensDb.$client.end(); + } + /** * Get ENSNode Metadata record *