@financica/ecb-client 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +13 -0
- package/LICENSE +21 -0
- package/README.md +102 -0
- package/dist/index.cjs +291 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +168 -0
- package/dist/index.d.ts +168 -0
- package/dist/index.js +282 -0
- package/dist/index.js.map +1 -0
- package/package.json +70 -0
- package/src/client.ts +246 -0
- package/src/csv.ts +67 -0
- package/src/currencies.ts +54 -0
- package/src/errors.ts +35 -0
- package/src/index.ts +29 -0
- package/src/types.ts +52 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.0
|
|
4
|
+
|
|
5
|
+
Initial release.
|
|
6
|
+
|
|
7
|
+
- `EcbClient` with `getRate`, `getRates`, and `convert` against the ECB data
|
|
8
|
+
API (dataflow `EXR`, `text/csv`).
|
|
9
|
+
- Historical single-day lookups with last-business-day fallback via
|
|
10
|
+
`lastNObservations`; the effective observation date is reported on every rate.
|
|
11
|
+
- Euro-crossed conversion between any two reference currencies.
|
|
12
|
+
- Injectable `fetch`, in-process snapshot cache, request timeout.
|
|
13
|
+
- Zero runtime dependencies; ESM + CJS builds with type declarations.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Financica
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# @financica/ecb-client
|
|
2
|
+
|
|
3
|
+
Tiny, zero-dependency TypeScript client for the **European Central Bank's euro
|
|
4
|
+
foreign-exchange reference rates**, with first-class support for **historical
|
|
5
|
+
single-day lookups** and **last-business-day fallback**.
|
|
6
|
+
|
|
7
|
+
- **Authoritative source.** Reads the ECB data API directly (dataflow `EXR`).
|
|
8
|
+
No third-party middleman, no API key, no SLA in front of your numbers. ECB
|
|
9
|
+
reference rates are the rates EU tax authorities accept for currency
|
|
10
|
+
conversion (VAT Directive art. 91).
|
|
11
|
+
- **Point-in-time.** Ask for a rate "on or before" any date; the ECB only
|
|
12
|
+
publishes on TARGET business days, so weekends and holidays transparently
|
|
13
|
+
resolve to the most recent prior business day. The effective date is reported
|
|
14
|
+
back on every rate.
|
|
15
|
+
- **Cross rates.** Every ECB rate is quoted against the euro; the client crosses
|
|
16
|
+
two foreign currencies through EUR for you.
|
|
17
|
+
- **Tiny & typed.** Zero runtime dependencies, ESM + CJS, strict types, an
|
|
18
|
+
injectable `fetch`, and an in-process snapshot cache.
|
|
19
|
+
|
|
20
|
+
## Install
|
|
21
|
+
|
|
22
|
+
```sh
|
|
23
|
+
npm install @financica/ecb-client
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Requires Node 18+ (uses the global `fetch`).
|
|
27
|
+
|
|
28
|
+
## Usage
|
|
29
|
+
|
|
30
|
+
```ts
|
|
31
|
+
import { EcbClient } from "@financica/ecb-client";
|
|
32
|
+
|
|
33
|
+
const ecb = new EcbClient();
|
|
34
|
+
|
|
35
|
+
// A single rate (units of the currency per 1 EUR) on a given day.
|
|
36
|
+
await ecb.getRate("USD", "2024-01-15");
|
|
37
|
+
// → { currency: "USD", rate: 1.0945, date: "2024-01-15" }
|
|
38
|
+
|
|
39
|
+
// Weekends/holidays fall back to the last business day; the effective
|
|
40
|
+
// date comes back on the result.
|
|
41
|
+
await ecb.getRate("USD", "2024-01-13"); // a Saturday
|
|
42
|
+
// → { currency: "USD", rate: 1.0942, date: "2024-01-12" }
|
|
43
|
+
|
|
44
|
+
// Convert an amount. Non-euro pairs cross through EUR.
|
|
45
|
+
await ecb.convert({ amount: 100, from: "USD", to: "EUR", date: "2024-01-15" });
|
|
46
|
+
// → { amount: 91.36…, rate: 0.9136…, from: "USD", to: "EUR",
|
|
47
|
+
// requestedDate: "2024-01-15", rateDate: "2024-01-15" }
|
|
48
|
+
|
|
49
|
+
// Several currencies for one date, in a single request.
|
|
50
|
+
const snapshot = await ecb.getRates("2024-01-15", ["USD", "GBP", "CHF"]);
|
|
51
|
+
// → { requestedDate: "2024-01-15", rates: [ { currency, rate, date }, … ] }
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## API
|
|
55
|
+
|
|
56
|
+
### `new EcbClient(options?)`
|
|
57
|
+
|
|
58
|
+
| option | default | description |
|
|
59
|
+
| ----------- | -------------------------------------------- | -------------------------------------------------------- |
|
|
60
|
+
| `baseUrl` | `https://data-api.ecb.europa.eu/service` | ECB data API base URL. |
|
|
61
|
+
| `fetch` | global `fetch` | Custom fetch (testing, proxying). |
|
|
62
|
+
| `cache` | unbounded in-process `Map` | Snapshot cache keyed by request. Pass `null` to disable. |
|
|
63
|
+
| `timeoutMs` | `15000` | Per-request timeout. `0` disables it. |
|
|
64
|
+
|
|
65
|
+
### `getRate(currency, date) → Promise<ReferenceRate>`
|
|
66
|
+
|
|
67
|
+
The rate for one currency on (or before) `date`. `date` is a `YYYY-MM-DD`
|
|
68
|
+
string or a `Date`. `EUR` returns a unit rate without a network call. Throws
|
|
69
|
+
`NoRateError` when no observation exists on or before the date.
|
|
70
|
+
|
|
71
|
+
### `getRates(date, currencies?) → Promise<RateSnapshot>`
|
|
72
|
+
|
|
73
|
+
Rates for several currencies on (or before) `date`. Omit `currencies` to fetch
|
|
74
|
+
every series the ECB publishes — note that **discontinued series resolve to
|
|
75
|
+
their own last-published date**, so check each rate's `date` rather than
|
|
76
|
+
assuming the requested one.
|
|
77
|
+
|
|
78
|
+
### `convert({ amount, from, to, date }) → Promise<ConvertResult>`
|
|
79
|
+
|
|
80
|
+
Convert `amount` from one currency to another. Identical currencies are a no-op
|
|
81
|
+
(no request). The result reports the applied `rate` and the `rateDate` actually
|
|
82
|
+
used.
|
|
83
|
+
|
|
84
|
+
## How rates are quoted
|
|
85
|
+
|
|
86
|
+
Every value is **units of the quoted currency per 1 EUR** (the ECB convention).
|
|
87
|
+
So `getRate("USD", …).rate === 1.0945` means `1 EUR = 1.0945 USD`. To go from
|
|
88
|
+
USD to EUR, divide; from EUR to USD, multiply; `convert()` handles both and the
|
|
89
|
+
cross-currency case.
|
|
90
|
+
|
|
91
|
+
## Caveats
|
|
92
|
+
|
|
93
|
+
- **Business days only.** No observation on weekends or TARGET holidays; the
|
|
94
|
+
client resolves to the last prior business day for you.
|
|
95
|
+
- **~30 active currencies, all against EUR.** Cross rates are computed through
|
|
96
|
+
EUR. Discontinued series (e.g. `ARS`) still return, at a stale date.
|
|
97
|
+
- **Be a good citizen.** The ECB data API is a public good with no SLA. The
|
|
98
|
+
built-in cache deduplicates repeated lookups; cache aggressively in your app.
|
|
99
|
+
|
|
100
|
+
## License
|
|
101
|
+
|
|
102
|
+
MIT © Financica
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// src/currencies.ts
|
|
4
|
+
var ECB_CURRENCIES = [
|
|
5
|
+
"AUD",
|
|
6
|
+
"BGN",
|
|
7
|
+
"BRL",
|
|
8
|
+
"CAD",
|
|
9
|
+
"CHF",
|
|
10
|
+
"CNY",
|
|
11
|
+
"CZK",
|
|
12
|
+
"DKK",
|
|
13
|
+
"GBP",
|
|
14
|
+
"HKD",
|
|
15
|
+
"HUF",
|
|
16
|
+
"IDR",
|
|
17
|
+
"ILS",
|
|
18
|
+
"INR",
|
|
19
|
+
"ISK",
|
|
20
|
+
"JPY",
|
|
21
|
+
"KRW",
|
|
22
|
+
"MXN",
|
|
23
|
+
"MYR",
|
|
24
|
+
"NOK",
|
|
25
|
+
"NZD",
|
|
26
|
+
"PHP",
|
|
27
|
+
"PLN",
|
|
28
|
+
"RON",
|
|
29
|
+
"SEK",
|
|
30
|
+
"SGD",
|
|
31
|
+
"THB",
|
|
32
|
+
"TRY",
|
|
33
|
+
"USD",
|
|
34
|
+
"ZAR"
|
|
35
|
+
];
|
|
36
|
+
var BASE_CURRENCY = "EUR";
|
|
37
|
+
var ECB_CURRENCY_SET = new Set(ECB_CURRENCIES);
|
|
38
|
+
var isEcbCurrency = (code) => ECB_CURRENCY_SET.has(code);
|
|
39
|
+
|
|
40
|
+
// src/csv.ts
|
|
41
|
+
var parseCsv = (text) => {
|
|
42
|
+
const rows = splitRows(text);
|
|
43
|
+
const header = rows.shift();
|
|
44
|
+
if (!header) return [];
|
|
45
|
+
const records = [];
|
|
46
|
+
for (const row of rows) {
|
|
47
|
+
if (row.length === 1 && row[0] === "") continue;
|
|
48
|
+
const record = {};
|
|
49
|
+
for (let i = 0; i < header.length; i++) {
|
|
50
|
+
record[header[i] ?? `column_${i}`] = row[i] ?? "";
|
|
51
|
+
}
|
|
52
|
+
records.push(record);
|
|
53
|
+
}
|
|
54
|
+
return records;
|
|
55
|
+
};
|
|
56
|
+
var splitRows = (text) => {
|
|
57
|
+
const rows = [];
|
|
58
|
+
let row = [];
|
|
59
|
+
let field = "";
|
|
60
|
+
let inQuotes = false;
|
|
61
|
+
for (let i = 0; i < text.length; i++) {
|
|
62
|
+
const char = text[i];
|
|
63
|
+
if (inQuotes) {
|
|
64
|
+
if (char === '"') {
|
|
65
|
+
if (text[i + 1] === '"') {
|
|
66
|
+
field += '"';
|
|
67
|
+
i++;
|
|
68
|
+
} else {
|
|
69
|
+
inQuotes = false;
|
|
70
|
+
}
|
|
71
|
+
} else {
|
|
72
|
+
field += char;
|
|
73
|
+
}
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
if (char === '"') {
|
|
77
|
+
inQuotes = true;
|
|
78
|
+
} else if (char === ",") {
|
|
79
|
+
row.push(field);
|
|
80
|
+
field = "";
|
|
81
|
+
} else if (char === "\n" || char === "\r") {
|
|
82
|
+
if (char === "\r" && text[i + 1] === "\n") i++;
|
|
83
|
+
row.push(field);
|
|
84
|
+
rows.push(row);
|
|
85
|
+
row = [];
|
|
86
|
+
field = "";
|
|
87
|
+
} else {
|
|
88
|
+
field += char;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
if (field !== "" || row.length > 0) {
|
|
92
|
+
row.push(field);
|
|
93
|
+
rows.push(row);
|
|
94
|
+
}
|
|
95
|
+
return rows;
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
// src/errors.ts
|
|
99
|
+
var EcbError = class extends Error {
|
|
100
|
+
constructor(message, options) {
|
|
101
|
+
super(message, options);
|
|
102
|
+
this.name = "EcbError";
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
var EcbHttpError = class extends EcbError {
|
|
106
|
+
status;
|
|
107
|
+
body;
|
|
108
|
+
constructor(status, body) {
|
|
109
|
+
super(`ECB data API returned HTTP ${status}`);
|
|
110
|
+
this.name = "EcbHttpError";
|
|
111
|
+
this.status = status;
|
|
112
|
+
this.body = body;
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
var NoRateError = class extends EcbError {
|
|
116
|
+
currency;
|
|
117
|
+
date;
|
|
118
|
+
constructor(currency, date) {
|
|
119
|
+
super(`No ECB reference rate for ${currency} on or before ${date}`);
|
|
120
|
+
this.name = "NoRateError";
|
|
121
|
+
this.currency = currency;
|
|
122
|
+
this.date = date;
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
// src/client.ts
|
|
127
|
+
var DEFAULT_BASE_URL = "https://data-api.ecb.europa.eu/service";
|
|
128
|
+
var DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
|
|
129
|
+
var EcbClient = class {
|
|
130
|
+
baseUrl;
|
|
131
|
+
fetchImpl;
|
|
132
|
+
cache;
|
|
133
|
+
timeoutMs;
|
|
134
|
+
constructor(options = {}) {
|
|
135
|
+
this.baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
|
|
136
|
+
const resolvedFetch = options.fetch ?? globalThis.fetch;
|
|
137
|
+
if (!resolvedFetch) {
|
|
138
|
+
throw new EcbError(
|
|
139
|
+
"No fetch implementation available; pass `fetch` in EcbClientOptions"
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
this.fetchImpl = resolvedFetch;
|
|
143
|
+
this.cache = options.cache === void 0 ? new MapCache() : options.cache;
|
|
144
|
+
this.timeoutMs = options.timeoutMs ?? 15e3;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Resolve the reference rate for a single currency on (or before) a date.
|
|
148
|
+
* `EUR` returns a unit rate without a network call.
|
|
149
|
+
*/
|
|
150
|
+
async getRate(currency, date) {
|
|
151
|
+
const requestedDate = normalizeDate(date);
|
|
152
|
+
if (currency === BASE_CURRENCY) {
|
|
153
|
+
return { currency: BASE_CURRENCY, rate: 1, date: requestedDate };
|
|
154
|
+
}
|
|
155
|
+
const snapshot = await this.fetchSnapshot([currency], requestedDate);
|
|
156
|
+
const rate = snapshot.rates.find((r) => r.currency === currency);
|
|
157
|
+
if (!rate) throw new NoRateError(currency, requestedDate);
|
|
158
|
+
return rate;
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Resolve reference rates on (or before) a date. Pass `currencies` to limit
|
|
162
|
+
* the request, or omit it to fetch every series the ECB publishes (note
|
|
163
|
+
* that discontinued series resolve to their last published date).
|
|
164
|
+
*/
|
|
165
|
+
async getRates(date, currencies) {
|
|
166
|
+
const requestedDate = normalizeDate(date);
|
|
167
|
+
const wanted = currencies?.filter((c) => c !== BASE_CURRENCY);
|
|
168
|
+
const snapshot = wanted && wanted.length === 0 ? { requestedDate, rates: [] } : await this.fetchSnapshot(wanted ?? null, requestedDate);
|
|
169
|
+
if (currencies?.includes(BASE_CURRENCY)) {
|
|
170
|
+
return {
|
|
171
|
+
requestedDate,
|
|
172
|
+
rates: [
|
|
173
|
+
{ currency: BASE_CURRENCY, rate: 1, date: requestedDate },
|
|
174
|
+
...snapshot.rates
|
|
175
|
+
]
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
return snapshot;
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Convert `amount` from one currency to another using the reference rates on
|
|
182
|
+
* (or before) `date`. Non-euro pairs are crossed through the euro.
|
|
183
|
+
*/
|
|
184
|
+
async convert(params) {
|
|
185
|
+
const { amount, from, to } = params;
|
|
186
|
+
const requestedDate = normalizeDate(params.date);
|
|
187
|
+
if (from === to) {
|
|
188
|
+
return {
|
|
189
|
+
amount,
|
|
190
|
+
rate: 1,
|
|
191
|
+
from,
|
|
192
|
+
to,
|
|
193
|
+
requestedDate,
|
|
194
|
+
rateDate: requestedDate
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
const needed = [from, to].filter(
|
|
198
|
+
(c) => c !== BASE_CURRENCY
|
|
199
|
+
);
|
|
200
|
+
const snapshot = await this.fetchSnapshot(needed, requestedDate);
|
|
201
|
+
const fromLeg = this.legFor(from, snapshot, requestedDate);
|
|
202
|
+
const toLeg = this.legFor(to, snapshot, requestedDate);
|
|
203
|
+
const rate = toLeg.rate / fromLeg.rate;
|
|
204
|
+
return {
|
|
205
|
+
amount: amount * rate,
|
|
206
|
+
rate,
|
|
207
|
+
from,
|
|
208
|
+
to,
|
|
209
|
+
requestedDate,
|
|
210
|
+
rateDate: fromLeg.date > toLeg.date ? fromLeg.date : toLeg.date
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
legFor(currency, snapshot, requestedDate) {
|
|
214
|
+
if (currency === BASE_CURRENCY) {
|
|
215
|
+
return { currency: BASE_CURRENCY, rate: 1, date: requestedDate };
|
|
216
|
+
}
|
|
217
|
+
const leg = snapshot.rates.find((r) => r.currency === currency);
|
|
218
|
+
if (!leg) throw new NoRateError(currency, requestedDate);
|
|
219
|
+
return leg;
|
|
220
|
+
}
|
|
221
|
+
async fetchSnapshot(currencies, requestedDate) {
|
|
222
|
+
const key = currencies && currencies.length ? currencies.join("+") : "";
|
|
223
|
+
const url = `${this.baseUrl}/data/EXR/D.${key}.EUR.SP00.A?endPeriod=${requestedDate}&lastNObservations=1`;
|
|
224
|
+
const cached = this.cache?.get(url);
|
|
225
|
+
if (cached) return cached;
|
|
226
|
+
const text = await this.get(url);
|
|
227
|
+
const rates = [];
|
|
228
|
+
for (const record of parseCsv(text)) {
|
|
229
|
+
const currency = record["CURRENCY"];
|
|
230
|
+
const observedAt = record["TIME_PERIOD"];
|
|
231
|
+
const value = Number(record["OBS_VALUE"]);
|
|
232
|
+
if (!currency || !observedAt || !Number.isFinite(value)) continue;
|
|
233
|
+
rates.push({ currency, rate: value, date: observedAt });
|
|
234
|
+
}
|
|
235
|
+
const snapshot = { requestedDate, rates };
|
|
236
|
+
this.cache?.set(url, snapshot);
|
|
237
|
+
return snapshot;
|
|
238
|
+
}
|
|
239
|
+
async get(url) {
|
|
240
|
+
const controller = this.timeoutMs > 0 ? new AbortController() : void 0;
|
|
241
|
+
const timer = controller ? setTimeout(() => controller.abort(), this.timeoutMs) : void 0;
|
|
242
|
+
try {
|
|
243
|
+
const response = await this.fetchImpl(url, {
|
|
244
|
+
headers: { Accept: "text/csv" },
|
|
245
|
+
...controller ? { signal: controller.signal } : {}
|
|
246
|
+
});
|
|
247
|
+
const body = await response.text();
|
|
248
|
+
if (!response.ok) throw new EcbHttpError(response.status, body);
|
|
249
|
+
return body;
|
|
250
|
+
} catch (error) {
|
|
251
|
+
if (error instanceof EcbError) throw error;
|
|
252
|
+
throw new EcbError(`ECB data API request failed: ${url}`, {
|
|
253
|
+
cause: error
|
|
254
|
+
});
|
|
255
|
+
} finally {
|
|
256
|
+
if (timer) clearTimeout(timer);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
};
|
|
260
|
+
var MapCache = class {
|
|
261
|
+
store = /* @__PURE__ */ new Map();
|
|
262
|
+
get(key) {
|
|
263
|
+
return this.store.get(key);
|
|
264
|
+
}
|
|
265
|
+
set(key, value) {
|
|
266
|
+
this.store.set(key, value);
|
|
267
|
+
}
|
|
268
|
+
};
|
|
269
|
+
var normalizeDate = (date) => {
|
|
270
|
+
if (date instanceof Date) {
|
|
271
|
+
if (Number.isNaN(date.getTime())) {
|
|
272
|
+
throw new EcbError("Invalid Date passed for the rate date");
|
|
273
|
+
}
|
|
274
|
+
return date.toISOString().slice(0, 10);
|
|
275
|
+
}
|
|
276
|
+
if (!DATE_RE.test(date)) {
|
|
277
|
+
throw new EcbError(`Invalid date "${date}"; expected YYYY-MM-DD`);
|
|
278
|
+
}
|
|
279
|
+
return date;
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
exports.BASE_CURRENCY = BASE_CURRENCY;
|
|
283
|
+
exports.ECB_CURRENCIES = ECB_CURRENCIES;
|
|
284
|
+
exports.EcbClient = EcbClient;
|
|
285
|
+
exports.EcbError = EcbError;
|
|
286
|
+
exports.EcbHttpError = EcbHttpError;
|
|
287
|
+
exports.NoRateError = NoRateError;
|
|
288
|
+
exports.isEcbCurrency = isEcbCurrency;
|
|
289
|
+
exports.parseCsv = parseCsv;
|
|
290
|
+
//# sourceMappingURL=index.cjs.map
|
|
291
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/currencies.ts","../src/csv.ts","../src/errors.ts","../src/client.ts"],"names":[],"mappings":";;;AAUO,IAAM,cAAA,GAAiB;AAAA,EAC7B,KAAA;AAAA,EACA,KAAA;AAAA,EACA,KAAA;AAAA,EACA,KAAA;AAAA,EACA,KAAA;AAAA,EACA,KAAA;AAAA,EACA,KAAA;AAAA,EACA,KAAA;AAAA,EACA,KAAA;AAAA,EACA,KAAA;AAAA,EACA,KAAA;AAAA,EACA,KAAA;AAAA,EACA,KAAA;AAAA,EACA,KAAA;AAAA,EACA,KAAA;AAAA,EACA,KAAA;AAAA,EACA,KAAA;AAAA,EACA,KAAA;AAAA,EACA,KAAA;AAAA,EACA,KAAA;AAAA,EACA,KAAA;AAAA,EACA,KAAA;AAAA,EACA,KAAA;AAAA,EACA,KAAA;AAAA,EACA,KAAA;AAAA,EACA,KAAA;AAAA,EACA,KAAA;AAAA,EACA,KAAA;AAAA,EACA,KAAA;AAAA,EACA;AACD;AAMO,IAAM,aAAA,GAAgB;AAE7B,IAAM,gBAAA,GAAwC,IAAI,GAAA,CAAI,cAAc,CAAA;AAG7D,IAAM,aAAA,GAAgB,CAAC,IAAA,KAC7B,gBAAA,CAAiB,IAAI,IAAI;;;AC/CnB,IAAM,QAAA,GAAW,CAAC,IAAA,KAA2C;AACnE,EAAA,MAAM,IAAA,GAAO,UAAU,IAAI,CAAA;AAC3B,EAAA,MAAM,MAAA,GAAS,KAAK,KAAA,EAAM;AAC1B,EAAA,IAAI,CAAC,MAAA,EAAQ,OAAO,EAAC;AACrB,EAAA,MAAM,UAAoC,EAAC;AAC3C,EAAA,KAAA,MAAW,OAAO,IAAA,EAAM;AAEvB,IAAA,IAAI,IAAI,MAAA,KAAW,CAAA,IAAK,GAAA,CAAI,CAAC,MAAM,EAAA,EAAI;AACvC,IAAA,MAAM,SAAiC,EAAC;AACxC,IAAA,KAAA,IAAS,CAAA,GAAI,CAAA,EAAG,CAAA,GAAI,MAAA,CAAO,QAAQ,CAAA,EAAA,EAAK;AACvC,MAAA,MAAA,CAAO,MAAA,CAAO,CAAC,CAAA,IAAK,CAAA,OAAA,EAAU,CAAC,CAAA,CAAE,CAAA,GAAI,GAAA,CAAI,CAAC,CAAA,IAAK,EAAA;AAAA,IAChD;AACA,IAAA,OAAA,CAAQ,KAAK,MAAM,CAAA;AAAA,EACpB;AACA,EAAA,OAAO,OAAA;AACR;AAEA,IAAM,SAAA,GAAY,CAAC,IAAA,KAA6B;AAC/C,EAAA,MAAM,OAAmB,EAAC;AAC1B,EAAA,IAAI,MAAgB,EAAC;AACrB,EAAA,IAAI,KAAA,GAAQ,EAAA;AACZ,EAAA,IAAI,QAAA,GAAW,KAAA;AAEf,EAAA,KAAA,IAAS,CAAA,GAAI,CAAA,EAAG,CAAA,GAAI,IAAA,CAAK,QAAQ,CAAA,EAAA,EAAK;AACrC,IAAA,MAAM,IAAA,GAAO,KAAK,CAAC,CAAA;AACnB,IAAA,IAAI,QAAA,EAAU;AACb,MAAA,IAAI,SAAS,GAAA,EAAK;AACjB,QAAA,IAAI,IAAA,CAAK,CAAA,GAAI,CAAC,CAAA,KAAM,GAAA,EAAK;AACxB,UAAA,KAAA,IAAS,GAAA;AACT,UAAA,CAAA,EAAA;AAAA,QACD,CAAA,MAAO;AACN,UAAA,QAAA,GAAW,KAAA;AAAA,QACZ;AAAA,MACD,CAAA,MAAO;AACN,QAAA,KAAA,IAAS,IAAA;AAAA,MACV;AACA,MAAA;AAAA,IACD;AACA,IAAA,IAAI,SAAS,GAAA,EAAK;AACjB,MAAA,QAAA,GAAW,IAAA;AAAA,IACZ,CAAA,MAAA,IAAW,SAAS,GAAA,EAAK;AACxB,MAAA,GAAA,CAAI,KAAK,KAAK,CAAA;AACd,MAAA,KAAA,GAAQ,EAAA;AAAA,IACT,CAAA,MAAA,IAAW,IAAA,KAAS,IAAA,IAAQ,IAAA,KAAS,IAAA,EAAM;AAC1C,MAAA,IAAI,SAAS,IAAA,IAAQ,IAAA,CAAK,CAAA,GAAI,CAAC,MAAM,IAAA,EAAM,CAAA,EAAA;AAC3C,MAAA,GAAA,CAAI,KAAK,KAAK,CAAA;AACd,MAAA,IAAA,CAAK,KAAK,GAAG,CAAA;AACb,MAAA,GAAA,GAAM,EAAC;AACP,MAAA,KAAA,GAAQ,EAAA;AAAA,IACT,CAAA,MAAO;AACN,MAAA,KAAA,IAAS,IAAA;AAAA,IACV;AAAA,EACD;AAGA,EAAA,IAAI,KAAA,KAAU,EAAA,IAAM,GAAA,CAAI,MAAA,GAAS,CAAA,EAAG;AACnC,IAAA,GAAA,CAAI,KAAK,KAAK,CAAA;AACd,IAAA,IAAA,CAAK,KAAK,GAAG,CAAA;AAAA,EACd;AACA,EAAA,OAAO,IAAA;AACR,CAAA;;;AC/DO,IAAM,QAAA,GAAN,cAAuB,KAAA,CAAM;AAAA,EACnC,WAAA,CAAY,SAAiB,OAAA,EAA+B;AAC3D,IAAA,KAAA,CAAM,SAAS,OAAO,CAAA;AACtB,IAAA,IAAA,CAAK,IAAA,GAAO,UAAA;AAAA,EACb;AACD;AAGO,IAAM,YAAA,GAAN,cAA2B,QAAA,CAAS;AAAA,EACjC,MAAA;AAAA,EACA,IAAA;AAAA,EAET,WAAA,CAAY,QAAgB,IAAA,EAAc;AACzC,IAAA,KAAA,CAAM,CAAA,2BAAA,EAA8B,MAAM,CAAA,CAAE,CAAA;AAC5C,IAAA,IAAA,CAAK,IAAA,GAAO,cAAA;AACZ,IAAA,IAAA,CAAK,MAAA,GAAS,MAAA;AACd,IAAA,IAAA,CAAK,IAAA,GAAO,IAAA;AAAA,EACb;AACD;AAGO,IAAM,WAAA,GAAN,cAA0B,QAAA,CAAS;AAAA,EAChC,QAAA;AAAA,EACA,IAAA;AAAA,EAET,WAAA,CAAY,UAAwB,IAAA,EAAe;AAClD,IAAA,KAAA,CAAM,CAAA,0BAAA,EAA6B,QAAQ,CAAA,cAAA,EAAiB,IAAI,CAAA,CAAE,CAAA;AAClE,IAAA,IAAA,CAAK,IAAA,GAAO,aAAA;AACZ,IAAA,IAAA,CAAK,QAAA,GAAW,QAAA;AAChB,IAAA,IAAA,CAAK,IAAA,GAAO,IAAA;AAAA,EACb;AACD;;;ACUA,IAAM,gBAAA,GAAmB,wCAAA;AACzB,IAAM,OAAA,GAAU,qBAAA;AAWT,IAAM,YAAN,MAAgB;AAAA,EACL,OAAA;AAAA,EACA,SAAA;AAAA,EACA,KAAA;AAAA,EACA,SAAA;AAAA,EAEjB,WAAA,CAAY,OAAA,GAA4B,EAAC,EAAG;AAC3C,IAAA,IAAA,CAAK,WAAW,OAAA,CAAQ,OAAA,IAAW,gBAAA,EAAkB,OAAA,CAAQ,QAAQ,EAAE,CAAA;AACvE,IAAA,MAAM,aAAA,GACL,OAAA,CAAQ,KAAA,IAAU,UAAA,CAAW,KAAA;AAC9B,IAAA,IAAI,CAAC,aAAA,EAAe;AACnB,MAAA,MAAM,IAAI,QAAA;AAAA,QACT;AAAA,OACD;AAAA,IACD;AACA,IAAA,IAAA,CAAK,SAAA,GAAY,aAAA;AACjB,IAAA,IAAA,CAAK,QACJ,OAAA,CAAQ,KAAA,KAAU,SAAY,IAAI,QAAA,KAAa,OAAA,CAAQ,KAAA;AACxD,IAAA,IAAA,CAAK,SAAA,GAAY,QAAQ,SAAA,IAAa,IAAA;AAAA,EACvC;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAM,OAAA,CACL,QAAA,EACA,IAAA,EACyB;AACzB,IAAA,MAAM,aAAA,GAAgB,cAAc,IAAI,CAAA;AACxC,IAAA,IAAI,aAAa,aAAA,EAAe;AAC/B,MAAA,OAAO,EAAE,QAAA,EAAU,aAAA,EAAe,IAAA,EAAM,CAAA,EAAG,MAAM,aAAA,EAAc;AAAA,IAChE;AACA,IAAA,MAAM,WAAW,MAAM,IAAA,CAAK,cAAc,CAAC,QAAQ,GAAG,aAAa,CAAA;AACnE,IAAA,MAAM,IAAA,GAAO,SAAS,KAAA,CAAM,IAAA,CAAK,CAAC,CAAA,KAAM,CAAA,CAAE,aAAa,QAAQ,CAAA;AAC/D,IAAA,IAAI,CAAC,IAAA,EAAM,MAAM,IAAI,WAAA,CAAY,UAAU,aAAa,CAAA;AACxD,IAAA,OAAO,IAAA;AAAA,EACR;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,MAAM,QAAA,CACL,IAAA,EACA,UAAA,EACwB;AACxB,IAAA,MAAM,aAAA,GAAgB,cAAc,IAAI,CAAA;AACxC,IAAA,MAAM,SAAS,UAAA,EAAY,MAAA,CAAO,CAAC,CAAA,KAAM,MAAM,aAAa,CAAA;AAC5D,IAAA,MAAM,WACL,MAAA,IAAU,MAAA,CAAO,MAAA,KAAW,CAAA,GACzB,EAAE,aAAA,EAAe,KAAA,EAAO,EAAC,KACzB,MAAM,IAAA,CAAK,aAAA,CAAc,MAAA,IAAU,MAAM,aAAa,CAAA;AAC1D,IAAA,IAAI,UAAA,EAAY,QAAA,CAAS,aAAa,CAAA,EAAG;AACxC,MAAA,OAAO;AAAA,QACN,aAAA;AAAA,QACA,KAAA,EAAO;AAAA,UACN,EAAE,QAAA,EAAU,aAAA,EAAe,IAAA,EAAM,CAAA,EAAG,MAAM,aAAA,EAAc;AAAA,UACxD,GAAG,QAAA,CAAS;AAAA;AACb,OACD;AAAA,IACD;AACA,IAAA,OAAO,QAAA;AAAA,EACR;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAM,QAAQ,MAAA,EAKa;AAC1B,IAAA,MAAM,EAAE,MAAA,EAAQ,IAAA,EAAM,EAAA,EAAG,GAAI,MAAA;AAC7B,IAAA,MAAM,aAAA,GAAgB,aAAA,CAAc,MAAA,CAAO,IAAI,CAAA;AAC/C,IAAA,IAAI,SAAS,EAAA,EAAI;AAChB,MAAA,OAAO;AAAA,QACN,MAAA;AAAA,QACA,IAAA,EAAM,CAAA;AAAA,QACN,IAAA;AAAA,QACA,EAAA;AAAA,QACA,aAAA;AAAA,QACA,QAAA,EAAU;AAAA,OACX;AAAA,IACD;AACA,IAAA,MAAM,MAAA,GAAS,CAAC,IAAA,EAAM,EAAE,CAAA,CAAE,MAAA;AAAA,MACzB,CAAC,MAAyB,CAAA,KAAM;AAAA,KACjC;AACA,IAAA,MAAM,QAAA,GAAW,MAAM,IAAA,CAAK,aAAA,CAAc,QAAQ,aAAa,CAAA;AAC/D,IAAA,MAAM,OAAA,GAAU,IAAA,CAAK,MAAA,CAAO,IAAA,EAAM,UAAU,aAAa,CAAA;AACzD,IAAA,MAAM,KAAA,GAAQ,IAAA,CAAK,MAAA,CAAO,EAAA,EAAI,UAAU,aAAa,CAAA;AACrD,IAAA,MAAM,IAAA,GAAO,KAAA,CAAM,IAAA,GAAO,OAAA,CAAQ,IAAA;AAClC,IAAA,OAAO;AAAA,MACN,QAAQ,MAAA,GAAS,IAAA;AAAA,MACjB,IAAA;AAAA,MACA,IAAA;AAAA,MACA,EAAA;AAAA,MACA,aAAA;AAAA,MACA,UAAU,OAAA,CAAQ,IAAA,GAAO,MAAM,IAAA,GAAO,OAAA,CAAQ,OAAO,KAAA,CAAM;AAAA,KAC5D;AAAA,EACD;AAAA,EAEQ,MAAA,CACP,QAAA,EACA,QAAA,EACA,aAAA,EACgB;AAChB,IAAA,IAAI,aAAa,aAAA,EAAe;AAC/B,MAAA,OAAO,EAAE,QAAA,EAAU,aAAA,EAAe,IAAA,EAAM,CAAA,EAAG,MAAM,aAAA,EAAc;AAAA,IAChE;AACA,IAAA,MAAM,GAAA,GAAM,SAAS,KAAA,CAAM,IAAA,CAAK,CAAC,CAAA,KAAM,CAAA,CAAE,aAAa,QAAQ,CAAA;AAC9D,IAAA,IAAI,CAAC,GAAA,EAAK,MAAM,IAAI,WAAA,CAAY,UAAU,aAAa,CAAA;AACvD,IAAA,OAAO,GAAA;AAAA,EACR;AAAA,EAEA,MAAc,aAAA,CACb,UAAA,EACA,aAAA,EACwB;AACxB,IAAA,MAAM,MAAM,UAAA,IAAc,UAAA,CAAW,SAAS,UAAA,CAAW,IAAA,CAAK,GAAG,CAAA,GAAI,EAAA;AACrE,IAAA,MAAM,MAAM,CAAA,EAAG,IAAA,CAAK,OAAO,CAAA,YAAA,EAAe,GAAG,yBAAyB,aAAa,CAAA,oBAAA,CAAA;AAEnF,IAAA,MAAM,MAAA,GAAS,IAAA,CAAK,KAAA,EAAO,GAAA,CAAI,GAAG,CAAA;AAClC,IAAA,IAAI,QAAQ,OAAO,MAAA;AAEnB,IAAA,MAAM,IAAA,GAAO,MAAM,IAAA,CAAK,GAAA,CAAI,GAAG,CAAA;AAC/B,IAAA,MAAM,QAAyB,EAAC;AAChC,IAAA,KAAA,MAAW,MAAA,IAAU,QAAA,CAAS,IAAI,CAAA,EAAG;AACpC,MAAA,MAAM,QAAA,GAAW,OAAO,UAAU,CAAA;AAClC,MAAA,MAAM,UAAA,GAAa,OAAO,aAAa,CAAA;AACvC,MAAA,MAAM,KAAA,GAAQ,MAAA,CAAO,MAAA,CAAO,WAAW,CAAC,CAAA;AACxC,MAAA,IAAI,CAAC,YAAY,CAAC,UAAA,IAAc,CAAC,MAAA,CAAO,QAAA,CAAS,KAAK,CAAA,EAAG;AACzD,MAAA,KAAA,CAAM,KAAK,EAAE,QAAA,EAAU,MAAM,KAAA,EAAO,IAAA,EAAM,YAAY,CAAA;AAAA,IACvD;AAEA,IAAA,MAAM,QAAA,GAAyB,EAAE,aAAA,EAAe,KAAA,EAAM;AACtD,IAAA,IAAA,CAAK,KAAA,EAAO,GAAA,CAAI,GAAA,EAAK,QAAQ,CAAA;AAC7B,IAAA,OAAO,QAAA;AAAA,EACR;AAAA,EAEA,MAAc,IAAI,GAAA,EAA8B;AAC/C,IAAA,MAAM,aACL,IAAA,CAAK,SAAA,GAAY,CAAA,GAAI,IAAI,iBAAgB,GAAI,MAAA;AAC9C,IAAA,MAAM,KAAA,GAAQ,aACX,UAAA,CAAW,MAAM,WAAW,KAAA,EAAM,EAAG,IAAA,CAAK,SAAS,CAAA,GACnD,MAAA;AACH,IAAA,IAAI;AACH,MAAA,MAAM,QAAA,GAAW,MAAM,IAAA,CAAK,SAAA,CAAU,GAAA,EAAK;AAAA,QAC1C,OAAA,EAAS,EAAE,MAAA,EAAQ,UAAA,EAAW;AAAA,QAC9B,GAAI,UAAA,GAAa,EAAE,QAAQ,UAAA,CAAW,MAAA,KAAW;AAAC,OAClD,CAAA;AACD,MAAA,MAAM,IAAA,GAAO,MAAM,QAAA,CAAS,IAAA,EAAK;AACjC,MAAA,IAAI,CAAC,SAAS,EAAA,EAAI,MAAM,IAAI,YAAA,CAAa,QAAA,CAAS,QAAQ,IAAI,CAAA;AAC9D,MAAA,OAAO,IAAA;AAAA,IACR,SAAS,KAAA,EAAO;AACf,MAAA,IAAI,KAAA,YAAiB,UAAU,MAAM,KAAA;AACrC,MAAA,MAAM,IAAI,QAAA,CAAS,CAAA,6BAAA,EAAgC,GAAG,CAAA,CAAA,EAAI;AAAA,QACzD,KAAA,EAAO;AAAA,OACP,CAAA;AAAA,IACF,CAAA,SAAE;AACD,MAAA,IAAI,KAAA,eAAoB,KAAK,CAAA;AAAA,IAC9B;AAAA,EACD;AACD;AAEA,IAAM,WAAN,MAAoC;AAAA,EAClB,KAAA,uBAAY,GAAA,EAA0B;AAAA,EACvD,IAAI,GAAA,EAAuC;AAC1C,IAAA,OAAO,IAAA,CAAK,KAAA,CAAM,GAAA,CAAI,GAAG,CAAA;AAAA,EAC1B;AAAA,EACA,GAAA,CAAI,KAAa,KAAA,EAA2B;AAC3C,IAAA,IAAA,CAAK,KAAA,CAAM,GAAA,CAAI,GAAA,EAAK,KAAK,CAAA;AAAA,EAC1B;AACD,CAAA;AAEA,IAAM,aAAA,GAAgB,CAAC,IAAA,KAAkC;AACxD,EAAA,IAAI,gBAAgB,IAAA,EAAM;AACzB,IAAA,IAAI,MAAA,CAAO,KAAA,CAAM,IAAA,CAAK,OAAA,EAAS,CAAA,EAAG;AACjC,MAAA,MAAM,IAAI,SAAS,uCAAuC,CAAA;AAAA,IAC3D;AACA,IAAA,OAAO,IAAA,CAAK,WAAA,EAAY,CAAE,KAAA,CAAM,GAAG,EAAE,CAAA;AAAA,EACtC;AACA,EAAA,IAAI,CAAC,OAAA,CAAQ,IAAA,CAAK,IAAI,CAAA,EAAG;AACxB,IAAA,MAAM,IAAI,QAAA,CAAS,CAAA,cAAA,EAAiB,IAAI,CAAA,sBAAA,CAAwB,CAAA;AAAA,EACjE;AACA,EAAA,OAAO,IAAA;AACR,CAAA","file":"index.cjs","sourcesContent":["/**\n * The currencies for which the European Central Bank publishes daily euro\n * foreign-exchange reference rates, as of the ECB's current daily reference\n * list. Every rate is quoted as units of the currency per 1 EUR.\n *\n * The ECB occasionally adds or suspends currencies. This list is a convenience\n * for callers and the source of the {@link EcbCurrency} literal union; the\n * client never restricts lookups to it, so any ISO 4217 code the ECB exposes\n * (including discontinued series) can still be requested.\n */\nexport const ECB_CURRENCIES = [\n\t\"AUD\",\n\t\"BGN\",\n\t\"BRL\",\n\t\"CAD\",\n\t\"CHF\",\n\t\"CNY\",\n\t\"CZK\",\n\t\"DKK\",\n\t\"GBP\",\n\t\"HKD\",\n\t\"HUF\",\n\t\"IDR\",\n\t\"ILS\",\n\t\"INR\",\n\t\"ISK\",\n\t\"JPY\",\n\t\"KRW\",\n\t\"MXN\",\n\t\"MYR\",\n\t\"NOK\",\n\t\"NZD\",\n\t\"PHP\",\n\t\"PLN\",\n\t\"RON\",\n\t\"SEK\",\n\t\"SGD\",\n\t\"THB\",\n\t\"TRY\",\n\t\"USD\",\n\t\"ZAR\",\n] as const;\n\n/** A currency on the ECB's daily reference list. */\nexport type EcbCurrency = (typeof ECB_CURRENCIES)[number];\n\n/** The base currency of every ECB reference rate. */\nexport const BASE_CURRENCY = \"EUR\";\n\nconst ECB_CURRENCY_SET: ReadonlySet<string> = new Set(ECB_CURRENCIES);\n\n/** Narrowing guard: is `code` on the ECB daily reference list? */\nexport const isEcbCurrency = (code: string): code is EcbCurrency =>\n\tECB_CURRENCY_SET.has(code);\n","/**\n * Minimal RFC 4180 CSV parser, sufficient for the ECB data API's `text/csv`\n * responses. Handles quoted fields containing commas, line breaks and escaped\n * (doubled) quotes — the ECB's `TITLE_COMPL` column, for instance, embeds\n * commas inside quotes.\n */\nexport const parseCsv = (text: string): Record<string, string>[] => {\n\tconst rows = splitRows(text);\n\tconst header = rows.shift();\n\tif (!header) return [];\n\tconst records: Record<string, string>[] = [];\n\tfor (const row of rows) {\n\t\t// Skip blank trailing lines (a single empty field, no real data).\n\t\tif (row.length === 1 && row[0] === \"\") continue;\n\t\tconst record: Record<string, string> = {};\n\t\tfor (let i = 0; i < header.length; i++) {\n\t\t\trecord[header[i] ?? `column_${i}`] = row[i] ?? \"\";\n\t\t}\n\t\trecords.push(record);\n\t}\n\treturn records;\n};\n\nconst splitRows = (text: string): string[][] => {\n\tconst rows: string[][] = [];\n\tlet row: string[] = [];\n\tlet field = \"\";\n\tlet inQuotes = false;\n\n\tfor (let i = 0; i < text.length; i++) {\n\t\tconst char = text[i];\n\t\tif (inQuotes) {\n\t\t\tif (char === '\"') {\n\t\t\t\tif (text[i + 1] === '\"') {\n\t\t\t\t\tfield += '\"';\n\t\t\t\t\ti++;\n\t\t\t\t} else {\n\t\t\t\t\tinQuotes = false;\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tfield += char;\n\t\t\t}\n\t\t\tcontinue;\n\t\t}\n\t\tif (char === '\"') {\n\t\t\tinQuotes = true;\n\t\t} else if (char === \",\") {\n\t\t\trow.push(field);\n\t\t\tfield = \"\";\n\t\t} else if (char === \"\\n\" || char === \"\\r\") {\n\t\t\tif (char === \"\\r\" && text[i + 1] === \"\\n\") i++;\n\t\t\trow.push(field);\n\t\t\trows.push(row);\n\t\t\trow = [];\n\t\t\tfield = \"\";\n\t\t} else {\n\t\t\tfield += char;\n\t\t}\n\t}\n\n\t// Flush a final row when the text does not end with a line break.\n\tif (field !== \"\" || row.length > 0) {\n\t\trow.push(field);\n\t\trows.push(row);\n\t}\n\treturn rows;\n};\n","import type { CurrencyCode, IsoDate } from \"./types.js\";\n\n/** Base class for every error thrown by this package. */\nexport class EcbError extends Error {\n\tconstructor(message: string, options?: { cause?: unknown }) {\n\t\tsuper(message, options);\n\t\tthis.name = \"EcbError\";\n\t}\n}\n\n/** The ECB data API returned a non-2xx response. */\nexport class EcbHttpError extends EcbError {\n\treadonly status: number;\n\treadonly body: string;\n\n\tconstructor(status: number, body: string) {\n\t\tsuper(`ECB data API returned HTTP ${status}`);\n\t\tthis.name = \"EcbHttpError\";\n\t\tthis.status = status;\n\t\tthis.body = body;\n\t}\n}\n\n/** No reference rate exists for the currency on or before the requested date. */\nexport class NoRateError extends EcbError {\n\treadonly currency: CurrencyCode;\n\treadonly date: IsoDate;\n\n\tconstructor(currency: CurrencyCode, date: IsoDate) {\n\t\tsuper(`No ECB reference rate for ${currency} on or before ${date}`);\n\t\tthis.name = \"NoRateError\";\n\t\tthis.currency = currency;\n\t\tthis.date = date;\n\t}\n}\n","import { BASE_CURRENCY } from \"./currencies.js\";\nimport { parseCsv } from \"./csv.js\";\nimport { EcbError, EcbHttpError, NoRateError } from \"./errors.js\";\nimport type {\n\tConvertResult,\n\tCurrencyCode,\n\tIsoDate,\n\tRateSnapshot,\n\tReferenceRate,\n} from \"./types.js\";\n\n/**\n * The subset of the WHATWG `fetch` contract this client relies on. The global\n * `fetch` satisfies it, so callers rarely need to pass one — it exists to make\n * the client trivially testable and proxy-friendly.\n */\nexport type FetchLike = (\n\tinput: string,\n\tinit?: { headers?: Record<string, string>; signal?: AbortSignal },\n) => Promise<{ ok: boolean; status: number; text: () => Promise<string> }>;\n\n/** A cache for resolved snapshots, keyed by request URL. */\nexport interface RateCache {\n\tget(key: string): RateSnapshot | undefined;\n\tset(key: string, value: RateSnapshot): void;\n}\n\nexport interface EcbClientOptions {\n\t/**\n\t * Base URL of the ECB data API.\n\t * Default: `https://data-api.ecb.europa.eu/service`.\n\t */\n\tbaseUrl?: string;\n\t/** Custom fetch implementation. Defaults to the global `fetch`. */\n\tfetch?: FetchLike;\n\t/**\n\t * Snapshot cache. Defaults to an unbounded in-process `Map`. Pass `null` to\n\t * disable caching entirely.\n\t */\n\tcache?: RateCache | null;\n\t/** Request timeout in milliseconds. Default `15000`; `0` disables it. */\n\ttimeoutMs?: number;\n}\n\nconst DEFAULT_BASE_URL = \"https://data-api.ecb.europa.eu/service\";\nconst DATE_RE = /^\\d{4}-\\d{2}-\\d{2}$/;\n\n/**\n * A small client for the European Central Bank's euro foreign-exchange\n * reference rates, served from the ECB data API (dataflow `EXR`).\n *\n * Every rate is quoted against the euro (units of the currency per 1 EUR).\n * Requests for a date that is not a TARGET business day resolve to the most\n * recent prior business day, via the data API's `lastNObservations` parameter,\n * and the resolved date is reported on each {@link ReferenceRate}.\n */\nexport class EcbClient {\n\tprivate readonly baseUrl: string;\n\tprivate readonly fetchImpl: FetchLike;\n\tprivate readonly cache: RateCache | null;\n\tprivate readonly timeoutMs: number;\n\n\tconstructor(options: EcbClientOptions = {}) {\n\t\tthis.baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\\/+$/, \"\");\n\t\tconst resolvedFetch =\n\t\t\toptions.fetch ?? (globalThis.fetch as FetchLike | undefined);\n\t\tif (!resolvedFetch) {\n\t\t\tthrow new EcbError(\n\t\t\t\t\"No fetch implementation available; pass `fetch` in EcbClientOptions\",\n\t\t\t);\n\t\t}\n\t\tthis.fetchImpl = resolvedFetch;\n\t\tthis.cache =\n\t\t\toptions.cache === undefined ? new MapCache() : options.cache;\n\t\tthis.timeoutMs = options.timeoutMs ?? 15000;\n\t}\n\n\t/**\n\t * Resolve the reference rate for a single currency on (or before) a date.\n\t * `EUR` returns a unit rate without a network call.\n\t */\n\tasync getRate(\n\t\tcurrency: CurrencyCode,\n\t\tdate: Date | IsoDate,\n\t): Promise<ReferenceRate> {\n\t\tconst requestedDate = normalizeDate(date);\n\t\tif (currency === BASE_CURRENCY) {\n\t\t\treturn { currency: BASE_CURRENCY, rate: 1, date: requestedDate };\n\t\t}\n\t\tconst snapshot = await this.fetchSnapshot([currency], requestedDate);\n\t\tconst rate = snapshot.rates.find((r) => r.currency === currency);\n\t\tif (!rate) throw new NoRateError(currency, requestedDate);\n\t\treturn rate;\n\t}\n\n\t/**\n\t * Resolve reference rates on (or before) a date. Pass `currencies` to limit\n\t * the request, or omit it to fetch every series the ECB publishes (note\n\t * that discontinued series resolve to their last published date).\n\t */\n\tasync getRates(\n\t\tdate: Date | IsoDate,\n\t\tcurrencies?: readonly CurrencyCode[],\n\t): Promise<RateSnapshot> {\n\t\tconst requestedDate = normalizeDate(date);\n\t\tconst wanted = currencies?.filter((c) => c !== BASE_CURRENCY);\n\t\tconst snapshot =\n\t\t\twanted && wanted.length === 0\n\t\t\t\t? { requestedDate, rates: [] }\n\t\t\t\t: await this.fetchSnapshot(wanted ?? null, requestedDate);\n\t\tif (currencies?.includes(BASE_CURRENCY)) {\n\t\t\treturn {\n\t\t\t\trequestedDate,\n\t\t\t\trates: [\n\t\t\t\t\t{ currency: BASE_CURRENCY, rate: 1, date: requestedDate },\n\t\t\t\t\t...snapshot.rates,\n\t\t\t\t],\n\t\t\t};\n\t\t}\n\t\treturn snapshot;\n\t}\n\n\t/**\n\t * Convert `amount` from one currency to another using the reference rates on\n\t * (or before) `date`. Non-euro pairs are crossed through the euro.\n\t */\n\tasync convert(params: {\n\t\tamount: number;\n\t\tfrom: CurrencyCode;\n\t\tto: CurrencyCode;\n\t\tdate: Date | IsoDate;\n\t}): Promise<ConvertResult> {\n\t\tconst { amount, from, to } = params;\n\t\tconst requestedDate = normalizeDate(params.date);\n\t\tif (from === to) {\n\t\t\treturn {\n\t\t\t\tamount,\n\t\t\t\trate: 1,\n\t\t\t\tfrom,\n\t\t\t\tto,\n\t\t\t\trequestedDate,\n\t\t\t\trateDate: requestedDate,\n\t\t\t};\n\t\t}\n\t\tconst needed = [from, to].filter(\n\t\t\t(c): c is CurrencyCode => c !== BASE_CURRENCY,\n\t\t);\n\t\tconst snapshot = await this.fetchSnapshot(needed, requestedDate);\n\t\tconst fromLeg = this.legFor(from, snapshot, requestedDate);\n\t\tconst toLeg = this.legFor(to, snapshot, requestedDate);\n\t\tconst rate = toLeg.rate / fromLeg.rate;\n\t\treturn {\n\t\t\tamount: amount * rate,\n\t\t\trate,\n\t\t\tfrom,\n\t\t\tto,\n\t\t\trequestedDate,\n\t\t\trateDate: fromLeg.date > toLeg.date ? fromLeg.date : toLeg.date,\n\t\t};\n\t}\n\n\tprivate legFor(\n\t\tcurrency: CurrencyCode,\n\t\tsnapshot: RateSnapshot,\n\t\trequestedDate: IsoDate,\n\t): ReferenceRate {\n\t\tif (currency === BASE_CURRENCY) {\n\t\t\treturn { currency: BASE_CURRENCY, rate: 1, date: requestedDate };\n\t\t}\n\t\tconst leg = snapshot.rates.find((r) => r.currency === currency);\n\t\tif (!leg) throw new NoRateError(currency, requestedDate);\n\t\treturn leg;\n\t}\n\n\tprivate async fetchSnapshot(\n\t\tcurrencies: readonly CurrencyCode[] | null,\n\t\trequestedDate: IsoDate,\n\t): Promise<RateSnapshot> {\n\t\tconst key = currencies && currencies.length ? currencies.join(\"+\") : \"\";\n\t\tconst url = `${this.baseUrl}/data/EXR/D.${key}.EUR.SP00.A?endPeriod=${requestedDate}&lastNObservations=1`;\n\n\t\tconst cached = this.cache?.get(url);\n\t\tif (cached) return cached;\n\n\t\tconst text = await this.get(url);\n\t\tconst rates: ReferenceRate[] = [];\n\t\tfor (const record of parseCsv(text)) {\n\t\t\tconst currency = record[\"CURRENCY\"];\n\t\t\tconst observedAt = record[\"TIME_PERIOD\"];\n\t\t\tconst value = Number(record[\"OBS_VALUE\"]);\n\t\t\tif (!currency || !observedAt || !Number.isFinite(value)) continue;\n\t\t\trates.push({ currency, rate: value, date: observedAt });\n\t\t}\n\n\t\tconst snapshot: RateSnapshot = { requestedDate, rates };\n\t\tthis.cache?.set(url, snapshot);\n\t\treturn snapshot;\n\t}\n\n\tprivate async get(url: string): Promise<string> {\n\t\tconst controller =\n\t\t\tthis.timeoutMs > 0 ? new AbortController() : undefined;\n\t\tconst timer = controller\n\t\t\t? setTimeout(() => controller.abort(), this.timeoutMs)\n\t\t\t: undefined;\n\t\ttry {\n\t\t\tconst response = await this.fetchImpl(url, {\n\t\t\t\theaders: { Accept: \"text/csv\" },\n\t\t\t\t...(controller ? { signal: controller.signal } : {}),\n\t\t\t});\n\t\t\tconst body = await response.text();\n\t\t\tif (!response.ok) throw new EcbHttpError(response.status, body);\n\t\t\treturn body;\n\t\t} catch (error) {\n\t\t\tif (error instanceof EcbError) throw error;\n\t\t\tthrow new EcbError(`ECB data API request failed: ${url}`, {\n\t\t\t\tcause: error,\n\t\t\t});\n\t\t} finally {\n\t\t\tif (timer) clearTimeout(timer);\n\t\t}\n\t}\n}\n\nclass MapCache implements RateCache {\n\tprivate readonly store = new Map<string, RateSnapshot>();\n\tget(key: string): RateSnapshot | undefined {\n\t\treturn this.store.get(key);\n\t}\n\tset(key: string, value: RateSnapshot): void {\n\t\tthis.store.set(key, value);\n\t}\n}\n\nconst normalizeDate = (date: Date | IsoDate): IsoDate => {\n\tif (date instanceof Date) {\n\t\tif (Number.isNaN(date.getTime())) {\n\t\t\tthrow new EcbError(\"Invalid Date passed for the rate date\");\n\t\t}\n\t\treturn date.toISOString().slice(0, 10);\n\t}\n\tif (!DATE_RE.test(date)) {\n\t\tthrow new EcbError(`Invalid date \"${date}\"; expected YYYY-MM-DD`);\n\t}\n\treturn date;\n};\n"]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The currencies for which the European Central Bank publishes daily euro
|
|
3
|
+
* foreign-exchange reference rates, as of the ECB's current daily reference
|
|
4
|
+
* list. Every rate is quoted as units of the currency per 1 EUR.
|
|
5
|
+
*
|
|
6
|
+
* The ECB occasionally adds or suspends currencies. This list is a convenience
|
|
7
|
+
* for callers and the source of the {@link EcbCurrency} literal union; the
|
|
8
|
+
* client never restricts lookups to it, so any ISO 4217 code the ECB exposes
|
|
9
|
+
* (including discontinued series) can still be requested.
|
|
10
|
+
*/
|
|
11
|
+
declare const ECB_CURRENCIES: readonly ["AUD", "BGN", "BRL", "CAD", "CHF", "CNY", "CZK", "DKK", "GBP", "HKD", "HUF", "IDR", "ILS", "INR", "ISK", "JPY", "KRW", "MXN", "MYR", "NOK", "NZD", "PHP", "PLN", "RON", "SEK", "SGD", "THB", "TRY", "USD", "ZAR"];
|
|
12
|
+
/** A currency on the ECB's daily reference list. */
|
|
13
|
+
type EcbCurrency = (typeof ECB_CURRENCIES)[number];
|
|
14
|
+
/** The base currency of every ECB reference rate. */
|
|
15
|
+
declare const BASE_CURRENCY = "EUR";
|
|
16
|
+
/** Narrowing guard: is `code` on the ECB daily reference list? */
|
|
17
|
+
declare const isEcbCurrency: (code: string) => code is EcbCurrency;
|
|
18
|
+
|
|
19
|
+
/** An ISO 8601 calendar date, formatted `YYYY-MM-DD`. */
|
|
20
|
+
type IsoDate = string;
|
|
21
|
+
/**
|
|
22
|
+
* A currency code. ECB reference currencies are strongly typed for
|
|
23
|
+
* autocomplete; any other ISO 4217 code is still accepted. The `string & {}`
|
|
24
|
+
* member preserves literal autocomplete while widening to accept any string.
|
|
25
|
+
*/
|
|
26
|
+
type CurrencyCode = EcbCurrency | "EUR" | (string & {});
|
|
27
|
+
/** A single euro reference-rate observation. */
|
|
28
|
+
interface ReferenceRate {
|
|
29
|
+
/** ISO 4217 code of the quoted currency. */
|
|
30
|
+
currency: CurrencyCode;
|
|
31
|
+
/** Units of {@link currency} per 1 EUR. */
|
|
32
|
+
rate: number;
|
|
33
|
+
/**
|
|
34
|
+
* The date the observation actually applies to. Equal to the requested date
|
|
35
|
+
* on a TARGET business day, or the most recent prior business day when the
|
|
36
|
+
* requested date had no published rate (weekend, holiday).
|
|
37
|
+
*/
|
|
38
|
+
date: IsoDate;
|
|
39
|
+
}
|
|
40
|
+
/** A set of reference rates resolved for a single requested date. */
|
|
41
|
+
interface RateSnapshot {
|
|
42
|
+
/** The date that was requested, normalized to `YYYY-MM-DD`. */
|
|
43
|
+
requestedDate: IsoDate;
|
|
44
|
+
/**
|
|
45
|
+
* One entry per currency. Each entry's effective {@link ReferenceRate.date}
|
|
46
|
+
* may differ — discontinued series resolve to their last published date.
|
|
47
|
+
*/
|
|
48
|
+
rates: ReferenceRate[];
|
|
49
|
+
}
|
|
50
|
+
/** The outcome of converting an amount between two currencies. */
|
|
51
|
+
interface ConvertResult {
|
|
52
|
+
/** The converted amount, expressed in {@link to}. Not rounded. */
|
|
53
|
+
amount: number;
|
|
54
|
+
/** The effective {@link from} → {@link to} rate applied. */
|
|
55
|
+
rate: number;
|
|
56
|
+
/** Source currency. */
|
|
57
|
+
from: CurrencyCode;
|
|
58
|
+
/** Target currency. */
|
|
59
|
+
to: CurrencyCode;
|
|
60
|
+
/** The requested date, normalized to `YYYY-MM-DD`. */
|
|
61
|
+
requestedDate: IsoDate;
|
|
62
|
+
/** The effective observation date of the rate(s) applied. */
|
|
63
|
+
rateDate: IsoDate;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* The subset of the WHATWG `fetch` contract this client relies on. The global
|
|
68
|
+
* `fetch` satisfies it, so callers rarely need to pass one — it exists to make
|
|
69
|
+
* the client trivially testable and proxy-friendly.
|
|
70
|
+
*/
|
|
71
|
+
type FetchLike = (input: string, init?: {
|
|
72
|
+
headers?: Record<string, string>;
|
|
73
|
+
signal?: AbortSignal;
|
|
74
|
+
}) => Promise<{
|
|
75
|
+
ok: boolean;
|
|
76
|
+
status: number;
|
|
77
|
+
text: () => Promise<string>;
|
|
78
|
+
}>;
|
|
79
|
+
/** A cache for resolved snapshots, keyed by request URL. */
|
|
80
|
+
interface RateCache {
|
|
81
|
+
get(key: string): RateSnapshot | undefined;
|
|
82
|
+
set(key: string, value: RateSnapshot): void;
|
|
83
|
+
}
|
|
84
|
+
interface EcbClientOptions {
|
|
85
|
+
/**
|
|
86
|
+
* Base URL of the ECB data API.
|
|
87
|
+
* Default: `https://data-api.ecb.europa.eu/service`.
|
|
88
|
+
*/
|
|
89
|
+
baseUrl?: string;
|
|
90
|
+
/** Custom fetch implementation. Defaults to the global `fetch`. */
|
|
91
|
+
fetch?: FetchLike;
|
|
92
|
+
/**
|
|
93
|
+
* Snapshot cache. Defaults to an unbounded in-process `Map`. Pass `null` to
|
|
94
|
+
* disable caching entirely.
|
|
95
|
+
*/
|
|
96
|
+
cache?: RateCache | null;
|
|
97
|
+
/** Request timeout in milliseconds. Default `15000`; `0` disables it. */
|
|
98
|
+
timeoutMs?: number;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* A small client for the European Central Bank's euro foreign-exchange
|
|
102
|
+
* reference rates, served from the ECB data API (dataflow `EXR`).
|
|
103
|
+
*
|
|
104
|
+
* Every rate is quoted against the euro (units of the currency per 1 EUR).
|
|
105
|
+
* Requests for a date that is not a TARGET business day resolve to the most
|
|
106
|
+
* recent prior business day, via the data API's `lastNObservations` parameter,
|
|
107
|
+
* and the resolved date is reported on each {@link ReferenceRate}.
|
|
108
|
+
*/
|
|
109
|
+
declare class EcbClient {
|
|
110
|
+
private readonly baseUrl;
|
|
111
|
+
private readonly fetchImpl;
|
|
112
|
+
private readonly cache;
|
|
113
|
+
private readonly timeoutMs;
|
|
114
|
+
constructor(options?: EcbClientOptions);
|
|
115
|
+
/**
|
|
116
|
+
* Resolve the reference rate for a single currency on (or before) a date.
|
|
117
|
+
* `EUR` returns a unit rate without a network call.
|
|
118
|
+
*/
|
|
119
|
+
getRate(currency: CurrencyCode, date: Date | IsoDate): Promise<ReferenceRate>;
|
|
120
|
+
/**
|
|
121
|
+
* Resolve reference rates on (or before) a date. Pass `currencies` to limit
|
|
122
|
+
* the request, or omit it to fetch every series the ECB publishes (note
|
|
123
|
+
* that discontinued series resolve to their last published date).
|
|
124
|
+
*/
|
|
125
|
+
getRates(date: Date | IsoDate, currencies?: readonly CurrencyCode[]): Promise<RateSnapshot>;
|
|
126
|
+
/**
|
|
127
|
+
* Convert `amount` from one currency to another using the reference rates on
|
|
128
|
+
* (or before) `date`. Non-euro pairs are crossed through the euro.
|
|
129
|
+
*/
|
|
130
|
+
convert(params: {
|
|
131
|
+
amount: number;
|
|
132
|
+
from: CurrencyCode;
|
|
133
|
+
to: CurrencyCode;
|
|
134
|
+
date: Date | IsoDate;
|
|
135
|
+
}): Promise<ConvertResult>;
|
|
136
|
+
private legFor;
|
|
137
|
+
private fetchSnapshot;
|
|
138
|
+
private get;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** Base class for every error thrown by this package. */
|
|
142
|
+
declare class EcbError extends Error {
|
|
143
|
+
constructor(message: string, options?: {
|
|
144
|
+
cause?: unknown;
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
/** The ECB data API returned a non-2xx response. */
|
|
148
|
+
declare class EcbHttpError extends EcbError {
|
|
149
|
+
readonly status: number;
|
|
150
|
+
readonly body: string;
|
|
151
|
+
constructor(status: number, body: string);
|
|
152
|
+
}
|
|
153
|
+
/** No reference rate exists for the currency on or before the requested date. */
|
|
154
|
+
declare class NoRateError extends EcbError {
|
|
155
|
+
readonly currency: CurrencyCode;
|
|
156
|
+
readonly date: IsoDate;
|
|
157
|
+
constructor(currency: CurrencyCode, date: IsoDate);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Minimal RFC 4180 CSV parser, sufficient for the ECB data API's `text/csv`
|
|
162
|
+
* responses. Handles quoted fields containing commas, line breaks and escaped
|
|
163
|
+
* (doubled) quotes — the ECB's `TITLE_COMPL` column, for instance, embeds
|
|
164
|
+
* commas inside quotes.
|
|
165
|
+
*/
|
|
166
|
+
declare const parseCsv: (text: string) => Record<string, string>[];
|
|
167
|
+
|
|
168
|
+
export { BASE_CURRENCY, type ConvertResult, type CurrencyCode, ECB_CURRENCIES, EcbClient, type EcbClientOptions, type EcbCurrency, EcbError, EcbHttpError, type FetchLike, type IsoDate, NoRateError, type RateCache, type RateSnapshot, type ReferenceRate, isEcbCurrency, parseCsv };
|