From d5e9ddf15142b9b0239aadc8cc44792a775624e8 Mon Sep 17 00:00:00 2001 From: Adam Fuller <9612972+azf20@users.noreply.github.com> Date: Thu, 29 Oct 2026 16:30:04 +0000 Subject: [PATCH] feat(cli): let `amp query` target any Amp server - Add --url (falls back to AMP_QUERY_URL, then http://localhost:1602); grpc:// and grpc+tls:// are accepted alongside http(s):// - Fall back to AMP_AUTH_TOKEN when --token is not passed - Print why a query failed instead of exiting 1 silently - Add a `pnpm amp` script and a getting-started README Co-Authored-By: Claude Opus 5.5 --- README.md | 56 ++++++++++++++++++++++++++++++ package.json | 1 + packages/cli/src/commands/query.ts | 32 ++++++++++++++--- 3 files changed, 85 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 4d221a8..e69473f 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,59 @@ [![pkg.pr.new](https://pkg.pr.new/badge/edgeandnode/amp-typescript)](https://pkg.pr.new/~/edgeandnode/amp-typescript) # Amp Typescript SDK + +## Getting started + +Requires Node.js 22.7 or newer (CI uses 24) and pnpm. + +```bash +pnpm install +pnpm amp --help +``` + +`pnpm amp` runs the CLI from source (`packages/cli`). Query an Amp server by +passing its Arrow Flight URL and a token, or by setting `AMP_QUERY_URL` and +`AMP_AUTH_TOKEN`: + +```bash +export AMP_QUERY_URL=grpc+tls://gateway.amp.staging.edgeandnode.com:443 +export AMP_AUTH_TOKEN= + +pnpm amp query -f json \ + 'SELECT block_num, hash FROM "edgeandnode/ethereum_mainnet".blocks ORDER BY block_num DESC LIMIT 5' +``` + +Without a URL, the CLI queries a local `ampd` at `http://localhost:1602`. + +To use the SDK from code, provide the Arrow Flight service with a gRPC transport: + +```ts +import * as ArrowFlight from "@edgeandnode/amp/arrow-flight" +import * as NodeArrowFlight from "@edgeandnode/amp/arrow-flight/node" +import * as Effect from "effect/Effect" +import * as Layer from "effect/Layer" +import * as Redacted from "effect/Redacted" + +const token = process.env.AMP_AUTH_TOKEN +if (!token) throw new Error("Set AMP_AUTH_TOKEN to your Amp access token") + +const FlightLayer = ArrowFlight.layer.pipe( + Layer.provide(NodeArrowFlight.layerTransportGrpc({ baseUrl: "https://gateway.amp.staging.edgeandnode.com" })), + Layer.provide(ArrowFlight.layerInterceptorToken(Redacted.make(token))) +) + +const program = Effect.gen(function* () { + const flight = yield* ArrowFlight.ArrowFlight + const batches = yield* flight.query(`SELECT block_num FROM "edgeandnode/ethereum_mainnet".blocks LIMIT 5`) + console.log(batches.flatMap((batch) => batch.data)) +}) + +Effect.runPromise(program.pipe(Effect.provide(FlightLayer))) +``` + +The Admin API client lives in `@edgeandnode/amp/admin/service`: +`AdminApi.layer({ url: "https://gateway.amp.staging.edgeandnode.com/controller", jwt })`, +provided with an `HttpClient` such as `FetchHttpClient.layer`. + +Note: `@edgeandnode/amp` on npm (0.0.55) was published from the older SDK in the +`edgeandnode/amp` repository and does not match this code. diff --git a/package.json b/package.json index 464cdde..1989cf3 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,7 @@ "type": "module", "packageManager": "pnpm@12.8.1+sha512.f64ba907507f5ceafe06c8d38e6052d0179444580ec1279ddd5bfc11cb48aa8a2644b66598e07e761da84872a9fc57d5f902b87fa49d024198d558612aabbe45", "scripts": { + "amp": "node --experimental-transform-types --no-warnings packages/cli/src/bin.ts", "prepare": "effect-language-service patch", "build": "tspc -b tsconfig.packages.json && pnpm --recursive --parallel --filter \"./packages/**/*\" run build", "build:tsgo": "tsgo -b tsconfig.packages.json && pnpm --recursive --parallel --filter \"./packages/**/*\" run build:tsgo", diff --git a/packages/cli/src/commands/query.ts b/packages/cli/src/commands/query.ts index 73a3c71..f175b22 100644 --- a/packages/cli/src/commands/query.ts +++ b/packages/cli/src/commands/query.ts @@ -1,5 +1,6 @@ import * as ArrowFlight from "@edgeandnode/amp/arrow-flight" import * as NodeArrowFlight from "@edgeandnode/amp/arrow-flight/node" +import * as Config from "effect/Config" import * as Console from "effect/Console" import * as Data from "effect/Data" import * as Effect from "effect/Effect" @@ -37,16 +38,32 @@ const queryArg = Argument.String("query").pipe(Argument.withDescription("The SQL const tokenFlag = Flag.Redacted("token").pipe( Flag.withAlias("t"), - Flag.withDescription("The bearer token to use for authentication."), + Flag.withDescription("The bearer token to use for authentication. Defaults to $AMP_AUTH_TOKEN."), + Flag.withFallbackConfig(Config.Redacted("AMP_AUTH_TOKEN")), Flag.optional ) +const urlFlag = Flag.String("url").pipe( + Flag.withAlias("u"), + Flag.withDescription( + "The Arrow Flight URL of the Amp server, e.g. grpc+tls://gateway.amp.staging.edgeandnode.com:443. Defaults to $AMP_QUERY_URL." + ), + Flag.withFallbackConfig(Config.String("AMP_QUERY_URL")), + Flag.withDefault("http://localhost:1602") +) + +// The gRPC transport expects an HTTP(S) base URL, but Amp and its other +// clients spell Flight endpoints as grpc:// and grpc+tls:// +const toTransportUrl = (url: string): string => + url.replace(/^grpc\+tls:\/\//, "https://").replace(/^grpc:\/\//, "http://") + const queryCommandHandler = Effect.fnUntraced( function* (params: { readonly format: ResultFormat readonly limit: Option.Option readonly query: string readonly token: Option.Option> + readonly url: string }) { const flight = yield* ArrowFlight.ArrowFlight @@ -74,6 +91,12 @@ const queryCommandHandler = Effect.fnUntraced( } } }, + // Errors are not reported by the runtime, so print the reason here + Effect.tapError((cause) => + Console.error( + `Query failed: ${"cause" in cause && cause.cause instanceof Error ? cause.cause.message : cause.message || cause._tag}` + ) + ), Effect.mapError((cause) => new QueryCommandError({ cause })) ) @@ -81,11 +104,12 @@ export const QueryCommand = Command.make("query", { format: formatFlag, limit: limitFlag, query: queryArg, - token: tokenFlag + token: tokenFlag, + url: urlFlag }).pipe( Command.withDescription("Execute a SQL query with Amp"), Command.withHandler(queryCommandHandler), - Command.provide(({ token }) => { + Command.provide(({ token, url }) => { const layerInterceptorAuth = Option.match(token, { onSome: (bearer) => ArrowFlight.layerInterceptorToken(bearer), onNone: () => ArrowFlight.layerInterceptorBearerAuth @@ -93,7 +117,7 @@ export const QueryCommand = Command.make("query", { return ArrowFlight.layer.pipe( Layer.provide( NodeArrowFlight.layerTransportGrpc({ - baseUrl: "http://localhost:1602" + baseUrl: toTransportUrl(url) }) ), Layer.provide(layerInterceptorAuth)