From 8d0b9ef04d9dac34b3dc4ec335e57c36e007a195 Mon Sep 17 00:00:00 2001 From: dkijania Date: Sun, 28 Jun 2026 18:30:57 +0200 Subject: [PATCH 1/3] feat(config): validate configuration at startup; fix boolean env parsing (#74) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Booleans were read with ad-hoc truthiness — `if (process.env.ENABLE_LOGGING)`, `if (!process.env.ENABLE_INTROSPECTION)`, `if (!process.env.ENABLE_JAEGER ...)` — so the string "false" was truthy and *enabled* the feature. There was also no startup validation, so typos surfaced as confusing runtime behaviour. - Add `src/config.ts`: a `parseBoolean` that understands true/false, 1/0, yes/no, on/off (case-insensitive), plus `validateConfig`/`assertValidConfig` that aggregate problems (missing PG_CONN, non-numeric PORT/BLOCK_RANGE_SIZE, mistyped booleans) and fail fast with one clear message. - Route every boolean env read through `parseBoolean` (plugins, server, jaeger tracing), fixing the "false enables it" bug (#74) and making all the strict `=== 'true'` checks accept the same spellings. - Call `assertValidConfig()` first thing at startup. Unit tests cover boolean spellings (incl. the "false" case), each validation rule, error aggregation, and the throwing behaviour. Closes #174. Closes #74. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01QSuak9smCHbp4N17xjjLF6 --- docs/getting-started.md | 4 +- src/config.ts | 90 ++++++++++++++++++++++++++++++++ src/index.ts | 2 + src/server/plugins.ts | 7 +-- src/server/server.ts | 8 +-- src/tracing/jaeger-tracing.ts | 3 +- tests/unit/config.test.ts | 97 +++++++++++++++++++++++++++++++++++ 7 files changed, 203 insertions(+), 8 deletions(-) create mode 100644 src/config.ts create mode 100644 tests/unit/config.test.ts diff --git a/docs/getting-started.md b/docs/getting-started.md index 1c690186..bc8dd4b2 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -170,7 +170,9 @@ JAEGER_ENDPOINT=http://localhost:14268/api/traces ## Configuration -The server reads config from environment variables. `PG_CONN` is the only required one. +The server reads config from environment variables. `PG_CONN` is the only required one. Configuration is validated at startup — a missing `PG_CONN`, a non-numeric `PORT`, or a mistyped boolean makes the server exit immediately with a clear message rather than booting into a broken state. + +Boolean variables (`ENABLE_*`) accept `true`/`false`, `1`/`0`, `yes`/`no`, or `on`/`off` (case-insensitive); `false` reliably means off. | Variable | Default | Description | | --- | --- | --- | diff --git a/src/config.ts b/src/config.ts new file mode 100644 index 00000000..355b9804 --- /dev/null +++ b/src/config.ts @@ -0,0 +1,90 @@ +export { parseBoolean, validateConfig, assertValidConfig }; + +type EnvSource = Record; + +const TRUE_VALUES = new Set(['true', '1', 'yes', 'on']); +const FALSE_VALUES = new Set(['false', '0', 'no', 'off']); + +/** Env vars interpreted as booleans. */ +const BOOLEAN_VARS = [ + 'ENABLE_GRAPHIQL', + 'ENABLE_INTROSPECTION', + 'ENABLE_LOGGING', + 'ENABLE_METRICS', + 'ENABLE_JAEGER', + 'ENABLE_BLOCK_TRANSACTION_DETAILS', +] as const; + +/** Env vars that, when set, must be positive integers. */ +const POSITIVE_INT_VARS = ['PORT', 'BLOCK_RANGE_SIZE'] as const; + +/** + * Parse a boolean environment value. Recognises `true/false`, `1/0`, `yes/no`, + * `on/off` (case-insensitive). Anything unrecognised — including the empty + * string or `undefined` — yields `fallback`. + * + * This replaces ad-hoc truthiness checks like `if (process.env.ENABLE_X)`, which + * treated the string `"false"` as `true` (#74). + */ +function parseBoolean(value: string | undefined, fallback = false): boolean { + if (value === undefined) return fallback; + const normalized = value.trim().toLowerCase(); + if (normalized === '') return fallback; + if (TRUE_VALUES.has(normalized)) return true; + if (FALSE_VALUES.has(normalized)) return false; + return fallback; +} + +function isRecognisedBoolean(value: string): boolean { + const normalized = value.trim().toLowerCase(); + return TRUE_VALUES.has(normalized) || FALSE_VALUES.has(normalized); +} + +/** + * Validate the environment, returning a list of human-readable problems (empty + * when valid). Catches the common misconfigurations — a missing connection + * string, a non-numeric port, a mistyped boolean — so they surface at startup + * rather than as confusing runtime behaviour. + */ +function validateConfig(env: EnvSource = process.env): string[] { + const errors: string[] = []; + + if (!env.PG_CONN || env.PG_CONN.trim() === '') { + errors.push('PG_CONN is required (Postgres connection string).'); + } + + for (const name of BOOLEAN_VARS) { + const value = env[name]; + if (value !== undefined && value.trim() !== '' && !isRecognisedBoolean(value)) { + errors.push( + `${name} must be a boolean (true/false), got "${value}".` + ); + } + } + + for (const name of POSITIVE_INT_VARS) { + const value = env[name]; + if (value !== undefined && value.trim() !== '') { + const parsed = Number(value); + if (!Number.isInteger(parsed) || parsed <= 0) { + errors.push(`${name} must be a positive integer, got "${value}".`); + } + } + } + + return errors; +} + +/** + * Validate the environment and throw a single aggregated error if anything is + * wrong, so the process fails fast at startup with a clear message instead of + * booting into a broken state. + */ +function assertValidConfig(env: EnvSource = process.env): void { + const errors = validateConfig(env); + if (errors.length > 0) { + throw new Error( + `Invalid configuration:\n${errors.map((e) => ` - ${e}`).join('\n')}` + ); + } +} diff --git a/src/index.ts b/src/index.ts index 5978deaa..183f786b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,6 +4,7 @@ import { buildContext } from './context.js'; import { buildServer } from './server/server.js'; import { buildPlugins } from './server/plugins.js'; import { createGracefulShutdown } from './server/graceful-shutdown.js'; +import { assertValidConfig } from './config.js'; const PORT = process.env.PORT || 8080; const SHUTDOWN_TIMEOUT_MS = Number(process.env.SHUTDOWN_TIMEOUT_MS) || 20000; @@ -31,6 +32,7 @@ function withTimeout( (async function main() { try { + assertValidConfig(); const context = await buildContext(process.env.PG_CONN); const { plugins, provider } = await buildPlugins(); const server = buildServer(context, plugins); diff --git a/src/server/plugins.ts b/src/server/plugins.ts index 1042da70..ebaf253a 100644 --- a/src/server/plugins.ts +++ b/src/server/plugins.ts @@ -6,6 +6,7 @@ import { inspect } from 'node:util'; import type { BasicTracerProvider } from '@opentelemetry/sdk-trace-base'; import { initJaegerProvider } from '../tracing/jaeger-tracing.js'; +import { parseBoolean } from '../config.js'; import { useMetrics } from './metrics.js'; import { useRateLimit } from './rate-limit.js'; @@ -19,7 +20,7 @@ async function buildPlugins() { // so over-limit traffic is rejected as cheaply as possible. plugins.push(useRateLimit()); - if (process.env.ENABLE_METRICS === 'true') { + if (parseBoolean(process.env.ENABLE_METRICS)) { // Prometheus /metrics endpoint + RED metrics for every request. plugins.push(useMetrics()); } @@ -28,7 +29,7 @@ async function buildPlugins() { // Returned so the entry point can flush spans on shutdown. let provider: BasicTracerProvider | undefined; - if (process.env.ENABLE_LOGGING) { + if (parseBoolean(process.env.ENABLE_LOGGING)) { provider = await initJaegerProvider(); plugins.push( useOpenTelemetry( @@ -45,7 +46,7 @@ async function buildPlugins() { ); } - if (!process.env.ENABLE_INTROSPECTION) { + if (!parseBoolean(process.env.ENABLE_INTROSPECTION)) { plugins.push(useDisableIntrospection()); } diff --git a/src/server/server.ts b/src/server/server.ts index 49c8ecc9..26d67e61 100644 --- a/src/server/server.ts +++ b/src/server/server.ts @@ -3,6 +3,7 @@ import { createServer } from 'http'; import { Plugin } from '@envelop/core'; import { schema } from '../resolvers.js'; import type { GraphQLContext } from '../context.js'; +import { parseBoolean } from '../config.js'; import { useReadiness } from './readiness.js'; export { @@ -14,8 +15,9 @@ export { const LOG_LEVEL = (process.env.LOG_LEVEL as LogLevel) || 'info'; const BLOCK_RANGE_SIZE = Number(process.env.BLOCK_RANGE_SIZE) || 10000; -const ENABLE_BLOCK_TRANSACTION_DETAILS = - process.env.ENABLE_BLOCK_TRANSACTION_DETAILS === 'true'; +const ENABLE_BLOCK_TRANSACTION_DETAILS = parseBoolean( + process.env.ENABLE_BLOCK_TRANSACTION_DETAILS +); function buildYoga(context: GraphQLContext, plugins: Plugin[]) { return createYoga({ @@ -25,7 +27,7 @@ function buildYoga(context: GraphQLContext, plugins: Plugin[]) { landingPage: false, // Liveness — the process is up and serving HTTP. healthCheckEndpoint: '/healthcheck', - graphiql: process.env.ENABLE_GRAPHIQL === 'true' ? true : false, + graphiql: parseBoolean(process.env.ENABLE_GRAPHIQL), // Mask unexpected (non-GraphQLError) errors so internal details — SQL, // connection strings, stack traces — never reach clients. `isDev: false` // keeps Envelop from attaching original errors when NODE_ENV=development. diff --git a/src/tracing/jaeger-tracing.ts b/src/tracing/jaeger-tracing.ts index 17dac464..d153bbc0 100644 --- a/src/tracing/jaeger-tracing.ts +++ b/src/tracing/jaeger-tracing.ts @@ -11,6 +11,7 @@ import { parseEndpoint, checkJaegerEndpointAvailability, } from './jaeger-setup.js'; +import { parseBoolean } from '../config.js'; export { initJaegerProvider }; @@ -20,7 +21,7 @@ function createJaegerExporter(endpoint: string) { async function initJaegerProvider(): Promise { const jaegerEndpoint = process.env.JAEGER_ENDPOINT; - if (!process.env.ENABLE_JAEGER || !jaegerEndpoint) { + if (!parseBoolean(process.env.ENABLE_JAEGER) || !jaegerEndpoint) { return undefined; } diff --git a/tests/unit/config.test.ts b/tests/unit/config.test.ts new file mode 100644 index 00000000..cb3cbd92 --- /dev/null +++ b/tests/unit/config.test.ts @@ -0,0 +1,97 @@ +import { describe, test } from 'node:test'; +import assert from 'node:assert'; +import { + parseBoolean, + validateConfig, + assertValidConfig, +} from '../../src/config.js'; + +describe('parseBoolean', () => { + test('recognises truthy spellings', () => { + for (const value of ['true', 'TRUE', '1', 'yes', 'on', ' True ']) { + assert.strictEqual(parseBoolean(value), true, `expected ${value} → true`); + } + }); + + test('recognises falsy spellings, including the string "false" (#74)', () => { + for (const value of ['false', 'FALSE', '0', 'no', 'off', ' false ']) { + assert.strictEqual( + parseBoolean(value), + false, + `expected ${value} → false` + ); + } + }); + + test('uses the fallback for undefined/empty/unrecognised', () => { + assert.strictEqual(parseBoolean(undefined), false); + assert.strictEqual(parseBoolean(''), false); + assert.strictEqual(parseBoolean('maybe'), false); + assert.strictEqual(parseBoolean(undefined, true), true); + }); +}); + +describe('validateConfig', () => { + const valid = { PG_CONN: 'postgres://localhost:5432/archive' }; + + test('passes for a minimal valid environment', () => { + assert.deepStrictEqual(validateConfig(valid), []); + }); + + test('requires PG_CONN', () => { + const errors = validateConfig({}); + assert.strictEqual(errors.length, 1); + assert.match(errors[0], /PG_CONN is required/); + }); + + test('flags a non-boolean boolean var', () => { + const errors = validateConfig({ ...valid, ENABLE_JAEGER: 'sometimes' }); + assert.ok(errors.some((e) => /ENABLE_JAEGER must be a boolean/.test(e))); + }); + + test('accepts recognised boolean spellings', () => { + assert.deepStrictEqual( + validateConfig({ + ...valid, + ENABLE_LOGGING: 'no', + ENABLE_GRAPHIQL: '1', + ENABLE_METRICS: 'off', + }), + [] + ); + }); + + test('flags a non-positive-integer PORT', () => { + assert.ok( + validateConfig({ ...valid, PORT: 'abc' }).some((e) => /PORT/.test(e)) + ); + assert.ok( + validateConfig({ ...valid, PORT: '0' }).some((e) => /PORT/.test(e)) + ); + }); + + test('aggregates multiple problems', () => { + const errors = validateConfig({ PORT: '-1', ENABLE_LOGGING: 'huh' }); + assert.strictEqual(errors.length, 3); // PG_CONN, PORT, ENABLE_LOGGING + }); +}); + +describe('assertValidConfig', () => { + test('throws an aggregated error listing every problem', () => { + assert.throws( + () => assertValidConfig({ PORT: 'nope' }), + (err: Error) => { + assert.match(err.message, /Invalid configuration/); + assert.match(err.message, /PG_CONN/); + assert.match(err.message, /PORT/); + return true; + } + ); + }); + + test('does not throw for a valid environment', () => { + assert.doesNotThrow(() => + assertValidConfig({ PG_CONN: 'postgres://localhost/db' }) + ); + }); +}); From 4f3d117d5d66cf3a1b777e981ee8fa9185529b2e Mon Sep 17 00:00:00 2001 From: dkijania Date: Fri, 17 Jul 2026 11:30:21 +0200 Subject: [PATCH 2/3] test(config): lock in multi-host PG_CONN validation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PG_CONN is deliberately only checked for non-emptiness. That is what keeps the documented HA form (postgres://host1:5432,host2:5432/archive) working — a stricter URL parser here would reject it and break every HA deployment, including the archives the mina-explorer talks to. Pins it so a future "hardening" of this check fails loudly instead of silently. The behaviour-flip upgrade note is on the PR description, since the repo has no CHANGELOG. Addresses review feedback on #193. Co-Authored-By: Claude Opus 4.8 (1M context) --- tests/unit/config.test.ts | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/tests/unit/config.test.ts b/tests/unit/config.test.ts index cb3cbd92..9111214f 100644 --- a/tests/unit/config.test.ts +++ b/tests/unit/config.test.ts @@ -44,6 +44,26 @@ describe('validateConfig', () => { assert.match(errors[0], /PG_CONN is required/); }); + test('accepts multi-host HA connection strings', () => { + // The HA form documented in docs/getting-started.md. PG_CONN is + // deliberately only checked for non-emptiness: a stricter URL parser here + // would reject this and break every HA deployment, so this test exists to + // make a future "hardening" of the check fail loudly rather than silently. + assert.deepStrictEqual( + validateConfig({ PG_CONN: 'postgres://host1:5432,host2:5432/archive' }), + [] + ); + }); + + test('accepts a connection string with credentials and query params', () => { + assert.deepStrictEqual( + validateConfig({ + PG_CONN: 'postgres://user:pw@host1:5432,host2:5432/archive?sslmode=require', + }), + [] + ); + }); + test('flags a non-boolean boolean var', () => { const errors = validateConfig({ ...valid, ENABLE_JAEGER: 'sometimes' }); assert.ok(errors.some((e) => /ENABLE_JAEGER must be a boolean/.test(e))); From 9c58970d1fcd3c3e2b2ead08b68a732ad64629ad Mon Sep 17 00:00:00 2001 From: dkijania Date: Wed, 19 Aug 2026 13:40:03 +0200 Subject: [PATCH 3/3] fix(config): validate enabled query filters --- README.md | 3 ++- docs/getting-started.md | 38 +++++++++++++++++++++++++------------- src/config.ts | 37 +++++++++++++++++++++++++++++++++---- src/envionment.d.ts | 2 ++ tests/unit/config.test.ts | 26 +++++++++++++++++++++++++- 5 files changed, 87 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 6157b3bb..59c9612c 100644 --- a/README.md +++ b/README.md @@ -58,8 +58,9 @@ PG_CONN='postgres://postgres:postgres@localhost:5432/archive' \ | `PORT` | `8080` | Port the GraphQL server listens on | | `ENABLE_GRAPHIQL` | `false` | Serve the GraphiQL playground at `/` | | `ENABLE_INTROSPECTION` | `false` | Allow GraphQL schema introspection | -| `ENABLE_LOGGING` | `false` | Enable request logging | +| `ENABLE_LOGGING` | `false` | Enable OpenTelemetry request tracing | | `ENABLE_METRICS` | `false` | Expose Prometheus metrics at `/metrics` | +| `ENABLED_QUERIES` | _(all)_ | Comma-separated subset of root query fields | | `ENABLE_JAEGER` | `false` | Emit traces to a Jaeger collector | | `JAEGER_ENDPOINT` | — | e.g. `http://localhost:14268/api/traces` | diff --git a/docs/getting-started.md b/docs/getting-started.md index bc8dd4b2..3da6a460 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -170,9 +170,9 @@ JAEGER_ENDPOINT=http://localhost:14268/api/traces ## Configuration -The server reads config from environment variables. `PG_CONN` is the only required one. Configuration is validated at startup — a missing `PG_CONN`, a non-numeric `PORT`, or a mistyped boolean makes the server exit immediately with a clear message rather than booting into a broken state. +The server reads config from environment variables. `PG_CONN` is the only required one. Configuration is validated at startup — a missing `PG_CONN`, a non-numeric `PORT`, a mistyped boolean, or an invalid `ENABLED_QUERIES` value makes the server exit immediately with a clear message rather than booting into a broken state. -Boolean variables (`ENABLE_*`) accept `true`/`false`, `1`/`0`, `yes`/`no`, or `on`/`off` (case-insensitive); `false` reliably means off. +Boolean variables (`ENABLE_*`) accept `true`/`false`, `1`/`0`, `yes`/`no`, or `on`/`off` (case-insensitive); `false` reliably means off. Any other non-empty spelling now aborts startup instead of being interpreted inconsistently. `ENABLE_LOGGING`, `ENABLE_INTROSPECTION`, and `ENABLE_JAEGER` previously treated any non-empty value as on; `ENABLE_GRAPHIQL` and `ENABLE_BLOCK_TRANSACTION_DETAILS` previously only accepted the literal string `true` as on. | Variable | Default | Description | | --- | --- | --- | @@ -187,10 +187,11 @@ Boolean variables (`ENABLE_*`) accept `true`/`false`, `1`/`0`, `yes`/`no`, or `o | `TRUST_PROXY` | _(unset)_ | Number of trusted proxy hops in front of the API. Required when `RATE_LIMIT_MAX > 0`: the limiter stays disabled until it is set. `0` ignores `X-Forwarded-For` and keys on the socket address | | `ENABLE_GRAPHIQL` | `false` | If `true`, serves the GraphiQL playground at `/` | | `ENABLE_INTROSPECTION` | `false` | If `true`, allows GraphQL schema introspection | -| `ENABLE_LOGGING` | `false` | Enable request logging | +| `ENABLE_LOGGING` | `false` | Enable OpenTelemetry request tracing | | `ENABLE_METRICS` | `false` | If `true`, exposes unauthenticated Prometheus metrics at `/metrics` | | `BLOCK_RANGE_SIZE` | `10000` | Max block range a single query may span | | `ENABLE_BLOCK_TRANSACTION_DETAILS` | `false` | Include `userCommands` / `zkappCommands` / `feeTransfers` | +| `ENABLED_QUERIES` | _(all)_ | Comma-separated subset of `events,actions,networkState,blocks` to expose; omitted fields are removed from the schema | | `ENABLE_JAEGER` | `false` | Emit traces to a Jaeger collector | | `JAEGER_SERVICE_NAME` | `archive-api` | Service name reported to Jaeger | | `JAEGER_ENDPOINT` | — | e.g. `http://localhost:14268/api/traces` | @@ -279,9 +280,20 @@ This query returns the latest indexed block height — compare it with [MinaScan ```graphql query GetEvents { events(input: { address: "B62..." }) { - blockInfo { height stateHash timestamp chainStatus } - eventData { data } - transactionInfo { status hash memo } + blockInfo { + height + stateHash + timestamp + chainStatus + } + eventData { + data + } + transactionInfo { + status + hash + memo + } } } ``` @@ -292,14 +304,14 @@ Replace `B62...` with the address of the zkApp whose events you want. ## Troubleshooting -| Symptom | Likely cause | Fix | -| --- | --- | --- | -| `An error occurred: AggregateError [ECONNREFUSED]` | `PG_CONN` host/port wrong or DB not running | Verify with `psql "$PG_CONN" -c 'SELECT 1'` | -| `relation "..." does not exist` on startup | Postgres reachable but not an archive-node schema | Point `PG_CONN` at an actual archive-node DB | -| `/` returns 404 in the browser | `ENABLE_GRAPHIQL` not set | Set `ENABLE_GRAPHIQL=true` and restart | -| `EADDRINUSE: address already in use :::8080` | Port already taken | `PORT=` and restart | +| Symptom | Likely cause | Fix | +| ---------------------------------------------------------- | ------------------------------------------------- | -------------------------------------------------------- | +| `An error occurred: AggregateError [ECONNREFUSED]` | `PG_CONN` host/port wrong or DB not running | Verify with `psql "$PG_CONN" -c 'SELECT 1'` | +| `relation "..." does not exist` on startup | Postgres reachable but not an archive-node schema | Point `PG_CONN` at an actual archive-node DB | +| `/` returns 404 in the browser | `ENABLE_GRAPHIQL` not set | Set `ENABLE_GRAPHIQL=true` and restart | +| `EADDRINUSE: address already in use :::8080` | Port already taken | `PORT=` and restart | | Compose: API starts but logs `relation ... does not exist` | Snapshot still loading into Postgres on first run | Wait for the `postgres` container to finish initialising | -| Compose: snapshot download script fails | Network or storage limit | Re-run `./scripts/download_db.sh`; check disk space | +| Compose: snapshot download script fails | Network or storage limit | Re-run `./scripts/download_db.sh`; check disk space | --- diff --git a/src/config.ts b/src/config.ts index 355b9804..e206ab9a 100644 --- a/src/config.ts +++ b/src/config.ts @@ -18,6 +18,9 @@ const BOOLEAN_VARS = [ /** Env vars that, when set, must be positive integers. */ const POSITIVE_INT_VARS = ['PORT', 'BLOCK_RANGE_SIZE'] as const; +/** Root query fields in schema.graphql — keep in sync. */ +const KNOWN_QUERIES = ['events', 'actions', 'networkState', 'blocks'] as const; + /** * Parse a boolean environment value. Recognises `true/false`, `1/0`, `yes/no`, * `on/off` (case-insensitive). Anything unrecognised — including the empty @@ -55,10 +58,12 @@ function validateConfig(env: EnvSource = process.env): string[] { for (const name of BOOLEAN_VARS) { const value = env[name]; - if (value !== undefined && value.trim() !== '' && !isRecognisedBoolean(value)) { - errors.push( - `${name} must be a boolean (true/false), got "${value}".` - ); + if ( + value !== undefined && + value.trim() !== '' && + !isRecognisedBoolean(value) + ) { + errors.push(`${name} must be a boolean (true/false), got "${value}".`); } } @@ -72,6 +77,30 @@ function validateConfig(env: EnvSource = process.env): string[] { } } + const enabledQueries = env.ENABLED_QUERIES; + if (enabledQueries !== undefined) { + const names = enabledQueries + .split(',') + .map((query) => query.trim()) + .filter((query) => query !== ''); + if (names.length === 0) { + errors.push( + 'ENABLED_QUERIES is set but lists no queries; unset it to expose all of ' + + `${KNOWN_QUERIES.join(', ')}.` + ); + } + + const unknown = names.filter( + (name) => !(KNOWN_QUERIES as readonly string[]).includes(name) + ); + if (unknown.length > 0) { + errors.push( + `ENABLED_QUERIES contains unknown queries: ${unknown.join(', ')}. ` + + `Known queries: ${KNOWN_QUERIES.join(', ')}.` + ); + } + } + return errors; } diff --git a/src/envionment.d.ts b/src/envionment.d.ts index 2363573c..6248c66c 100644 --- a/src/envionment.d.ts +++ b/src/envionment.d.ts @@ -15,6 +15,8 @@ declare global { ENABLE_INTROSPECTION?: bool; ENABLE_GRAPHIQL?: bool; ENABLE_JAEGER?: bool; + ENABLE_BLOCK_TRANSACTION_DETAILS?: bool; + ENABLED_QUERIES?: string; JAEGER_ENDPOINT?: string; JAEGER_SERVICE_NAME?: string; } diff --git a/tests/unit/config.test.ts b/tests/unit/config.test.ts index 9111214f..26a1018a 100644 --- a/tests/unit/config.test.ts +++ b/tests/unit/config.test.ts @@ -58,7 +58,8 @@ describe('validateConfig', () => { test('accepts a connection string with credentials and query params', () => { assert.deepStrictEqual( validateConfig({ - PG_CONN: 'postgres://user:pw@host1:5432,host2:5432/archive?sslmode=require', + PG_CONN: + 'postgres://user:pw@host1:5432,host2:5432/archive?sslmode=require', }), [] ); @@ -81,6 +82,29 @@ describe('validateConfig', () => { ); }); + test('accepts a valid ENABLED_QUERIES subset', () => { + assert.deepStrictEqual( + validateConfig({ ...valid, ENABLED_QUERIES: 'blocks, networkState' }), + [] + ); + }); + + test('rejects a typo in ENABLED_QUERIES that would delete a root field', () => { + const errors = validateConfig({ + ...valid, + ENABLED_QUERIES: 'blocks,event', + }); + assert.ok(errors.some((e) => /unknown queries: event/.test(e))); + }); + + test('rejects an empty ENABLED_QUERIES list', () => { + assert.ok( + validateConfig({ ...valid, ENABLED_QUERIES: '' }).some((e) => + /ENABLED_QUERIES/.test(e) + ) + ); + }); + test('flags a non-positive-integer PORT', () => { assert.ok( validateConfig({ ...valid, PORT: 'abc' }).some((e) => /PORT/.test(e))