-
Notifications
You must be signed in to change notification settings - Fork 15
refactor route definitions #1631
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,37 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Generates the OpenAPI specification JSON for ENSApi. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * This script can run without any environment variables or config because it | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * imports only from the lightweight `.routes.ts` description files via | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * `createRoutesForSpec()`, which have zero config dependencies. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * The version is resolved from (in order of precedence): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * 1. OPENAPI_VERSION_OVERRIDE env var | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * 2. package.json version | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * 3. openapiDocumentation.info.version (placeholder fallback) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Usage: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * pnpm generate:openapi > openapi.json | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * pnpm generate:openapi # prints to stdout | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * OPENAPI_VERSION_OVERRIDE=1.2.3 pnpm generate:openapi | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import packageJson from "@/../package.json" with { type: "json" }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { openapiDocumentation } from "@/openapi"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { createRoutesForSpec } from "@/openapi-routes"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const version = | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| process.env.OPENAPI_VERSION_OVERRIDE || packageJson.version || openapiDocumentation.info.version; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const app = createRoutesForSpec(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const spec = app.getOpenAPI31Document({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ...openapiDocumentation, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| info: { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ...openapiDocumentation.info, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| version, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
26
to
35
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const app = createRoutesForSpec(); | |
| const spec = app.getOpenAPI31Document(openapiDocumentation); | |
| import * as fs from "fs"; | |
| import * as path from "path"; | |
| const app = createRoutesForSpec(); | |
| const packageJsonPath = path.join(__dirname, "..", "package.json"); | |
| const packageJsonRaw = fs.readFileSync(packageJsonPath, "utf8"); | |
| const packageJson = JSON.parse(packageJsonRaw) as { version?: string }; | |
| const resolvedVersion = | |
| process.env.OPENAPI_VERSION_OVERRIDE || | |
| packageJson.version || | |
| openapiDocumentation.info?.version; | |
| const openapiDocumentationWithVersion = { | |
| ...openapiDocumentation, | |
| info: { | |
| ...openapiDocumentation.info, | |
| version: resolvedVersion, | |
| }, | |
| }; | |
| const spec = app.getOpenAPI31Document(openapiDocumentationWithVersion); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,106 @@ | ||
| /** | ||
| * Application setup: creates the Hono app with all middleware and routes. | ||
| * | ||
| * Separated from index.ts (server startup/shutdown) so the app configuration | ||
| * is distinct from the runtime lifecycle concerns. | ||
| */ | ||
|
|
||
| import packageJson from "@/../package.json" with { type: "json" }; | ||
| import config from "@/config"; | ||
|
|
||
| import { otel } from "@hono/otel"; | ||
| import { cors } from "hono/cors"; | ||
| import { html } from "hono/html"; | ||
|
|
||
| import { errorResponse } from "@/lib/handlers/error-response"; | ||
| import { createApp } from "@/lib/hono-factory"; | ||
| import logger from "@/lib/logger"; | ||
| import { indexingStatusMiddleware } from "@/middleware/indexing-status.middleware"; | ||
| import { openapiDocumentation } from "@/openapi"; | ||
|
|
||
| import amIRealtimeApi from "./handlers/amirealtime-api"; | ||
| import { basePath as amIRealtimeBasePath } from "./handlers/amirealtime-api.routes"; | ||
| import ensanalyticsApi from "./handlers/ensanalytics-api"; | ||
| import { basePath as ensanalyticsBasePath } from "./handlers/ensanalytics-api.routes"; | ||
| import ensanalyticsApiV1 from "./handlers/ensanalytics-api-v1"; | ||
| import { basePath as ensanalyticsV1BasePath } from "./handlers/ensanalytics-api-v1.routes"; | ||
| import ensNodeApi from "./handlers/ensnode-api"; | ||
| import { basePath as ensnodeBasePath } from "./handlers/ensnode-api.routes"; | ||
| import subgraphApi from "./handlers/subgraph-api"; | ||
|
|
||
| const app = createApp(); | ||
|
|
||
| // set the X-ENSNode-Version header to the current version | ||
| app.use(async (ctx, next) => { | ||
| ctx.header("x-ensnode-version", packageJson.version); | ||
| return next(); | ||
| }); | ||
|
|
||
| // use CORS middleware | ||
| app.use(cors({ origin: "*" })); | ||
|
|
||
| // include automatic OpenTelemetry instrumentation for incoming requests | ||
| app.use(otel()); | ||
|
|
||
| // add ENSIndexer Indexing Status Middleware to all routes for convenience | ||
| app.use(indexingStatusMiddleware); | ||
|
Comment on lines
+45
to
+46
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick | 🔵 Trivial
The global 🤖 Prompt for AI Agents |
||
|
|
||
| // host welcome page | ||
| app.get("/", (c) => | ||
| c.html(html` | ||
| <!DOCTYPE html> | ||
| <html lang="en"> | ||
| <head> | ||
| <meta charset="UTF-8"> | ||
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | ||
| <title>ENSApi</title> | ||
| </head> | ||
| <body> | ||
| <h1>Hello, World!</h1> | ||
| <p>You've reached the root of an ENSApi instance. You might be looking for the <a href="https://ensnode.io/docs/">ENSNode documentation</a>.</p> | ||
| </body> | ||
| </html> | ||
| `), | ||
| ); | ||
|
|
||
| // use ENSNode HTTP API at /api | ||
| app.route(ensnodeBasePath, ensNodeApi); | ||
|
|
||
| // Subgraph API is a GraphQL middleware handler, not an OpenAPI route, | ||
| // so it has no .routes.ts file or basePath export. | ||
| app.route("/subgraph", subgraphApi); | ||
|
|
||
| // use ENSAnalytics API at /ensanalytics (v0, implicit) | ||
| app.route(ensanalyticsBasePath, ensanalyticsApi); | ||
|
|
||
| // use ENSAnalytics API v1 at /v1/ensanalytics | ||
| app.route(ensanalyticsV1BasePath, ensanalyticsApiV1); | ||
|
|
||
| // use Am I Realtime API at /amirealtime | ||
| app.route(amIRealtimeBasePath, amIRealtimeApi); | ||
|
|
||
| // use OpenAPI Schema | ||
| app.doc31("/openapi.json", { | ||
| ...openapiDocumentation, | ||
| info: { | ||
| ...openapiDocumentation.info, | ||
| version: packageJson.version, | ||
| }, | ||
| servers: [ | ||
| ...openapiDocumentation.servers, | ||
| { url: `http://localhost:${config.port}`, description: "Local Development" }, | ||
| ], | ||
| }); | ||
|
|
||
| // will automatically 503 if config is not available due to ensIndexerPublicConfigMiddleware | ||
| app.get("/health", async (c) => { | ||
| return c.json({ message: "fallback ok" }); | ||
| }); | ||
|
Comment on lines
+95
to
+98
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Misleading comment on The comment on Line 95 says the endpoint "will automatically 503 if config is not available due to ensIndexerPublicConfigMiddleware," but no such middleware is applied in this file. The Either remove the stale comment or wire the middleware that would actually produce 503 responses on this endpoint. 🤖 Prompt for AI Agents |
||
|
|
||
| // log hono errors to console | ||
| app.onError((error, ctx) => { | ||
| logger.error(error); | ||
| return errorResponse(ctx, "Internal Server Error"); | ||
| }); | ||
|
|
||
| export default app; | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,43 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { createRoute } from "@hono/zod-openapi"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { minutesToSeconds } from "date-fns"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { z } from "zod/v4"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import type { Duration } from "@ensnode/ensnode-sdk"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { makeDurationSchema } from "@ensnode/ensnode-sdk/internal"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { params } from "@/lib/handlers/params.schema"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export const basePath = "/amirealtime"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Set default `maxWorstCaseDistance` for `GET /amirealtime` endpoint to one minute. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export const AMIREALTIME_DEFAULT_MAX_WORST_CASE_DISTANCE: Duration = minutesToSeconds(1); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export const amIRealtimeGetMeta = createRoute({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| method: "get", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| path: "/", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| tags: ["Meta"], | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| summary: "Check indexing progress", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| description: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "Checks if the indexing progress is guaranteed to be within a requested worst-case distance of realtime", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| request: { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| query: z.object({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| maxWorstCaseDistance: params.queryParam | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .optional() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .default(AMIREALTIME_DEFAULT_MAX_WORST_CASE_DISTANCE) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .pipe(makeDurationSchema("maxWorstCaseDistance query param")) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .describe("Maximum acceptable worst-case indexing distance in seconds"), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| responses: { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 200: { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| description: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "Indexing progress is guaranteed to be within the requested distance of realtime", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 503: { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| description: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "Indexing progress is not guaranteed to be within the requested distance of realtime or indexing status unavailable", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+31
to
+40
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 200 and 503 response bodies are undocumented in the spec. Both responses declare only
The OpenAPI spec will incorrectly indicate body-less responses for both status codes. ✏️ Proposed fix (add response schemas)+import { z } from "zod/v4";
export const getAmIRealtimeRoute = createRoute({
...
responses: {
description:
"Indexing progress is guaranteed to be within the requested distance of realtime",
+ content: {
+ "application/json": {
+ schema: z.object({
+ maxWorstCaseDistance: z.number(),
+ slowestChainIndexingCursor: z.number().nullable(),
+ worstCaseDistance: z.number(),
+ }),
+ },
+ },
},
description:
"Indexing progress is not guaranteed to be within the requested distance of realtime or indexing status unavailable",
+ content: {
+ "application/json": {
+ schema: z.object({ message: z.string() }),
+ },
+ },
},
},
});📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export const routes = [amIRealtimeGetMeta]; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
Repository: namehash/ensnode
Length of output: 830
🏁 Script executed:
Repository: namehash/ensnode
Length of output: 50374
🏁 Script executed:
Repository: namehash/ensnode
Length of output: 1888
🏁 Script executed:
Repository: namehash/ensnode
Length of output: 2512
🏁 Script executed:
Repository: namehash/ensnode
Length of output: 1988
🏁 Script executed:
Repository: namehash/ensnode
Length of output: 42
🏁 Script executed:
Repository: namehash/ensnode
Length of output: 3503
🏁 Script executed:
Repository: namehash/ensnode
Length of output: 42
🏁 Script executed:
Repository: namehash/ensnode
Length of output: 752
🏁 Script executed:
Repository: namehash/ensnode
Length of output: 42
Typecheck script uses unavailable
tsgotool;generate:openapiscript viatsxis correct.The
typecheckscript referencestsgo, but this tool is not found in the project's dependencies, lockfile (pnpm-lock.yaml), or catalog configuration. Thegenerate:openapiscript correctly usestsx(version 4.20.6 installed). Addtsgoas a dev dependency across the workspace or switch to the standard TypeScript compiler (tsc).🤖 Prompt for AI Agents