Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 56 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -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=<your 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.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
32 changes: 28 additions & 4 deletions packages/cli/src/commands/query.ts
Original file line number Diff line number Diff line change
@@ -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"
Expand Down Expand Up @@ -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<number>
readonly query: string
readonly token: Option.Option<Redacted.Redacted<string>>
readonly url: string
}) {
const flight = yield* ArrowFlight.ArrowFlight

Expand Down Expand Up @@ -74,26 +91,33 @@ 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 }))
)

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
})
return ArrowFlight.layer.pipe(
Layer.provide(
NodeArrowFlight.layerTransportGrpc({
baseUrl: "http://localhost:1602"
baseUrl: toTransportUrl(url)
})
),
Layer.provide(layerInterceptorAuth)
Expand Down
Loading