first commit
This commit is contained in:
@@ -0,0 +1,148 @@
|
||||
# `OpenExchangeRatesProvider`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
`OpenExchangeRatesProvider` is the `@suaveplan/currency` provider for
|
||||
[openexchangerates.org](https://openexchangerates.org). It wraps the
|
||||
`/latest.json` bulk-fetch endpoint, validates the upstream payload, and
|
||||
surfaces every failure mode through the typed `CurrencyProviderError`
|
||||
hierarchy from `@suaveplan/currency-providers`. The provider is universal
|
||||
— it relies only on `globalThis.fetch` (or an injected `FetchLike`) and the
|
||||
slim-core `httpRequest()` helper, so it runs unchanged in Node, Bun, browsers,
|
||||
and Web Workers.
|
||||
|
||||
## Features
|
||||
|
||||
- Implements `CurrencyProvider` with `name === "open-exchange-rates"`,
|
||||
`supportsFiat === true`, `supportsCrypto === false`.
|
||||
- Constructor takes the OXR `app_id` as `apiKey`. Empty string raises
|
||||
`CurrencyProviderError(MISSING_CONFIG)` synchronously.
|
||||
- Free-tier safe by default: any `RateFetchOptions.base !== "USD"` raises
|
||||
`CurrencyProviderError(UNSUPPORTED_BASE)` before any network call. Paid-plan
|
||||
callers opt in with `allowNonUsdBase: true`.
|
||||
- Routes every request through `@suaveplan/currency-providers` `httpRequest()`
|
||||
so non-2xx responses surface `CurrencyProviderError(HTTP_ERROR)` with
|
||||
`status` and `providerName` populated. Caller `AbortSignal` from
|
||||
`RateFetchOptions.signal` is forwarded.
|
||||
- Validates the upstream `{ base, rates, timestamp }` shape. Mismatches raise
|
||||
`CurrencyProviderError(PARSE_ERROR)`.
|
||||
- Detects the OXR 2xx error-payload shape (`{ error: true, status, message }`)
|
||||
and raises `CurrencyProviderError(UPSTREAM_ERROR)` with `upstreamMessage`
|
||||
carrying the upstream `message` field.
|
||||
- Filters the rates map to `RateFetchOptions.targets`. Missing target codes
|
||||
raise `CurrencyProviderError(UNKNOWN_CURRENCY)`.
|
||||
- Converts the OXR Unix-seconds timestamp to milliseconds.
|
||||
- Injectable `FetchLike` for offline testing; defaults to `globalThis.fetch`.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```typescript
|
||||
import { OpenExchangeRatesProvider } from "@suaveplan/currency-provider-open-exchange-rates";
|
||||
|
||||
const provider = new OpenExchangeRatesProvider({
|
||||
apiKey: process.env.OXR_APP_ID ?? "",
|
||||
});
|
||||
|
||||
const result = await provider.fetchRates({ base: "USD" });
|
||||
console.log(result.base, result.rates.EUR, result.timestamp);
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
Wiring the provider into `CurrencyConverter` with a paid-tier app_id, custom
|
||||
`baseUrl` for staging, and `AbortSignal`-driven cancellation:
|
||||
|
||||
```typescript
|
||||
import { CurrencyConverter } from "@suaveplan/currency";
|
||||
import { OpenExchangeRatesProvider } from "@suaveplan/currency-provider-open-exchange-rates";
|
||||
|
||||
const oxr = new OpenExchangeRatesProvider({
|
||||
apiKey: process.env.OXR_APP_ID!,
|
||||
allowNonUsdBase: true,
|
||||
baseUrl: process.env.OXR_BASE_URL ?? "https://openexchangerates.org/api",
|
||||
});
|
||||
|
||||
const converter = new CurrencyConverter({
|
||||
provider: oxr,
|
||||
staleDurationMs: 30 * 60 * 1000, // 30 minutes
|
||||
});
|
||||
|
||||
const controller = new AbortController();
|
||||
setTimeout(() => controller.abort(new Error("timeout")), 5000);
|
||||
|
||||
const rates = await oxr.fetchRates({
|
||||
base: "EUR",
|
||||
targets: ["USD", "GBP", "JPY"],
|
||||
signal: controller.signal,
|
||||
});
|
||||
```
|
||||
|
||||
The slim-core `CurrencyProviderRegistry` is also supported when the consumer
|
||||
wires multiple providers behind a single lookup surface — see
|
||||
`@suaveplan/currency-providers` README.
|
||||
|
||||
## API Reference
|
||||
|
||||
### `OpenExchangeRatesProviderConfig`
|
||||
|
||||
Extends `ApiKeyConfig` from `@suaveplan/currency-providers`:
|
||||
|
||||
- `apiKey: string` — required. The OXR `app_id`.
|
||||
- `baseUrl?: string` — defaults to `"https://openexchangerates.org/api"`.
|
||||
- `fetch?: FetchLike` — defaults to `globalThis.fetch`.
|
||||
- `allowNonUsdBase?: boolean` — defaults to `false`. When `false`, any
|
||||
non-USD base raises `UNSUPPORTED_BASE` before any network call.
|
||||
|
||||
### `class OpenExchangeRatesProvider`
|
||||
|
||||
#### Properties
|
||||
|
||||
- `readonly name: "open-exchange-rates"`
|
||||
- `readonly supportsFiat: true`
|
||||
- `readonly supportsCrypto: false`
|
||||
|
||||
#### `constructor(config: OpenExchangeRatesProviderConfig)`
|
||||
|
||||
Throws `CurrencyProviderError(MISSING_CONFIG)` when `config.apiKey === ""`.
|
||||
|
||||
#### `fetchRates(options?: RateFetchOptions): Promise<RateFetchResult>`
|
||||
|
||||
Issues `GET ${baseUrl}/latest.json?app_id=<apiKey>&base=<base>` via
|
||||
`httpRequest()`. Returns `{ base, rates, timestamp }` with `timestamp` in
|
||||
milliseconds.
|
||||
|
||||
Throws:
|
||||
|
||||
- `MISSING_CONFIG` — from the constructor when `apiKey === ""`.
|
||||
- `UNSUPPORTED_BASE` — when `options.base !== "USD"` and `allowNonUsdBase`
|
||||
is not set to `true`.
|
||||
- `HTTP_ERROR` — via `httpRequest()` for any non-2xx response.
|
||||
- `PARSE_ERROR` — when the response body is not JSON, not an object, or is
|
||||
missing one of `base` / `rates` / `timestamp` in the expected shape.
|
||||
- `UPSTREAM_ERROR` — when the response is 2xx but the body is the OXR error
|
||||
shape (`{ error: true, status, message }`).
|
||||
- `UNKNOWN_CURRENCY` — when `options.targets` contains a code not present in
|
||||
the upstream `rates` map.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- The OXR free tier restricts the base currency to USD; the provider enforces
|
||||
this client-side so the failure surfaces as a typed `UNSUPPORTED_BASE`
|
||||
rather than a generic upstream error. Paid plans pass `allowNonUsdBase:
|
||||
true` to opt out.
|
||||
- OXR returns `timestamp` in Unix seconds; the provider multiplies by 1000 so
|
||||
every `CurrencyProvider` answer is in milliseconds, matching
|
||||
`RateFetchResult.timestamp`'s contract.
|
||||
- OXR's error-payload shape is returned with a 2xx HTTP status, which would
|
||||
otherwise pass `httpRequest()`'s non-2xx check silently. The provider
|
||||
detects `{ error: true, … }` after parsing and surfaces `UPSTREAM_ERROR`
|
||||
with `status` and `upstreamMessage` populated.
|
||||
- The provider does not retry, dedupe, or rate-limit by itself — the slim
|
||||
core's `## Out of scope` defers those concerns to a cross-cutting follow-up
|
||||
wave so every sibling provider gains them in lockstep.
|
||||
- `apiKey` is `encodeURIComponent`-wrapped in the query string to prevent
|
||||
injection when callers pass keys containing reserved characters.
|
||||
Reference in New Issue
Block a user