Repository navigation
fix(api): gate candles routes with the refresh worker - #215
Conversation
|
@Wang1rrr Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits. You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀 |
Miracle656
left a comment
There was a problem hiding this comment.
A lot here is genuinely careful and I want to credit it before the objections. The overlap guard in startOhlcRefreshWorker (the running flag) and the switch to Promise.allSettled so a failed bucket can't leave queries pending into the next tick are both the right instincts. The interval validation is unusually thorough — rejecting non-integers, zero and blank, and explicitly rejecting > 2^31-1 because Node clamps an oversized delay to 1 ms is the kind of thing most people find out in production. Disabled by default, wired into the shutdown path, and SKIP_INDEXER respected. And the $queryRawUnsafe parameter fix in queryCandlesFromAggregate is a real bug caught: main passed [contractId, limit, offset] as a single positional argument, so $1/$2/$3 never bound. The ${table} interpolation is safe because bucket comes out of a z.enum before it's used — I checked.
Verified locally on the merge with main: tsc --noEmit and tsc -p tsconfig.test.json both clean, and all three new suites pass — 41 tests across ohlcRefreshWorker, ohlcStartup and candles-route.
Three things I can't merge past.
1. POST /candles/refresh is mounted with no authentication (src/api/candles.ts:98, mounted at src/api.ts:199)
This is a new, anonymous, mutating endpoint that runs ohlc.refresh_candles_1m/1h/1d() — three full aggregate rebuilds over the transfers table — against the production database. The only thing in front of it is the global rate limiter at src/api.ts:65, which is 60 requests/minute per IP. Sixty full aggregate rebuilds a minute per IP, from anyone, on a Postgres instance this deployment shares. openapi.json now advertises it too.
The irony is that this PR makes the endpoint unnecessary: startOhlcRefreshWorker already does exactly this on a timer. Either drop the public route entirely and let the worker own refreshes, or put it behind the same bearer-token check #216 just added to /offramp. Dropping it is cleaner.
2. The read route answers for a network it then ignores
networkMiddleware is mounted app-wide at src/api.ts:155, so GET /candles/1h/C…?network=mainnet validates and resolves the network — and then queryCandlesFromAggregate never uses it. Your own README text says why: "These legacy aggregates do not separate networks, so they must not be used with a database containing transfers from multiple networks." Thank you for writing that down honestly, but a README note doesn't stop the route from returning 200 with mainnet and testnet candles blended together while the caller believes they asked for one. That's the failure mode src/middleware/network.ts is written against in so many words — "silently conflating them is how a dashboard ends up confidently showing zero."
Since the tables also aren't installed by any current startup path (again, your README), the route 500s on every request on every deployment today. Gate the mount behind the same opt-in that gates the worker: if OHLC_REFRESH_INTERVAL_MS is unset, don't mount /candles and don't register it in openapi.json. Then an operator who installs the aggregates and accepts the single-network constraint opts into both together, and nobody else gets a documented endpoint that only ever 500s.
3. src/db.ts:39 — log: [] silences Prisma for the whole application
Going from [query, warn, error] in development and [warn, error] in production to [] everywhere is a global observability change that has nothing to do with mounting candles routes. The motivation is right — this repo's rule is that nothing logs a raw database error or connection string — but it's worth its own commit and its own reasoning, because as written it also drops Prisma's connection-pool and deprecation warnings, which don't carry query text. log: ["warn"] would satisfy the rule and keep those. Either way, please split it out.
One smaller note, no action required: catch { console.error("[candles] Query failed"); } now discards the error object completely. Not leaking the message is right; losing the error class means a missing relation and a connection timeout produce byte-identical logs. Something like err instanceof Error ? err.name : "unknown" keeps it diagnosable without leaking anything.
Solid work — the worker is in good shape. It's the public surface that needs tightening.
19826df to
afd8074
Compare
Keep connection-pool and deprecation warnings available while handling raw database errors at the application boundary. Query and error stdout logging can expose database details before handlers sanitize failures.
afd8074 to
c79ed01
Compare
|
Merged. The gate is the right shape and the default is the right default. What convinced me: the legacy OHLC aggregates have no network dimension, so in a multi-network process the route would have served candles mixed across testnet and mainnet with nothing in the response to admit it. Off-by-default beats confidently-wrong, and your Good catch on the interval ceiling. Node clamps a Removing the public refresh endpoint is also right. A refresh belongs to the worker because the worker is what holds the overlap guard; an HTTP caller could start a second pass while the first one's queries were still outstanding. One hunk I reverted
Prisma's I kept You did flag this as a deliberately separate commit for reviewability, which I appreciate — but an application-wide logging change still wants its own PR rather than riding along with a candles gate. If you think Merge notesREADME conflicted: Verified on the merge result rather than your branch: Also noted your point about the database prerequisite — |
…t schema Conflicts in src/openapi/build.ts and src/openapi/schemas.ts: this branch added /fees/report while main added /fees (Miracle656#215 area) and the gated /candles route at the same place. Both kept. openapi.json regenerated from the merged source in a clean environment (npm run docs:openapi): identical to main's plus /fees/report. clients/react-query/src/schema.d.ts regenerated from it. The branch had changed openapi.json without regenerating the client, which fails the "Generate, typecheck & test" job (git diff --exit-code src/schema.d.ts). Verified: typecheck, tsc --noEmit, build; jest 63 suites / 961 tests (the openapiCoverage failure noted in the PR description is gone); clients/react-query generate (no diff), typecheck, test, build. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LvL6Yz2Zfg7spM1F9DXXtz
`validateEnv()` now refuses to start when `DATABASE_URL` or `DIRECT_DATABASE_URL` is missing, or when mainnet is enabled with no mainnet RPC URL, naming what is missing. Previously those surfaced much later as connection errors that said nothing about configuration. `validateEnv` itself is a pure function taking `env` as a parameter, which is what makes it testable without touching the process environment. Three changes while merging: - **`src/index.ts` conflicted.** The PR converted every top-level `import` to `const x = require(...) as typeof import(...)` so `validateEnv()` could run before module side effects. Kept `main`'s imports instead: the require block predated #215 and omitted `startOhlcRefreshWorker` and `getOhlcRefreshIntervalMs`, so taking it would have silently unwired the OHLC worker. In a `module: commonjs` build an `import` is a sequential `require` anyway, so ordering is achievable without rewriting the entrypoint's style. - **`validateEnv()` moved into `main()`** rather than module scope. At module scope it threw whenever anything imported the entrypoint, which broke `ohlcStartup.test.ts` — that suite boots the real `index.ts`. It still runs before any listener opens, which is the stated goal. - **`docker-compose.yml` had drifted from `.env.example` by 36 keys**, which `dockerCompose.test.ts` caught. Added them as unset passthroughs (`KEY: ${KEY:-}`), so a compose run matches a host run and no secret value is committed. `ohlcStartup.test.ts` now sets the database URLs and a mainnet RPC in its `beforeEach`, because it starts the real entrypoint and a real start requires them. Nothing connects to either — every module that would is mocked. Verified on the merge result: `tsc --noEmit` silent, 998 tests pass across 65 suites. Claude-Session: https://claude.ai/code/session_01USgemLt4Rnz4SGB1Srf3GB
Description
Connect the existing candles reader and refresh worker behind one explicit opt-in. A valid
OHLC_REFRESH_INTERVAL_MSenables both only for a single enabled network matchingSTELLAR_NETWORK; unset, invalid, zero, API-only mode and incompatible network configurations leave the route unmounted and the worker stopped. The OpenAPI generator uses the same gate, so the default specification omits candles.Remove the public refresh endpoint. Refreshes belong to the worker, whose overlap guard and
Promise.allSettledhandling prevent a failed bucket from allowing another refresh to start before the remaining queries finish. Shutdown retains and calls the worker's stop callback.The read route uses the existing parameter schemas, binds SQL values separately, and sanitizes database failures. A separate commit retains Prisma operational warnings while disabling raw query/error stdout logging; this keeps the application-wide logging change independently reviewable.
Closes #190.
Validation
Validated on 2026-10-09:
npm run db:generate,npm run typecheckandnpm run build: passed.npm test: 57 suites / 707 tests passed after rebasing on current main, including the configured coverage thresholds.npm run docs:openapialso passed with the default disabled setting.git diff --check: passed.The database integration suite was not run for this follow-up; HTTP and worker regressions use mocked database/RPC boundaries.
Existing database prerequisite
The operator must install working OHLC tables/functions and ensure the database contains transfers from only the configured network before opting in. The legacy aggregates have no network dimension, and the existing
sql/001_ohlc_aggregates.sqlrequires separate repair and deployment; this PR does not install or execute it. Restricting the current process configuration cannot certify the contents of an existing database. README and.env.exampleexplain the prerequisite and how to regenerate OpenAPI with the deployment's settings.AI assistance was used for this review follow-up; the changes were checked against the review and the full unit test suite.