diff --git a/apps/ensapi/src/config/config.schema.ts b/apps/ensapi/src/config/config.schema.ts index af74bd41c..83fce361e 100644 --- a/apps/ensapi/src/config/config.schema.ts +++ b/apps/ensapi/src/config/config.schema.ts @@ -4,7 +4,7 @@ import pRetry from "p-retry"; import { parse as parseConnectionString } from "pg-connection-string"; import { prettifyError, ZodError, z } from "zod/v4"; -import { type ENSApiPublicConfig, serializeENSIndexerPublicConfig } from "@ensnode/ensnode-sdk"; +import type { EnsApiPublicConfig } from "@ensnode/ensnode-sdk"; import { buildRpcConfigsFromEnv, canFallbackToTheGraph, @@ -103,7 +103,7 @@ export async function buildConfigFromEnvironment(env: EnsApiEnvironment): Promis databaseUrl: env.DATABASE_URL, ensIndexerUrl: env.ENSINDEXER_URL, theGraphApiKey: env.THEGRAPH_API_KEY, - ensIndexerPublicConfig: serializeENSIndexerPublicConfig(ensIndexerPublicConfig), + ensIndexerPublicConfig, namespace: ensIndexerPublicConfig.namespace, databaseSchemaName: ensIndexerPublicConfig.databaseSchemaName, rpcConfigs, @@ -128,7 +128,7 @@ export async function buildConfigFromEnvironment(env: EnsApiEnvironment): Promis * @param config - The validated EnsApiConfig object * @returns A complete ENSApiPublicConfig object */ -export function buildEnsApiPublicConfig(config: EnsApiConfig): ENSApiPublicConfig { +export function buildEnsApiPublicConfig(config: EnsApiConfig): EnsApiPublicConfig { return { version: packageJson.version, theGraphFallback: canFallbackToTheGraph({ diff --git a/packages/ensnode-sdk/src/ensapi/api/config/deserialize.ts b/packages/ensnode-sdk/src/ensapi/api/config/deserialize.ts index 3fff577bc..eabd78097 100644 --- a/packages/ensnode-sdk/src/ensapi/api/config/deserialize.ts +++ b/packages/ensnode-sdk/src/ensapi/api/config/deserialize.ts @@ -1,3 +1,4 @@ +import type { Unvalidated } from "../../../shared/types"; import { deserializeEnsApiPublicConfig } from "../../config/deserialize"; import type { EnsApiConfigResponse } from "./response"; import type { SerializedEnsApiConfigResponse } from "./serialized-response"; @@ -6,9 +7,9 @@ import type { SerializedEnsApiConfigResponse } from "./serialized-response"; * Deserialize a {@link EnsApiConfigResponse} object. */ export function deserializeEnsApiConfigResponse( - serializedResponse: SerializedEnsApiConfigResponse, + maybeResponse: Unvalidated, ): EnsApiConfigResponse { - return deserializeEnsApiPublicConfig(serializedResponse); + return deserializeEnsApiPublicConfig(maybeResponse); } /** diff --git a/packages/ensnode-sdk/src/ensapi/config/deserialize.ts b/packages/ensnode-sdk/src/ensapi/config/deserialize.ts index 52cc1472c..78aa79412 100644 --- a/packages/ensnode-sdk/src/ensapi/config/deserialize.ts +++ b/packages/ensnode-sdk/src/ensapi/config/deserialize.ts @@ -1,26 +1,49 @@ -import { prettifyError, ZodError } from "zod/v4"; +import { prettifyError } from "zod/v4"; +import { buildUnvalidatedEnsIndexerPublicConfig } from "../../ensindexer/config/deserialize"; +import type { Unvalidated } from "../../shared/types"; import type { SerializedEnsApiPublicConfig } from "./serialized-types"; import type { EnsApiPublicConfig } from "./types"; -import { makeEnsApiPublicConfigSchema } from "./zod-schemas"; +import { + makeEnsApiPublicConfigSchema, + makeSerializedEnsApiPublicConfigSchema, +} from "./zod-schemas"; /** - * Deserialize a {@link EnsApiPublicConfig} object. + * Builds an unvalidated {@link EnsApiPublicConfig} object to be + * validated with {@link makeEnsApiPublicConfigSchema}. + * + * @param serializedPublicConfig - The serialized public config to build from. + * @return An unvalidated {@link EnsApiPublicConfig} object. + */ +function buildUnvalidatedEnsApiPublicConfig( + serializedPublicConfig: SerializedEnsApiPublicConfig, +): Unvalidated { + return { + ...serializedPublicConfig, + ensIndexerPublicConfig: buildUnvalidatedEnsIndexerPublicConfig( + serializedPublicConfig.ensIndexerPublicConfig, + ), + }; +} + +/** + * Deserialize value into {@link EnsApiPublicConfig} object. */ export function deserializeEnsApiPublicConfig( - maybeConfig: SerializedEnsApiPublicConfig, + maybePublicConfig: Unvalidated, valueLabel?: string, ): EnsApiPublicConfig { - const schema = makeEnsApiPublicConfigSchema(valueLabel); - try { - return schema.parse(maybeConfig); - } catch (error) { - if (error instanceof ZodError) { - throw new Error(`Cannot deserialize EnsApiPublicConfig:\n${prettifyError(error)}\n`); - } + const parsed = makeSerializedEnsApiPublicConfigSchema(valueLabel) + .transform(buildUnvalidatedEnsApiPublicConfig) + .pipe(makeEnsApiPublicConfigSchema(valueLabel)) + .safeParse(maybePublicConfig); - throw error; + if (parsed.error) { + throw new Error(`Cannot deserialize EnsApiPublicConfig:\n${prettifyError(parsed.error)}\n`); } + + return parsed.data; } /** diff --git a/packages/ensnode-sdk/src/ensapi/config/zod-schemas.ts b/packages/ensnode-sdk/src/ensapi/config/zod-schemas.ts index 670153d76..7ed65cfcc 100644 --- a/packages/ensnode-sdk/src/ensapi/config/zod-schemas.ts +++ b/packages/ensnode-sdk/src/ensapi/config/zod-schemas.ts @@ -1,6 +1,9 @@ import { z } from "zod/v4"; -import { makeEnsIndexerPublicConfigSchema } from "../../ensindexer/config/zod-schemas"; +import { + makeEnsIndexerPublicConfigSchema, + makeSerializedEnsIndexerPublicConfigSchema, +} from "../../ensindexer/config/zod-schemas"; import { TheGraphCannotFallbackReasonSchema, TheGraphFallbackSchema, @@ -9,14 +12,14 @@ import { export { TheGraphCannotFallbackReasonSchema, TheGraphFallbackSchema }; /** - * Create a Zod schema for validating a serialized ENSApiPublicConfig. + * Create a Zod schema for validating ENSApiPublicConfig. * * @param valueLabel - Optional label for the value being validated (used in error messages) */ export function makeEnsApiPublicConfigSchema(valueLabel?: string) { const label = valueLabel ?? "ENSApiPublicConfig"; - return z.strictObject({ + return z.object({ version: z.string().min(1, `${label}.version must be a non-empty string`), theGraphFallback: TheGraphFallbackSchema, ensIndexerPublicConfig: makeEnsIndexerPublicConfigSchema(`${label}.ensIndexerPublicConfig`), @@ -29,3 +32,15 @@ export function makeEnsApiPublicConfigSchema(valueLabel?: string) { * @deprecated Use {@link makeEnsApiPublicConfigSchema} instead. */ export const makeENSApiPublicConfigSchema = makeEnsApiPublicConfigSchema; + +export function makeSerializedEnsApiPublicConfigSchema(valueLabel?: string) { + const label = valueLabel ?? "ENSApiPublicConfig"; + + return z.object({ + version: z.string().min(1, `${label}.version must be a non-empty string`), + theGraphFallback: TheGraphFallbackSchema, + ensIndexerPublicConfig: makeSerializedEnsIndexerPublicConfigSchema( + `${label}.ensIndexerPublicConfig`, + ), + }); +} diff --git a/packages/ensnode-sdk/src/ensindexer/config/deserialize.ts b/packages/ensnode-sdk/src/ensindexer/config/deserialize.ts index 33264c078..e136fbbe3 100644 --- a/packages/ensnode-sdk/src/ensindexer/config/deserialize.ts +++ b/packages/ensnode-sdk/src/ensindexer/config/deserialize.ts @@ -1,18 +1,40 @@ import { prettifyError } from "zod/v4"; +import type { Unvalidated } from "../../shared/types"; import type { SerializedEnsIndexerPublicConfig } from "./serialized-types"; import type { EnsIndexerPublicConfig } from "./types"; -import { makeEnsIndexerPublicConfigSchema } from "./zod-schemas"; +import { + makeEnsIndexerPublicConfigSchema, + makeSerializedEnsIndexerPublicConfigSchema, +} from "./zod-schemas"; /** - * Deserialize object into a {@link EnsIndexerPublicConfig} object. + * Builds an unvalidated {@link EnsIndexerPublicConfig} object to be + * validated with {@link makeEnsIndexerPublicConfigSchema}. + * + * @param serializedPublicConfig - The serialized public config to build from. + * @return An unvalidated {@link EnsIndexerPublicConfig} object. + */ +export function buildUnvalidatedEnsIndexerPublicConfig( + serializedPublicConfig: SerializedEnsIndexerPublicConfig, +): Unvalidated { + return { + ...serializedPublicConfig, + indexedChainIds: new Set(serializedPublicConfig.indexedChainIds), + }; +} + +/** + * Deserialize value into {@link EnsIndexerPublicConfig} object. */ export function deserializeEnsIndexerPublicConfig( - maybeConfig: SerializedEnsIndexerPublicConfig, + maybePublicConfig: Unvalidated, valueLabel?: string, ): EnsIndexerPublicConfig { - const schema = makeEnsIndexerPublicConfigSchema(valueLabel); - const parsed = schema.safeParse(maybeConfig); + const parsed = makeSerializedEnsIndexerPublicConfigSchema(valueLabel) + .transform(buildUnvalidatedEnsIndexerPublicConfig) + .pipe(makeEnsIndexerPublicConfigSchema(valueLabel)) + .safeParse(maybePublicConfig); if (parsed.error) { throw new Error(`Cannot deserialize EnsIndexerPublicConfig:\n${prettifyError(parsed.error)}\n`); diff --git a/packages/ensnode-sdk/src/ensindexer/config/zod-schemas.test.ts b/packages/ensnode-sdk/src/ensindexer/config/zod-schemas.test.ts index a195eb40c..60bc78860 100644 --- a/packages/ensnode-sdk/src/ensindexer/config/zod-schemas.test.ts +++ b/packages/ensnode-sdk/src/ensindexer/config/zod-schemas.test.ts @@ -1,6 +1,8 @@ import { describe, expect, it } from "vitest"; import { prettifyError, type ZodSafeParseResult } from "zod/v4"; +import { buildUnvalidatedEnsIndexerPublicConfig } from "./deserialize"; +import type { SerializedEnsIndexerPublicConfig } from "./serialized-types"; import { type EnsIndexerVersionInfo, PluginName } from "./types"; import { makeDatabaseSchemaNameSchema, @@ -9,6 +11,7 @@ import { makeFullyPinnedLabelSetSchema, makeIndexedChainIdsSchema, makePluginsListSchema, + makeSerializedEnsIndexerPublicConfigSchema, } from "./zod-schemas"; describe("ENSIndexer: Config", () => { @@ -56,16 +59,16 @@ describe("ENSIndexer: Config", () => { }); it("can parse indexed chain ID values", () => { - expect(makeIndexedChainIdsSchema().parse([1, 10, 8543])).toStrictEqual( + expect(makeIndexedChainIdsSchema().parse(new Set([1, 10, 8543]))).toStrictEqual( new Set([1, 10, 8543]), ); expect(formatParseError(makeIndexedChainIdsSchema().safeParse("1,10"))).toContain( - "Indexed Chain IDs must be an array", + "Indexed Chain IDs must be a set", ); - expect(formatParseError(makeIndexedChainIdsSchema().safeParse([]))).toContain( - "Indexed Chain IDs list must include at least one element", + expect(formatParseError(makeIndexedChainIdsSchema().safeParse(new Set([])))).toContain( + "Indexed Chain IDs must be a set with at least one chain ID.", ); }); @@ -167,38 +170,38 @@ describe("ENSIndexer: Config", () => { ensNormalize: "1.11.1", ensRainbow: "0.32.0", ensRainbowSchema: 2, - } satisfies EnsIndexerVersionInfo, - }; + }, + } satisfies SerializedEnsIndexerPublicConfig; - const parsedConfig = makeEnsIndexerPublicConfigSchema().parse(validConfig); + const parsedConfig = makeSerializedEnsIndexerPublicConfigSchema().parse(validConfig); - // The schema transforms URLs and arrays, so we need to check the transformed values - expect(parsedConfig.indexedChainIds).toBeInstanceOf(Set); - expect(Array.from(parsedConfig.indexedChainIds)).toEqual([1]); - expect(parsedConfig.labelSet).toEqual(validConfig.labelSet); - expect(parsedConfig.isSubgraphCompatible).toBe(validConfig.isSubgraphCompatible); - expect(parsedConfig.namespace).toBe(validConfig.namespace); - expect(parsedConfig.plugins).toEqual(validConfig.plugins); - expect(parsedConfig.databaseSchemaName).toBe(validConfig.databaseSchemaName); - expect(parsedConfig.versionInfo).toEqual(validConfig.versionInfo); + // Verify that the parsed config has the expected values and types + expect(parsedConfig).toStrictEqual(validConfig); // Test invalid labelSetId expect( formatParseError( - makeEnsIndexerPublicConfigSchema().safeParse({ - ...validConfig, - labelSet: { ...validConfig.labelSet, labelSetId: "" }, - }), + makeEnsIndexerPublicConfigSchema().safeParse( + buildUnvalidatedEnsIndexerPublicConfig({ + ...validConfig, + labelSet: { ...validConfig.labelSet, labelSetId: "" }, + }), + ), ), ).toContain("labelSet.labelSetId must be 1-50 characters long"); // Test invalid labelSetVersion expect( formatParseError( - makeEnsIndexerPublicConfigSchema().safeParse({ - ...validConfig, - labelSet: { ...validConfig.labelSet, labelSetVersion: "not-a-number" }, - }), + makeEnsIndexerPublicConfigSchema().safeParse( + buildUnvalidatedEnsIndexerPublicConfig({ + ...validConfig, + labelSet: { + ...validConfig.labelSet, + labelSetVersion: "not-a-number" as unknown as number, + }, + }), + ), ), ).toContain("labelSet.labelSetVersion must be an integer"); }); diff --git a/packages/ensnode-sdk/src/ensindexer/config/zod-schemas.ts b/packages/ensnode-sdk/src/ensindexer/config/zod-schemas.ts index 33adb4856..5f221d558 100644 --- a/packages/ensnode-sdk/src/ensindexer/config/zod-schemas.ts +++ b/packages/ensnode-sdk/src/ensindexer/config/zod-schemas.ts @@ -25,12 +25,18 @@ import { invariant_ensDbVersionIsSameAsEnsIndexerVersion } from "./validations"; * Makes a schema for parsing {@link IndexedChainIds}. */ export const makeIndexedChainIdsSchema = (valueLabel: string = "Indexed Chain IDs") => + z.set(makeChainIdSchema(valueLabel), { error: `${valueLabel} must be a set` }).min(1, { + error: `${valueLabel} must be a set with at least one chain ID.`, + }); + +export const makeSerializedIndexedChainIdsSchema = (valueLabel: string = "Indexed Chain IDs") => z .array(makeChainIdSchema(valueLabel), { error: `${valueLabel} must be an array.`, }) - .min(1, { error: `${valueLabel} list must include at least one element.` }) - .transform((v) => new Set(v)); + .min(1, { + error: `${valueLabel} must be an array with at least one chain ID.`, + }); /** * Makes a schema for parsing a list of strings that (for future-proofing) @@ -91,7 +97,7 @@ export const makeLabelSetIdSchema = (valueLabel: string) => { */ export const makeLabelSetVersionSchema = (valueLabel: string) => { return z.coerce - .number({ error: `${valueLabel} must be an integer.` }) + .number({ error: `${valueLabel} must be an integer.` }) .pipe(makeNonNegativeIntegerSchema(valueLabel)); }; @@ -170,11 +176,13 @@ export const makeEnsIndexerPublicConfigSchema = (valueLabel: string = "ENSIndexe .object({ labelSet: makeFullyPinnedLabelSetSchema(`${valueLabel}.labelSet`), indexedChainIds: makeIndexedChainIdsSchema(`${valueLabel}.indexedChainIds`), - isSubgraphCompatible: z.boolean({ error: `${valueLabel}.isSubgraphCompatible` }), + isSubgraphCompatible: z.boolean({ + error: `${valueLabel}.isSubgraphCompatible must be a boolean value.`, + }), namespace: makeENSNamespaceIdSchema(`${valueLabel}.namespace`), plugins: makePluginsListSchema(`${valueLabel}.plugins`), databaseSchemaName: makeDatabaseSchemaNameSchema(`${valueLabel}.databaseSchemaName`), - versionInfo: makeENSIndexerVersionInfoSchema(`${valueLabel}.versionInfo`), + versionInfo: makeEnsIndexerVersionInfoSchema(`${valueLabel}.versionInfo`), }) /** * Validations @@ -189,3 +197,18 @@ export const makeEnsIndexerPublicConfigSchema = (valueLabel: string = "ENSIndexe * @deprecated Use {@link makeEnsIndexerPublicConfigSchema} instead. */ export const makeENSIndexerPublicConfigSchema = makeEnsIndexerPublicConfigSchema; + +export const makeSerializedEnsIndexerPublicConfigSchema = ( + valueLabel: string = "Serialized ENSIndexerPublicConfig", +) => + z.object({ + labelSet: makeFullyPinnedLabelSetSchema(`${valueLabel}.labelSet`), + indexedChainIds: makeSerializedIndexedChainIdsSchema(`${valueLabel}.indexedChainIds`), + isSubgraphCompatible: z.boolean({ + error: `${valueLabel}.isSubgraphCompatible must be a boolean value.`, + }), + namespace: makeENSNamespaceIdSchema(`${valueLabel}.namespace`), + plugins: makePluginsListSchema(`${valueLabel}.plugins`), + databaseSchemaName: makeDatabaseSchemaNameSchema(`${valueLabel}.databaseSchemaName`), + versionInfo: makeEnsIndexerVersionInfoSchema(`${valueLabel}.versionInfo`), + });